Linux下低延迟串行通信

18
我正在Linux上通过串口实现一种协议。该协议基于请求-响应模式,因此吞吐量受发送数据包并获得响应所需的时间限制。这些设备大多是基于ARM架构且运行Linux >=3.0。我在试图将往返时间降至10ms以下(115200波特率,8数据位,无校验,每个消息7字节)时遇到了麻烦。
哪种IO接口提供的延迟最低:select、poll、epoll还是使用ioctl手动轮询?阻塞或非阻塞IO会影响延迟吗?
我尝试使用setserial设置low_latency标志,但似乎没有效果。
是否有其他方法可以尝试降低延迟?由于我控制所有设备,甚至可能对内核进行修补,但最好不要。
----编辑----
串行控制器使用的是16550A。

你使用的是哪种串口接口?USB/串口接口可能会有点慢。 - user149341
你需要检查哪里花费了10毫秒的时间,因为如果这些时间是由另一个设备丢失的,那么你无法进行更多的优化。 - Ottavio Campana
请求和响应消息的大小是多少?如果两者都超过100字节,则无法在<10毫秒(使用115200)内完成往返时间。 - SKi
115200非常慢,因此在传输字节时会有很大的延迟。你最好将波特率提高到921600或更高,甚至更好的选择是切换到千兆以太网。 - TJD
@OttavioCampana 目前时间花费在等待输入上。我正在轮询,直到 ioctl 告诉我有输入可用,然后我读取它。 - JustMaximumPower
显示剩余2条评论
7个回答

11

请求/响应方案往往效率低下,并且在串行端口上很快就会显现出来。如果您对吞吐量感兴趣,请查看窗口协议,例如Kermit文件发送协议。

现在,如果您想坚持使用自己的协议并减少延迟,选择、轮询和读取都会给您带来大致相同的延迟,因为正如Andy Ross所指出的那样,真正的延迟在于硬件FIFO处理。

如果您幸运的话,可以在不打补丁的情况下调整驱动程序的行为,但仍需要查看驱动程序代码。然而,让ARM处理10 kHz的中断率肯定不利于整个系统性能...

另一个选项是填充您的数据包,使其每次达到FIFO阈值。这也将确认是否存在FIFO阈值问题。

以8N1为例,10毫秒@115200足以传输100字节,因此您现在看到的可能是因为未设置low_latency标志。请尝试:

setserial /dev/<tty_name> low_latency
它会设置"low_latency"标志,当内核在tty层向上移动数据时使用该标志:
void tty_flip_buffer_push(struct tty_struct *tty)
{
         unsigned long flags;
         spin_lock_irqsave(&tty->buf.lock, flags);
         if (tty->buf.tail != NULL)
                 tty->buf.tail->commit = tty->buf.tail->used;
         spin_unlock_irqrestore(&tty->buf.lock, flags);
 
         if (tty->low_latency)
                 flush_to_ldisc(&tty->buf.work);
         else
                 schedule_work(&tty->buf.work);
}

schedule_work调用可能是导致您观察到的10毫秒延迟的原因。


我使用请求/响应方案的原因是为了控制总线的访问,因为可能有许多设备连接到它。填充数据包对延迟没有影响。 - JustMaximumPower
@JustMaximumPower:尝试为您的平台编译setserial,并执行setserial /dev/ttySx low_latency。 - shodanex
1
阅读了 low_latency 的文档后,我认为这正是我所需要的。然而,它对我的程序没有任何影响。我有一个理论需要测试。我会回复你的。 - JustMaximumPower
1
函数 tty_flip_buffer_push 不再检查 low_latency 标志,并且将始终调用 schedule_work。我不知道这种行为是何时改变的。 - Ruud Althuizen

9
经过与更多工程师的交流,我得出结论:这个问题无法在用户空间内解决。因为我们需要跨越到内核空间,所以我们计划实现一个内核模块,该模块使用我们的协议并提供小于1毫秒的延迟。
--- 编辑 ---
事实证明我完全错了。所有必要的只是增加内核滴答率。默认的100个滴答会添加10毫秒的延迟。 1000Hz和串行进程的负优先级值可以让我达到想要的时间行为。

有没有相关的链接或文档可以参考?我正在寻找基于虚拟串口的USB通信,速度要比10毫秒更快。需要从微控制器接收数据,并在100微秒左右内立即做出响应。如果可能,我可以尝试1毫秒的速度。 - Rick2047
1
@Rick2047 这里有一些关于内核滴答率的文档 https://elinux.org/Kernel_Timer_Systems 。请记住,这个问题/答案是来自2012年的。自那时以来,很多事情已经发生了变化。我不确定这在无滴答内核中是否仍然适用。 - JustMaximumPower

7
在Linux上,串口被“包装”成Unix风格的终端结构,这会给你带来1个滴答的延迟,即10毫秒。尝试使用stty -F /dev/ttySx raw low_latency命令进行优化,但不保证有效。
在PC上,你可以直接与标准串口通信,使用setserial /dev/ttySx uart none命令将Linux驱动程序从串口硬件中解除绑定,并通过inb/outb访问端口寄存器来控制端口。我已经尝试过了,效果很好。
缺点是当数据到达时,你无法获得中断信号,必须经常轮询寄存器。
在ARM设备端,你应该能够做同样的事情,但对于异类串口硬件可能会更加困难。

4
以下是关于如何使用setserial在端口的文件描述符上设置低延迟的内容:
ioctl(fd, TIOCGSERIAL, &serial);
serial.flags |= ASYNC_LOW_LATENCY;
ioctl(fd, TIOCSSERIAL, &serial);

我正在使用代码进行操作,它会更改延迟计时器(如cat所示),但实际上并不影响延迟。我需要关闭和打开端口,或在设置标志后执行任何操作吗? - Patrick
你不应该需要做任何其他事情。 - Kuba hasn't forgotten Monica
1
似乎你需要以root身份运行代码才能使其正常工作。 - Patrick
2
@Patrick 这就是没有检查返回代码的后果 :) - Kuba hasn't forgotten Monica
2
serial_struct 位于 <linux/serial.h> 中,顺便一提。 - Alnitak

2
简而言之:使用USB适配器和ASYNC_LOW_LATENCY。
我在Modbus上使用了基于FT232RL的USB适配器,速率为115.2 kbs。
使用ASYNC_LOW_LATENCY时,我可以在大约20毫秒内完成5次交易(针对4个设备)。这包括向一个反应时间较慢的设备发送两个交易(响应时间为4毫秒)。
如果不使用ASYNC_LOW_LATENCY,则总时间约为60毫秒。
对于FTDI USB适配器,ASYNC_LOW_LATENCY将芯片本身的字符间计时器设置为1毫秒(而不是默认值16毫秒)。
我目前正在使用自制的USB适配器,我可以将适配器本身的延迟设置为任何我想要的值。将其设置为200微秒可以再节省1毫秒的时间。

1

这些系统调用都不会影响延迟。如果您想要尽可能快地从用户空间读取和写入一个字节,那么您真的不会比使用简单的read()/write()更好。尝试使用来自另一个用户空间进程的套接字替换串行流,并查看延迟是否有所改善。如果没有改善,则问题可能是CPU速度和硬件限制。

您确定您的硬件能够胜任吗?发现具有引入许多字节延迟的缓冲设计的UART并不罕见。


好的,看起来你是正确的。该板使用一个16550A芯片,它具有接收FIFO,并在接收到14个字节后触发中断。现在的问题是如何更改FIFO大小或解决这个问题? - JustMaximumPower
这只是一个PC串口吗?如果是的话,您可以在Linux驱动程序中查找(看起来在drivers/tty/serial/8250中)是否有一个ioctl提供对FIFO深度的编程控制。 - Andy Ross

0

在这些线速度下,无论您如何检查准备情况,都不应该看到那么大的延迟。

您需要确保串行端口处于原始模式(因此您要执行“非规范化读取”),并且VMIN和VTIME设置正确。您希望确保VTIME为零,以便字符间定时器永远不会启动。我可能会从将VMIN设置为1开始,然后进行调整。

系统调用开销与传输时间相比微不足道,因此select() vs. poll()等的选择不太可能产生差异。


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