首页 > 解决方案 > 可靠地以 56K 逐字节读取 linux 中的串行数据

问题描述

我正在尝试创建一个延迟最小的函数,以检查串行端口是否有数据,如果有,它会读取每个字节并以十六进制格式打印每个字节,直到没有更多字节可用。如果没有数据,函数必须立即返回。

这是我的代码:

int fd=open("/dev/ttyS0", O_RDWR | O_NOCTTY | O_SYNC);  

// Trying to set correct options here
struct termios o;
tcgetattr(fd,&o);
cfsetispeed(&o,57600);
cfsetospeed(&o,57600);
/* 8 bits, no parity, 1 stop bit */
o.c_cflag &= ~PARENB;o.c_cflag &= ~CSTOPB;o.c_cflag &= ~CSIZE;o.c_cflag |= CS8;
/* no hardware flow control */
o.c_cflag &= ~CRTSCTS;
/* enable receiver, ignore status lines */
o.c_cflag |= CREAD | CLOCAL;
/* disable input/output flow control, disable restart chars */
o.c_iflag &= ~(IXON | IXOFF | IXANY);
/* disable canonical input, disable echo, disable visually erase chars, disable terminal-generated signals */
o.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
/* disable output processing */
o.c_oflag &= ~OPOST;
o.c_cc[VMIN] = 0; //to prevent delay in read();
o.c_cc[VTIME] = 0;
tcsetattr(fd, TCSANOW, &o);
tcflush(fd, TCIFLUSH);

char sp[1]; //hold 1 byte
int bytes=read(fd,sp,1); //Good news: this function doesn't lock
if (bytes > 0){
    //this is never reached even if a byte is 
    //present on the serial line. why?
    printf("Read: ");
    while(bytes > 0){
        printf("%X ",sp[0]);
        bytes=read(fd,sp,1);
    }
}
fclose(fd);

有没有办法来解决这个问题?

这整个功能(减去串行端口选项部分)最终将在无限循环中运行,因为我不断地扫描我的端口以获取数据然后打印它。然后稍后我将添加更多功能,无论接收到什么数据,我都会在预定义的时间将数据写入端口。

PS不确定这是否有帮助,但我正在使用的目标设备是8051微控制器周围的定制硬件,它的串行fifo缓冲区只有1个字节,而我认为PC的缓冲区是14或16个字节。

标签: clinuxserial-porttimeoutbuffer

解决方案


如果您事先知道需要写入设备的时间,您可以使用select()poll()等待输入,直到您下次希望/打算写入。

一种更简单、更健壮的方法——因为你的读取和写入不是按定义的顺序,而且你的硬件是全双工的——是使用单独的线程进行读取和写入。基本上,您使用阻塞读取和写入(c_cc[VMIN] = 1用于c_cc[VTIME] = 0读取,O_NONBLOCK 而不是文件打开标志)。不过,您应该允许更大的缓冲区;reads 将返回到目前为止已收到的所有内容,但只要至少收到一个字符,这些设置就会唤醒阅读器。对于写入,我建议您tcdrain(fd);在每次写入完成对设备的命令/消息后执行一次,以确保它是由内核在线发送的。(请记住,对串行端口的写入可能很短;您需要一个写入循环。)

在所有情况下,主机端的内核都会缓存发送和接收的数据。根据硬件和驱动程序的不同,即使是阻塞write()也可能比所有数据实际在线时更早返回。负责串行数据的正确时序的是硬件和内核驱动程序,而不是主机软件。

在主机端使用一字节缓冲区根本不会影响微控制器,您只会执行不必​​要的系统调用,浪费 CPU 资源并可能会稍微减慢您的程序。在 57600 波特、8 个数据位、无奇偶校验、1 个停止位和隐式起始位时,实际数据速率为 46080 位/秒(通常允许 ±5%)或 5760 字节/秒。微控制器将始终有大约 1 s /5760 ≃ 0.0001736 秒或超过 173 微秒来处理每个传入字节。(我将我的固件设计为不允许更高优先级的中断等延迟处理超过 100 微秒左右,即使在最坏的情况下,以确保没有字符被丢弃。如果您在中断处理程序中收到字符,我d 使用一个小的循环缓冲区和一个指示符,\r或者\n,因此如果接收到这样的字符,则会为主程序引发一个标志,以通知已接收到一个新的完整命令。循环缓冲区应该足够长以容纳两个完整的命令,或者如果某些命令可能需要更长的时间来解析/处理/处理,则可以更长。)

如果主机操作系统在接收到第一个字符后 1 毫秒唤醒进程,则另外四个或五个字符同时到达。因为这种延迟在某些系统上可能更高,所以我会使用更大的缓冲区,比如最多 256 个字符,以避免在内核由于某种原因延迟唤醒读取器线程时执行多余的系统调用。是的,它通常只会读取 1 个字符,这很好;但是当系统超载时,您不想通过执行数百个多余的系统调用来增加负载,而一个就足够了。

请记住,带有 的termios接口VMIN=1, VTIME=0将导致阻塞读取尽快被唤醒,即使接收到单个字符。只是你不能确保你的程序持续运行,除非你通过原地旋转浪费了大约 100% 的 CPU 功率(如果你这样做了,没有人会想要运行你的程序)。根据系统的不同,唤醒阻塞读取可能会有延迟,在此期间可能会接收到更多数据,因此使用更大的read()缓冲区绝对是明智的。

类似地,您可以安全地使用尽可能多的写入(最大限制为 2 GiB),尽管大多数串行驱动程序可以返回短计数,因此无论如何您都需要一个循环。串行端口描述符上的tcdrain(fd)将阻塞,直到所有写入的数据都已实际传输,因此您可能想要使用它。(如果你不这样做,你可以只写更多的数据;内核驱动程序会处理细节,而不是重新排序/弄乱数据。)

使用两个线程,一个用于读取,一个用于写入,听起来可能令人生畏/奇怪,但它实际上是实现健壮通信的最简单方法。您甚至可以使用并且可选地编写线程函数pthread_setcancelstate(),以便您可以简单地取消线程(使用)来停止它们,即使它们具有关键部分(例如将接收到的消息添加到受互斥体保护的某个队列中)。pthread_setcanceltype()pthread_testcancel()pthread_cancel()


推荐阅读