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双缓冲

// 定义双缓冲区,大小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空闲中断

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)

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 全局变量与状态机

#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 空闲中断处理

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 定时器超时处理

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 主循环处理帧数据

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干预,同时通过超时机制保证了帧边界的准确性。在实际项目中,可根据需求调整缓冲区大小和超时参数,以适应不同通信协议。希望本文能帮助你在嵌入式开发中少走弯路。