UART + DMA:串口接收不再丢字节

stm32 uart dma

术语速查

缩写全称大白话
DMADirect Memory Access 直接存储器访问CPU 不用亲自搬字节,DMA 控制器代劳
RXNEReceive Not Empty 接收寄存器非空UART 收到一个字节,通知你去读
IDLEIDLE line detection 总线空闲检测一串数据发完了,总线安静了,触发中断
HTHalf Transfer 半传输中断缓冲区填到一半时触发
TCTransfer Complete 传输完成中断缓冲区填满时触发

问题场景

STM32 的 UART 接收数据,默认做法是在 USART1_IRQHandler() 里一个字节一个字节地读 RDR 寄存器。

9600bps 勉强能应付。但 115200bps 时每个字节间隔只有 87μs——CPU 稍微忙一下别的中断,字节就丢了。

DMA 循环模式怎么解决

初始化

// 启动 DMA 循环接收
HAL_UARTEx_ReceiveToIdle_DMA(
    &huart1,
    rx_buf,        // 目标缓冲区
    RX_BUF_SIZE    // 256 字节
);

// 关掉半传输中断(我们只要 IDLE)
__HAL_DMA_DISABLE_IT(
    &hdma_usart1_rx,
    DMA_IT_HT
);

原理

告诉 DMA:「把 UART 的数据自动搬到 rx_buf[256],填满了就回到开头循环。」

CPU 只在 IDLE 中断(一帧发完)才介入处理数据。平常不用管。

关键:关闭半传输中断 (HT)。DMA 填到 128 字节也会触发中断,干扰 IDLE 检测。除非你需要双缓冲,否则关掉。

中断回调

void HAL_UARTEx_RxEventCallback(
    UART_HandleTypeDef *huart, 
    uint16_t Size)      // 本次收到的字节数
{
    if (huart == &huart1) {
        // rx_buf[0..Size-1] 是刚收到的数据
        process_data(rx_buf, Size);
        
        // 重新启动 DMA 接收
        HAL_UARTEx_ReceiveToIdle_DMA(
            &huart1, 
            rx_buf, 
            RX_BUF_SIZE
        );
    }
}

数据流:一帧 115200bps 的旅程

时刻发生什么谁在工作
T0初始化 HAL_UARTEx_ReceiveToIdle_DMACPU
T1~Tn每个字节到达 UART RDR → DMA 自动搬到 rx_bufDMA
Tn+1总线空闲 ≥ 1 字节时间 → IDLE 中断触发UART → NVIC → CPU
Tn+2CPU 处理 rx_buf[0..Size-1],重新启动 DMACPU

常见坑

原因解法
IDLE 中断不触发 忘了清 IDLE 标志位:__HAL_UART_CLEAR_IDLEFLAG(huart) 回调第一行就清
DMA 数据错位 上一次传输没完成就重启了 DMA 重启前调用 HAL_UART_DMAStop()
频繁 OVF 溢出 CPU 处理太慢,DMA 覆盖了旧数据 增大 rx_buf 或降低波特率