# STM32F4 DMA双缓冲ADC采集:如何精确对齐采样时刻与PWM触发边沿 ## 为什么需要精确对齐? 在电机FOC控制、数字电源等应用中,PWM信号驱动功率级,ADC负责采集电流或电压反馈。理想情况下,采样应发生在PWM周期的特定时刻(如中心对齐波形的谷底),以避开开关噪声并获取平均电流。若采样时刻与PWM边沿存在抖动或偏移,将引入相位误差,导致控制性能下降甚至系统不稳定。STM32F4的定时器、ADC和DMA提供了硬件级同步机制,但配置不当仍会出现采样点漂移。 ## 核心原理:硬件触发链 STM32F4中,ADC采样可由定时器事件(如更新事件、比较匹配)触发。典型链路: - **定时器**(如TIM1)产生PWM,同时配置其触发输出(TRGO)连接到ADC。 - **ADC** 在收到触发信号后启动转换,转换完成产生EOC事件。 - **DMA** 在EOC时自动搬运数据到内存,无需CPU干预。 关键点:**触发边沿的选择**。定时器TRGO可配置为更新事件(周期开始)或比较匹配(占空比变化点)。对于中心对齐PWM,通常选择更新事件(周期中点)或特定比较值,以匹配采样窗口。 ## 双缓冲机制与采样连续性 双缓冲(DMA双缓冲模式)允许DMA在两个内存缓冲区之间交替传输,一个缓冲区被填充时,CPU可处理另一个,实现无间隙采集。STM32F4的DMA控制器支持双缓冲,通过`DMA_SxCR`的`DBUF`位启用。 **对齐挑战**:每次PWM周期触发一次ADC转换,DMA搬运一个样本。若缓冲区大小等于采样点数,则DMA传输完成中断(TC)发生在缓冲区边界,但该中断与PWM边沿无直接关联。要精确对齐,需确保每次触发对应的样本在缓冲区中的位置固定,且切换缓冲区时不影响采样时序。 ## 配置步骤 ### 1. 定时器配置(以TIM1为例) - 设置PWM模式(如中心对齐模式1),频率和占空比。 - 配置触发输出:`TIM_SelectOutputTrigger(TIM1, TIM_TRGO_UPDATE)`,即每次更新事件(周期中点)触发TRGO。 - 使能主输出:`TIM_CtrlPWMOutputs(TIM1, ENABLE)`。 ### 2. ADC配置 - 选择ADC1,设置扫描模式(若多通道)和采样时间。 - 设置外部触发:`ADC_ExternalTrigConvCmd(ADC1, ENABLE)`,并选择`ADC_ExternalTrigConv_T1_TRGO`。 - 启用DMA请求:`ADC_DMACmd(ADC1, ENABLE)`。 ### 3. DMA双缓冲配置 ```c // 假设两个缓冲区:buffer0[BUFFER_SIZE], buffer1[BUFFER_SIZE] DMA_InitTypeDef DMA_InitStructure; // 使能DMA2时钟(ADC1在DMA2 Stream0) RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); DMA_DeInit(DMA2_Stream0); DMA_InitStructure.DMA_Channel = DMA_Channel_0; // ADC1映射 DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)buffer0; DMA_InitStructure.DMA_Memory1BaseAddr = (uint32_t)buffer1; // 第二缓冲区 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory; DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE; 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_Disable; DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single; DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull; // 启用双缓冲 DMA_DoubleBufferModeCmd(DMA2_Stream0, ENABLE); // 设置当前目标缓冲区为buffer0 DMA_MemoryTargetConfig(DMA2_Stream0, (uint32_t)buffer0, DMA_Memory_0); DMA_Init(DMA2_Stream0, &DMA_InitStructure); // 使能传输完成中断(用于切换缓冲区处理) DMA_ITConfig(DMA2_Stream0, DMA_IT_TC, ENABLE); // 使能DMA流 DMA_Cmd(DMA2_Stream0, ENABLE); ``` ### 4. 中断处理与对齐校准 在DMA传输完成中断中,切换缓冲区并处理数据。但注意:中断发生时刻与PWM边沿有固定延迟(由DMA传输和中断响应时间决定)。为精确对齐,需测量该延迟并补偿。 ```c void DMA2_Stream0_IRQHandler(void) { if (DMA_GetITStatus(DMA2_Stream0, DMA_IT_TC)) { DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_TC); // 切换缓冲区:当前使用buffer0,则下次使用buffer1 if (current_buffer == 0) { DMA_MemoryTargetConfig(DMA2_Stream0, (uint32_t)buffer1, DMA_Memory_0); current_buffer = 1; process_data(buffer0, BUFFER_SIZE); // 处理已满的缓冲区 } else { DMA_MemoryTargetConfig(DMA2_Stream0, (uint32_t)buffer0, DMA_Memory_0); current_buffer = 0; process_data(buffer1, BUFFER_SIZE); } } } ``` **对齐校准技巧**:在PWM输出引脚上同时产生一个同步信号(如另一个定时器通道),用示波器观察该信号与ADC采样点(可通过DAC输出或GPIO翻转)的关系,调整定时器比较值或ADC采样时间,使采样点落在期望边沿。 ## 完整代码示例(简化) ```c // 初始化函数 void ADC_DMA_DoubleBuffer_Init(void) { // 1. 配置TIM1 PWM和TRGO TIM_TimeBaseInitTypeDef TIM_BaseStruct; TIM_OCInitTypeDef TIM_OCStruct; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); TIM_BaseStruct.TIM_Prescaler = 84-1; // 84MHz/84=1MHz TIM_BaseStruct.TIM_CounterMode = TIM_CounterMode_CenterAligned1; TIM_BaseStruct.TIM_Period = 1000-1; // 1kHz PWM TIM_BaseStruct.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM1, &TIM_BaseStruct); TIM_OCStruct.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCStruct.TIM_Pulse = 500; // 50%占空比 TIM_OCStruct.TIM_OutputState = TIM_OutputState_Enable; TIM_OC1Init(TIM1, &TIM_OCStruct); // TRGO选择更新事件 TIM_SelectOutputTrigger(TIM1, TIM_TRGO_UPDATE); TIM_CtrlPWMOutputs(TIM1, ENABLE); TIM_Cmd(TIM1, ENABLE); // 2. 配置ADC1 ADC_InitTypeDef ADC_Struct; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_Struct; GPIO_Struct.GPIO_Pin = GPIO_Pin_0; GPIO_Struct.GPIO_Mode = GPIO_Mode_AN; GPIO_Struct.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_Init(GPIOA, &GPIO_Struct); ADC_Struct.ADC_Resolution = ADC_Resolution_12b; ADC_Struct.ADC_ScanConvMode = DISABLE; ADC_Struct.ADC_ContinuousConvMode = DISABLE; ADC_Struct.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_Rising; ADC_Struct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T1_TRGO; ADC_Struct.ADC_DataAlign = ADC_DataAlign_Right; ADC_Struct.ADC_NbrOfConversion = 1; ADC_Init(ADC1, &ADC_Struct); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_84Cycles); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); // 校准ADC(可选) ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); // 3. 配置DMA(如上) // ... } // 主循环 int main(void) { // 初始化系统时钟等 ADC_DMA_DoubleBuffer_Init(); while(1) { // 处理数据(在中断中已处理) } } ``` ## 注意事项 - **触发边沿选择**:上升沿触发通常对应更新事件,但若使用比较匹配,需确认边沿极性。中心对齐模式下,更新事件发生在计数器溢出(周期中点),适合采样平均电流。 - **DMA缓冲区大小**:应等于每次PWM周期采样点数(通常为1),否则DMA传输完成中断频率会低于PWM频率,导致缓冲区切换不及时。若需多通道,则缓冲区大小为通道数。 - **中断优先级**:DMA中断应设为较高优先级,避免被其他中断延迟,影响缓冲区切换的实时性。 - **延迟补偿**:从触发到DMA搬运完成存在固定延迟(约几个时钟周期),可通过调整ADC采样时间或定时器比较值微调。建议用示波器验证。 - **双缓冲切换时机**:在DMA_TC中断中切换目标缓冲区时,必须确保DMA已停止(或使用循环模式下的自动切换)。STM32F4的循环模式+双缓冲会自动切换,但需在中断中重新配置目标地址,否则会覆盖同一缓冲区。 - **调试技巧**:在中断中翻转一个GPIO,用逻辑分析仪观察其与PWM边沿的相位关系,辅助校准。 ## 总结 通过合理配置定时器TRGO、ADC外部触发和DMA双缓冲,STM32F4可以实现与PWM边沿精确对齐的ADC采样。关键在于理解硬件触发链的时序,并通过中断处理实现无间隙数据流。实际项目中,务必用示波器验证采样点位置,并根据系统时钟频率计算延迟进行补偿。掌握这一技术,将显著提升嵌入式控制系统的性能。