ARMv7架构UART串口非阻塞收发丢字节问题求助
问题背景
你开发了一款针对ARMv7架构的C++串口程序(基于Linaro gnueabihf工具链+树莓派根文件系统编译),用于控制工业屏背光,通信参数为9600波特率、无校验、8位数据位、1位停止位,设备以CRLF结尾返回响应。目前发送功能正常,但接收环节存在三类问题:
- 非阻塞模式(无select)+500ms超时:偶发丢字节(比如预期
#BLIGHT_ON!OK,实际收到#BLI_ON!O) - 纯阻塞模式:接收内容正确,但最后一次read会挂起
- 结合select的阻塞调用:频繁丢字节,甚至select返回可读但read仍挂起
另外,单独用cat -v < /dev/ttyS3能看到完整响应,但程序和cat同时运行会瓜分数据。之前用QtSerial和STM32通信无此问题。
核心原因分析
1. 非阻塞模式丢字节:超时逻辑的缺陷
你当前的超时逻辑是每次收到字节就重置超时计时器,但如果设备的响应是分批次发送的(比如因为串口缓冲区大小、硬件流控或者设备端的发送延迟),最后一批字节发送间隔如果超过500ms,就会被提前截断。另外,FNDELAY模式下read返回-1时直接continue,没有区分是EAGAIN(无数据)还是真的错误,可能漏掉错误处理导致隐性问题。
2. 纯阻塞模式挂起:没有明确的结束条件
串口是流式设备,没有内置的“数据包结束”标识,纯阻塞read会一直等待直到有数据或出错,而你没有根据设备的响应特征(CRLF结尾)来判断是否停止接收,所以会在收到完整响应后继续挂起等待不存在的后续数据。
3. select模式异常:串口参数配置与select使用的问题
- 你调用了
cfmakeraw(),这个函数会把串口设置为原始模式,其中c_cc[VMIN]和c_cc[VTIME]会被设为0,这会影响select的行为:当串口缓冲区有哪怕1字节数据,select就会返回可读,但read可能因为后续数据没到而出现部分读取,而你没有循环读取直到满足结束条件(CRLF),导致丢字节。 - 另外,select返回可读后,read的长度设置不合理,或者没有处理
EINTR(被信号中断)的情况,可能导致read挂起。
4. 与cat共存时瓜分数据:串口设备的独占性问题
Linux的串口设备默认是可以被多个进程同时打开的,多个进程read时会瓜分缓冲区里的数据,这是正常的设备行为,不是程序bug,只要保证同一时刻只有一个进程访问串口即可避免。
对比QtSerial无问题的原因
QtSerial内部已经封装了完善的串口处理逻辑:包括基于事件循环的异步读取、根据结束符(比如CRLF)自动分包、超时处理、错误重试等,不需要你手动处理这些底层细节,所以不会出现丢字节或阻塞问题。
解决方案:基于CRLF结束符的可靠接收逻辑
针对你的场景,最可靠的方式是基于设备响应的CRLF结束符来判断接收完成,同时结合select实现超时,避免阻塞挂起。以下是修改后的核心思路和代码:
关键修改点
- 配置串口时设置合理的VMIN和VTIME:在
configure_port函数中,设置c_cc[VMIN] = 0(表示read可以返回0字节),c_cc[VTIME] = 1(100ms的超时,用于单字节读取的超时),这样既不会无限阻塞,又能保证数据读取的完整性。 - 替换超时逻辑为CRLF检测:循环读取直到收到
\r\n,同时用select设置整体的接收超时(比如2秒),避免无限等待。 - 正确处理非阻塞和select的错误:区分
EAGAIN、EINTR等错误,避免漏掉有效数据或错误。 - 移除FNDELAY模式:改用select来实现超时控制,比单纯的非阻塞+计时器更可靠。
修改后的完整代码
#include <stdio.h> #include <fcntl.h> #include <termios.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <errno.h> #include <time.h> #include <sys/select.h> int open_port(char* portname) { int fd; // 移除O_NDELAY,改用select控制超时 fd = open(portname, O_RDWR | O_NOCTTY); if(fd == -1) { printf("Error: open_port: Unable to open %s. \n", portname); } else { // 设置为阻塞模式,后续用select实现超时 fcntl(fd, F_SETFL, 0); } return(fd); } int configure_port(int fd, int baud_rate) { struct termios port_settings; if (tcgetattr(fd, &port_settings) != 0) { printf("Error: tcgetattr failed: %s\n", strerror(errno)); return -1; } cfsetispeed(&port_settings, baud_rate); cfsetospeed(&port_settings, baud_rate); cfmakeraw(&port_settings); port_settings.c_cflag &= ~PARENB; port_settings.c_cflag &= ~CSTOPB; port_settings.c_cflag &= ~CSIZE; port_settings.c_cflag |= CS8; // 启用接收 port_settings.c_cflag |= CREAD; // 设置VMIN和VTIME:VMIN=0表示read可返回0,VTIME=1表示100ms超时(每字节) port_settings.c_cc[VMIN] = 0; port_settings.c_cc[VTIME] = 1; if (tcsetattr(fd, TCSANOW, &port_settings) != 0) { printf("Error: tcsetattr failed: %s\n", strerror(errno)); return -1; } // 刷新输入输出缓冲区 tcflush(fd, TCIOFLUSH); return(fd); } int get_baud(int baud) { switch (baud) { case 9600: return B9600; case 19200: return B19200; case 38400: return B38400; case 57600: return B57600; case 115200: return B115200; case 230400: return B230400; case 460800: return B460800; case 500000: return B500000; case 576000: return B576000; case 921600: return B921600; case 1000000: return B1000000; case 1152000: return B1152000; case 1500000: return B1500000; case 2000000: return B2000000; case 2500000: return B2500000; case 3000000: return B3000000; case 3500000: return B3500000; case 4000000: return B4000000; default: return -1; } } unsigned char* datahex(char* string) { if(string == NULL) return NULL; size_t slength = strlen(string); if((slength % 2) != 0) return NULL; size_t dlength = slength / 2; unsigned char* data = (unsigned char*)malloc(dlength); memset(data, 0, dlength); size_t index = 0; while (index < slength) { char c = string[index]; int value = 0; if(c >= '0' && c <= '9') value = (c - '0'); else if (c >= 'A' && c <= 'F') value = (10 + (c - 'A')); else if (c >= 'a' && c <= 'f') value = (10 + (c - 'a')); else { free(data); return NULL; } data[(index/2)] += value << (((index + 1) % 2) * 4); index++; } return data; } // 从串口读取直到收到CRLF或超时 int read_until_crlf(int fd, unsigned char* buffer, size_t buffer_size, int timeout_sec) { size_t total_read = 0; fd_set read_fds; struct timeval timeout; while (total_read < buffer_size - 1) { FD_ZERO(&read_fds); FD_SET(fd, &read_fds); timeout.tv_sec = timeout_sec; timeout.tv_usec = 0; int select_ret = select(fd + 1, &read_fds, NULL, NULL, &timeout); if (select_ret == -1) { // 处理被信号中断的情况,继续等待 if (errno == EINTR) continue; printf("Error: select failed: %s\n", strerror(errno)); return -1; } else if (select_ret == 0) { // 超时 printf("Warning: Read timeout after %d seconds\n", timeout_sec); break; } if (FD_ISSET(fd, &read_fds)) { int ret = read(fd, buffer + total_read, buffer_size - total_read - 1); if (ret == -1) { if (errno == EAGAIN || errno == EINTR) continue; printf("Error: read failed: %s\n", strerror(errno)); return -1; } else if (ret == 0) { // 串口被关闭 break; } total_read += ret; buffer[total_read] = '\0'; // 检查是否收到CRLF if (total_read >= 2 && buffer[total_read - 2] == '\r' && buffer[total_read - 1] == '\n') { // 去掉CRLF(可选,根据你的需求) buffer[total_read - 2] = '\0'; total_read -= 2; break; } } } return total_read; } int main(int argc, char **argv) { if (argc < 3) { printf("Usage: %s <baud> <data> [hex] [debug] [no-reply] [/dev/ttyXXX]\n", argv[0]); return 1; } int baud_rate = get_baud(atoi(argv[1])); if(baud_rate == -1) { printf("Error: Cannot convert baud rate %s, using 9600\n", argv[1]); baud_rate = B9600; } bool convertHex = false; char portName[24] = "/dev/ttyS0"; bool debug = false; bool noreply = false; for(int i = 3; i < argc; i++) { if(!strcmp(argv[i], "hex")) convertHex = true; else if(strstr(argv[i], "/dev/") != NULL) strncpy(portName, argv[i], sizeof(portName)-1); // 留空间给结束符 else if(!strcmp(argv[i], "debug")) debug = true; else if(!strcmp(argv[i], "no-reply")) noreply = true; } unsigned char* data = nullptr; size_t len = 0; if(convertHex) { data = datahex(argv[2]); if(data == NULL) { convertHex = false; printf("Error: Couldn't convert hex value! Needs to be even length (2 chars per byte)\n"); } else { len = strlen(argv[2])/2; } } if(!convertHex) { data = (unsigned char*)argv[2]; len = strlen(argv[2]); } int fd = open_port(portName); if(fd == -1) { printf("Error: Couldn't open port %s\n", portName); if(convertHex) free(data); return 1; } if (configure_port(fd, baud_rate) == -1) { printf("Error: Couldn't configure port %s\n", portName); close(fd); if(convertHex) free(data); return 1; } if(debug) { printf("Sending data (raw): "); for(int i =0; i< len; i++) { printf("%02X", data[i]); } printf("\n"); } size_t writelen = write(fd, data, len); if(debug) printf("Sent %zu/%zu bytes\n", writelen, len); if(writelen != len) printf("Error: not all bytes were sent (%zu/%zu)\n", writelen, len); else if(noreply) { printf("WRITE OK"); close(fd); if(convertHex) free(data); return 0; } // 接收响应 unsigned char ibuff[512] = {0}; int curlen = read_until_crlf(fd, ibuff, sizeof(ibuff), 2); // 2秒超时 if(curlen > 0) { printf("RESPONSE: %s\n", ibuff); } else if(curlen == 0) { printf("No response received\n"); } close(fd); if(convertHex) free(data); return 0; }
额外注意事项
- 串口独占访问:确保运行程序时没有其他进程(比如cat)同时访问同一个串口设备,否则会出现数据瓜分的情况。可以在open时添加
O_EXCL标志,但这个标志只对某些设备有效,更可靠的方式是通过锁文件或者进程间通信保证独占。 - 硬件流控:如果工业屏支持硬件流控(RTS/CTS),可以在configure_port中添加
port_settings.c_cflag |= CRTSCTS;,进一步提升通信可靠性。 - 缓冲区刷新:在发送前调用
tcflush(fd, TCIOFLUSH)清空输入输出缓冲区,避免之前的残留数据干扰。
内容的提问来源于stack exchange,提问作者AJ Cole

