4、UART通信调试:波特率误差计算与修正、中断丢失与FIFO溢出、DMA传输完成标志判断
UART 这玩意儿,看着简单,用起来坑不少。我调试过不下几十个串口项目,从简单的日志输出到高速传感器数据流,几乎每种异常都遇到过。今天咱们就把 UART 通信里最头疼的三个问题掰开揉碎了讲:波特率误差、中断丢失、DMA 完成判断。
核心观点:UART 调试的本质,就是确保发送方和接收方在「时间」和「数据流」上完全同步。时间靠波特率,数据流靠中断/DMA 管理。
4.1 波特率误差计算与修正
波特率误差,说白了就是两边时钟不同步。STM32 的 USART 波特率由系统时钟分频得到,公式很简单:
USARTDIV = fCK / (16 × BaudRate)
实际波特率 = fCK / (16 × USARTDIV)
误差 = |(实际 - 目标) / 目标| × 100%
但问题往往出在「fCK」上。我遇到过好几次,明明代码里配的 115200,示波器一看实际只有 114800 左右。为什么?因为系统时钟源变了,或者 APB 时钟分频没算对。
| 系统时钟 (MHz) | 目标波特率 | 理论分频值 | 实际波特率 | 误差 (%) |
|---|---|---|---|---|
| 72 | 115200 | 39.0625 | 115384 | 0.16% |
| 72 | 921600 | 4.8828 | 923076 | 0.16% |
| 72 | 3000000 | 1.5 | 3000000 | 0.00% |
| 48 | 115200 | 26.0417 | 115384 | 0.16% |
| 48 | 921600 | 3.2552 | 923076 | 0.16% |
你看,72MHz 下 115200 的误差只有 0.16%,完全在 ±2% 的容忍范围内。但如果你用内部 HSI 振荡器,频率可能漂到 70MHz 甚至更低,误差就大了。
我的习惯:每次配完 UART,第一件事就是拿逻辑分析仪抓起始位宽度。起始位宽度 = 1/波特率。比如 115200,起始位应该是 8.68μs。差太多,赶紧查时钟配置。
修正方法其实就两条路:
- 调整系统时钟:选一个能让 USARTDIV 接近整数的时钟频率。比如 72MHz 对 115200 就很友好。
- 使用 USART 的 OVER8 模式:把采样率从 16 倍降到 8 倍,分频值选择更灵活,但抗干扰能力会下降。
注意:当误差超过 2% 时,数据帧的最后一个 bit 很可能采样错误。我曾经在 48MHz 下配 250000 波特率,误差 1.73%,结果 10 个字节里错 1 个。这种软错误最难查。
4.2 中断丢失与 FIFO 溢出
中断丢失,是 UART 调试里最常见的噩梦。你想想看,MCU 正在处理一个高优先级中断,这时候 UART 收到一个字节,RXNE 标志置位。等 MCU 忙完回来,第二个字节已经覆盖了第一个——这就是溢出错误。
STM32 的 USART 硬件其实有解决方案:FIFO(先入先出缓冲区)。但很多开发者不知道,或者不会用。
// 使能 USART2 的 FIFO 模式(STM32G4/H7 系列)
USART2->CR1 |= USART_CR1_FIFOEN;
// 设置 RX FIFO 阈值为 1/4 满时触发中断
USART2->CR3 |= USART_CR3_RXFTCFG_0; // 阈值 = 1/4
FIFO 的好处很明显:
- MCU 不用每个字节都进中断,可以攒一批再处理
- 给高优先级任务留出处理窗口
- 减少中断上下文切换的开销
但 FIFO 不是万能的。我遇到过一个问题:传感器以 2Mbps 发数据,每包 256 字节。FIFO 深度只有 16 字节,如果中断响应时间超过 8 个字节的传输时间(约 40μs),照样溢出。
避坑指南:我曾经在一个电机控制项目里,UART 中断被 TIM 更新中断频繁打断。每次 UART 中断进来,RXNE 已经置位,但读 DR 时数据已经错了。后来我把 UART 中断优先级提到最高,才解决。记住:UART 中断优先级不能太低,尤其在高波特率下。
判断是否溢出很简单:检查 USART_SR 的 ORE 标志。如果置位,说明有数据丢失。
if (USART1->SR & USART_SR_ORE) {
// 溢出错误!需要清标志
USART1->SR &= ~USART_SR_ORE;
// 读 DR 清空数据
volatile uint32_t dummy = USART1->DR;
}
4.3 DMA 传输完成标志判断
DMA 是解决中断丢失的终极方案。数据从外设到内存,全程不经过 CPU。但问题来了:你怎么知道 DMA 传输完了?
很多人直接用 DMA 的 TCIF(传输完成中断)。但这里有个坑:DMA 的 TCIF 在「当前传输完成」时触发,而不是「所有传输完成」。如果你配置的是循环模式,TCIF 每传一次就触发一次。
正确的做法是:
- 对于单次传输:用 DMA_IT_TC 中断,在中断服务函数里关闭 DMA 或启动下一次传输。
- 对于循环传输:用半传输中断(HTIF)和全传输中断(TCIF)配合,实现双缓冲。
- 对于不定长数据:用 UART 的空闲中断(IDLE)判断帧结束,再查 DMA 当前计数值。
// DMA 传输完成中断示例
void DMA1_Channel5_IRQHandler(void) {
if (DMA1->ISR & DMA_ISR_TCIF5) {
// 清除标志
DMA1->IFCR |= DMA_IFCR_CTCIF5;
// 停止 DMA
DMA1_Channel5->CCR &= ~DMA_CCR_EN;
// 处理接收到的数据
process_uart_data(rx_buffer, RX_BUFFER_SIZE);
// 重新启动 DMA
DMA1_Channel5->CNDTR = RX_BUFFER_SIZE;
DMA1_Channel5->CCR |= DMA_CCR_EN;
}
}
我的经验:判断 DMA 传输完成,最可靠的方法是查 DMA_CNDTR 寄存器。这个寄存器存的是「还剩多少字节没传」。当 CNDTR 为 0 时,传输完成。但注意:CNDTR 在硬件传输完成瞬间可能还没更新,最好加个 1μs 的延迟再读。
还有一个容易忽略的点:DMA 传输完成标志的清除顺序。必须先读 ISR,再写 IFCR。如果反过来,可能会漏掉中断。
重要:DMA 传输完成中断里不要做耗时操作。我曾经在里面调用了 printf,结果 DMA 中断还没退出,下一个 TCIF 又来了,直接死循环。正确的做法是:中断里只设标志位,主循环里处理数据。
总结一下今天的内容:波特率误差靠计算和实测验证,中断丢失靠 FIFO 和优先级管理,DMA 完成靠 CNDTR 和中断配合。这三个问题搞定了,UART 通信基本就稳了。