# 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_SxCR`的`DBM`位使能。缓冲区切换由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次采样。 ```c // 缓冲区定义 #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传输速率匹配。上述策略已在多个工业采集项目中验证,可有效防止数据错帧,提升系统实时性。