近期开发了一项通过UART进行读写操作的功能。说起来并不难,但是实际操作起来还是遇到了不少问题,解决问题也费了一番周折。因此记录下来作为积累,也供遇到类似问题的同学参考。
-
问题背景
当前的项目需要开发一项功能:BMC通过UART串口与另一设备通信,进行读写操作。听起来并不难,网上应该都能找到相关的程序,因此很快就着手开始去做。 -
初步调试
设备环境还没准备好,由于BMC串口也是UART设备,发送命令也可以获取返回值,于是就先用BMC串口进行初步的功能测试。
参考网上的资料,先写了如下测试代码uart.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <termios.h>
#include <fcntl.h>
int main() {
int fd;
struct termios options;
char *serialPort = "/dev/ttyUSB0"; // 串口设备文件路径,BMC串口为ttyUSB0
speed_t baudrate = B115200; // 设定波特率
int status;
char buffer[255];
int bytes_read;
// 打开串口设备
fd = open(serialPort, O_RDWR | O_NOCTTY | O_NDELAY);
if (fd == -1) {
perror("open_port: Unable to open serial port - ");
return(-1);
}
// 获取并配置串口选项
tcgetattr(fd, &options);
cfsetispeed(&options, baudrate); // 输入波特率
cfsetospeed(&options, baudrate); // 输出波特率
// 启用接收和发送
options.c_cflag |= (CLOCAL | CREAD);
// 更新串口配置
status = tcsetattr(fd, TCSANOW, &options);
if (status != 0) {
perror("tcsetattr");
return -1;
}
// 清空串口
tcflush(fd, TCIFLUSH);
// 向串口写入数据
const char *data = "ifconfig\n";
write(fd, data, sizeof(data));
// 从串口读取数据
bytes_read = read(fd, buffer, sizeof(buffer));
if (bytes_read > 0) {
buffer[bytes_read] = '\0'; // 确保字符串以null结尾
printf("Received: '%s'\n", buffer);
}
// 关闭串口
close(fd);
return 0;
}
使用此代码生成可执行文件;
gcc -o uart.c uart
BMC串口连接到笔记本USB,编译完成后将可执行文件放到笔记本中进行执行。这部分代码实现的是在BMC串口下发送ifconfig命令,并读取返回的内容。
2.1 输入回车问题
运行uart程序,同时打开minicom查看串口的输入输出情况。下图中上半部分为程序运行情况,下半部分为minicom下查看的串口输入输出情况。
从图中可以看出,minicom查看的串口中出现了输入的命令“ifconfig“,但没有得到执行,也没有返回内容。虽然以上代码中写入了”ifconfig\n“,但是似乎没有输入回车,导致ifconfig的命令没有得到执行。
在网上查资料,尝试在命令结尾加上‘\n’, '\r’都没有生效。最后发现在写入命令的字符串之后,再单独写入一个‘\n’,可以实现回车的效果:
// 写入数据到串口
const char *data = "ifconfig";
write(fd, data, sizeof(data));
const char *data2 = "\n";
write(fd, data2, sizeof(data2));
minicom展示的串口中出现了ifconfig命令的执行结果:
2.2 uart可执行文件无法读取到返回内容问题
从mincom展示的串口中已经可以看到输出内容,但运行uart程序并没有收集到这些内容。打印上面代码中的bytes_read,发现是-1。
我觉得很奇怪,先是怀疑ifconfig命令返回的速度太快,代码中写入之后立即进行读取仍然没有捕捉到返回内容。
于是编译了一个程序uart2一直读取串口的内容:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <termios.h>
#include <fcntl.h>
int main() {
int fd;
struct termios options;
char *serialPort = "/dev/ttyUSB0"; // 串口设备文件路径,BMC串口为ttyUSB0
speed_t baudrate = B115200; // 设定波特率
int status;
char buffer[255];
int bytes_read;
// 打开串口设备
fd = open(serialPort, O_RDWR | O_NOCTTY | O_NDELAY);
if (fd == -1) {
perror("open_port: Unable to open serial port - ");
return(-1);
}
// 获取并配置串口选项
tcgetattr(fd, &options);
cfsetispeed(&options, baudrate); // 输入波特率
cfsetospeed(&options, baudrate); // 输出波特率
// 启用接收和发送
options.c_cflag |= (CLOCAL | CREAD);
// 更新配置到串口
status = tcsetattr(fd, TCSANOW, &options);
if (status != 0) {
perror("tcsetattr");
return -1;
}
// 清空串口
tcflush(fd, TCIFLUSH);
do
{
bytes_read = read(fd,buffer, sizeof(buffer));
if(bytes_read > 0)
{
printf("===bytes_read:%d, buffer:%s\n",bytes_read, buffer);
}
}while(1)
修改程序uart.c,修改为只向串口写入数据。
先将程序uart2运行起来,再通过uart程序写入数据,此时发现uart2程序能够读取到串口中的返回内容。
分别打印了uart程序写入数据结束和uart2程序第一次读取到数据时的时间,发现二者之间的间隔有几百毫秒,在uart程序中写入完毕到开始读取的时间间隔远小于此,不会存在来不及读取的问题。
经过多方查找网上的资料以及测试,发现配置串口参数时忽略的两个重要参数才是这个问题的关键:VTIME和VMIN。查找这两个参数的相关解释如下:
VTIME指定了等待的时间,VMIN指定了读取字符的最小数量。
它们不同组合地取值会得到不同的结果,分别如下:
1.当VTIME>0,VMIN>0时。read调用将保持阻塞直到读取到第一个字符,读到了第一个字符之后开始计时,此后若 时间到了VTIME或者时间未到但已读够了VMIN个字符则会返回;若在时间未到之前又读到了一个字符(但此时读到的总数仍不够VMIN)则计时重新开始。
2. 当VTIME>0,VMIN=0时。read调用读到数据则立即返回,否则将为每个字符最多等待VTIME时间。
3. 当VTIME=0,VMIN>0时。read调用一直阻塞,直到读到VMIN个字符后立即返回。
4. 若在open或fcntl设置了O_NDELALY或O_NONBLOCK标志,read调用不会阻塞而是立即返回,那么VTIME和VMIN就没有意义,效果等同于与把VTIME和VMIN都设为了0。
而我的代码在打开文件的时候确实设置了O_NDELALY标志,导致的结果就是read只读一次,没有等到能够读取到数据就立即返回了。
于是据此将以上串口初始化部分封装为函数:
int UartInit(char* SerialPort, speed_t baudrate) {
// UART串口配置
struct termios options;
int status;
// 打开串口设备
int fd = open(SerialPort, O_RDWR | O_NOCTTY);
if (fd == -1) {
perror("open_port: Unable to open serial port - ");
return (-1);
}
// 获取并配置串口选项
tcgetattr(fd, &options);
cfsetispeed(&options, baudrate); // 输入波特率
cfsetospeed(&options, baudrate); // 输出波特率
// 设置串口选项:无奇偶校验位,8位数据位,1位停止位,无软件流控
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8;
options.c_cflag &= ~CRTSCTS; // 无硬件流控
// 最少读取1字节
options.c_cc[VMIN] = 1;
options.c_cc[VTIME] = 0;
tcflush(fd, TCIFLUSH); // 清空输入缓冲区
// 使用配置后的选项
status = tcsetattr(fd, TCSANOW, &options);
if (status != 0) {
perror("tcsetattr");
return -1;
}
return fd;
}
这样修改之后,只需要运行一个程序,写入命令之后再读取即可。
2.3 读取返回内容何时终止问题
读写的问题解决了,但是又面临这样一个问题:发送命令后,设备的返回内容长度是不确定的,如何判断读取了全部的返回内容呢?也就是说何时停止读取并处理读取到的数据呢?
其实通过串口与设备的交互与在Linux系统类似,在终端下发送一条命令,按下回车,返回所需内容之后会显示一个root@localhost样式的提示符,可以根据设备实际返回的提示符确认何时停止读取。
于是将初始化,读,写分别封装成了函数,修改后的代码如下:
#include "./include/uart.h"
/*
Func:UartInit
serialport:串口设备名称
baudrate:串口波特率
ret:fd,打开串口设备的文件描述符
*/
int UartInit(char* SerialPort, speed_t baudrate) {
// UART串口配置
struct termios options;
int status;
// 打开串口设备
int fd = open(SerialPort, O_RDWR | O_NOCTTY);
if (fd == -1) {
perror("open_port: Unable to open serial port - ");
return -1;
}
// 获取并配置串口选项
tcgetattr(fd, &options);
cfsetispeed(&options, baudrate); // 输入波特率
cfsetospeed(&options, baudrate); // 输出波特率
// 设置串口选项:无奇偶校验位,8位数据位,1位停止位,无软件流控
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8;
options.c_cflag &= ~CRTSCTS; // 无硬件流控
// 最少读取1字节
options.c_cc[VMIN] = 1;
options.c_cc[VTIME] = 0;
tcflush(fd, TCIFLUSH); // 清空输入缓冲区
// 使用配置后的选项
status = tcsetattr(fd, TCSANOW, &options);
if (status != 0) {
perror("tcsetattr");
return -1;
}
return fd;
}
/*
Func:UartWrite
fd: 打开的串口文件描述符;
data:向串口发送的命令;
datalength:发送命令长度;
*/
int UartWrite(int fd, const char* data, int datalength)
{
if(data == NULL)
{
return -1;
}
// 清空串口
tcflush(fd, TCIFLUSH);
//向串口写入命令
write(fd, data, datalength);
//写入命令后写入回车,发送命令
char cr = '\r';
write(fd, &cr, 1);
return 0;
}
/*
Func:UartRead,
fd: 打开的串口文件描述符;
data: 读取的内容
endstr:标志读取内容结束的字符串
ret: datalength:读取内容的长度
*/
int UartRead(int fd, char* data, const char* endstr)
{
char buffer[255];
int bytes_read = 0;
int datalength = 0;
do
{
bytes_read = read(fd, buffer, sizeof(buffer));
if (bytes_read > 0)
{
strncat(data, buffer, bytes_read);
datalength += bytes_read;
if(datalength > sizeof(endstr) )
{
if(strncmp(&(data[datalength-strlen(endstr)]),endstr,strlen(endstr) ) == 0)
{
//End reading
datalength = datalength - strlen(endstr);
break;
}
}
}
} while(1);
data[datalength] = '\0';
return datalength;
}
这样在程序中根据需要调用UartInit, UartWrite, UartRead函数,传入适当的参数,就可以实现与设备的正常通信了。
问题解决之后回头来看,其实原理和代码都不难,主要还是因为自己没有接触过,实践经验比较少,以后继续在实践中积累,学到的东西无论多少,都是一种进步。
标签:读取,UART,实践,问题,read,int,fd,串口,options From: https://blog.csdn.net/Ocean1994/article/details/140961544