# STM32F4无中断高精度PWM采样:DMA双缓冲与定时器触发的协同设计 ## 为什么需要无中断采样? 在嵌入式控制系统中,PWM信号(如电机驱动、开关电源)的占空比和频率是核心反馈量。传统方法通过ADC中断读取采样值,但存在两大痛点: - **中断延迟抖动**:CPU响应中断的时间受总线仲裁、优先级抢占影响,导致采样时刻不确定,引入相位噪声。 - **CPU占用**:高频采样(如100kHz)时,频繁中断会消耗大量CPU时间,挤占控制算法执行。 STM32F4的DMA双缓冲模式配合定时器触发,可让ADC在精确时刻自动转换,数据由DMA交替存入两个缓冲区,CPU仅在缓冲区切换时获得通知(或完全轮询),实现零中断、零抖动的采样链路。 ## 硬件架构与工作原理 ### 核心组件协同 - **定时器(如TIM2)**:产生PWM信号,同时输出触发信号(TRGO)到ADC。 - **ADC**:配置为外部触发模式,接收TRGO后启动转换。 - **DMA**:在ADC转换完成后自动搬运数据到内存,双缓冲模式使用两个缓冲区交替接收。 ### 双缓冲机制 DMA双缓冲(Double Buffer)模式下,DMA控制器维护两个内存缓冲区(Memory0和Memory1)。当DMA传输完当前缓冲区后,自动切换至另一缓冲区,并触发传输完成中断(或更新标志)。这避免了单缓冲模式下,CPU必须在DMA传输完成前读走数据,否则数据被覆盖的问题。 ### 触发时序 1. TIM2产生PWM,并配置更新事件(或比较事件)作为TRGO。 2. TRGO上升沿触发ADC启动转换。 3. ADC转换结束(EOC)触发DMA请求。 4. DMA将结果存入当前缓冲区,计数器递减。 5. 当缓冲区填满(传输次数完成),DMA切换缓冲区并置位传输完成标志。 整个过程CPU零参与,采样周期严格由定时器决定,消除了软件抖动。 ## 配置步骤(以STM32F407为例) ### 1. 时钟配置 使用HAL库,启用ADC、DMA、TIM2时钟。 ```c __HAL_RCC_ADC1_CLK_ENABLE(); __HAL_RCC_DMA2_CLK_ENABLE(); __HAL_RCC_TIM2_CLK_ENABLE(); ``` ### 2. 定时器PWM与触发配置 TIM2产生20kHz PWM,并设置更新事件为TRGO。 ```c TIM_HandleTypeDef htim2; htim2.Instance = TIM2; htim2.Init.Prescaler = 84-1; // 84MHz/84=1MHz htim2.Init.Period = 50-1; // 1MHz/50=20kHz htim2.Init.CounterMode = TIM_COUNTERMODE_UP; HAL_TIM_PWM_Init(&htim2); // 配置TRGO为更新事件 TIM_MasterConfigTypeDef sMasterConfig = {0}; sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE; sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig); // 启动PWM通道1(PA0) TIM_OC_InitTypeDef sConfigOC = {0}; sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 25; // 50%占空比 sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); ``` ### 3. ADC配置 ADC1采样通道0(PA0),外部触发,DMA请求使能。 ```c ADC_HandleTypeDef hadc1; hadc1.Instance = ADC1; hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode = DISABLE; hadc1.Init.ContinuousConvMode = DISABLE; // 非连续,由外部触发 hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T2_TRGO; // TIM2 TRGO hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 1; hadc1.Init.DMAContinuousRequests = ENABLE; // 每次转换后触发DMA HAL_ADC_Init(&hadc1); // 配置通道 ADC_ChannelConfTypeDef sConfig = {0}; sConfig.Channel = ADC_CHANNEL_0; sConfig.Rank = 1; sConfig.SamplingTime = ADC_SAMPLETIME_84CYCLES; HAL_ADC_ConfigChannel(&hadc1, &sConfig); ``` ### 4. DMA双缓冲配置 使用DMA2 Stream0,双缓冲模式,外设地址固定,内存地址交替。 ```c DMA_HandleTypeDef hdma_adc; hdma_adc.Instance = DMA2_Stream0; hdma_adc.Init.Channel = DMA_CHANNEL_0; hdma_adc.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_adc.Init.PeriphInc = DMA_PINC_DISABLE; hdma_adc.Init.MemInc = DMA_MINC_ENABLE; hdma_adc.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; hdma_adc.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma_adc.Init.Mode = DMA_CIRCULAR; // 循环模式,双缓冲必须 hdma_adc.Init.Priority = DMA_PRIORITY_HIGH; HAL_DMA_Init(&hdma_adc); // 关联DMA与ADC __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc); // 启动双缓冲传输 uint16_t buffer0[100]; uint16_t buffer1[100]; HAL_ADC_Start_DMA(&hadc1, (uint32_t*)buffer0, 100); // 初始指向buffer0 // 注意:HAL库的Start_DMA不支持直接双缓冲,需手动设置DMA双缓冲寄存器 // 手动配置双缓冲(以寄存器方式) DMA2_Stream0->M0AR = (uint32_t)buffer0; DMA2_Stream0->M1AR = (uint32_t)buffer1; DMA2_Stream0->CR |= DMA_SxCR_DBM; // 使能双缓冲模式 ``` ### 5. 启动ADC与定时器 ```c HAL_ADC_Start(&hadc1); // 使能ADC,等待外部触发 HAL_TIM_Base_Start(&htim2); // 启动定时器,开始产生PWM和触发信号 ``` ## 完整代码示例(关键部分) ```c // 全局缓冲区 #define BUFFER_SIZE 100 uint16_t adc_buf0[BUFFER_SIZE]; uint16_t adc_buf1[BUFFER_SIZE]; volatile uint8_t buffer_index = 0; // 0表示当前使用buf0,1表示buf1 // DMA传输完成中断回调(可选,用于通知CPU处理) void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if (hadc->Instance == ADC1) { // 检查当前活跃缓冲区(通过DMA的CR寄存器NDTR或M0AR/M1AR) if ((DMA2_Stream0->CR & DMA_SxCR_CT) == 0) { // 当前使用M0AR(buffer0),则刚完成的是buffer1 buffer_index = 1; // 处理buffer1中的数据 process_samples(adc_buf1, BUFFER_SIZE); } else { buffer_index = 0; process_samples(adc_buf0, BUFFER_SIZE); } } } void process_samples(uint16_t* buf, uint32_t len) { // 计算平均值、占空比等 uint32_t sum = 0; for (uint32_t i = 0; i < len; i++) { sum += buf[i]; } float average = (float)sum / len; // 根据PWM占空比计算电压等 } int main(void) { HAL_Init(); SystemClock_Config(); // ... 初始化GPIO、ADC、DMA、TIM2(如上配置) // 启动DMA双缓冲(使用HAL库的Start_DMA后手动改双缓冲) HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf0, BUFFER_SIZE); // 手动设置双缓冲 DMA2_Stream0->M1AR = (uint32_t)adc_buf1; DMA2_Stream0->CR |= DMA_SxCR_DBM; // 启动定时器 HAL_TIM_Base_Start(&htim2); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); while (1) { // 主循环可做其他任务,采样数据在回调中处理 // 或者轮询DMA标志位(无中断模式) if (DMA2_Stream0->CR & DMA_SxCR_TCIF) { DMA2_Stream0->CR &= ~DMA_SxCR_TCIF; // 清除标志 // 处理数据... } } } ``` ## 注意事项与优化技巧 - **缓冲区大小**:必须与ADC转换次数匹配。若采样率20kHz,缓冲区100点,则每5ms切换一次缓冲区,CPU有足够时间处理。 - **DMA模式**:必须使用`DMA_CIRCULAR`循环模式,否则双缓冲不会自动切换。 - **数据一致性**:在回调中处理数据时,确保当前缓冲区不再被DMA写入。通过检查`CT`位(当前目标)判断哪个缓冲区正在使用。 - **触发源选择**:若需采样PWM中点(如电流采样),可配置定时器比较事件作为TRGO,而非更新事件。 - **ADC采样时间**:根据信号源阻抗调整采样时间,避免采样电容充电不足。 - **中断优先级**:若使用DMA传输完成中断,建议设为最高优先级,确保及时处理。 - **内存对齐**:缓冲区建议4字节对齐,提高DMA访问效率。 - **调试技巧**:利用逻辑分析仪观察TRGO和ADC转换的时序,验证触发同步。 ## 总结 通过定时器触发ADC、DMA双缓冲搬运,STM32F4实现了完全无中断的PWM采样。该方法不仅消除了中断延迟抖动,还大幅降低了CPU负载,适用于高实时性控制场景。本文提供的配置方法和代码可直接应用于实际项目,开发者可根据需求调整采样率、缓冲区大小及触发源,构建更复杂的采样系统。