STM32 使用DMA+双缓冲实现ADC连续采样时,缓冲区切换时序错乱的根本原因与修复方法

引言

在嵌入式数据采集系统中,ADC连续采样配合DMA双缓冲(Double Buffer)是提升吞吐率、降低CPU负载的经典组合。然而,不少开发者(包括有经验的工程师)在实现时,会遇到数据错乱、缓冲区覆盖或中断丢失等问题。这些现象往往源于对DMA传输完成中断与缓冲区切换时序的误解。本文将从硬件原理出发,剖析时序错乱的根源,并给出两种经过验证的修复方案。

一、DMA+双缓冲的工作原理

1.1 基本架构

STM32的DMA控制器支持双缓冲模式(Double Buffer Mode,DBUF),常见于F4/H7系列。该模式下,DMA在内存中维护两个缓冲区(Buffer0和Buffer1),当ADC触发DMA传输时,数据交替写入两个缓冲区。

  • 硬件双缓冲:DMA自动切换目标地址,无需CPU干预。
  • 软件双缓冲:通过中断手动切换缓冲区指针。

1.2 关键中断标志

  • 半传输中断(HT):当传输达到缓冲区一半时触发,表示第一个缓冲区已满。
  • 传输完成中断(TC):当整个缓冲区(两个半区)传输完成时触发。

在双缓冲模式下,HT和TC交替出现,分别对应Buffer0和Buffer1的填充完成。

二、时序错乱的根源剖析

2.1 竞争条件(Race Condition)

最常见的问题是:在中断服务函数(ISR)中处理缓冲区数据时,主循环(或另一个中断)也在访问同一缓冲区。例如:

  • 当HT中断触发时,CPU开始处理Buffer0,但此时DMA可能已经开始向Buffer1写入数据。
  • 如果处理时间过长,DMA可能再次触发TC中断,而CPU尚未完成Buffer0的处理,导致数据覆盖。

2.2 标志位误用

许多开发者错误地使用DMA_GetFlagStatus来检测传输完成,但忽略了双缓冲模式下标志位的自动清除机制。例如,在TC中断中清除标志后,硬件可能立即再次置位,导致中断重复进入。

2.3 缓冲区大小与传输配置不匹配

若DMA配置的传输数据长度(Data Length)与实际缓冲区大小不一致,会导致半传输/全传输中断的触发位置偏移,进而造成缓冲区切换逻辑混乱。

三、修复方案一:硬件双缓冲(推荐)

3.1 原理

利用STM32硬件双缓冲特性,DMA自动交替写入两个缓冲区,并在每个缓冲区填充完成后触发中断。CPU只需在中断中处理已满的缓冲区,无需手动切换地址。

3.2 配置步骤(以STM32F407为例)

  1. 初始化ADC:配置为连续转换模式,触发方式为定时器触发或软件触发。
  2. 配置DMA
    • 选择DMA2 Stream0,通道0(ADC1)。
    • 设置数据宽度为半字(16位)。
    • 使能双缓冲模式(DMA_DoubleBufferModeConfig)。
    • 设置内存地址0和内存地址1。
    • 传输数据长度设为缓冲区大小(例如1000个半字)。
  3. 使能中断:开启HT和TC中断。
  4. 编写中断服务函数
    • 在HT中断中,处理Buffer0。
    • 在TC中断中,处理Buffer1。

3.3 代码示例

#define BUFFER_SIZE 1000
uint16_t buffer0[BUFFER_SIZE];
uint16_t buffer1[BUFFER_SIZE];
volatile uint8_t buffer_index = 0;

void DMA1_Stream0_IRQHandler(void) {
    if (DMA_GetITStatus(DMA2_Stream0, DMA_IT_HT)) {
        DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_HT);
        process_buffer(buffer0, BUFFER_SIZE); // 处理第一个缓冲区
    }
    if (DMA_GetITStatus(DMA2_Stream0, DMA_IT_TC)) {
        DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_TC);
        process_buffer(buffer1, BUFFER_SIZE); // 处理第二个缓冲区
    }
}

void process_buffer(uint16_t* buf, uint32_t len) {
    // 数据处理,例如计算平均值或存储
    for (uint32_t i = 0; i < len; i++) {
        // 处理buf[i]
    }
}

void ADC_DMA_Init(void) {
    // 省略GPIO和ADC初始化
    
    DMA_InitTypeDef DMA_InitStructure;
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
    
    DMA_DeInit(DMA2_Stream0);
    DMA_InitStructure.DMA_Channel = DMA_Channel_0;
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
    DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)buffer0;
    DMA_InitStructure.DMA_Memory1BaseAddr = (uint32_t)buffer1;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
    DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
    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_HalfFull;
    DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
    DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
    DMA_Init(DMA2_Stream0, &DMA_InitStructure);
    
    // 配置双缓冲模式
    DMA_DoubleBufferModeConfig(DMA2_Stream0, (uint32_t)buffer1, DMA_Memory_1);
    DMA_DoubleBufferModeCmd(DMA2_Stream0, ENABLE);
    
    // 使能中断
    DMA_ITConfig(DMA2_Stream0, DMA_IT_HT | DMA_IT_TC, ENABLE);
    
    // 使能DMA流
    DMA_Cmd(DMA2_Stream0, ENABLE);
}

3.4 注意事项

  • 确保中断优先级高于任何可能长时间占用CPU的任务。
  • 在中断中处理数据要尽量快速,避免阻塞。
  • 若使用RTOS,可考虑使用信号量通知任务处理,但需注意中断与任务间的同步。

四、修复方案二:软件双缓冲(适用于无硬件DBUF的型号)

4.1 原理

对于不支持硬件双缓冲的STM32(如F1系列),可采用软件双缓冲:在DMA传输完成中断中,手动切换DMA的内存地址,并处理已满的缓冲区。

4.2 配置步骤

  1. 配置DMA为循环模式,内存地址指向Buffer0。
  2. 使能传输完成中断(TC)。
  3. 在TC中断中:
    • 先处理当前缓冲区(例如Buffer0)。
    • 然后修改DMA内存地址为Buffer1,并重新设置传输长度。
    • 注意:在修改DMA配置前,必须禁用DMA,否则可能导致未定义行为。

4.3 代码示例

uint16_t buffer0[BUFFER_SIZE];
uint16_t buffer1[BUFFER_SIZE];
volatile uint8_t active_buffer = 0;

void DMA1_Channel1_IRQHandler(void) {
    if (DMA_GetITStatus(DMA1_IT_TC1)) {
        DMA_ClearITPendingBit(DMA1_IT_TC1);
        
        // 处理当前缓冲区
        if (active_buffer == 0) {
            process_buffer(buffer0, BUFFER_SIZE);
        } else {
            process_buffer(buffer1, BUFFER_SIZE);
        }
        
        // 切换缓冲区
        DMA_Cmd(DMA1_Channel1, DISABLE);
        if (active_buffer == 0) {
            DMA_SetCurrDataCounter(DMA1_Channel1, BUFFER_SIZE);
            DMA_MemoryTargetConfig(DMA1_Channel1, (uint32_t)buffer1, DMA_Memory_1);
            active_buffer = 1;
        } else {
            DMA_SetCurrDataCounter(DMA1_Channel1, BUFFER_SIZE);
            DMA_MemoryTargetConfig(DMA1_Channel1, (uint32_t)buffer0, DMA_Memory_0);
            active_buffer = 0;
        }
        DMA_Cmd(DMA1_Channel1, ENABLE);
    }
}

4.4 注意事项

  • 在禁用DMA期间,ADC可能继续产生转换数据,但不会传输,可能导致数据丢失。因此,需确保处理时间远小于采样周期。
  • 若采样率很高,建议使用硬件双缓冲或增加缓冲区深度。

五、总结

DMA+双缓冲的时序错乱,本质上是中断处理与DMA传输之间的竞争条件。硬件双缓冲通过硬件自动切换,避免了CPU干预,是最可靠的方案。软件双缓冲则需谨慎处理DMA配置的时序,适用于对成本敏感的场合。

在实际开发中,建议:

  • 优先使用硬件双缓冲(如果MCU支持)。
  • 在中断中只做数据搬移或标记,避免复杂计算。
  • 使用调试器观察缓冲区数据,验证时序正确性。

希望本文能帮助你彻底解决DMA双缓冲的时序问题,让数据采集系统稳定运行。