引言
在电机控制、振动监测等需要高速连续采样的嵌入式应用中,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时,会执行以下操作:
- 更新当前内存地址指针到另一个缓冲区。
- 重新加载传输计数为N。
- 清除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上实现稳定可靠的高速数据采集。