UART + DMA:串口接收不再丢字节
stm32 uart dma术语速查
| 缩写 | 全称 | 大白话 |
|---|---|---|
| DMA | Direct Memory Access 直接存储器访问 | CPU 不用亲自搬字节,DMA 控制器代劳 |
| RXNE | Receive Not Empty 接收寄存器非空 | UART 收到一个字节,通知你去读 |
| IDLE | IDLE line detection 总线空闲检测 | 一串数据发完了,总线安静了,触发中断 |
| HT | Half Transfer 半传输中断 | 缓冲区填到一半时触发 |
| TC | Transfer 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_DMA | CPU |
| T1~Tn | 每个字节到达 UART RDR → DMA 自动搬到 rx_buf | DMA |
| Tn+1 | 总线空闲 ≥ 1 字节时间 → IDLE 中断触发 | UART → NVIC → CPU |
| Tn+2 | CPU 处理 rx_buf[0..Size-1],重新启动 DMA | CPU |
常见坑
| 坑 | 原因 | 解法 |
|---|---|---|
| IDLE 中断不触发 | 忘了清 IDLE 标志位:__HAL_UART_CLEAR_IDLEFLAG(huart) |
回调第一行就清 |
| DMA 数据错位 | 上一次传输没完成就重启了 DMA | 重启前调用 HAL_UART_DMAStop() |
| 频繁 OVF 溢出 | CPU 处理太慢,DMA 覆盖了旧数据 | 增大 rx_buf 或降低波特率 |