我正在测试仅基于可打印字符的串行通信协议。该装置有一台pc通过USB连接到arduino板。PC机USB串口在规范模式下运行,无回声,无流量控制,9600波特。由于请求读取超时,因此在串行读取之前调用pselect。arduino板简单地回显每个接收到的字符,而不进行任何处理。PC机操作系统为Linux Neon,内核5.13.0-40 generic。
当特定长度的行从PC发送并由arduino回显时,除了最后缺少的新行外,它们都被正确接收。进一步读取,返回空行(之前丢失的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: %sn",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: %sn", 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: %sn", 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 - %sn",retval,strerror(errno));
else if (retval == 0)
printf("serial read timeoutn");
/* 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 %dn",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;
}
}
当执行基于流的通信时,无论是通过管道,串行端口还是TCP套接字,您都可以永远不能依赖于读取总是返回一个完整的"传输单元";(在本例中是一行,但也可以是固定大小的块)。原因是基于流的通信总是可以被传输堆栈的任何部分(甚至可能是发送方)分割成多个块,并且永远不能保证单个读取总是读取整个块。
例如,当你调用read()时,你可能总是会遇到竞争条件,你的微控制器仍然在发送部分消息,所以不是所有的字符都被准确地读取。在你的情况下,这不是你所看到的,因为这更像是一种随机现象(随着计算机上中断负载的增加,情况会更糟),而且不那么容易重现。相反,由于这里讨论的是数字64,因此您遇到的是内核tty驱动程序中使用的静态缓冲区大小,无论指定的读取大小实际是多少,它一次最多只能返回64字节。然而,在其他情况下,您仍然可能会看到内核在第一次read()中只返回一行的前两个字符,而在第二次read()中返回其余的字符,这取决于精确的时间细节——您可能还没有看到这种情况,但它肯定会在某个时候发生。
在流环境(串行端口,管道,TCP套接字等)中正确实现通信协议的唯一可靠方法是考虑以下内容:
- 对于固定大小的数据(例如,大小始终为N字节的通信单元),在read()调用周围循环,直到您读取了正确数量的字节(在不完整读取之后的读取显然会比原始读取请求更少的字节,只是为了弥补差异)
- 对于可变大小的数据(例如由行结束字符分隔的通信单元),您有两个选项:要么一次只读取一个字符,直到到达行结束字符(效率低下,使用大量系统调用),要么通过一个足够大的缓冲区来跟踪通信状态,不断地用read()操作填充缓冲区,直到缓冲区包含行结束字符,此时从缓冲区中删除该行(但保留其余部分)并处理它。
作为一个完整的题外话,如果您正在使用串行通信,我可以非常推荐优秀的libserialport库(LGPLv3许可证),它使串行端口的工作成为lot更简单——并且具有跨平台的优势。(对你的问题没有帮助,只是觉得我应该提一下。)
从linux内核版本5.13.0-40-generic升级到5.13.0-51-generic解决了这个问题。