串口DMA模式与不定长数据收发

串口:DMA模式与收发不定长数据

一、两个正交的维度

串口收发涉及两个可以自由组合的维度:

数据传输方式(3种,CPU参与程度从高到低):阻塞 / 中断(_IT) / DMA(_DMA)
                    ×
接收长度判断方式(2种):定长(Receive) / 不定长(ReceiveToIdle)

二、维度1:数据传输方式

模式

函数后缀

原理

特点

阻塞(Blocking / Polling)

无后缀,如 HAL_UART_Receive

CPU死等,不停轮询状态寄存器,直到收发完成才继续

最费CPU

中断(Interrupt)

_IT,如 HAL_UART_Receive_IT

每收/发一个字节触发一次中断,把CPU叫回来搬一个字节

比阻塞省CPU,但字节多时中断很频繁

DMA

_DMA,如 HAL_UART_Receive_DMA

DMA硬件自动搬运数据,全部搬完才触发一次中断通知CPU

最省CPU

DMA模式配置步骤

  1. CubeMX中打开UART配置界面 → DMA Settings → 点击 Add,分别为TX和RX添加DMA通道。

  2. 关键参数说明:

    • 方向:TX方向为"内存→外设",RX方向为"外设→内存"

    • 地址自增:外设端(寄存器)不自增(只有1字节长度);内存端自增(依次存到数组不同位置)

    • 数据宽度:一般为1字节

    • 模式:Normal(正常模式)即可,Circular(循环模式)留待更复杂场景

  3. 生成代码后,把发送/接收函数的后缀从 _IT 改为 _DMA 即可,其余HAL库已封装好。

  4. 注意:即使用了DMA,回调(如 RxCpltCallback)仍然是通过中断触发的——只是触发源从"串口中断"变成了"DMA传输完成中断"。


三、维度2:接收长度判断方式

定长接收(之前学过)

  • 函数:HAL_UART_Receive / _IT / _DMA

  • 使用前需提前告诉函数要接收的字节数,收满才触发 HAL_UART_RxCpltCallback

不定长接收(本期新内容)

  • 函数:HAL_UARTEx_ReceiveToIdle / _IT / _DMA

  • 原理:依靠串口空闲中断——只要RX引脚上一段时间没有新数据进来(即从"忙碌"转为"空闲"),就认为一帧数据接收完成,触发回调。这个触发条件与接收字节数无关。

  • 与定长接收的区别:

    1. 第三个参数含义不同:不是"要接收的长度",而是"一次最多能接收的长度"(数组要开大一点,或用 sizeof 获取长度,防止越界)。

    2. 回调函数不同:不是 HAL_UART_RxCpltCallback,而是 HAL_UARTEx_RxEventCallback,多了一个 Size 入参,用来告诉你本次实际收到了几个字节。

    3. 别忘了在回调函数里重新启动一次接收

    4. 好习惯:在回调里先判断是哪个串口(huart 指针)触发的,再处理。


四、DMA + 不定长接收 的坑:传输过半中断

使用 HAL_UARTEx_ReceiveToIdle_DMA 时,除了空闲中断会触发 RxEventCallbackDMA的"传输过半中断"也会触发同一个回调——即接收字节数达到设定最大长度的一半时,就会提前触发一次回调,此时数据实际上还没收完,容易被误当作"一帧完整数据"处理。

解决方法:关闭DMA的传输过半中断。

HAL_DMA_DisableIT(&hdma_usart2_rx, DMA_IT_HT);
  • 第一个参数:DMA通道指针(这里是UART2的RX DMA通道)

  • 第二个参数:要关闭的中断类型,DMA_IT_HT 即"过半中断"(Half Transfer)

  • 注意:程序开始时、以及每次重启接收后都要关闭一次。

之所以之前测试没发现这个问题,是因为接收缓冲区开得比较大、发送的数据比较短,没达到"过半"的阈值。


五、常用函数速查表

场景

函数

定长 + 阻塞

HAL_UART_Receive

定长 + 中断

HAL_UART_Receive_IT

定长 + DMA

HAL_UART_Receive_DMA

不定长 + 阻塞

HAL_UARTEx_ReceiveToIdle

不定长 + 中断

HAL_UARTEx_ReceiveToIdle_IT

不定长 + DMA(本期重点)

HAL_UARTEx_ReceiveToIdle_DMA


六、一句话总结

想省CPU → 把 _IT 改成 _DMA(并在CubeMX里加DMA通道);
想收不定长数据 → 把 Receive 改成 ReceiveToIdle,回调函数从 RxCpltCallback 改成 RxEventCallback
两者可以叠加使用(ReceiveToIdle_DMA),但要记得关闭DMA的"传输过半中断"(DMA_IT_HT),否则数据未收完就可能被提前处理。