Linux USB串行线输入可能会丢失尾随的新线路

发布于 2025-02-10 15:00:56 字数 4936 浏览 1 评论 0原文

我仅根据可打印字符来测试串行通信协议。 该设置的PC由USB连接到Arduino板。 PC USB串行以规范模式运行,没有回声,没有流量控制,9600波特。 由于请求读取超时,因此在串行读取之前调用Pselect。 Arduino董事会只是回荡了每个接收的角色而无需进行任何处理。 PC OS是Linux霓虹灯,内核5.13.0-40代。

当特定长度的线从PC传输并由Arduino回声时,除了丢失的最终新线路外,它们会正确接收。 进一步读取,返回一个空行(以前缺少的NL)。 具有不同长度的线条可传输并正确接收,包括尾随的NL。

这种行为是完全可重复且稳定的。以下代码重现了一条以65个字符(包括NL)的线路传输的线路,并以64个长度(NL缺失)接收。其他线长度正常。

感谢您的提示。

/* remote serial loop test 20220626 */

#include <errno.h>
#include <fcntl.h> 
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <sys/select.h>
#include <string.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>


#define TX_MINLEN 63
#define TX_MAXLEN 66
#define DATA_MAXLEN 128

#define LINK_DEVICE "/dev/ttyUSB0"
#define LINK_SPEED B9600
#define RECEIVE_TIMEOUT 2000



int main()
{
    int wlen;
    int retval;
    int msglen;

    uint8_t tx_data[257] = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
    for (int i=16; i < 256; i++) tx_data[i] = tx_data[i & 0xf];
    uint8_t rx_data[257];

  /* serial interface */
  char * device;
  int speed;
  int fd;
  fd_set fdset;
  struct timespec receive_timeout;
  struct timespec *p_receive_timeout = &receive_timeout;
  struct termios tty;

  /* open serial device in blocking mode */
  fd = open(LINK_DEVICE, O_RDWR | O_NOCTTY);
  if (fd < 0) {
    printf("Error opening %s: %s\n",LINK_DEVICE,strerror(errno));
    return -1;
  }

  /* prepare serial read by select to have read timeout */
  FD_ZERO(&(fdset));
  FD_SET(fd,&(fdset));

  if (RECEIVE_TIMEOUT >= 0) {
    p_receive_timeout->tv_sec = RECEIVE_TIMEOUT / 1000;
    p_receive_timeout->tv_nsec = RECEIVE_TIMEOUT % 1000 * 1000000;
  }
  else
    p_receive_timeout = NULL;

  /* get termios structure */
  if (tcgetattr(fd, &tty) < 0) {
      printf("Error from tcgetattr: %s\n", strerror(errno));
      return -1;
  }

  /* set tx and rx baudrate */
  cfsetospeed(&tty, (speed_t)LINK_SPEED);
  cfsetispeed(&tty, (speed_t)LINK_SPEED);

  /* set no modem ctrl, 8 bit, no parity, 1 stop */
  tty.c_cflag |= (CLOCAL | CREAD);    /* ignore modem controls */
  tty.c_cflag &= ~CSIZE;
  tty.c_cflag |= CS8;         /* 8-bit characters */
  tty.c_cflag &= ~PARENB;     /* no parity bit */
  tty.c_cflag &= ~CSTOPB;     /* only need 1 stop bit */
  tty.c_cflag &= ~CRTSCTS;    /* no hardware flowcontrol */

  /* canonical mode: one line at a time (\n is line terminator) */
  tty.c_lflag |= ICANON | ISIG;
  tty.c_lflag &= ~(ECHO | ECHOE | ECHONL | IEXTEN);

  /* input control */
  tty.c_iflag &= ~IGNCR;  /* preserve carriage return */
  tty.c_iflag &= ~INPCK;  /* no parity checking */
  tty.c_iflag &= ~INLCR;  /* no NL to CR traslation */
  tty.c_iflag &= ~ICRNL;  /* no CR to NL traslation */
  tty.c_iflag &= ~IUCLC;  /* no upper to lower case mapping */
  tty.c_iflag &= ~IMAXBEL;/* no ring bell at rx buffer full */
  tty.c_iflag &= ~(IXON | IXOFF | IXANY);/* no SW flowcontrol */

  /* no output remapping, no char dependent delays */
  tty.c_oflag = 0;

  /* no additional EOL chars, confirm EOF to be 0x04 */
  tty.c_cc[VEOL] = 0x00;
  tty.c_cc[VEOL2] = 0x00;
  tty.c_cc[VEOF] = 0x04;

  /* set changed attributes really */
  if (tcsetattr(fd, TCSANOW, &tty) != 0) {
      printf("Error from tcsetattr: %s\n", strerror(errno));
      return -1;
  }

  /* wait for serial link hardware to settle, required by arduino reset
   * triggered by serial control lines */
  sleep(2);

  /* empty serial buffers, both tx and rx */
  tcflush(fd,TCIOFLUSH);


  /* repeat transmit and receive, each time reducing data length by 1 char */
  for (int l=TX_MAXLEN; l > TX_MINLEN - 1; l--) {

    /* prepare data: set EOL and null terminator for current length */
    tx_data[l] = '\n';
    tx_data[l+1] = 0;

    /* send data */
    int sent = write(fd,tx_data,l+1);

    /* receive data */

    /* wait for received data or for timeout */
    retval = pselect(fd+1,&(fdset),NULL,NULL,p_receive_timeout,NULL);

    /* check for error or timeout */
    if (retval < 0)
      printf("pselect error: %d - %s\n",retval,strerror(errno));
    else if (retval == 0)
      printf("serial read timeout\n");

    /* there is enough data for a non block read: do read */
    msglen = read(fd,&rx_data,DATA_MAXLEN);

    /* check rx data length */  
    if (msglen != l+1)
      printf("******** RX ERROR: sent %d, received %d\n",l+1,msglen);
    else
      continue;

    /* check received data, including new line if present */
    for (int i=0; i < msglen; i++) {
      if (tx_data[i] == rx_data[i])
        continue;
      else {
        printf("different rx data:|%s|\n",rx_data);
        break;
      }
    }

    /* clear RX buffer */
    for (int i=0; i < msglen + 1; i++) rx_data[i] = 0;

  }
}


I am testing a serial communication protocol based on printable characters only.
The setup has a pc connected to an arduino board by USB. The PC USB serial is operated in canonical mode, with no echo, no flow control, 9600 baud.
Since a read timeout is requested, pselect is called before the serial read. The arduino board simply echoes back every received character without any processing. The PC OS is Linux Neon with kernel 5.13.0-40-generic.

When lines of a specific length are transmitted from the PC and echoed back by the arduino, they are received correctly except for the final new line that is missing.
A further read, returns an empty line (the previously missing NL).
Lines with different length are transmitted and received correctly, including the trailing NL.

This behavior is fully repeatable and stable. The following code reproduce the problem for a line transmitted with a length of 65 characters (including NL) and received with a length of 64 (NL missing). Other line lengths work fine.

Thanks for any hints.

/* remote serial loop test 20220626 */

#include <errno.h>
#include <fcntl.h> 
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <sys/select.h>
#include <string.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>


#define TX_MINLEN 63
#define TX_MAXLEN 66
#define DATA_MAXLEN 128

#define LINK_DEVICE "/dev/ttyUSB0"
#define LINK_SPEED B9600
#define RECEIVE_TIMEOUT 2000



int main()
{
    int wlen;
    int retval;
    int msglen;

    uint8_t tx_data[257] = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
    for (int i=16; i < 256; i++) tx_data[i] = tx_data[i & 0xf];
    uint8_t rx_data[257];

  /* serial interface */
  char * device;
  int speed;
  int fd;
  fd_set fdset;
  struct timespec receive_timeout;
  struct timespec *p_receive_timeout = &receive_timeout;
  struct termios tty;

  /* open serial device in blocking mode */
  fd = open(LINK_DEVICE, O_RDWR | O_NOCTTY);
  if (fd < 0) {
    printf("Error opening %s: %s\n",LINK_DEVICE,strerror(errno));
    return -1;
  }

  /* prepare serial read by select to have read timeout */
  FD_ZERO(&(fdset));
  FD_SET(fd,&(fdset));

  if (RECEIVE_TIMEOUT >= 0) {
    p_receive_timeout->tv_sec = RECEIVE_TIMEOUT / 1000;
    p_receive_timeout->tv_nsec = RECEIVE_TIMEOUT % 1000 * 1000000;
  }
  else
    p_receive_timeout = NULL;

  /* get termios structure */
  if (tcgetattr(fd, &tty) < 0) {
      printf("Error from tcgetattr: %s\n", strerror(errno));
      return -1;
  }

  /* set tx and rx baudrate */
  cfsetospeed(&tty, (speed_t)LINK_SPEED);
  cfsetispeed(&tty, (speed_t)LINK_SPEED);

  /* set no modem ctrl, 8 bit, no parity, 1 stop */
  tty.c_cflag |= (CLOCAL | CREAD);    /* ignore modem controls */
  tty.c_cflag &= ~CSIZE;
  tty.c_cflag |= CS8;         /* 8-bit characters */
  tty.c_cflag &= ~PARENB;     /* no parity bit */
  tty.c_cflag &= ~CSTOPB;     /* only need 1 stop bit */
  tty.c_cflag &= ~CRTSCTS;    /* no hardware flowcontrol */

  /* canonical mode: one line at a time (\n is line terminator) */
  tty.c_lflag |= ICANON | ISIG;
  tty.c_lflag &= ~(ECHO | ECHOE | ECHONL | IEXTEN);

  /* input control */
  tty.c_iflag &= ~IGNCR;  /* preserve carriage return */
  tty.c_iflag &= ~INPCK;  /* no parity checking */
  tty.c_iflag &= ~INLCR;  /* no NL to CR traslation */
  tty.c_iflag &= ~ICRNL;  /* no CR to NL traslation */
  tty.c_iflag &= ~IUCLC;  /* no upper to lower case mapping */
  tty.c_iflag &= ~IMAXBEL;/* no ring bell at rx buffer full */
  tty.c_iflag &= ~(IXON | IXOFF | IXANY);/* no SW flowcontrol */

  /* no output remapping, no char dependent delays */
  tty.c_oflag = 0;

  /* no additional EOL chars, confirm EOF to be 0x04 */
  tty.c_cc[VEOL] = 0x00;
  tty.c_cc[VEOL2] = 0x00;
  tty.c_cc[VEOF] = 0x04;

  /* set changed attributes really */
  if (tcsetattr(fd, TCSANOW, &tty) != 0) {
      printf("Error from tcsetattr: %s\n", strerror(errno));
      return -1;
  }

  /* wait for serial link hardware to settle, required by arduino reset
   * triggered by serial control lines */
  sleep(2);

  /* empty serial buffers, both tx and rx */
  tcflush(fd,TCIOFLUSH);


  /* repeat transmit and receive, each time reducing data length by 1 char */
  for (int l=TX_MAXLEN; l > TX_MINLEN - 1; l--) {

    /* prepare data: set EOL and null terminator for current length */
    tx_data[l] = '\n';
    tx_data[l+1] = 0;

    /* send data */
    int sent = write(fd,tx_data,l+1);

    /* receive data */

    /* wait for received data or for timeout */
    retval = pselect(fd+1,&(fdset),NULL,NULL,p_receive_timeout,NULL);

    /* check for error or timeout */
    if (retval < 0)
      printf("pselect error: %d - %s\n",retval,strerror(errno));
    else if (retval == 0)
      printf("serial read timeout\n");

    /* there is enough data for a non block read: do read */
    msglen = read(fd,&rx_data,DATA_MAXLEN);

    /* check rx data length */  
    if (msglen != l+1)
      printf("******** RX ERROR: sent %d, received %d\n",l+1,msglen);
    else
      continue;

    /* check received data, including new line if present */
    for (int i=0; i < msglen; i++) {
      if (tx_data[i] == rx_data[i])
        continue;
      else {
        printf("different rx data:|%s|\n",rx_data);
        break;
      }
    }

    /* clear RX buffer */
    for (int i=0; i < msglen + 1; i++) rx_data[i] = 0;

  }
}


如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

肥爪爪 2025-02-17 15:00:56

进行基于流的通信时,无论是通过管道,串行端口还是TCP插座,您都可以从不也可能是固定尺寸的块)。原因是,基于流的通信始终可以将传输堆栈(甚至可能是发件人)的任何部分分为多个块,并且永远没有保证单个读取将始终读取完整块。

例如,您始终可以遇到MicroController在调用read()时仍在发送消息的竞赛条件,因此并非所有字符都经过准确的读取。就您而言,这不是您所看到的,因为那将是一种随机现象(随着计算机上的中断负载的增加会更糟),并且不容易再现。取而代之的是,由于您在这里谈论数字64,所以您遇到了静态缓冲区大小在内核的tty驱动程序中使用,它最多只能一次返回64个字节,而不管指定的读取大小实际是什么。但是,在其他情况下,仍然可能会看到内核仅返回第一个读取()中的第几个字符,而第二个字符,其余的,具体取决于精确的定时细节 - 您可能还没有看到这一点,但是它一定会在某个时候发生。

在流中正确实施通信协议的唯一可靠方法(串行端口,管道,TCP插座等)是考虑以下内容:

  • 对于固定尺寸的数据(例如,始终是n size in size in size的通信单元)以循环循环读取()调用直到您完全阅读适量的字节(遵循不完整读取的读取显然会要求比原始读取的字节更少,只是为了弥补差异),
  • 以弥补可变大小的数据(例如,通信由线端端字符分开的单元)有两个选择:要么您一次仅读取一个字符,直到您达到线结束字符(效率低下,使用大量Syscalls),或者您继续跟踪通信通过足够大的缓冲区来陈述,您可以不断地填充read()操作,直到缓冲区包含线端字符,此时您可以从缓冲区中删除该线路(但要保留其余)并处理该线路。

顺便说一句,如果您在串行通信中做任何事情,我可以非常推荐优秀的 libserialport 库(LGPLV3许可证),使使用串行端口的工作更容易 - 并且具有跨平台的好处。 (没有帮助您的问题,只是想我要提到的。)

When performing stream-based communication, be it via pipes, serial ports, or TCP sockets, you can never rely on reads to always return a full "unit of transmission" (in this case a line, but could also be a fixed-size block). The reason is that stream-based communication can always be split by any part of the transmission stack (even potentially the sender) into multiple blocks, and there is never a guarantee that a single read will always read a full block.

For example, you could always run into the race condition that your microcontroller is still sending parts of the message when you call read(), so not all characters are read exactly. In your case, that's not what you're seeing, because that would be more of a stochastic phenomenon (that would be worse with an increased interrupt load on the computer) and not so easily reproducible. Instead, because you're talking about the number 64 here, you're running into the static buffer size used in the kernel's tty driver that will only ever return at most 64 bytes at once, regardless of what the specified read size actually is. However, in other cases it could still be that you'll see additional failures by the kernel returning only the first couple of characters of a line in the first read(), and the rest in the second, depending on precise timing details -- you've probably not seen that yet, but it's bound to happen at some point.

The only reliable way to properly implement communication protocols in streaming situations (serial port, pipes, TCP sockets, etc.) is to consider the following:

  • For fixed-size data (e.g. communication units that are always N bytes in size) to loop around a read() call until you've read exactly the right amount of bytes (reads that follow an incomplete read would obviously ask for less bytes than the original read, just to make up the difference)
  • For variable-size data (for example communication units that are separated by a line end character) you have two options: either you read only one character at a time until you reach the end-of-line character (inefficient, uses lots of syscalls), or you keep track of the communication state via a large enough buffer that you constantly fill with read() operations until the buffer contains a line-end character, at which point you remove that line from the buffer (but keep the rest) and process that.

As a complete aside, if you're doing anything with serial communication, I can very much recommend the excellent libserialport library (LGPLv3 license) that makes working with serial ports a lot easier -- and has the benefit of being cross-platform. (Doesn't help with your issue, just thought that I'd mention it.)

谎言 2025-02-17 15:00:56

从Linux内核版本升级5.13.0-40代到5.13.0-51总计解决了问题。

Upgrading from linux kernel version 5.13.0-40-generic to 5.13.0-51-generic solved the problem.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文