引言

在电机控制、振动监测等需要高速连续采样的嵌入式应用中,STM32F4的ADC+DMA+双缓冲是经典组合。然而,不少开发者发现:当DMA切换缓冲区时,偶尔会丢失一个或几个采样点,导致波形出现毛刺。本文将从STM32F4的DMA和ADC硬件行为出发,分析丢码的根因,并给出可落地的规避策略。

1. 双缓冲ADC采样的基本流程

典型配置如下:

  • ADC1 由定时器TRGO触发,采样率固定(如100kHz)。
  • DMA1 Stream0 配置为循环模式,双缓冲(Memory-to-Memory模式,但实际是外设到内存)。
  • 两个缓冲区BufferA和BufferB,每个大小N(如1024个半字)。
  • 当DMA写满一个缓冲区时,触发传输完成中断,在中断中处理已满缓冲区,同时DMA自动切换到另一个缓冲区。
// DMA配置核心代码(简化)
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_Channel = DMA_Channel_0;  // ADC1
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)BufferA;
DMA_InitStructure.DMA_BufferSize = N;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
DMA_DoubleBufferModeConfig(DMA1_Stream0, (uint32_t)BufferB, DMA_Memory_0);
DMA_DoubleBufferModeCmd(DMA1_Stream0, ENABLE);
DMA_ITConfig(DMA1_Stream0, DMA_IT_TC, ENABLE);
DMA_Cmd(DMA1_Stream0, ENABLE);

2. 丢码的根因分析

2.1 DMA传输完成中断与ADC采样的竞争

DMA在写满一个缓冲区(即传输N个数据)后,会置位传输完成标志(TCIF),并产生中断。但DMA切换到另一个缓冲区是硬件自动完成的,无需软件干预。然而,如果我们在中断服务函数(ISR)中处理数据(如拷贝、计算),而ISR执行时间过长,就可能阻塞DMA的下一次传输吗?

实际上,DMA传输由ADC的触发信号驱动,与CPU无关。即使CPU在ISR中,DMA仍可继续传输。但问题出在双缓冲切换的瞬间

  • DMA在完成第N次传输后,会立即将内存地址切换到另一个缓冲区(硬件行为)。
  • 如果此时ADC已经触发了下一次转换,而DMA的切换需要几个时钟周期(通常为2-3个AHB时钟),那么这次转换的数据可能丢失,因为DMA尚未准备好接收新数据。

2.2 双缓冲切换的固有延迟

STM32F4的DMA控制器在双缓冲模式下,当传输计数从1减到0时,会执行以下操作:

  1. 更新当前内存地址指针到另一个缓冲区。
  2. 重新加载传输计数为N。
  3. 清除TCIF标志(如果中断使能)。

这个过程需要几个AHB时钟周期。如果ADC的采样率很高(如>1Msps),且触发间隔小于DMA切换时间,那么ADC转换完成的数据可能无法及时被DMA取走,导致数据丢失。

2.3 中断处理不当导致的覆盖

另一种常见情况是:在ISR中处理BufferA时,DMA已经切换到BufferB并开始填充。如果ISR处理时间过长,且BufferB也被填满,DMA会再次切换到BufferA(此时ISR可能还没处理完BufferA),导致BufferA被覆盖,造成数据丢失。

3. 规避策略

3.1 使用定时器触发ADC,并同步DMA切换

通过定时器(如TIM2)的更新事件触发ADC,并配置DMA在传输完成时产生事件,但不在ISR中做复杂处理。更关键的是,确保ADC采样率低于DMA切换速率。对于STM32F4,DMA切换时间约为几个AHB周期(如72MHz下约50ns),因此采样间隔应大于1us(即采样率<1Msps)时,丢码风险较低。

3.2 增加FIFO缓冲(硬件FIFO)

STM32F4的DMA具有4级FIFO(每级32位)。启用FIFO模式并设置阈值,可以缓冲多个数据,避免切换瞬间的丢失。

DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Enable;
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull; // 或Full
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_INC4; // 突发传输
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;

启用FIFO后,DMA会先填充FIFO,再写入内存。当切换缓冲区时,FIFO中已有的数据可以继续写入新缓冲区,从而避免丢失。但注意,FIFO会增加延迟,且需要保证FIFO不溢出(即采样率不能过高)。

3.3 优化中断处理,使用双缓冲标志

在ISR中,仅设置标志位,将数据处理放到主循环或低优先级任务中。同时,利用DMA的当前内存地址寄存器(CMAR)判断当前正在填充哪个缓冲区,避免处理尚未填满的缓冲区。

volatile uint8_t buf_ready = 0;
void DMA1_Stream0_IRQHandler(void) {
    if (DMA_GetITStatus(DMA1_Stream0, DMA_IT_TC)) {
        DMA_ClearITPendingBit(DMA1_Stream0, DMA_IT_TC);
        // 获取当前内存地址,判断哪个缓冲区已满
        uint32_t cur_addr = DMA_GetCurrentMemoryTarget(DMA1_Stream0);
        if (cur_addr == (uint32_t)BufferA) {
            // BufferA已满,但DMA已切换到BufferB,所以处理BufferA
            process_buffer(BufferA, N);
        } else {
            process_buffer(BufferB, N);
        }
    }
}

注意:DMA_GetCurrentMemoryTarget返回的是当前正在使用的缓冲区,即DMA正在填充的缓冲区。因此,当TCIF置位时,当前缓冲区是刚切换到的那个,而另一个缓冲区才是刚填满的。上面的代码逻辑正确。

3.4 降低采样率或增加缓冲区大小

如果应用允许,降低ADC采样率或增加缓冲区长度,可以显著减少切换频率,从而降低丢码概率。

4. 完整代码示例(基于STM32F407,ADC1+TIM2触发+DMA双缓冲)

#include "stm32f4xx.h"

#define N 1024
uint16_t BufferA[N];
uint16_t BufferB[N];
volatile uint8_t flag = 0;

void ADC_DMA_Init(void) {
    GPIO_InitTypeDef GPIO_InitStructure;
    ADC_InitTypeDef ADC_InitStructure;
    TIM_TimeBaseInitTypeDef TIM_InitStructure;
    DMA_InitTypeDef DMA_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    // 时钟使能
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA | RCC_AHB1Periph_DMA1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

    // GPIO PA1 模拟输入
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // ADC1 配置
    ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;
    ADC_InitStructure.ADC_ScanConvMode = DISABLE;
    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // 由定时器触发
    ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_Rising;
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_TRGO;
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfConversion = 1;
    ADC_Init(ADC1, &ADC_InitStructure);

    ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_3Cycles);
    ADC_DMACmd(ADC1, ENABLE);
    ADC_Cmd(ADC1, ENABLE);

    // TIM2 触发配置:更新事件触发,频率=84MHz/(84*10)=100kHz
    TIM_InitStructure.TIM_Prescaler = 84-1; // 84MHz/84=1MHz
    TIM_InitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_InitStructure.TIM_Period = 10-1;    // 1MHz/10=100kHz
    TIM_InitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInit(TIM2, &TIM_InitStructure);
    TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update);
    TIM_Cmd(TIM2, ENABLE);

    // DMA1 Stream0 配置
    DMA_DeInit(DMA1_Stream0);
    DMA_InitStructure.DMA_Channel = DMA_Channel_0;
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
    DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)BufferA;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
    DMA_InitStructure.DMA_BufferSize = N;
    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_Enable; // 启用FIFO
    DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
    DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_INC4;
    DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
    DMA_Init(DMA1_Stream0, &DMA_InitStructure);

    // 双缓冲配置
    DMA_DoubleBufferModeConfig(DMA1_Stream0, (uint32_t)BufferB, DMA_Memory_0);
    DMA_DoubleBufferModeCmd(DMA1_Stream0, ENABLE);

    // 中断配置
    DMA_ITConfig(DMA1_Stream0, DMA_IT_TC, ENABLE);
    NVIC_InitStructure.NVIC_IRQChannel = DMA1_Stream0_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    DMA_Cmd(DMA1_Stream0, ENABLE);
}

void DMA1_Stream0_IRQHandler(void) {
    if (DMA_GetITStatus(DMA1_Stream0, DMA_IT_TC)) {
        DMA_ClearITPendingBit(DMA1_Stream0, DMA_IT_TC);
        // 简单处理:置标志,主循环处理
        flag = 1;
    }
}

int main(void) {
    ADC_DMA_Init();
    while (1) {
        if (flag) {
            flag = 0;
            // 获取当前内存目标,确定哪个缓冲区已满
            uint32_t cur = DMA_GetCurrentMemoryTarget(DMA1_Stream0);
            if (cur == (uint32_t)BufferA) {
                // BufferA正在被填充,BufferB已满
                process_data(BufferB, N);
            } else {
                process_data(BufferA, N);
            }
        }
    }
}

void process_data(uint16_t *buf, uint16_t len) {
    // 数据处理,例如计算平均值、FFT等
}

5. 注意事项

  • 采样率与DMA切换速率:STM32F4的DMA切换时间约为2-3个AHB时钟周期(如72MHz下约30-40ns)。若ADC采样间隔小于此值,丢码不可避免。建议采样率不超过1Msps,或使用FIFO缓冲。
  • FIFO模式的影响:启用FIFO后,DMA会以突发方式写入内存,可能增加内存带宽占用,但能有效吸收切换延迟。需确保FIFO阈值设置合理,避免溢出。
  • 中断优先级:将DMA中断优先级设为最高,确保TCIF及时响应,减少处理延迟。但ISR中只做标志操作,避免长时间占用CPU。
  • 使用CMAR寄存器:在双缓冲模式下,可通过读取DMA_GetCurrentMemoryTarget获取当前内存地址,但注意该函数返回的是当前正在使用的缓冲区,而非刚完成的。
  • 调试技巧:在ISR中记录时间戳或计数,观察丢码是否与中断响应时间相关。可使用逻辑分析仪抓取DMA的TCIF信号和ADC的EOC信号,对比时序。

结语

DMA双缓冲ADC丢码问题源于硬件切换延迟与采样时序的竞争。通过合理配置FIFO、优化中断处理、控制采样率,可以完全避免或显著降低丢码率。理解底层硬件行为,是解决此类问题的关键。希望本文能帮助你在STM32F4上实现稳定可靠的高速数据采集。