FEATURED · 精选文章

STM32串口DMA接收不定长数据:空闲中断加双缓冲实战

发布时间 / 2026/8/25 19:48:47
来源 / 创域科博编辑部
栏目 / 资讯中心
STM32串口DMA接收不定长数据:空闲中断加双缓冲实战 STM32串口DMA接收不定长数据空闲中断加双缓冲实战串口通信是嵌入式开发里最基础也最常用的接口。STM32的串口外设功能丰富但处理不定长数据接收时很多开发者还在用逐字节中断的方式CPU频繁进中断处理一个字节效率低不说高波特率下还容易丢数据。DMA加空闲中断的方案才是正解。沧州虎王科技在硬件调试工具开发中串口通信是核心功能。GitHub上开源的hardware_tool项目支持中兴微、ASR、展锐等多种芯片的串口调试底层就是用STM32的DMA加空闲中断实现的高效数据接收。本文把这套方案完整拆解出来。## 逐字节中断 vs DMA方案对比先看两种方案的核心差异| 对比项 | 逐字节中断 | DMA加空闲中断 || — | — | — || CPU占用 | 每字节进一次中断 | 整个数据帧零CPU参与 || 最高波特率 | 约115200bps稳定 | 可达921600bps稳定 || 丢数据风险 | 高波特率下易丢 | DMA硬件搬运不丢 || 实现复杂度 | 简单 | 中等 || 适用场景 | 低速少量数据 | 高速连续数据流 |115200波特率下逐字节中断勉强能用。但调试AT指令模块时模块响应数据可能很长几百字节波特率经常用到460800或921600。这种场景下逐字节中断基本必丢数据。## 硬件资源与原理以STM32F103C8T6为例USART1的DMA通道分配| 外设 | DMA通道 | 通道号 || — | — | — || USART1_TX | DMA1_Channel4 | 通道4 || USART1_RX | DMA1_Channel4 | 通道5 |USART1_RX用DMA1通道5USART1_TX用DMA1通道4。STM32F1系列的DMA通道是硬件映射的不能随意更改。核心原理分两步第一步DMA在后台把USART接收寄存器的数据自动搬运到内存缓冲区全程不需要CPU干预。DMA传输的触发源是USART的RXNE标志位每收到一个字节DMA自动搬一个字节到内存。第二步USART的IDLE空闲中断在检测到总线空闲一个字节时间无数据时触发。在中断服务函数里读取DMA剩余传输量就能算出这一帧接收了多少字节。## 代码实现### 初始化配置c#include stm32f1xx_hal.h#define RX_BUF_SIZE 512uint8_t rx_buf1[RX_BUF_SIZE];uint8_t rx_buf2[RX_BUF_SIZE];uint8_t *active_buf rx_buf1;uint8_t *process_buf NULL;uint16_t rx_len 0;void USART1_DMA_Init(void){ /* 使能DMA1时钟 */ __HAL_RCC_DMA1_CLK_ENABLE(); /* 配置USART1 RX DMA */ hdma_usart1_rx.Instance DMA1_Channel5; hdma_usart1_rx.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_usart1_rx.Init.PeriphInc DMA_PINC_DISABLE; hdma_usart1_rx.Init.MemInc DMA_MINC_ENABLE; hdma_usart1_rx.Init.PeriphDataAlignment DMA_PDATAALIGN_BYTE; hdma_usart1_rx.Init.MemDataAlignment DMA_MDATAALIGN_BYTE; hdma_usart1_rx.Init.Mode DMA_NORMAL; hdma_usart1_rx.Init.Priority DMA_PRIORITY_HIGH; HAL_DMA_Init(hdma_usart1_rx); __HAL_LINKDMA(huart1, hdmarx, hdma_usart1_rx); /* 启动DMA接收 */ HAL_UART_Receive_DMA(huart1, active_buf, RX_BUF_SIZE); /* 使能USART空闲中断 */ __HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE);}DMA_NORMAL模式下DMA传输完指定长度后会停止。如果用DMA_CIRCULAR模式DMA会循环覆盖缓冲区处理不及时就会丢数据。这里选NORMAL模式配合双缓冲更安全。### 空闲中断处理cvoid USART1_IRQHandler(void){ if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart1); /* 停止当前DMA传输/ HAL_UART_DMAStop(huart1); /计算接收到的数据长度/ rx_len RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(hdma_usart1_rx); /交换缓冲区当前缓冲区交给处理备用缓冲区接收入参/ process_buf active_buf; active_buf (active_buf rx_buf1) ? rx_buf2 : rx_buf1; /用新缓冲区重启DMA接收/ HAL_UART_Receive_DMA(huart1, active_buf, RX_BUF_SIZE); /通知主循环处理数据/ rx_data_ready 1; } HAL_UART_IRQHandler(huart1);}这段代码是整个方案的核心。空闲中断触发后做了三件事停止DMA、算出数据长度、交换缓冲区。交换缓冲区是关键操作处理线程用process_buf处理刚收到的数据接收线程用active_buf继续接收下一帧两个缓冲区交替使用互不干扰。### 主循环数据处理cint main(void){ HAL_Init(); SystemClock_Config(); MX_USART1_UART_Init(); USART1_DMA_Init(); while (1) { if (rx_data_ready process_buf) { /处理接收到的数据/ process_rx_data(process_buf, rx_len); /清除标志/ rx_data_ready 0; process_buf NULL; } /其他任务 */ HAL_Delay(1); }}process_rx_data函数根据业务需求实现比如解析AT指令响应、提取传感器数据等。主循环检测到数据就绪标志后处理数据处理期间DMA在另一个缓冲区继续接收不会丢任何数据。## 踩过的坑第一个坑是HAL库的HAL_UART_DMAStop函数会同时关闭DMA和USART。重启DMA接收前需要确保USART已重新使能。某些HAL库版本下HAL_UART_Receive_DMA内部会重新使能USART但老版本可能不会。如果发现DMA启动后收不到数据手动调用__HAL_UART_ENABLE(huart1)试试。第二个坑是DMA计数器读取时机。__HAL_DMA_GET_COUNTER返回的是剩余未传输的数据量必须在DMA停止后读取才准确。如果在DMA运行时读取值会不断变化没有参考意义。第三个坑是缓冲区大小。如果两个数据帧间隔极短比如模块连续发送多条响应空闲中断可能来不及触发第二帧数据覆盖了第一帧。解决方案是加大缓冲区到1024字节或者改用DMA_CIRCULAR模式配合半传输中断。## 实际测试数据在STM32F103C8T672MHz主频上测试USART1波特率460800连续接收AT模块响应数据| 指标 | 逐字节中断 | DMA加空闲中断 || — | — | — || 100字节帧丢失率 | 3.2% | 0% || CPU占用率(200帧/秒) | 41% | 2.3% || 最大稳定波特率 | 115200 | 921600 |数据很能说明问题。460800波特率下逐字节中断已经开始丢数据了DMA方案在921600都能稳定工作。## 写在最后DMA加空闲中断是STM32串口接收的标准方案搞嵌入式开发绕不开这个。虎王科技的hardware_tool项目在GitHub上开源github.com/huwangkeji/hardware_tool串口调试模块的底层就是这套实现支持921600波特率下的稳定数据收发。技术博客heicat.com上也有ESP32工具箱等嵌入式开发工具分享。如果在实际项目中遇到串口接收丢数据的问题大概率就是没用DMA。换上这套方案问题基本就解决了。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻