3. UART通信调试:波特率不匹配问题、数据帧格式错误、DMA传输异常、中断丢失与溢出错误
UART 这个外设,看起来简单,就两根线(TX/RX),但调试起来坑是真不少。我这些年经手的项目里,UART 出问题的频率仅次于 GPIO 和时钟。说白了,串口调不通,大部分时候不是硬件坏了,而是配置没对齐。
这一章,我把 UART 调试里最常见的四类问题拆开揉碎了讲。每个问题我都会结合自己的踩坑经历,给你一套可复现的排查思路。
3.1 波特率不匹配问题
波特率不匹配,是 UART 通信里最基础也最容易犯的错。两边波特率不一样,收出来的数据全是乱码。
3.1.1 现象与根因
现象很典型:发送端发 "Hello",接收端收到一堆乱码,比如 "0x48 0x65 0x6C" 变成 "0xA4 0x32 0xDB"。用逻辑分析仪抓波形,你会发现每个 bit 的宽度跟预期对不上。
根因就两个:
- 时钟源偏差:STM32 的 UART 波特率由 APB 时钟分频得到。如果 APB 时钟不准(比如 HSE 起振失败,自动切到 HSI),波特率就偏了。
- 分频计算误差:某些波特率(如 115200)在特定时钟下无法整除,误差超过 ±2% 就会丢数据。
关键指标:UART 通信的波特率误差必须控制在 ±2% 以内。超过这个范围,数据采样点会偏移到 bit 边界上,导致误码。
3.1.2 排查步骤
- 确认时钟源:用示波器或逻辑分析仪测量 MCU 的 MCO 引脚,确认 HSE 频率正确。
- 计算实际波特率:根据 APB 时钟和 USART_BRR 寄存器的值,反算实际波特率。
- 对比误差:实际波特率与目标波特率的差值,除以目标波特率,得到误差百分比。
- 调整分频:如果误差超标,换一个能整除的波特率(如 115200 不行就换 57600),或者调整 APB 时钟频率。
我的习惯:调试阶段,我会在代码里加一个串口初始化后的自检流程——发一个已知字节(如 0x55,二进制 01010101),然后用逻辑分析仪抓波形,量每个 bit 的宽度。宽度 = 1/波特率,一量就知道偏没偏。
3.1.3 代码示例:波特率计算与验证
/* 假设 APB2 时钟 = 72 MHz,目标波特率 = 115200 */
uint32_t apb_clock = 72000000;
uint32_t target_baud = 115200;
/* 计算 USARTDIV */
float usartdiv = (float)apb_clock / (16 * target_baud);
uint32_t brr_value = (uint32_t)(usartdiv * 16) & 0xFFFF;
/* 反算实际波特率 */
float actual_baud = (float)apb_clock / (16 * (brr_value / 16.0));
float error = (actual_baud - target_baud) / target_baud * 100;
printf("目标波特率: %d\n", target_baud);
printf("实际波特率: %.2f\n", actual_baud);
printf("误差: %.2f%%\n", error);
if (error > 2.0 || error < -2.0) {
printf("警告:波特率误差超标!\n");
}
3.2 数据帧格式错误
波特率对了,但数据还是不对?那八成是帧格式没对齐。UART 的帧格式包括:数据位、停止位、校验位。两边必须完全一致。
3.2.1 常见配置组合
| 参数 | 常见值 | 说明 |
|---|---|---|
| 数据位 | 8 / 9 | 8 位最常用,9 位用于多机通信或带校验 |
| 停止位 | 0.5 / 1 / 1.5 / 2 | 1 位停止位最常用 |
| 校验位 | 无 / 偶校验 / 奇校验 | 无校验最常用,校验位会占用数据位中的 1 位 |
注意:如果使能了校验位,数据位要设置为 9 位(8 位数据 + 1 位校验)。很多人在这里搞错,设成 8 位数据 + 校验,结果收发永远对不上。
3.2.2 排查方法
我曾经在一个 Modbus 项目里被帧格式坑过。设备 A 发 8N1(8 数据位、无校验、1 停止位),设备 B 配成了 8E1(8 数据位、偶校验、1 停止位)。结果 B 收到的每个字节都多了一个校验位,数据全错位。
排查方法很简单:
- 用逻辑分析仪抓一帧数据,数一下 bit 数。
- 起始位(低电平)1 bit + 数据位(LSB 先发)+ 校验位(如果有)+ 停止位(高电平)。
- 数出来是 10 bit 就是 8N1,11 bit 就是 8E1 或 9N1。
3.3 DMA传输异常
UART + DMA 是高性能通信的标配,但 DMA 出问题的时候,排查起来比中断模式麻烦得多。
3.3.1 典型现象
- 只发一次:DMA 传输完第一个 buffer 后就停了,不再触发下一次。
- 数据错位:收到的数据顺序不对,或者中间有重复/缺失。
- 传输未完成:DMA 传输了一半,卡住了,再也进不了完成中断。
3.3.2 根因分析
我遇到过最典型的一个问题:DMA 配置了循环模式,但 UART 的发送完成标志(TC)没有清除,导致 DMA 请求一直被阻塞。说白了,DMA 把数据搬到 UART 的 DR 寄存器后,UART 要等 TC 置位才能发下一个字节。如果 TC 没清,DMA 就卡死了。
另一个常见问题是 DMA 中断优先级配置不当。如果 DMA 中断优先级低于某个频繁触发的中断,DMA 传输会被频繁打断,导致数据溢出。
3.3.3 排查清单
| 检查项 | 正确配置 | 错误示例 |
|---|---|---|
| DMA 通道 | UART_TX 用 DMA 通道 4(STM32F1) | 用了 SPI 的 DMA 通道 |
| DMA 方向 | 发送:内存→外设;接收:外设→内存 | 方向设反 |
| DMA 模式 | 发送用 Normal,接收用 Circular | 发送用了 Circular,导致无限重发 |
| UART 请求 | 使能 UART 的 DMA 发送/接收请求 | 只配了 DMA,没配 UART 的 DMA 使能位 |
| 中断优先级 | DMA 中断优先级高于 UART 中断 | DMA 优先级低于定时器中断 |
我的调试技巧:DMA 出问题时,先关掉 DMA,用轮询模式发几个字节。如果轮询模式正常,说明 UART 本身没问题,问题一定在 DMA 配置上。这叫「二分法」——先隔离外设,再查 DMA。
3.4 中断丢失与溢出错误
中断丢失和溢出错误,是 UART 接收端最常见的问题。尤其是在高波特率或连续大数据量传输时,稍不注意就会丢数据。
3.4.1 中断丢失
中断丢失的意思是:UART 收到了数据,产生了 RXNE(接收非空)中断,但 CPU 没来得及响应,下一个数据又来了,把当前数据覆盖了。
为什么会这样?
- 中断优先级太低:UART 中断被其他更高优先级的中断长时间阻塞。
- 中断服务函数太长:在 ISR 里做数据处理、打印、延时等耗时操作,导致来不及响应下一个中断。
- 中断嵌套:多个中断同时触发,UART 中断被挤掉了。
3.4.2 溢出错误(ORE)
溢出错误是 UART 硬件检测到的错误。当 RXNE 为 1(数据还没读走),新的数据又到达时,硬件会置位 ORE 标志,并丢弃新数据。
我曾在一次 921600 波特率的调试中遇到 ORE 频繁触发。排查后发现,ISR 里用了 HAL_UART_Receive_IT() 重新使能接收,但这个函数本身有几十个时钟周期的开销。在高波特率下,这个时间窗口足够让下一个字节覆盖上来。
3.4.3 解决方案
- 提高 UART 中断优先级:在 NVIC 中把 UART 中断优先级设到最高(或次高)。
- ISR 里只做轻量操作:在中断里只读 DR 寄存器,把数据放到环形缓冲区,数据处理放到主循环或任务里。
- 使用 DMA 接收:DMA 不需要 CPU 逐字节干预,能从根本上解决中断丢失问题。
- 检查 ORE 标志:在 ISR 中检查 ORE,如果置位,先清除它,再读 DR。
/* UART 中断服务函数示例 - 使用环形缓冲区 */
void USART1_IRQHandler(void)
{
uint8_t data;
if (LL_USART_IsActiveFlag_RXNE(USART1)) {
data = LL_USART_ReceiveData8(USART1);
/* 检查溢出错误 */
if (LL_USART_IsActiveFlag_ORE(USART1)) {
LL_USART_ClearFlag_ORE(USART1); /* 先清 ORE */
/* 记录溢出次数,用于调试 */
overflow_count++;
}
/* 将数据放入环形缓冲区 */
ring_buffer_put(&rx_buffer, data);
}
}
重要:读取 USART_DR 寄存器会自动清除 RXNE 标志。但 ORE 标志不会自动清除,必须手动写 0。很多人只清 RXNE,忘了清 ORE,导致 ORE 一直挂起,后续数据再也收不进来。
3.5 知识体系总览
下面这张图把 UART 调试的四类问题以及它们之间的关联关系画出来了。你可以把它当作排查时的路线图。
这张图把 UART 调试的四个核心问题以及它们的子原因都列出来了。你排查的时候,按「先物理层、再协议层、最后软件层」的顺序走,能省不少时间。
嗯,UART 调试说难不难,说简单也不简单。关键是养成好习惯:每次配置完,先用逻辑分析仪抓波形确认一下。别问我为什么强调这个——我当年因为少抓一次波形,硬是调了三天才发现是杜邦线接触不良。