STM32 DMA双缓冲ADC连续采样:定时器触发与缓冲区切换的时序对齐策略
👁 8 阅读 · 2026-08-31 · 嵌入式
在STM32高性能数据采集系统中,DMA双缓冲与定时器触发ADC的组合可实现无中断连续采样,但缓冲区切换与触发时序的错位会导致数据错帧或丢失。本文深入剖析双缓冲机制下的时序风险,给出基于半传输/传输完成中断的精确对齐方法,并提供可落地的配置代码与工程注意事项,帮助开发者构建稳定可靠的嵌入式采集链路。
# 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传输速率匹配。上述策略已在多个工业采集项目中验证,可有效防止数据错帧,提升系统实时性。