CUBEMX+STM32+HAL库 串口DMA空闲中断任意数据收发
前言
本文主要介绍串口的DMA数据搬运与空闲中断的使用,不讨论阻塞式串口收发和普通中断串口收发。
若无特殊说明,串口配置默认为115200 Baud,8位数据位,1位停止位,无校验位。
DMA?空闲中断?
-
DMA(Direct Memory Access,直接内存访问):一种数据传输机制,允许数据在外设和内存之间直接传输,无需CPU介入。
-
串口+DMA:在串口收发数据的同时,单片机可以执行其他任务。
-
空闲中断:当总线在一个字节时间内未接收到数据时触发中断,表示数据接收完毕。
CUBEMX配置
新建工程
新建CUBEMX工程(略),打开串口配置页面,选择异步模式,其余配置保持默认。

添加DMA
在DMA配置页面,添加RX和TX的DMA通道,模式选择Normal。

Normal和Circular模式的区别:
- Normal(默认/单次):DMA传输完成后停止工作,需要重新启动才能开启下一次传输。
- Circular(循环):DMA会持续工作(自动重新启动),不断搬运数据。
大多数场景下,我们需要对串口数据进行后处理,因此中断尤为重要。本文使用空闲中断,在接收完一轮数据后进入中断处理,处理完成后手动开启下一次DMA接收,因此应配置为Normal模式。
配置中断
在NVIC配置页面,开启串口和DMA的全局中断。
配置完DMA后通常会自动开启。
生成代码
配置工程名、Code Generator等基本设置,生成代码。

Keil代码
函数介绍
HAL库的DMA串口收发函数如下:
1 | HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size) |
其中,三个参数分别为串口号、首位发送数据地址、数据长度。
示例:
1 | HAL_UART_Transmit_DMA(&huart1, tx_buf, sizeof(tx_buf)); |
初始化
- 定义接收数据缓存区:
1 | /* USER CODE BEGIN PV */ |
变量名和大小请根据实际需求修改。
- 开启DMA接收与空闲中断:
1 | /* USER CODE BEGIN 2 */ |
- 关闭半满中断:
1 | __HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT); |
在HAL库中,调用HAL_UARTEx_ReceiveToIdle_DMA函数后会自动开启半满中断,但大多数场景下仅需传输完一组数据后的空闲中断,因此需手动关闭半满中断。
需要注意,hdma_usart1_rx仅在usart.c文件中定义,未在usart.h中声明,直接在main.c中使用会报错。因此需在usart.h中添加以下代码:
1 | /* USER CODE BEGIN Private defines */ |
重写中断回调函数
中断回调函数在接收一轮数据后触发,数据处理逻辑通常放在此处。在main.c中添加以下代码:
1 | /* USER CODE BEGIN 4 */ |
开启下一轮接收
在stm32fxxx_it.c文件中的以下位置,添加两行代码:
1 | /** |
由于rx_buf仅在main.c中定义,因此需在stm32fxxx_it.c中引用:
1 | /* USER CODE BEGIN PV */ |
- 注意:重新开启DMA接收与空闲中断的函数必须放在USART1_IRQHandler中,不能放在HAL_UARTEx_RxEventCallback回调函数中,具体原因将在文章最后说明。
重写报错回调函数
通常情况下,当串口发生错误时,会进入此回调函数,用户可以在函数中进行错误处理、重新开启串口等操作,在main.c中添加以下代码:
1 | /* USER CODE BEGIN 4 */ |
大坑!
前文 开启下一轮接收 部分提到,重新开启的函数必须放在stm32fxxx_it.c文件下的USART1_IRQHandler函数中。
很多人习惯在回调函数的最后直接开启下一轮串口接收,从直观感受上似乎也说得通。
然而,当串口波特率偶然发生变化时,将不会进入回调函数,此轮接收结束后,串口空闲中断和DMA将被关闭。又由于无法进入回调函数,串口空闲中断和DMA无法被重新开启,导致串口接收永久失效!
如果将重启函数写在USART1_IRQHandler函数中,即使串口波特率发生变化,USART1_IRQHandler函数也会被调用,串口空闲中断和DMA会被重新开启,不影响下一轮数据接收。
有人可能会说:“我保持一个波特率不就好了?”但实际上并不容易。此处 串口波特率偶然发生变化 不仅指用户手动修改波特率,MCU和外设初始化时、设备受到较大震动、串口线缆松动等情况都有可能导致波特率偶然出错。
举两个惨痛教训:
-
笔者所在的实验室在调试一套激光定位系统时,分控板连接了4个激光测距模块,有时分控板会突然停止吐数据,发送引脚无任何波形,但进入debug发现串口缓冲区仍有激光的数据更新。当时查了好久,而且一进debug问题就消失了(之后分析是因为进debug使串口被重新初始化,重新打开了接收),最后终于发现没有进回调函数,才醒悟过来是这个问题。推测是定位系统在高速运动时受到震动,串口波特率不稳定。
-
大一某次使用串口屏时,若MCU和串口屏同时上电,表现出串口屏触摸无法被单片机读取的情况;按一下MCU的rst或者进入debug,问题就解决了。当时使用了上电后连续初始化数次,每次间隔一段时间这种蠢办法,才临时解决了这个问题(之后看其实是上电瞬间MCU和串口屏两边波特率没对上,连续初始化的时间让MCU端跨过了这个时间段),其实也是因为重新开启的函数放错位置才导致此问题。
关于这个坑的详细描述及测试,可以参考 《HAL库 串口空闲中断+DMA接收不定长数据 详解及踩坑》 这篇文章,笔者也是有幸读到这篇文章才第一次意识到这个问题。


