# STM32F4 DMA双缓冲环形队列接收不定长串口数据 ## 一、为什么需要双缓冲与环形队列? 在STM32F4中,串口接收数据时,若使用单DMA缓冲,当数据量超过缓冲长度或处理不及时,新数据会覆盖旧数据,导致丢帧。双缓冲机制允许DMA在两个缓冲区之间交替传输,而CPU可同时处理另一缓冲区数据,避免冲突。环形队列则提供灵活的存储管理,适配不定长数据,防止缓存溢出。 ## 二、双缓冲与环形队列原理 ### 1. DMA双缓冲模式 STM32F4的DMA控制器支持双缓冲模式(Double Buffer Mode),通过设置DMA_SxCR的DBM位启用。此时,DMA在内存地址0和内存地址1之间切换,当当前缓冲区传输完成,自动切换至另一缓冲区,并触发传输完成中断(TC)。CPU可在中断中处理已完成缓冲区,而DMA继续填充另一缓冲区,实现流水线操作。 ### 2. 环形队列设计 环形队列(Ring Buffer)使用固定大小数组,通过读写指针管理数据。写指针由DMA中断更新,读指针由应用层消费。当写指针追上读指针时,队列满;读指针追上写指针时,队列空。为避免覆盖,需在写入前检查剩余空间,若不足则丢弃或报警。 ## 三、硬件与软件配置步骤 ### 1. 硬件连接 - STM32F407开发板(或其他F4系列) - USB转TTL模块连接USART2(PA2=TX,PA3=RX) - 确保共地 ### 2. 软件配置(使用STM32CubeMX + HAL库) #### 步骤1:配置USART2 - 模式:异步模式,波特率115200,8位数据,无校验,1停止位 - 开启DMA接收,选择DMA2 Stream5(USART2_RX),方向外设到内存,模式循环(Circular)或双缓冲(Double Buffer) - 注意:HAL库的DMA双缓冲需手动设置,CubeMX仅支持循环模式,但可后续修改 #### 步骤2:配置DMA 在CubeMX中,DMA设置选择USART2_RX,优先级高,数据宽度字节。生成代码后,修改DMA初始化结构体,启用双缓冲: ```c // 在main.c中修改DMA句柄 DMA_HandleTypeDef hdma_usart2_rx; // 初始化后,设置双缓冲 hdma_usart2_rx.Init.Mode = DMA_DOUBLE_BUFFER_MODE; // 双缓冲模式 hdma_usart2_rx.Init.MemoryDataAlignment = DMA_MDATAALIGN_BYTE; HAL_DMA_Init(&hdma_usart2_rx); // 设置两个缓冲区地址 HAL_DMAEx_MultiBufferStart(&hdma_usart2_rx, (uint32_t)&huart2.Instance->DR, (uint32_t)rxBuf1, (uint32_t)rxBuf2, RX_BUF_SIZE); ``` #### 步骤3:定义缓冲区与环形队列 ```c #define RX_BUF_SIZE 256 uint8_t rxBuf1[RX_BUF_SIZE]; uint8_t rxBuf2[RX_BUF_SIZE]; // 环形队列结构 typedef struct { uint8_t *buf; // 指向实际数据区(这里用两个DMA缓冲区组合) volatile uint16_t head; // 写指针(由DMA中断更新) volatile uint16_t tail; // 读指针(应用层更新) uint16_t size; // 队列总大小(2*RX_BUF_SIZE) } RingQueue; RingQueue rxQueue; ``` ## 四、完整代码实现 ### 1. 初始化与启动 ```c // 初始化环形队列 void RingQueue_Init(RingQueue *q, uint8_t *buf, uint16_t size) { q->buf = buf; q->head = 0; q->tail = 0; q->size = size; } // 启动DMA接收 void UART_RX_Start(void) { // 将两个DMA缓冲区视为连续内存,但实际不连续,需特殊处理 // 这里采用简单方法:使用一个大的静态数组,然后让DMA交替写入不同区域 // 为简化,我们使用两个独立缓冲区,但环形队列逻辑需调整 // 推荐:使用一个2*RX_BUF_SIZE的数组,DMA双缓冲时,地址0指向数组前半,地址1指向后半 // 但DMA要求内存地址连续,双缓冲模式允许两个独立地址,故环形队列需分别管理 // 此处采用更简单方案:使用循环DMA模式(单缓冲),配合空闲中断,但本文重点双缓冲,故继续 // 启动双缓冲DMA HAL_UART_Receive_DMA(&huart2, rxBuf1, RX_BUF_SIZE); // 第一次启动,实际会使用双缓冲配置 // 注意:HAL_UART_Receive_DMA内部会调用HAL_DMA_Start_IT,但双缓冲需用HAL_DMAEx_MultiBufferStart // 因此需手动调用 HAL_DMAEx_MultiBufferStart(&hdma_usart2_rx, (uint32_t)&huart2.Instance->DR, (uint32_t)rxBuf1, (uint32_t)rxBuf2, RX_BUF_SIZE); __HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE); // 使能空闲中断,用于判断帧结束 } ``` ### 2. DMA中断回调 ```c // 在stm32f4xx_it.c中调用HAL_DMA_IRQHandler,然后重写回调 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART2) { // 判断当前DMA使用哪个缓冲区(通过DMA的NDTR或当前内存地址) uint32_t curMemAddr = (uint32_t)hdma_usart2_rx.Instance->M0AR; // 当前内存地址 uint8_t *completedBuf; if (curMemAddr == (uint32_t)rxBuf1) { completedBuf = rxBuf2; // 因为DMA刚切换到buf2,所以buf1已完成 } else { completedBuf = rxBuf1; } // 将数据写入环形队列(这里需注意,环形队列需支持两个独立缓冲区,简化处理:直接复制到队列) // 实际应用中,可让环形队列的buf指向一个连续区域,DMA双缓冲的每个缓冲区独立,但可复制到队列 // 为高效,可设计双缓冲环形队列,但复杂。此处演示复制法: for (int i = 0; i < RX_BUF_SIZE; i++) { if (RingQueue_IsFull(&rxQueue)) { // 队列满,丢弃数据或置溢出标志 break; } rxQueue.buf[rxQueue.head] = completedBuf[i]; rxQueue.head = (rxQueue.head + 1) % rxQueue.size; } // 重新启动DMA(双缓冲模式会自动切换,无需重启,但需确保NDTR重新加载) // 在双缓冲模式下,DMA自动切换,无需额外操作,但需注意:当缓冲区传输完成,DMA会停在另一缓冲区,等待下一次触发? // 实际上,双缓冲模式在传输完成后,DMA会自动切换并继续传输,但需要重新设置NDTR? // 正确做法:在回调中重新调用HAL_DMAEx_MultiBufferStart,但这样会重置,导致丢数据。 // 更好的方法是使用循环双缓冲,但STM32F4不支持,因此需在回调中重新配置。 // 这里简化:使用循环DMA模式(单缓冲)更简单,但本文主题双缓冲,故采用手动重启。 // 注意:重启前需确保DMA已停止,否则冲突。 HAL_DMA_Abort(&hdma_usart2_rx); HAL_DMAEx_MultiBufferStart(&hdma_usart2_rx, (uint32_t)&huart2.Instance->DR, (uint32_t)rxBuf1, (uint32_t)rxBuf2, RX_BUF_SIZE); } } ``` ### 3. 空闲中断处理(用于不定长帧) ```c // 在USART2_IRQHandler中调用HAL_UART_IRQHandler,然后重写 void HAL_UART_IdleCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART2) { // 清除空闲标志 __HAL_UART_CLEAR_IDLEFLAG(&huart2); // 此时DMA可能正在接收,但空闲表示一帧结束,可处理当前缓冲区数据 // 但双缓冲下,可能两个缓冲区都有数据,需全部处理。 // 简单做法:停止DMA,处理所有数据,然后重启。 HAL_DMA_Abort(&hdma_usart2_rx); // 处理两个缓冲区中已接收的数据(通过NDTR判断实际接收长度) uint16_t ndtr1 = __HAL_DMA_GET_COUNTER(&hdma_usart2_rx); // 当前缓冲区剩余 // 但双缓冲下,NDTR只反映当前缓冲区,需结合状态。 // 为简化,这里不深入,实际应用建议使用循环DMA+空闲中断。 // 重启DMA HAL_DMAEx_MultiBufferStart(&hdma_usart2_rx, (uint32_t)&huart2.Instance->DR, (uint32_t)rxBuf1, (uint32_t)rxBuf2, RX_BUF_SIZE); } } ``` ### 4. 应用层读取数据 ```c uint16_t RingQueue_Read(RingQueue *q, uint8_t *outBuf, uint16_t len) { uint16_t cnt = 0; while (cnt < len && q->tail != q->head) { outBuf[cnt++] = q->buf[q->tail]; q->tail = (q->tail + 1) % q->size; } return cnt; } ``` ## 五、注意事项与优化建议 - **双缓冲模式限制**:STM32F4的DMA双缓冲模式在每次传输完成时需重新配置,否则不会自动连续传输,这会导致中断频繁且可能丢数据。实际工程中,更推荐使用**循环DMA模式**(单缓冲)配合空闲中断,或使用FIFO(如STM32F7/F4的FIFO)实现双缓冲效果。 - **缓存覆盖问题**:在双缓冲回调中,若处理时间过长,DMA可能已覆盖下一缓冲区,因此需保证处理速度。可考虑将数据复制到更大的队列,或使用双缓冲+乒乓处理。 - **丢帧问题**:空闲中断用于帧结束判断,但若数据连续无空闲,可能无法触发。可结合超时定时器(如定时器中断)检测帧间隔。 - **内存对齐**:DMA缓冲区需按1字节对齐(或4字节,取决于外设),但建议使用`__attribute__((aligned(4)))`。 - **中断优先级**:DMA中断和串口中断优先级需合理设置,避免嵌套冲突。 - **HAL库版本**:不同HAL库版本对双缓冲支持不同,建议查阅参考手册。 ## 六、总结 本文介绍了STM32F4使用DMA双缓冲环形队列接收不定长串口数据的方法,但需注意双缓冲模式在F4上的局限性。实际开发中,更稳健的方案是采用**循环DMA+空闲中断**,或使用**DMA传输完成中断+环形队列**。核心思想是:利用DMA减轻CPU负担,通过环形队列管理数据,结合空闲中断识别帧边界,从而避免缓存覆盖与丢帧。希望本文能为你提供有价值的参考。