# STM32F4 DMA+双缓冲串口接收:帧边界与超时判定的精准实现 ## 1. 为什么需要DMA+双缓冲? 在串口通信中,不定长数据的接收是常见需求。传统中断逐字节接收会频繁打断CPU,尤其在高速率(如921600bps)下,CPU占用率极高。DMA(直接内存访问)可自动将数据从外设搬运到内存,配合双缓冲机制,能实现数据接收与处理的流水线化,大幅降低CPU负载。 双缓冲的核心思想是:两个缓冲区交替使用,一个用于DMA写入,另一个供应用程序读取,从而避免数据覆盖和等待。 ## 2. 帧边界识别与超时判定的挑战 不定长数据意味着接收方必须自行判断一帧数据的结束。常见方法有: - **空闲中断(IDLE)**:当总线空闲一个字节时间后触发,适合帧间间隔固定的场景。 - **超时判定**:通过定时器监测接收间隔,超过阈值则认为帧结束,适用于帧间隔不固定的场景。 但DMA+双缓冲下,数据可能跨缓冲区,且空闲中断与DMA传输完成中断的时序关系复杂,需精心设计才能避免丢帧或错帧。 ## 3. 硬件与软件架构 ### 3.1 硬件资源 - STM32F407(或其他F4系列) - USART1(PA9/PA10) - DMA2 Stream2(USART1_RX) - TIM2(用于超时检测) ### 3.2 软件设计思路 1. 配置USART1的DMA接收,使用双缓冲模式(DMA_CTRL_MEMORY_INC | DMA_CTRL_CIRCULAR)。 2. 使能USART1的空闲中断(IDLE)。 3. 在空闲中断中,计算当前DMA接收位置,提取一帧数据,并切换缓冲区。 4. 同时启动一个定时器(如TIM2)作为超时保护,若在设定时间内未收到新数据,则强制结束当前帧。 ## 4. 关键配置步骤 ### 4.1 初始化DMA双缓冲 ```c // 定义双缓冲区,大小256字节 uint8_t dma_buf1[256]; uint8_t dma_buf2[256]; void DMA_Config(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); DMA_DeInit(DMA2_Stream2); DMA_InitStructure.DMA_Channel = DMA_Channel_4; // USART1_RX DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)dma_buf1; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory; DMA_InitStructure.DMA_BufferSize = 256; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环模式,实现双缓冲 DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full; DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single; DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; DMA_Init(DMA2_Stream2, &DMA_InitStructure); // 使能DMA传输完成中断(用于切换缓冲区) DMA_ITConfig(DMA2_Stream2, DMA_IT_TC, ENABLE); DMA_Cmd(DMA2_Stream2, ENABLE); } ``` ### 4.2 配置USART1空闲中断 ```c void USART_Config(void) { USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); USART_InitStructure.USART_BaudRate = 115200; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_RX | USART_Mode_TX; USART_Init(USART1, &USART_InitStructure); // 使能空闲中断 USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); USART_DMACmd(USART1, USART_DMAReq_RX, ENABLE); USART_Cmd(USART1, ENABLE); } ``` ### 4.3 定时器超时检测(TIM2) ```c void TIM2_Config(void) { TIM_TimeBaseInitTypeDef TIM_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_InitStructure.TIM_Period = 1000; // 1ms @ 1MHz计数 TIM_InitStructure.TIM_Prescaler = 84 - 1; // 84MHz/84 = 1MHz TIM_InitStructure.TIM_ClockDivision = 0; TIM_InitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_InitStructure); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); TIM_Cmd(TIM2, DISABLE); // 初始关闭 } ``` ## 5. 帧处理逻辑与代码实现 核心思想:在空闲中断中,根据DMA当前计数位置,确定数据长度,并将数据从当前缓冲区拷贝到应用层。同时,启动定时器作为超时保护,若定时器溢出(即超时),则强制结束当前帧。 ### 5.1 全局变量与状态机 ```c #define BUF_SIZE 256 volatile uint8_t rx_complete_flag = 0; // 帧完成标志 volatile uint16_t rx_len = 0; // 当前帧长度 volatile uint8_t *current_buf; // 当前DMA写入的缓冲区 // 缓冲区索引管理 volatile uint8_t buf_index = 0; // 0: buf1, 1: buf2 ``` ### 5.2 空闲中断处理 ```c void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_IDLE)) { USART_ClearITPendingBit(USART1, USART_IT_IDLE); // 读取DMA剩余数据计数 uint16_t remaining = DMA_GetCurrDataCounter(DMA2_Stream2); uint16_t received = BUF_SIZE - remaining; // 本次接收到的字节数 // 确定当前缓冲区 if (buf_index == 0) { current_buf = dma_buf1; } else { current_buf = dma_buf2; } // 如果接收长度大于0,则处理帧 if (received > 0) { rx_len = received; // 此处可调用帧处理函数,例如:ProcessFrame(current_buf, rx_len); rx_complete_flag = 1; } // 切换缓冲区(双缓冲) buf_index ^= 1; if (buf_index == 0) { DMA_SetCurrDataCounter(DMA2_Stream2, BUF_SIZE); DMA_MemoryTargetConfig(DMA2_Stream2, (uint32_t)dma_buf1, DMA_Memory_0); } else { DMA_SetCurrDataCounter(DMA2_Stream2, BUF_SIZE); DMA_MemoryTargetConfig(DMA2_Stream2, (uint32_t)dma_buf2, DMA_Memory_0); } // 启动超时定时器(例如10ms) TIM_SetCounter(TIM2, 0); TIM_Cmd(TIM2, ENABLE); } } ``` ### 5.3 定时器超时处理 ```c void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update)) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); TIM_Cmd(TIM2, DISABLE); // 超时未收到新数据,强制结束当前帧 if (rx_complete_flag == 0) { // 处理残留在缓冲区中的数据(可能不完整) // 例如:丢弃或作为错误处理 rx_len = 0; } } } ``` ### 5.4 主循环处理帧数据 ```c int main(void) { // 初始化... while (1) { if (rx_complete_flag) { rx_complete_flag = 0; // 处理帧数据,current_buf指向有效数据 ProcessFrame(current_buf, rx_len); } } } ``` ## 6. 注意事项与常见坑点 - **缓冲区大小与DMA计数**:DMA的剩余计数在空闲中断时读取,注意在切换缓冲区后必须重新设置计数,否则会错乱。 - **双缓冲切换时机**:必须在空闲中断中完成切换,避免DMA正在写入时切换导致数据错位。 - **超时时间选择**:应根据波特率和帧间隔设置,一般取帧间隔的2-3倍,过短会误判,过长影响实时性。 - **中断优先级**:USART空闲中断优先级应高于定时器,避免超时中断抢占导致帧丢失。 - **DMA传输完成中断**:在循环模式下,传输完成中断会在缓冲区满时触发,但通常我们不依赖它,而是用空闲中断。若需处理半满情况,可启用DMA_IT_HT。 - **数据拷贝**:在空闲中断中处理数据时,应尽快拷贝到应用层缓冲区,避免占用中断过长时间。 ## 7. 总结 通过DMA双缓冲结合空闲中断和定时器超时,我们实现了高效且可靠的不定长串口接收。该方法充分利用了硬件特性,减少了CPU干预,同时通过超时机制保证了帧边界的准确性。在实际项目中,可根据需求调整缓冲区大小和超时参数,以适应不同通信协议。希望本文能帮助你在嵌入式开发中少走弯路。