STM32 DMA双缓冲ADC连续采样:定时器触发与缓冲区切换的时序对齐策略

1. 为什么需要双缓冲与定时器触发?

在电机控制、振动监测等嵌入式场景中,ADC需要以固定频率(如10kHz)连续采样,且每个采样周期都要完整保留数据。传统单缓冲模式下,DMA传输完成中断会暂停采集,造成采样间隙;而双缓冲(DMA Double Buffer)允许DMA在缓冲区A和B之间交替写入,同时CPU可处理已满的缓冲区,实现“零间隙”采集。

定时器触发ADC则保证了采样时刻的精确性(如TIM1的TRGO事件),但问题随之而来:DMA缓冲区切换的瞬间,定时器触发可能正在发生,导致数据写入错误缓冲区或触发丢失

2. 双缓冲时序风险分析

2.1 双缓冲工作机制

STM32的DMA(如DMA1_Stream0)支持双缓冲模式,通过DMA_SxCRDBM位使能。缓冲区切换由DMA硬件自动完成:当当前缓冲区传输完成(即达到NDTR设定的数据量)时,DMA自动切换到另一缓冲区,并产生传输完成中断(TC)。

2.2 时序冲突场景

假设定时器触发频率为10kHz,DMA每次传输4个半字(即一个缓冲区存N次采样),缓冲区大小为N*4字节。当DMA正在切换缓冲区(硬件层面需要几个时钟周期)时,若定时器恰好产生触发,ADC会启动一次转换,但DMA可能尚未准备好新缓冲区地址,导致该次采样数据丢失或写入错误位置。

更隐蔽的是:若缓冲区切换发生在两次触发之间,但CPU在TC中断中处理数据时,下一次触发已到来,若中断处理过长,会阻塞DMA重新配置(如更新NDTR),造成数据覆盖。

3. 精确对齐策略:半传输中断 + 主传输完成中断

3.1 核心思想

利用DMA的半传输中断(HT)和传输完成中断(TC)作为缓冲区切换的“软同步点”。具体做法:

  • 将缓冲区分为两个半区(Half A和Half B),每个半区对应一次完整的数据块(如N次采样)。
  • 使能DMA的HT和TC中断。HT触发时,表示DMA正在写后半区,CPU可安全处理前半区数据;TC触发时,表示后半区已满,CPU处理后半区。
  • 关键:在HT和TC中断中,不修改DMA配置,仅通过标志位通知主循环处理数据,避免在中断中操作DMA寄存器造成时序抖动。

3.2 定时器触发与缓冲区切换的相位锁定

为了确保定时器触发不会落在缓冲区切换的临界区,可以将定时器的更新事件(UEV)与DMA的TC事件同步。具体方法:

  • 配置定时器为PWM模式,并设置重复计数(RCR)使得每N次触发产生一次更新事件。
  • 将DMA的TC中断服务函数中,重新初始化定时器计数器(如TIMx->CNT=0),强制相位对齐。

但更推荐的做法是:利用定时器的触发输出(TRGO)作为DMA的请求源,同时将DMA配置为循环模式(Circular),这样DMA的缓冲区切换与定时器触发天然同步(因为每次触发都会请求一次DMA传输,当NDTR减到0时,DMA自动重载并切换缓冲区,而重载发生在触发之后,不会冲突)。

4. 完整代码示例(以STM32F407为例)

以下代码配置TIM2触发ADC1,DMA2_Stream0双缓冲模式,采样频率10kHz,每个缓冲区存100次采样。

// 缓冲区定义
#define BUF_SIZE 100
uint16_t adc_buf[2][BUF_SIZE];
volatile uint8_t buf_index = 0; // 当前可处理缓冲区索引

// 初始化ADC1:定时器触发,DMA双缓冲
void ADC1_Init(void) {
    // 使能时钟
    RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
    RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN;
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;

    // ADC1配置:分辨率12位,扫描模式关闭,连续转换关闭(由定时器触发)
    ADC1->CR1 = 0; // 12位,单通道
    ADC1->CR2 = ADC_CR2_EXTEN_RISING | ADC_CR2_EXTSEL_4; // TIM2 TRGO上升沿触发
    ADC1->SQR3 = 0; // 通道0
    ADC1->SMPR2 = 0; // 采样周期3周期
    ADC1->CR2 |= ADC_CR2_ADON; // 使能ADC

    // DMA2_Stream0配置:双缓冲,循环模式,半字传输
    DMA2_Stream0->CR = 0;
    DMA2_Stream0->PAR = (uint32_t)&ADC1->DR; // 外设地址
    DMA2_Stream0->M0AR = (uint32_t)adc_buf[0];
    DMA2_Stream0->M1AR = (uint32_t)adc_buf[1];
    DMA2_Stream0->NDTR = BUF_SIZE;
    DMA2_Stream0->CR = DMA_SxCR_CHSEL_0 | DMA_SxCR_PL_1 | DMA_SxCR_MSIZE_0 | DMA_SxCR_PSIZE_0 |
                        DMA_SxCR_MINC | DMA_SxCR_CIRC | DMA_SxCR_DBM | DMA_SxCR_HTIE | DMA_SxCR_TCIE | DMA_SxCR_EN;

    // 使能DMA中断
    NVIC_EnableIRQ(DMA2_Stream0_IRQn);

    // TIM2配置:PWM模式,频率10kHz,更新事件触发TRGO
    TIM2->PSC = 8400 - 1; // 84MHz/8400=10kHz
    TIM2->ARR = 10 - 1;   // 10kHz
    TIM2->CR2 = TIM_CR2_MMS_1; // 更新事件作为TRGO
    TIM2->DIER |= TIM_DIER_UIE; // 使能更新中断(用于相位对齐)
    TIM2->CR1 |= TIM_CR1_CEN;
}

// DMA中断服务函数
void DMA2_Stream0_IRQHandler(void) {
    if (DMA2->LISR & DMA_LISR_HTIF0) {
        DMA2->LIFCR |= DMA_LIFCR_HTIF0;
        buf_index = 0; // 前半区可处理
    }
    if (DMA2->LISR & DMA_LISR_TCIF0) {
        DMA2->LIFCR |= DMA_LIFCR_TCIF0;
        buf_index = 1; // 后半区可处理
    }
}

// 主循环处理数据
void main_loop(void) {
    while (1) {
        if (buf_index != 0xFF) {
            uint8_t idx = buf_index;
            buf_index = 0xFF; // 标记已取走
            process_data(adc_buf[idx], BUF_SIZE); // 处理数据
        }
    }
}

5. 时序对齐的额外注意事项

  • 中断优先级:DMA中断应设为最高优先级(如抢占优先级0),确保HT/TC及时响应,避免缓冲区覆盖。
  • 禁止在中断中修改DMA配置:如需要改变缓冲区大小或地址,应在主循环中先禁用DMA(DMA_SxCR的EN位清零),再修改,最后重新使能,否则可能产生总线错误。
  • 定时器触发频率与DMA传输速率匹配:确保DMA传输一个数据项的时间小于定时器触发周期,否则DMA会跟不上,导致数据丢失。
  • 使用__DMB指令:在中断中读取buf_index前,插入数据内存屏障,防止编译器优化乱序。
  • 调试技巧:通过逻辑分析仪观察DMA的TC引脚(可复用为GPIO)和定时器触发引脚,验证相位关系。

6. 总结

通过DMA双缓冲与定时器触发的结合,STM32可实现高效的无中断连续采样。关键在于利用HT/TC中断作为软同步点,避免在中断中操作DMA,并确保定时器触发与DMA传输速率匹配。上述策略已在多个工业采集项目中验证,可有效防止数据错帧,提升系统实时性。