摘要
很多人调试 STM32 串口,只停留在 HAL 库HAL_UART_RxCpltCallback回调函数,遇到乱码、丢字节、偶发接收异常,只会怀疑波特率或者上位机。
但实际上大量工程里的串口玄学 bug,根源来自USART 硬件中断标志理解错误、硬件 FIFO 溢出、帧错误 / 噪声标志未及时清除。
本文从寄存器底层讲清楚 STM32 USART 硬件机制,拆解中断标志、硬件 FIFO 工作逻辑,从硬件层面定位乱码、丢包的真实原因,附带极简测试代码与工程排查方法。
一、STM32 USART 硬件基础:不是简单 “收发寄存器”
STM32 的 USART 外设是独立硬件外设,内核 CPU 只是读取状态、搬运数据,串口收发是硬件自动移位完成。
核心硬件单元:移位寄存器、TDR 发送数据寄存器、RDR 接收数据寄存器、状态寄存器 SR、硬件 FIFO(F4/F7/H7 等带 FIFO,F1 没有硬件 FIFO)。
关键底层逻辑:
- 发送:CPU 写入 TDR → 硬件自动把 TDR 数据转到移位寄存器,逐位输出;TDR 为空就置位 TXE 标志。
- 接收:硬件引脚采样到串行 bit,移入移位寄存器,一帧收完存入 RDR,置位 RXNE 标志。
⚠️ RDR 寄存器只有 1 字节缓存(F1 无 FIFO),如果 CPU 没及时读取 RDR,新数据进来直接覆盖旧数据,造成丢字节。
硬件 FIFO 作用(F4/H7 等带 FIFO 型号)
硬件 FIFO 是串口内置的硬件缓冲区,独立于 CPU。
- 接收 FIFO:可以缓存多字节,缓解 CPU 来不及读取的压力,减少 RXNE 中断触发频次;
- 发送 FIFO:批量填入待发数据,硬件自动依次发送,降低 CPU 占用。
坑点:很多人开启 FIFO 但不懂 FIFO 阈值,FIFO 满溢出(ORE 溢出标志),同样会丢数据、产生乱码。
二、核心中断标志位深度解析(SR 状态寄存器)
新手最容易踩坑:只看 RXNE,忽略 ORE、FE、NE 这些错误标志。
寄存器 SR(Status Register)关键标志:
- RXNE 接收数据非空
收到 1 帧数据,RDR 寄存器有效,置 RXNE,触发接收中断。
清零方式:读取 RDR 寄存器,硬件自动清除 RXNE。
坑:只清中断位,但不读 RDR,RXNE 标志永远不会清零,会反复进中断,程序卡死。
- TXE 发送寄存器空
TDR 寄存器为空,可以写入下一个待发送字节。
坑:大量新手在中断里循环写数据,没判断 TXE,造成重复发送乱码。
- TC 发送完成标志****移位寄存器全部发送完毕,不是 TDR 写完。
TXE=TDR 空;TC = 所有 bit 全部发送到 TX 引脚。
项目场景:需要等待一整条报文发送完毕再切换 IO(比如 RS485 收发切换),必须用 TC,不能用 TXE,这是工控 RS485 最常见错误。 - ORE 溢出错误 Overrun error【乱码头号元凶】
底层原理:RDR 已经存有未读数据,新的一帧接收完成,硬件无法存放新数据 → ORE 置位。
后果:旧 RDR 数据丢失,新数据直接覆盖,出现随机丢字节、乱码。
触发场景:中断被长时间阻塞(高优先级中断长时间占用 CPU),CPU 来不及读取 RDR。
重点:ORE 标志必须软件清除,如果不清除,后续串口持续异常。
- FE 帧错误、NE 噪声错误
FE:停止位异常(波特率偏差、信号毛刺、断线);
NE:采样收到噪声,收到的 bit 不稳定。
出现 NE/FE,硬件依然会把采样到的数据存入 RDR,这就是收到乱码数据的来源。
很多代码完全不判断 FE/NE,把错误帧直接当成有效数据解析,协议直接错乱。
底层重点:
只要 SR 寄存器里存在错误标志(ORE/NE/FE),RXNE 中断依然会触发。如果代码只读取 RDR,不清除错误标志,错误锁死,后续所有接收都会异常。
三、乱码、丢字节底层根源分类
1. 硬件层面
- 波特率误差过大:串口外设时钟分频配置错误,采样点偏移,NE 噪声标志置位,采样出错;
- 信号质量差:长线传输、缺少上拉,信号毛刺引入噪声;
- 无硬件 FIFO 芯片(F1):高中断负载,CPU 读取不及时触发 ORE 溢出。
2. 软件层面(项目最高发)
- 中断服务函数里面写大量业务逻辑、延时、循环,阻塞中断;
- 只判断 RXNE,完全不检测 ORE、FE、NE 错误标志;
- 错误标志没有清除,错误状态锁死,后续接收持续异常;
- RS485 场景:用 TXE 判断发送完成,提前切换收发引脚,截断报文;
- 开启硬件 FIFO,FIFO 溢出阈值配置不合理,FIFO 溢出。
四、精简底层示例代码(标准库,F103,无硬件 FIFO)
只做硬件状态检测 + 数据接收,中断里面不做协议解析,业务逻辑放到主循环,工程推荐写法
void USART1_IRQHandler(void)
{
uint8_t ch;
if(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) != RESET)
{
ch = USART_ReceiveData(USART1); //读RDR,硬件清RXNE
rx_buf[rx_idx++] = ch;
if(rx_idx >= RX_BUF_MAX) rx_idx = 0;
}
// 检测溢出、帧错误、噪声错误
if(USART_GetFlagStatus(USART1, USART_FLAG_ORE | USART_FLAG_FE | USART_FLAG_NE) != RESET)
{
// 清除错误标志:读SR + 读DR,硬件清除错误标记
USART_ClearFlag(USART1, USART_FLAG_ORE | USART_FLAG_FE | USART_FLAG_NE);
}
}
代码要点:
- 中断仅做读取字节 + 缓存,解析放到主循环;
- 捕获 ORE/FE/NE 错误,一旦出现,直接清除标志,防止锁死串口;
- 不要在中断里调用 printf、复杂运算、延时。
HAL 库简要说明
HAL 库封装了底层寄存器,但是底层硬件机制不变。HAL_UART_RxCpltCallback回调,本质是 RXNE 中断触发后 HAL 库读取 RDR。
HAL 大坑:HAL 默认不会自动处理 ORE 溢出标志,一旦发生溢出,串口直接卡死,很多项目踩坑。
HAL 库需要重写中断处理,检测HAL_UART_GetState(),识别 ORE 错误,调用HAL_UART_ErrorCallback做恢复。
五、工控项目实战排查步骤(定位串口乱码)
- 第一步:不看数据,读取 USART SR 寄存器,查看 ORE/NE/FE 标志位。
- 有 ORE:CPU 来不及读取,中断阻塞、优先级配置不合理;
- 有 NE/FE:信号质量、波特率、时钟问题;
- 第二步:抓取中断进入时机,确认接收中断是否被其他高优先级中断长时间抢占;
- 第三步:F4/H7 带 FIFO 型号,查看 FIFO 溢出标志,调整 FIFO 接收阈值;
- 第四步:RS485 场景,使用 TC 标志判断发送结束,再切换 DE/RE 引脚。
六、工程设计规范总结
- 中断服务函数极简:只搬运数据、清除硬件标志,禁止复杂业务;
- 必须检测硬件错误标志,并且做错误恢复,不能忽略 ORE、FE;
- 区分 TXE 和 TC:TXE = 寄存器空;TC = 全部 bit 发送完毕,RS485 必须用 TC;
- 带硬件 FIFO 的芯片,合理配置 FIFO 阈值,避免 FIFO 溢出;
- 串口协议解析放到主循环状态机,中断只负责原始字节缓存。
结尾
串口看着简单,但是所有异常都和硬件寄存器、硬件 FIFO、中断标志深度绑定。
很多人停留在 API 调用层面,遇到偶发乱码、丢包就无从下手。读懂 USART 硬件行为,才能从根源定位问题,而不是靠不断换波特率、改上位机去试错。