串口 FIFO 缓冲深度:那 16 字节队列如何决定串口会不会丢字节

  2026-11-11   hermes     3   0  

从一次偶发丢字节说起

一台工控机挂着 8 口串口卡,波特率开到 921600,线上数据连续不断。示波器看波形干净,起始位、停止位都规整,但应用层隔一段时间就收到一帧校验失败的数据;把驱动里的错误计数读出来,溢出(overrun)计数在缓慢增长。换线、换端口、换终端设备都不解决,最后发现瓶颈不在线缆,而在 UART 内部那个只有十几个字节的硬件队列——FIFO。

FIFO 是 First In First Out 的缩写,即先进先出的队列。它在串口通信里扮演缓冲角色:数据到达的速度由波特率决定,取走数据的速度由 CPU 和驱动决定,两者不可能时刻同步,中间必须有一段缓冲来吸收差值。这段缓冲有多深、在什么水位通知 CPU,直接决定了高速通信时会不会丢字节,也决定了中断有多密。

FIFO 到底是什么:UART 内部的两级缓冲

一颗典型 UART 的接收通路可以拆成两级。第一级是接收移位寄存器,它按位采样,把线上的起始位、数据位、校验位、停止位拼成一个完整字节;第二级就是接收 FIFO,拼好的字节按到达顺序排进队列,等 CPU 或 DMA 来取。发送方向对称:CPU 把待发字节写进发送 FIFO,UART 再逐个搬进发送移位寄存器,按位推上线路。

这两级的分工解释了 FIFO 的价值。以经典的 16550A 为例,它带有 16 字节的接收 FIFO 和 16 字节的发送 FIFO,接收中断的触发水位可以通过 FIFO 控制寄存器设置为 1、4、8 或 14 字节。也就是说,CPU 不必每来一个字节就被打断一次,可以等到队列里攒到设定水位再一次性取走最多 16 个字节。更早的 8250 和 16450 只有 1 字节接收缓冲,每收到一个字节就产生一次中断;速率一高、中断服务稍慢一步,新到的字符就会因为缓冲已满而无法转入、被覆盖丢失,而已经留在接收缓冲里的那个旧字节仍然保留。

FIFO 模式不是无条件生效的。16550 系列需要在写 FIFO 控制寄存器时置起最低有效位(FCR0)打开 FIFO,并同时设置触发水位——写其他 FCR 位时 FCR0 必须为 1,否则这些位不会生效。早期 16550 曾因 FIFO 逻辑缺陷无法正常使用,修正后的版本才被称作 16550A。这个历史细节在今天仍有现实意义:原始 16550 有缺陷时只能按 1 字节缓冲方式工作,而 Linux 串口驱动对报告为普通 16650 的器件也只按单字节缓冲对待,不会冒险启用 FIFO。

溢出是怎么发生的:丢的是最新那个字节

很多人以为 FIFO 满了会把最老的字节挤掉,实际机制恰好相反。按 16550 数据手册的描述,在 FIFO 模式下,数据会一直填到触发水位以上;只有当 FIFO 已经全满、并且下一个字符已经完整收进移位寄存器时,才会置起溢出错误(OE)。被覆盖的是移位寄存器里这个刚收完的字符,它不会被转入 FIFO;FIFO 里已经排队的旧字节仍然保留。

这个顺序带来两个可观察的特征。第一,丢失发生在"最新到达"的一端,所以日志里往往是数据流中间少了一个字节,而不是整段末尾缺失。第二,溢出标志是"事后"出现的:等你读到 OE 时,那个字节已经没了,软件能做的只是记录并请求重传,无法补救。

还有一条容易被忽略的路径:接收 FIFO 的超时中断。当 FIFO 里至少有一个字符,最近一次收到字符已超过 4 个连续字符时间,并且最近一次 CPU 读取 FIFO 也已超过 4 个连续字符时间时,UART 会产生一次超时中断,把不足触发水位的零散字节交给软件。没有这条机制,短报文会一直卡在 FIFO 里等不到"攒够 14 字节"的时机。数据手册给出的一个边界是:300 波特、12 位字符(1 起始位、8 数据位、1 校验位、2 停止位)时,从收到字符到超时中断最长约 160 毫秒。

为什么重要:两个不同的时间,别算混

串口是异步链路,每个字符占用的时间由波特率和帧格式共同决定。以最常见的 8N1 为例,一个字符共 10 位(1 位起始、8 位数据、1 位停止),字符时间等于 10 除以波特率。把这个时间和 FIFO 深度放在一起,会得到两个容易混淆的量:攒够触发水位需要多久,以及中断触发后还剩多少余量。

波特率(8N1)单字符时间从空队列攒到触发水位 14触发后到溢出的余量(16 字节 FIFO)触发 14 时的中断频率(单口)
9600约 1041.7 微秒约 14.6 毫秒约 3.13 毫秒约 69 次/秒
115200约 86.8 微秒约 1.22 毫秒约 260 微秒约 823 次/秒
460800约 21.7 微秒约 304 微秒约 65 微秒约 3291 次/秒
921600约 10.85 微秒约 152 微秒约 33 微秒约 6583 次/秒

第一列时间解释的是"中断为什么变少":FIFO 为空时开始收数,要收到第 14 个字节才触发中断,115200 下这就是 1.22 毫秒,期间 CPU 可以干别的。但真正决定会不会溢出的是倒数第二列——触发中断之后,FIFO 只剩 2 个空位,再加移位寄存器里正在收的 1 个字符,约 3 个字符时间之后就会溢出。115200 下是 260 微秒,921600 下只有 33 微秒。中断服务只要被更高优先级中断、临界区或虚拟化退出拖过这个窗口,OE 就会置起。

这也纠正了一个常见误解:"触发水位设得高,就更安全"。触发水位高确实减少中断次数,但把每次中断的响应截止时间压到了 (深度 − 水位 + 1) 个字符;触发水位低则相反,余量变大、中断变密。若把 16550A 的触发水位设成 1,16 字节 FIFO 能提供 16 个字符的余量,921600 下约 174 微秒、115200 下约 1.39 毫秒,代价是中断率变成触发 14 时的约 14 倍(每 1 字节一次中断对每 14 字节一次中断):115200 下单口 11520 次每秒,8 口约 9.2 万次每秒。

需要更多余量又不想堆中断,只有两条路:更深的 FIFO,或者用 DMA 批量搬走。可以按延迟预算反推深度,公式是 所需深度 ≥ 触发水位 − 1 + 最坏中断延迟 ÷ 字符时间。举例来说,若系统最坏中断延迟是 2 毫秒,115200 波特、触发水位 14 时至少需要约 37 字节;若把触发水位降到 1,则至少要 24 字节。若要求 921600 下容忍 500 微秒,触发 14 时至少约需 60 字节,触发 1 时至少要 47 字节。经典 16 字节 FIFO 在这些条件下都没有余量,这正是许多多串口卡和 SoC 串口控制器把 FIFO 做到 32、64 甚至 128 字节的原因。

FIFO 深度如何影响选型与性能

第一,先看端口数与波特率的乘积。多口卡的中断压力是各端口之和。8 口全开 921600、触发 14,中断率约 5.3 万次每秒;同样的卡跑 115200 只有约 6600 次每秒。选型时如果把"最高波特率"和"最大端口数"同时拉满,就必须把 FIFO 深度和中断合并能力一起算进去,不能只看单口指标。

第二,区分接收与发送。接收 FIFO 太浅会丢数据,这是硬损失;发送 FIFO 太浅通常只造成发送节奏变碎、吞吐下降,因为发送缓冲空时 UART 停发等待软件补数据,不会覆盖未发出的字节。评估"会不会丢数据"时,重点应放在接收侧。

第三,不要指望深 FIFO 替代流控。对经典 16550 这类不带自动流控的 UART,FIFO 本身不理解流控语义,停止请求由软件或协议层处理;请求生效前已经排在 FIFO 里、甚至已经进入移位寄存器的字节仍会发出去,FIFO 越深,这段"停不下来"的数据越多。部分现代 UART 集成了自动 RTS/CTS,能按接收 FIFO 水位直接控制 RTS 引脚,但停止请求同样只能阻止后续字符,无法撤回已经上线的数据。这一点在 RS-485 半双工总线上尤其关键:方向切换必须等到发送 FIFO 和移位寄存器都空(看发送完成状态,而不是"发送寄存器空"),否则最后几个字节会在总线方向翻转时丢掉或与别的站冲突。

第四,更高的触发水位会推迟软件看到数据的时刻。FIFO 不改变线速,但触发水位 14 加超时机制意味着:不足 14 字节的报文,要等最后一个字符之后约 4 个字符时间(且距上次 CPU 读取 FIFO 也超过 4 个字符时间)才会被上交,所以首个字符的软件可见延迟可能超过 4 个字符时间;长报文则要等攒到 14 字节。对 Modbus RTU 这类靠字符间隔判断帧边界的协议,主机侧如果在 FIFO 之上做帧间隔检测,就必须意识到这批字节是"批量到达"的,不能用到达时刻直接还原线上的字符间隔。

第五,FIFO 与 DMA 要配套。16550 系列可以把 RXRDY、TXRDY 两个引脚切到另一种模式,用它们向 DMA 控制器发请求,让数据按块搬进内存。此时 FIFO 深度与 DMA 的传输粒度要匹配:如果 DMA 每次只搬一个字节,FIFO 的批量优势就被抵消;如果 DMA 一次搬整段,触发水位应设得足够高,避免频繁的短传输。没有 DMA 的场合,深 FIFO 加中断合并也能达到类似效果,只是 CPU 仍要参与每次搬运。

第六,USB 转串口要另算。USB 转串口方案的 FIFO 在桥接芯片内部,主机取数还要经过 USB 的轮询与批量传输节奏,不能直接套用板载 UART 的"16 字节、微秒级窗口"模型。判断这类设备是否够用,要看桥接芯片的缓冲深度、驱动配置的延迟定时器,以及实际负载下的溢出计数,而不是只看标称波特率。

工程上怎么判断 FIFO 够不够

把上面的关系整理成一套可执行的检查顺序:

  • 算字符时间:字符时间 = 每帧位数 ÷ 波特率。8N1 取 10 位,8E1 取 11 位,8N2 取 11 位,7E1 取 10 位。
  • 估最坏延迟:把驱动临界区、更高优先级中断、虚拟化退出、SMP 调度这些因素合起来,给出一个保守值;不要用平均中断延迟。
  • 反推深度:所需深度 ≥ 触发水位 − 1 + 最坏延迟 ÷ 字符时间。触发水位越低、深度越大,余量越足;把预算代进去,就能看出 16 字节 FIFO 在目标波特率下是否真的够用。
  • 查触发水位可调范围:确认 FIFO 是否默认开启、水位是否可设、驱动是否把它暴露出来。有些驱动只使用固定水位,改注册表或模块参数才生效。
  • 看溢出计数:Linux 下可查看串口驱动导出的统计(不同内核版本字段与路径不同,常见的有 /proc/tty/driver/serial);Windows 下可在设备管理器的端口高级设置里看到 FIFO 缓冲滑块,并配合厂商工具读取错误计数。计数持续增长才是真凭据,单次现象不足以定论。
  • 按代价从低到高调整:先降触发水位换余量;再降低同卡并发端口数或波特率;仍不够则换更深 FIFO 的芯片方案或改用带 DMA 的板卡;最后才考虑更换总线或主机。
常见说法实际情况
"FIFO 越大越好"深度买的是响应时间,代价是流控停不下来的字节更多、软件可见延迟更大,要按场景权衡
"开了 FIFO 就不会丢数据"FIFO 只吸收短时抖动;中断延迟超过 (深度 − 水位 + 1) 个字符的窗口,照样溢出
"触发水位设得越高越安全"水位越高中断越少,但触发后到溢出的余量只剩两三个字符;高波特率下要按毫秒甚至几十微秒算这笔账
"发送没发完可以直接切方向"RS-485 要等发送 FIFO 与移位寄存器都空再切,否则末尾字节丢失或总线冲突

小结

串口 FIFO 的作用可以用一句话概括:它是 UART 与 CPU 之间的一段减速带,把"每个字节一次响应"变成"每十几个字节一次响应"。它不提高线速,也不替代流控,而是用队列深度换来两个结果——中断频率下降,以及从触发到溢出之间那点有限的响应余量。判断一颗 UART 或一块多串口卡够不够用,关键不是记住"16 字节"这个数字,而是用字符时间、最坏中断延迟、触发水位和端口数四个量算出窗口,再对照芯片手册里的 FIFO 深度与可调水位。窗口够,FIFO 就是稳定的缓冲;若深度不足以覆盖最坏延迟,溢出只是被推迟,需要按公式加深度或改用 DMA。

客服微信:18025338165点击复制并跳转微信