You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ARMv7架构UART串口非阻塞收发丢字节问题求助

问题分析与解决方案:ARMv7串口通信丢字节与阻塞问题

问题背景

你开发了一款针对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实现超时,避免阻塞挂起。以下是修改后的核心思路和代码:

关键修改点

  1. 配置串口时设置合理的VMIN和VTIME:在configure_port函数中,设置c_cc[VMIN] = 0(表示read可以返回0字节),c_cc[VTIME] = 1(100ms的超时,用于单字节读取的超时),这样既不会无限阻塞,又能保证数据读取的完整性。
  2. 替换超时逻辑为CRLF检测:循环读取直到收到\r\n,同时用select设置整体的接收超时(比如2秒),避免无限等待。
  3. 正确处理非阻塞和select的错误:区分EAGAIN、EINTR等错误,避免漏掉有效数据或错误。
  4. 移除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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 09:08:17