如何处理串行数据缓冲问题

3
我正在努力寻找一种优雅的解决方案来读取串行数据,以及在 read() 完成但包含不完整消息时该怎么做。
设备之间的预期消息具有定义的起始和结束字节,因此很容易看到消息的起始和结束。
我可以正常打开串行端口并从中读取数据。但是我遇到了电脑读取速度比数据传输速度更快的情况,导致我收到了不完整的消息。
针对本例,假设期望的消息是:
0x10 0xFF 0xFF 0xFF 0xFF 0x11

0x10是起始,0x11是结束,0xFF是数据字节。

我是C语言新手,可能会忽略一些显而易见的东西, 我的当前解决方案

int main() {
     /* Ommited serial port opening and checking*/
     char read_buffer[80];
     char message_buffer[80];
     int message_buffer_index = 0;
     int start_index = -1;
     int end_index = -1;
     int read_bytes;
     read_bytes = read(serial_port, read_buffer, sizeof(read_buffer) - 1);
     /* Now lets say read_bytes returns 3 and read buffer is {0x10, 0xFF, 0xFF} */
     /* What should I do with the read_buffer? Currently appending to message buffer*/
     memcpy(&message_buffer[message_buffer_index], &read_buffer[0], read_bytes);
     /* Now check the message buffer for a full message */
     for (int i = 0; i < 80; i++) {
          if (message_buffer[i] = 0x10) {
               start_index = i;
               continue;
          }
          if (message_buffer[i] = 0x11) {
               end_index = i;
          }
          if (start_index != -1 && end_index != -1) {
               /* Found a message, do something with it, not super important here */
               process_message();
               /* Now how to erase the full message from the 
               buffer and push any non processed data to the 
               front? */
               remove_message();
          }
    }
}

int process_message();  
int remove_message();

发布的代码缺少几个关键项。1)需要包含所需头文件的#include 2)子函数的原型。请发布一个[mcve]。 - user3629249
关于:read_bytes = read(serial_port, read_buffer, sizeof(read_buffer),建议(虽然效率较低)在循环中一次读取一个字节,同时检查“开始”和“结束”字节。只有在读取到“开始”字节后才开始累积数据。当读取到“结束”字节时停止累积数据。 - user3629249
当子函数无法访问数据缓冲区时,它们如何执行任何操作?建议将指向数据缓冲区的指针传递给这些子函数。建议将“80”作为#define值,以便子函数知道要处理的最大数据量。建议将消息的实际长度传递给子函数。 - user3629249
哈!你说“电脑读取速度比数据传输速度快”,但如果数据传输速度比你阅读速度快得多,那将会更糟糕!你处于一个令人羡慕的位置:不需要流量控制。 - Weather Vane
3个回答

5
为了减少使用许多小字节数的read()系统调用(例如每次只读取一个字节的错误解决方案)的开销,您可以在代码中使用中间缓冲区。
串行终端的read()应该处于阻塞模式,以避免返回零字节的返回码。
#define BLEN    1024
unsigned char rbuf[BLEN];
unsigned char *rp = &rbuf[BLEN];
int bufcnt = 0;

/* get a byte from intermediate buffer of serial terminal */
static unsigned char getbyte(void)
{
    if ((rp - rbuf) >= bufcnt) {
        /* buffer needs refill */
        bufcnt = read(fd, rbuf, BLEN);
        if (bufcnt <= 0) {
            /* report error, then abort */
        }
        rp = rbuf;
    }
    return *rp++;
}

如需正确初始化串行终端的termios代码,请参见此答案。您应该将VMIN参数增加到接近BLEN值或至少最长预期消息的长度,并将VTIME设置为1。

现在,您可以方便地以逐字节的方式访问接收到的数据,而几乎不会影响性能。

#define MLEN    1024  /* choose appropriate value for message protocol */
int main() 
{
    unsigned char mesg[MLEN];
    ...

    while (1) {
        while (getbyte() != 0x10)
            /* discard data until start found */ ;
    
        length = 0;
        while ((mesg[length] = getbyte()) != 0x11) {
            /* accumulate data until end found */ 
            length++;
        }

        /* process the message */
        ...

    
    }  /* loop for next message */
...
}

请注意,您对消息帧的检测不够健壮。
如果数据是二进制的,并且可以使用与这些起始和结束字节相同的值,则接收数据的解析容易出现消息帧错位的情况。
请参见此答案,了解正确算法的描述。

2
你需要循环缓冲区。将数据放入缓冲区,当有足够的数据或在任何方便的时刻时,进程会取走它们。
维基百科有一篇关于此的优秀文章 https://en.wikipedia.org/wiki/Circular_buffer

enter image description here


这个答案需要处理更多的预订(因此对于初学者编程人员来说非常容易出错)。 - user3629249
1
“但这是唯一正确的方法” - 当您没有操作系统时。否则,操作系统将接收并缓冲用户进程的数据。串行终端的read()系统调用仅仅是从系统缓冲区复制字节,而不是执行任何I/O操作。 - sawdust

0
最好使用stdio来进行读取,例如以下代码:
FILE *fp = fdopen(serial_port, "r");

while (blabla) {
  while (fgetc(fp) != 0x10)
    ;  // wait until start
  while ((c = fgetc(fp)) != 0x11)
    message_buffer[message_buffer_index++] = c;
  // here you have a complete message
}

如有必要,请插入EOF和错误检查


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接