UART通信基础知识
UART 经常被当成“接上 TX、RX 就能用”的接口。
真正排查乱码、偶发丢字或半双工冲突时,问题可能分布在完全不同的层:
- MCU 引脚的逻辑电平不兼容;
- RS-232 或 RS-485 收发器接错;
- 数据位、校验位或停止位配置不同;
- 分频器产生的实际波特率有误差;
- 接收采样点在一帧内逐渐漂移;
- FIFO 没有及时清空,发生硬件 overrun;
- 字节都收对了,但上层协议失去帧边界。
这篇按这几层逐步拆开。
文中的波形和误差区域由明确的理想模型生成,不是逻辑分析仪截图。
附件会复算文章里的数值;器件的真实采样规则、容差和错误标志,仍以对应型号的数据手册为准。
先把“串口”的几个层次分开
UART 是 Universal Asynchronous Receiver/Transmitter。
它是把并行数据转换成异步串行比特流、再把接收比特流还原为数据字的硬件模块。
UART 主要定义这些内容:
- 空闲、起始、数据、校验和停止位的时序;
- 数据位宽和低位先发等规则;
- 发送、接收寄存器或 FIFO;
- 波特率发生器、采样器和错误标志。
UART 本身不等于 RS-232,也不等于 RS-485。
三者处在不同层次:
| 名称 | 主要解决的问题 | 典型信号 |
|---|---|---|
| MCU 逻辑 UART | 字节怎样变成异步串行帧 | TXD、RXD,逻辑 0/1 |
| RS-232 | 单端线路上的电压、极性和接口 | TX、RX、GND,经专用收发器 |
| RS-485 | 差分、多点线路与驱动使能 | A、B,常见 DE、RE 控制 |
| 上层协议 | 字节流如何划分消息、校验和重传 | 长度、转义、CRC、序号等 |
把 USB 转 TTL、USB 转 RS-232 和 USB 转 RS-485 都叫“串口线”,不代表它们能互换。
逻辑电平 UART
MCU 的 UART 引脚通常是芯片 I/O 电平,例如 1.8 V、3.3 V 或 5 V。
输入高低阈值、是否 5 V tolerant、上电默认状态都应查具体数据手册。
普通非隔离连接通常至少需要:
1 | |
VCC 不能因为针脚名字一样就直接相连。
两块板已经各自供电、工作电压不同,或者接口带隔离时,VCC 的处理会完全不同。
只监听目标输出时,可以只接目标 TX、采集设备 RX 和参考地。
要进行双向通信,TX 和 RX 才都需要交叉连接。
RS-232 不是“更高电压的 TTL UART”
RS-232 在线缆侧使用正负电压,并且线路逻辑通常相对 MCU UART 引脚反相。
MCU 引脚不能直接接到 RS-232 线上。
Analog Devices 的 RS-232 说明给出的接口边界很清楚:
UART 负责起始、停止、校验和串并转换,RS-232 line driver/receiver 负责电平转换与线路侧驱动。
看到 DB9 接头也不能只凭外形判断针脚定义。
DTE/DCE、直连线与交叉线、是否使用 RTS/CTS,都要结合设备文档和实测确认。
RS-485 还多了总线方向问题
RS-485 使用差分线路,常见两线制是半双工。
UART TXD/RXD 仍是逻辑信号,A/B 由 RS-485 收发器驱动和接收。
半双工发送通常还要控制 DE:
- 置 DE,等待驱动器进入发送状态;
- 向 UART 写入整帧;
- 等最后一个停止位真正离开移位寄存器;
- 再释放 DE,让其他节点发送。
“发送数据寄存器空”只说明可以继续写下一个字,不一定说明线上最后一位已经完成。
切换 DE 时应使用外设的 transmission complete 条件,而不是只看 TX empty。
Analog Devices AN-960还说明了多点总线的三态驱动、终端、偏置和空闲 fail-safe。
终端电阻不是“有 RS-485 就随便加一个”;位置、线缆阻抗、总线长度和拓扑都要一起判断。
另外,不同厂商对 A/B、+/− 的命名习惯可能造成混淆。
接线时应核对收发器数据手册中的 truth table,不能只按字母颜色连接。
UART 帧:先算清每个字节到底占多少时间
常见非反相 UART 在空闲时为高电平。
一次帧通常包含:
1 | |
115200 8N1 表示:
- 目标波特率 115200;
- 8 个数据位;
- N:无 parity;
- 1 个停止位。
在普通二进制 UART 中,一个符号承载一个 bit,所以 baud 数值与 bit/s 数值相同。
这不表示应用层每秒能传 115200 个有效数据 bit,因为起始、校验和停止位也占时间。
设:
- 数据位数为
D; - 有 parity 时
P=1,否则P=0; - 停止位长度为
S; - 波特率为
B。
一帧的 bit 数和时间为:
1 | |
对 8N1:
1 | |
它在线路层已经有 20% 的非数据开销。
8E1 或 8N2 都是每帧 11 bit:
1 | |
如果协议每 64 字节还加 8 字节头尾,有效负载率还要乘:
1 | |
RTS/CTS 等流控、RS-485 换向保护时间、帧间空闲以及重传都会继续降低吞吐。
手工解一个 0x53
0x53 的通常二进制写法是 01010011。
UART 数据位一般低位先发,所以线上 D0 到 D7 是:
1 | |
加上 8N1 的起始和停止位:
1 | |
停止位不要求一定出现新的上升沿。
如果最后一个数据位本来就是 1,线路可以从 D7 一直保持高到停止位和下一段空闲。
parity 能发现什么
偶校验要求数据位和 parity 位中 1 的数量为偶数;奇校验要求总数为奇数。
0x53 有四个 1:
- 8E1 的 parity 位是 0;
- 8O1 的 parity 位是 1。
一次奇数个 bit 翻转通常会改变 parity。
两个 bit 同时翻转可能互相抵消,所以 parity 不能替代消息级 CRC。
它也不能指出哪个 bit 错了,更不能提供消息认证。
某些外设的“9 bit 模式”会把第九位用作普通数据、地址标记或 parity,具体寄存器语义并不统一。
读取数据寄存器时也要确认 parity 位是否被硬件剥离。
接收器怎样从一个起始沿恢复采样时刻
异步链路没有共享时钟。
接收器看到空闲高到起始低的下降沿后,用自己的时钟安排后续采样。
一种常见实现是 16× oversampling:
- 检测起始下降沿;
- 在起始位中间验证它仍为低;
- 之后每隔 16 个采样 tick 读取下一位;
- 在停止位位置期望读到高电平。
Microchip 的采样调整文档给出了一个具体实现:
16× 模式下使用第 7、8、9 个样本做多数表决;8× 模式使用第 3、4、5 个样本。
这只是一个可核对的器件实现,不是所有 UART 必须采用的算法。
其他外设可能使用单点采样、不同的多数表决位置、数字滤波或边沿再同步。
起始检测也有量化误差
数据下降沿与接收器采样时钟是异步的。
下降沿刚好落在两个采样 tick 之间时,接收器只能在后续 tick 识别它。
在理想 16× 模型中,这个初始误差可接近一个 oversample tick:
1 | |
若以起始位为第 0 个 cell,并在每个 cell 中央采样,中心位置近似为:
1 | |
其中 k=0 是起始位中心,k=1 是 D0,k=9 是 8N1 的停止位中心。
波特率误差为何在一帧内累积
发送端和接收端只在起始沿附近重新对齐一次。
若两边 bit period 不同,之后每一位都会再增加一点偏移。
用一阶近似表示第 k 个 cell 中心的偏移:
1 | |
帧越长,最后一个数据位和停止位越容易靠近相邻 bit 的边界。
误差在停止位后不会无限积累。
下一帧新的起始沿会重新同步;真正需要覆盖的是一帧内的最坏位置。
为什么不能背一个“允许相差 10%”
理想情况下,中心采样点偏移半个位就会碰到边界。
真实链路却不能一直用到边界:
- 起始检测有相位量化;
- 上升、下降沿需要时间;
- 阈值、噪声和抖动会减少稳定窗口;
- 多数表决位置与滤波规则影响边界;
- 数据位数、parity 和停止位改变一帧长度;
- 接收器和发送器的时钟误差都要计入。
Analog Devices 的 UART 时钟精度分析在明确假设下给出两个示例:
可靠采样窗口取 bit 中部 75% 时,10-bit frame 的预算约为 ±3.3%;窗口只有中部 50% 时约为 ±2%。
这些数字来自对应模型,不是跨芯片通用的“UART 标准容差”。
工程上更可靠的做法是建立预算表:
1 | |
只知道“双方都设置成 115200”不够,还要知道它们实际产生了多少。
一个可运行的 16× 理想采样实验
1 | |
模型使用:
- 瞬时、无振铃的理想边沿;
- 固定的发送端 bit period;
- 接收端固定频差;
- 起始检测延迟在一个接收 oversample tick 内变化;
- 第 7、8、9 个 tick 三点多数表决;
- 测试字节
0x55,使相邻数据位频繁翻转。
本次使用 Python 3.11 运行得到:
1 | |
-4.85%..+5.55% 只属于这段代码的理想边沿、0x55、三点采样和相位扫描。
区间不对称,是因为模型把起始沿检测延迟定义为 [0, 1) 个接收 tick,并从检测时刻向后安排采样。
它没有模拟:
- 输入阈值和迟滞;
- 传播延迟与上升下降时间;
- 随机噪声、周期抖动和突发干扰;
- 接收器内部滤波或重同步;
- 收发器、隔离器与线缆的失真。
因此,这张图适合解释“误差怎样积累”,不适合作为选型时的最大容差。
波特率发生器:设置值不等于实际值
整数分频
假设 UART 输入时钟为 f_clk,16× 模式使用整数分频 N:
1 | |
以 48 MHz 和 115200 为例:
1 | |
误差同时用百分比和 ppm 表示:
1 | |
1/64 小数分频示例
Microchip 的 fractional baud generator 文档描述了低 6 bit 保存分频小数部分的实现。
若把理想分频值乘 64 后取整:
1 | |
小数分频把本例的量化误差从约 1603 ppm 降到约 200 ppm。
寄存器公式、舍入规则、oversampling 模式和允许范围依芯片而异,不能把这里的 1667 写到另一类外设。
时钟源误差仍然存在
波特率寄存器只解决分频量化。
晶振、RC 振荡器、PLL 和外设时钟树还会带来:
- 初始频率误差;
- 温度漂移;
- 电压与老化影响;
- PLL 或时钟切换产生的抖动;
- 低功耗模式下时钟源变化。
两个端点误差方向相反时,链路相对误差会变大。
做最坏情况预算时,应组合两边数据手册给出的边界,而不是只加典型值。
自动波特率检测也不是无限容错。
它依赖特定同步字符或边沿模式,得到的计数仍受输入边沿、时钟量化和实现规则影响。
Framing、Parity、Noise 与 Overrun 不是一类错误
接收端经常暴露多个独立标志:
| 标志 | 硬件观察到的现象 | 常见方向 |
|---|---|---|
| Framing error / FE | 在期望的停止位位置读到低 | 波特率、位宽、反相、break、噪声 |
| Parity error / PE | 数据与 parity 不满足配置 | parity 配置不同或 bit 受干扰 |
| Noise error / NE | 多点采样不一致或检测到噪声 | 信号完整性、阈值、线缆与地 |
| Overrun / ORE | 新数据到达时前一数据未被取走 | FIFO、IRQ 延迟、DMA 或软件阻塞 |
| IDLE | 接收线在规定时间没有新帧 | 可作为通知,不天然等于协议结尾 |
| Break | 线路持续低电平超过普通字符时间 | break 信号、短路、错误极性等 |
ST 的 LPUART 培训资料分别列出 overrun、parity、framing 和 noise 标志,也说明 RX ready、IDLE 和 DMA 请求属于不同事件。
Framing error
FE 的直接含义是停止位检查失败。
可能原因包括:
- 两端波特率差太大;
- 8N1 与 7E1 等帧配置不一致;
- 线路反相;
- 起始位是噪声尖峰;
- break 持续低电平覆盖停止位;
- 边沿太慢或采样窗口被噪声侵入。
FE 不等于“某个文本字符编码错了”。
错误发生在 UART frame 层。
Parity error
PE 表示硬件计算出的 parity 与接收 parity 位不一致。
如果一端 8N1、另一端 8E1,后续 bit 边界也可能被错误解释,同时出现 PE 或 FE。
记录错误时应保存原始数据字和状态寄存器读取顺序。
部分外设在读取状态或数据寄存器后清除标志,具体步骤必须查参考手册。
Overrun
ORE 与线路采样可能完全无关。
字节已经正确到达外设,但软件没有及时腾出接收寄存器或 FIFO。
如果“刚开始正常,持续大流量后才丢字”,优先观察:
- 中断是否被高优先级任务长时间屏蔽;
- ISR 是否做了日志格式化或阻塞 I/O;
- FIFO 阈值是否留下足够余量;
- DMA 是否处于 circular 模式;
- 环形缓冲的 consumer 是否跟得上 producer;
- 错误回调是否停止了接收却没有重新启动。
FIFO、IRQ、DMA 与环形缓冲的时间预算
设一帧占 N_frame bit,波特率为 B:
1 | |
如果硬件 FIFO 还有 F_free 个空位,软件完全不服务的理论上限是:
1 | |
115200 8N1、FIFO 还有 16 个空位:
1 | |
这不是允许 ISR 延迟 1.3889 ms 的承诺。
真实预算还要扣除:
- 中断触发阈值已经占用的 FIFO 空间;
- 当前字符仍在移入;
- 总线和 DMA 竞争;
- 临界区、关中断区间与调度抖动;
- 错误处理和缓存一致性开销。
每字节中断
优点是简单、延迟低。
缺点是高波特率下中断频繁,ISR 稍重就可能抖动或 overrun。
ISR 最好只做有界操作:
1 | |
不要在接收 ISR 中执行 JSON 解析、printf、Flash 写入或等待互斥锁。
FIFO 阈值中断
FIFO 可以降低中断频率,但阈值越高,触发时剩余空间越少。
选择阈值时要同时看吞吐和最坏中断延迟。
高阈值不自动提高可靠性。
如果一次 ISR 只取一个字节,反而可能一直处于临界状态。
Circular DMA + ring buffer
DMA 可以持续把 RDR 数据搬到内存,但 DMA 数组本身仍是有限环。
软件要维护“DMA 已写到哪里”和“解析器已读到哪里”。
一个常见思路是:
1 | |
工程实现还要处理:
- 读取 DMA 计数时的竞态;
- HT、TC、IDLE 回调可能连续发生;
- cacheable 内存上的一致性;
- consumer 落后一整圈时怎样识别;
- 溢出后丢新数据、丢旧数据还是重置协议;
- 多核或 ISR/任务之间的内存顺序。
只用取模后的 head == tail 无法区分空缓冲和整圈覆盖。
可以保留一个空槽、维护单调递增计数,或额外记录 generation。
ST 官方的 Receive-to-IDLE Circular DMA 示例展示了 HT、TC 和 IDLE 三类通知以及回调中的索引含义。
示例中的 IDLE 是接收线在最后一个字节后保持一个 frame time 空闲触发。
IDLE 很适合通知“当前有一批字节可处理”,但它不是普适消息边界:
- 协议允许消息内部停顿时,IDLE 会提前出现;
- 连续消息之间没有空闲时,IDLE 不会分开它们;
- 调度延迟会让多个事件在处理时合并。
UART 只提供字节流,上层必须自己定义消息
假设连续收到:
1 | |
UART 不知道:
- 第一个字节是不是帧头;
00 05是不是长度;7E是数据还是分隔符;- 最后两个字节是不是 CRC;
- 一次 read 返回的是半帧、一帧还是多帧。
应用协议必须选一种可重同步的结构。
| 方法 | 优点 | 主要风险 |
|---|---|---|
| 固定长度 | 状态最简单 | 扩展困难,错一字节后需要外部重同步 |
| 分隔符 | 人类可读,容易找边界 | 数据中出现分隔符时需要转义 |
| 长度字段 | 支持二进制和变长 payload | 损坏长度可导致等待过久或越界 |
| 空闲间隔 | 某些总线协议实现方便 | 调度和链路停顿可能破坏边界 |
| 编码帧,如 COBS | 边界明确,开销可控 | 双方必须实现同一编码规则 |
实际协议常组合:
1 | |
一个有界、可重同步的解析示例
示例定义如下,不把“CRC16”写成含糊名字:
1 | |
解析器在读到长度后立即检查:
1 | |
这一步必须发生在:
- 按长度分配内存之前;
- 等待剩余 payload 之前;
- 进行
header + length算术而未检查溢出之前。
收到新的 0x7E 时,解析器丢弃未完成旧帧并重新开始。
CRC 错误和超长长度不会让它无限缓存数据。
运行:
1 | |
本次输出:
1 | |
测试流故意包含:
- 分块输入,模拟一次 read 只返回部分字节;
- payload 内的
0x7E和0x7D; - 一帧 CRC 损坏;
- 一个超出
MAX_PAYLOAD的长度; - 一帧被截断后又出现新 SOF;
- 噪声之后的两个合法帧。
CRC 能发现常见传输错误,但不提供认证。
攻击者可以修改数据并重算 CRC;有对抗性需求时应使用经过审查的消息认证或 AEAD 设计,并同时考虑重放和密钥管理。
解析器的工程边界
不要把一次 read 当成一帧
无论是串口驱动、DMA 回调还是操作系统 read():
- 可能只返回半个 header;
- 可能返回恰好一帧;
- 可能一次返回三帧再加下一帧开头。
正确解析器应跨调用保存状态,并在一个 chunk 中循环取出所有完整帧。
为每个等待状态设置上限
除了 MAX_PAYLOAD,还可记录:
- 从 SOF 开始已经接收的字节数;
- 当前帧开始时间;
- 连续 escape 的异常情况;
- CRC、长度和重同步计数;
- 序号跳变与重复帧。
若协议允许长时间停顿,timeout 不能替代长度与容量边界。
timeout 只是另一项状态清理策略。
错误后如何恢复要写进协议
只说“CRC 错就丢包”还不完整。
还要说明下一步怎样找到新边界:
- 等待下一个未转义 SOF;
- 丢弃固定长度;
- 使用编码保证分隔符唯一;
- 通过序号确认是否需要重传。
如果帧头可能自然出现在未转义 payload 中,扫描一个 magic 值并不能可靠重同步。
从症状反推层次
完全没有字节
依次确认:
- 目标是否真的发送;
- 测量点是在 MCU 逻辑侧还是收发器线路侧;
- TX/RX 是否交叉;
- 是否共地或正确隔离;
- 逻辑电平、反相和收发器使能;
- 引脚复用与外设时钟;
- RS-485 DE/RE 和总线偏置。
每个字节都像固定变换后的乱码
检查:
- 线路是否反相;
- 7/8/9 数据位和 parity 是否不同;
- bit order 是否被上层错误解释;
- 终端是否把二进制当 UTF-8 或 GBK 显示;
- 实际波特率而不只是配置值。
先保存十六进制原始字节。
终端显示不正常,不等于 UART 解码错误。
前面正确,越到帧尾越容易错
这很像采样时钟差在帧内积累。
测量多个 bit 宽度、计算双方实际分频、查看 FE,并与器件容差表比较。
不要只测一个低脉冲。
连续几个相同 bit 会合并成更宽的电平区间,看起来像一个“很慢的 bit”。
低流量正常,持续流量丢字
优先看 ORE、FIFO watermark、最大关中断时间、DMA 环覆盖和解析任务吞吐。
可以给每一级留计数器:
1 | |
把硬件错误和协议错误分开计数,才知道该换探头、改时钟还是改软件。
RS-485 只在自己发送后出错
检查:
- DE 是否过早释放,截断最后停止位;
- DE 是否太晚释放,与对端响应冲突;
- 本机接收器是否回读自己的发送;
- 多节点是否同时驱动;
- 终端和偏置是否合适;
- 协议是否规定 turnaround 时间。
用 TX complete 而不是 TX register empty 判断最后一位发完。
采集与复现时应记录什么
一次能复核的 UART 问题记录至少包括:
1 | |
逻辑分析仪采样率应足够高,阈值也要与测量侧电平匹配。
软件 UART decoder 的自动 baud 猜测只能作为线索,最好用长采样中的多个边沿复算 bit period。
在 RS-232 或 RS-485 线路侧采集时,普通逻辑分析仪可能既不耐受电压范围,也不能正确解释差分信号。
应使用相应探头、接收器或在线路收发器逻辑侧测量。
本文实验的可复核边界
| 项目 | 本文做了什么 | 没有声称什么 |
|---|---|---|
| 8N1 波形 | 从确定 bit 序列绘制 | 不是实物采样 |
| 16× timing lab | 扫描固定频差与检测相位 | 不是芯片最大容差 |
| baud divider | 复算 48 MHz 的整数与 1/64 示例 | 不代表其他寄存器格式 |
| FIFO 时间 | 按完全无人消费计算理论窗口 | 不代替实时调度分析 |
| packet parser | 测试分块、转义、CRC、长度和重同步 | CRC 不提供认证 |
| RS-232/RS-485 | 说明收发器与控制边界 | 不给具体板卡接线结论 |
附件使用标准库,可在没有串口硬件时复算:
真正接入设备后,应把理想模型替换为:
- 目标 MCU 参考手册中的采样与容差公式;
- 时钟源在电压、温度和寿命范围内的误差;
- 收发器、隔离器、终端和线缆参数;
- 设备上观测到的错误标志、DMA 指针和原始波形。
参考资料
- Analog Devices:UART — A Hardware Communication Protocol
- Analog Devices:Determining Clock Accuracy Requirements for UART Communications
- Analog Devices:Fundamentals of RS-232 Serial Communications
- Analog Devices AN-960:RS-485/RS-422 Circuit Implementation Guide
- Microchip:USART Sample Adjustment
- Microchip:The Fractional Baud Rate Generator
- ST:STM32 LPUART training material
- ST:UART Reception-to-IDLE Circular DMA example
