文章目录
在嵌入式开发中,如何高效接收不定长度的串口数据是一个经典问题。传统的按字节中断接收不仅浪费 CPU 资源,还容易在大数据量时丢包。
为什么选择 DMA + IDLE?
传统的按字节中断接收(RXNE)就像是快递员每送一个包裹都要敲一次你的门,当你忙碌(CPU 负载高)时,包裹就会堆积甚至丢失。我们通过利用了 STM32 硬件层面的两个核心特性:
- DMA (搬运工)
- 串口寄存器(DR/RDR)每收到一个字节,DMA 硬件直接通过总线将其搬运到指定的内存缓冲区(RAM)。
- 可以完全解放 CPU。在整个传输过程中,CPU 不需要进入任何中断服务程序(ISR),可以全力处理业务逻辑。
- IDLE 信号 (断句器)
- 触发机制是硬件检测到 RX 引脚在出现起始位后,保持高电平(空闲态)超过 1 个字符时间,自动触发空闲中断。
- 完美解决"不定长"难题。它能自动识别一帧数据的结束,无需在协议中预设长度字段,也不需要复杂的超时判断逻辑。
核心原理
这种方案结合了 DMA(直接存储器访问) 和 IDLE(空闲中断) 的各自优势:
| 技术点 | 作用 | 意义 |
|---|---|---|
| DMA | 自动搬运:串口每收到一个字节,由硬件自动搬运到内存。 | 解放 CPU,传输过程中不需要 CPU 干预。 |
| IDLE 中断 | 帧结束判断:当串口总线连续一个字节时间没有数据时触发。 | 自动断句,完美解决不定长数据的边界识别。 |
| Normal 模式 | 单次触发:完成一次接收事件(满或空闲)后 DMA 停止。 | 数据安全,防止处理过程中新数据覆盖旧数据。 |
下图直观地展示了 UART 总线在空闲帧触发时的逻辑状态变化

CubeMX配置
- USART 设置:模式选择 Asynchronous,并在 NVIC 选项卡中勾选 USARTx global interrupt。

- DMA 设置:添加 USARTx_RX 通道,Mode 选择 Normal(单次模式),其余保持默认。

建议将串口中断优先级设置得稍高,以保证空闲信号能被及时捕获。
代码实现
在CubeMX配置完成之后,我们生成,在程序初始化阶段,需要执行以下代码启动接收。
c
// 1. 启动接收
// rx_buffer: 缓冲区; sizeof(rx_buffer): 最大期望接收长度
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buffer, sizeof(rx_buffer));
// 2. 屏蔽半满中断 (HT)
// 防止缓冲区搬运到一半时误触发回调,只在"发完"或"收满"时进回调
__HAL_DMA_DISABLE_IT(huart1.hdmarx, DMA_IT_HT);
之后我们需要开始编写事件回调函数,当硬件检测到"空闲"或"缓冲区满"时,会自动跳入此函数。
c
/* * 说明:Size 参数是由 HAL 库自动计算的,代表本次实际收到的字节数。
*/
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart->Instance == USART1)
{
// --- 业务处理开始 ---
// 此时数据已完整存放在 rx_buffer 中,长度为 Size
HAL_UART_Transmit(&huart1, rx_buffer, Size, 100); // 示例:回传数据
// --- 业务处理结束 ---
// --- 关键:手动重启接收 ---
// 在 Normal 模式下,回调结束后接收会停止。必须再次调用以启动下一包的监听。
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buffer, sizeof(rx_buffer));
__HAL_DMA_DISABLE_IT(huart1.hdmarx, DMA_IT_HT); // 记得再次关闭半满中断
}
}
建议在空闲中断中,将数据通过 memcpy 函数拷贝到 FIFO 或另一个缓冲区,然后立即重启 DMA。不要在中断处理。
注意事项
如果你发送的数据长度超过了 rx_buffer 的设定值(例如发送 20 字节,Buffer 只有 10 字节),你会发现回调函数进入了两次。
- 第一次触发(DMA 满中断):前 10 字节填满缓冲区,DMA 停止并触发回调,Size = 10。
- 第二次触发(空闲中断):你在回调里重启了接收,后 10 字节继续进入,发完后总线空闲,再次触发回调,Size = 10。
解决方案:请确保 rx_buffer 的长度大于你预期单包数据的最大长度。
健壮性优化
在工业现场,电磁干扰可能触发 Overrun (ORE) 错误,导致 DMA 接收永久停止。必须重写错误回调函数:
c
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
// 清除溢出错误标志(HAL库内部通常已处理,但显式重启是必要的)
// 重启接收以恢复通信
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buffer, sizeof(rx_buffer));
__HAL_DMA_DISABLE_IT(huart1.hdmarx, DMA_IT_HT);
}
}