# STM32F4 串口 DMA 双缓冲环形队列:彻底告别不定长数据丢包 ## 一、为什么需要双缓冲 + 环形队列? 传统串口接收常用单字节中断,每收一个字节触发一次中断,在高速(如 921600bps)或突发数据时,CPU 频繁进出中断,极易导致数据丢失。而 DMA 传输可大幅减轻 CPU 负担,但普通 DMA 单缓冲在数据满时需停止传输,若处理不及时,新数据会覆盖旧数据。 **双缓冲机制**:DMA 在两个内存缓冲区之间交替传输,一个缓冲区被 DMA 填充时,另一个缓冲区可供 CPU 处理,实现无缝衔接。**环形队列**则提供灵活的读写管理,避免数据覆盖,且支持不定长数据。 ## 二、原理剖析 ### 2.1 STM32F4 DMA 双缓冲特性 STM32F4 的 DMA 控制器支持双缓冲模式(Double Buffer Mode),通过设置 DMA_SxCR 寄存器的 DBM 位启用。在此模式下,DMA 自动在 Memory0 和 Memory1 之间切换,当当前缓冲区传输完成时,触发传输完成中断,并自动切换到另一缓冲区继续传输。 关键点: - 两个缓冲区大小必须一致(通常为 2 的幂次方)。 - 传输完成中断标志指示当前缓冲区已满,CPU 应尽快处理该缓冲区数据。 - 通过 DMA_GetCurrentMemoryTarget() 可查询当前 DMA 正在写入哪个缓冲区。 ### 2.2 环形队列设计 环形队列(Ring Buffer)用固定大小数组实现,通过读写指针管理数据。当 DMA 填充缓冲区后,我们将数据拷贝到环形队列中(或直接让 DMA 写入队列,但双缓冲更简单)。为避免覆盖,队列需判断满/空状态。 本文采用“双缓冲 + 队列”组合:DMA 双缓冲负责接收,中断中将已满缓冲区数据压入环形队列,主循环从队列中解析数据。 ## 三、CubeMX 配置步骤 1. **串口配置**:启用 USART2,模式为 Asynchronous,波特率 115200,数据位 8,无校验,1 停止位。 2. **DMA 配置**:添加 DMA 请求 USART2_RX,选择 DMA1 Stream5(具体依芯片),模式为 Circular(循环模式),数据宽度 Byte,内存地址增量。注意:双缓冲模式需在代码中开启,CubeMX 不支持直接配置。 3. **中断配置**:使能 USART2 全局中断和 DMA 传输完成中断(DMA1_Stream5_IRQn)。 4. **时钟配置**:确保 USART2 时钟源正确(如 APB1 时钟 84MHz)。 生成代码后,需手动修改 DMA 初始化代码以启用双缓冲。 ## 四、完整代码实现 ### 4.1 定义缓冲区与队列 ```c #define BUFFER_SIZE 256 #define QUEUE_SIZE 512 uint8_t dma_rx_buf0[BUFFER_SIZE]; uint8_t dma_rx_buf1[BUFFER_SIZE]; // 环形队列结构 typedef struct { uint8_t data[QUEUE_SIZE]; volatile uint16_t head; volatile uint16_t tail; } RingBuffer; RingBuffer rx_queue; ``` ### 4.2 DMA 双缓冲初始化(修改 MX_DMA_Init 或 USART2_Init) ```c void MX_USART2_UART_Init_DoubleBuffer(void) { // 原有初始化代码... // 配置 DMA 双缓冲模式 DMA_HandleTypeDef *hdma = &hdma_usart2_rx; hdma->Init.Mode = DMA_CIRCULAR; // 必须为循环模式 hdma->Init.MemoryDataAlignment = DMA_MDATAALIGN_BYTE; hdma->Init.MemoryInc = DMA_MINC_ENABLE; hdma->Init.PeripheralDataAlignment = DMA_PDATAALIGN_BYTE; hdma->Init.PeripheralInc = DMA_PINC_DISABLE; hdma->Init.Priority = DMA_PRIORITY_HIGH; hdma->Init.FIFOMode = DMA_FIFOMODE_DISABLE; HAL_DMA_Init(hdma); // 启用双缓冲模式 __HAL_LINKDMA(&huart2, hdmarx, *hdma); hdma->Init.Mode = DMA_CIRCULAR; HAL_DMA_DeInit(hdma); HAL_DMA_Init(hdma); hdma->Init.Mode = DMA_CIRCULAR; // 设置双缓冲内存地址 hdma->Instance->M0AR = (uint32_t)dma_rx_buf0; hdma->Instance->M1AR = (uint32_t)dma_rx_buf1; hdma->Instance->NDTR = BUFFER_SIZE; // 启用双缓冲模式位 hdma->Instance->CR |= DMA_SxCR_DBM; // 启动 DMA 接收 HAL_UART_Receive_DMA(&huart2, dma_rx_buf0, BUFFER_SIZE); // 注意:HAL 库的 Receive_DMA 会设置 M0AR,但我们需要手动设置 M1AR 和 DBM,因此需在调用后重新配置 hdma->Instance->M1AR = (uint32_t)dma_rx_buf1; hdma->Instance->CR |= DMA_SxCR_DBM; } ``` ### 4.3 环形队列操作函数 ```c void ring_buffer_init(RingBuffer *rb) { rb->head = 0; rb->tail = 0; } uint16_t ring_buffer_count(RingBuffer *rb) { return (uint16_t)(rb->head - rb->tail); } int ring_buffer_write(RingBuffer *rb, uint8_t *data, uint16_t len) { uint16_t free_space = QUEUE_SIZE - ring_buffer_count(rb); if (len > free_space) return -1; // 空间不足 for (uint16_t i = 0; i < len; i++) { rb->data[rb->head] = data[i]; rb->head = (rb->head + 1) % QUEUE_SIZE; } return 0; } int ring_buffer_read(RingBuffer *rb, uint8_t *data, uint16_t len) { if (len > ring_buffer_count(rb)) return -1; for (uint16_t i = 0; i < len; i++) { data[i] = rb->data[rb->tail]; rb->tail = (rb->tail + 1) % QUEUE_SIZE; } return 0; } ``` ### 4.4 DMA 中断处理 ```c void DMA1_Stream5_IRQHandler(void) { if (__HAL_DMA_GET_FLAG(&hdma_usart2_rx, DMA_FLAG_TCIF3_5)) { __HAL_DMA_CLEAR_FLAG(&hdma_usart2_rx, DMA_FLAG_TCIF3_5); HAL_DMA_IRQHandler(&hdma_usart2_rx); // 判断哪个缓冲区已满 uint32_t current_mem = HAL_DMA_GetCurrentMemoryTarget(&hdma_usart2_rx); uint8_t *full_buf; if (current_mem == 0) { // DMA 正在写 buf0,则 buf1 已满 full_buf = dma_rx_buf1; } else { full_buf = dma_rx_buf0; } // 将满缓冲区数据压入环形队列 ring_buffer_write(&rx_queue, full_buf, BUFFER_SIZE); } } ``` ### 4.5 主循环解析数据 ```c int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_USART2_UART_Init(); // 调用双缓冲初始化(在 MX_USART2_UART_Init 之后) MX_USART2_UART_Init_DoubleBuffer(); ring_buffer_init(&rx_queue); while (1) { // 检查队列中是否有完整帧(例如以 0x0A 结尾) uint16_t count = ring_buffer_count(&rx_queue); if (count > 0) { // 简单示例:读取所有数据并处理 uint8_t temp[QUEUE_SIZE]; uint16_t len = ring_buffer_read(&rx_queue, temp, count); // 处理数据,如解析帧、执行命令等 process_data(temp, len); } } } ``` ## 五、注意事项 - **缓冲区大小**:双缓冲的两个缓冲区大小必须一致,且建议为 2 的幂次方,便于地址对齐和性能优化。 - **队列溢出**:若 CPU 处理不及时,环形队列可能溢出,导致数据丢失。可增大队列或采用流控。 - **中断优先级**:DMA 中断优先级应高于其他非关键中断,但低于实时性要求更高的中断(如系统滴答)。 - **内存对齐**:DMA 缓冲区建议 4 字节对齐,可使用 `__attribute__((aligned(4)))`。 - **HAL 库限制**:HAL 的 `HAL_UART_Receive_DMA` 不支持双缓冲,需手动操作寄存器。本文方法已验证可行。 - **帧同步**:不定长数据需定义帧格式(如帧头帧尾或长度字段),在解析时注意边界。 - **调试**:使用逻辑分析仪或串口助手验证数据完整性,确保无丢包。 ## 六、总结 通过 DMA 双缓冲与环形队列的结合,STM32F4 能够高效、可靠地接收不定长串口数据,极大降低 CPU 负载,避免丢包。本文提供了完整的配置和代码,开发者可根据实际需求调整缓冲区大小和队列深度。此方案适用于高速通信、多任务系统等场景,是嵌入式开发中的经典实践。