# STM32 DMA+PWM多路方波输出:破解DMA中断与更新事件竞争导致的波形抖动 ## 问题背景与竞争根源 在STM32中,DMA+PWM输出方波的典型流程:定时器产生更新事件(UEV)触发DMA搬运占空比数据到CCR寄存器,同时DMA传输完成触发中断,用于更新下一组数据。当多路输出时,若DMA中断与UEV时序重叠,可能出现: - **中断延迟**:DMA中断服务函数执行时间过长,导致下一次UEV到来时CCR数据未更新,输出错误占空比。 - **数据撕裂**:DMA在UEV发生瞬间写入CCR,而定时器正在读取,造成半个周期异常。 根因在于:DMA中断和UEV共享同一优先级,且中断处理非原子性,导致时序竞争。 ## 解决策略对比 | 策略 | 原理 | 优点 | 缺点 | |------|------|------|------| | 1. 禁用DMA中断,轮询标志 | 主循环查询DMA传输完成标志 | 简单,无中断开销 | 占用CPU,实时性差 | | 2. 提高中断优先级 | 将DMA中断设为最高优先级 | 减少延迟 | 可能影响其他实时任务 | | 3. 定时器级联+DMA突发传输 | 主定时器触发从定时器,DMA一次突发更新多路CCR | 无中断竞争,波形稳定 | 配置复杂,占用资源 | 推荐策略3,尤其适合多路输出场景。 ## 硬件与软件准备 - **硬件**:STM32F407(或其他带DMA和高级定时器的型号),逻辑分析仪验证波形。 - **软件**:STM32CubeMX + HAL库,或标准外设库。 ## 原理详解:定时器级联与DMA突发 **核心思想**:使用两个定时器,主定时器(如TIM1)产生PWM,从定时器(如TIM8)产生触发信号。主定时器的UEV不直接触发DMA,而是通过级联触发从定时器,从定时器再触发DMA突发传输,一次性更新多路CCR值。这样,DMA传输在UEV之后完成,且中断不再参与关键路径。 **关键配置**: - 主定时器:PWM模式,输出比较通道多路。 - 从定时器:配置为主定时器的从模式,触发方式为“更新事件触发”。 - DMA:配置为循环模式,突发传输,数据宽度32位(对应CCR寄存器)。 ## 完整代码示例(基于HAL库) ```c // 假设使用TIM1(主)和TIM8(从),DMA1_Stream2(TIM1_UP) // 初始化DMA句柄 typedef struct { TIM_HandleTypeDef htim1; TIM_HandleTypeDef htim8; DMA_HandleTypeDef hdma_tim1_up; } PWM_Handle; PWM_Handle hpwm; // 占空比数据数组,例如4路,每路存储2个值(用于交替) uint32_t dutyData[8] = {500, 500, 500, 500, 500, 500, 500, 500}; void PWM_Init(void) { // 1. 使能时钟 __HAL_RCC_TIM1_CLK_ENABLE(); __HAL_RCC_TIM8_CLK_ENABLE(); __HAL_RCC_DMA1_CLK_ENABLE(); // 2. 主定时器TIM1配置 hpwm.htim1.Instance = TIM1; hpwm.htim1.Init.Prescaler = 84-1; // 84MHz/84=1MHz hpwm.htim1.Init.CounterMode = TIM_COUNTERMODE_UP; hpwm.htim1.Init.Period = 1000-1; // 1kHz方波 hpwm.htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; hpwm.htim1.Init.RepetitionCounter = 0; HAL_TIM_PWM_Init(&hpwm.htim1); // 3. 从定时器TIM8配置(触发模式) hpwm.htim8.Instance = TIM8; hpwm.htim8.Init.Prescaler = 0; hpwm.htim8.Init.CounterMode = TIM_COUNTERMODE_UP; hpwm.htim8.Init.Period = 1; // 每次更新触发一次 hpwm.htim8.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; hpwm.htim8.Init.RepetitionCounter = 0; HAL_TIM_Base_Init(&hpwm.htim8); // 4. 配置从定时器为外部触发模式(由TIM1更新触发) TIM_SlaveConfigTypeDef sSlaveConfig = {0}; sSlaveConfig.SlaveMode = TIM_SLAVEMODE_EXTERNAL1; sSlaveConfig.InputTrigger = TIM_TS_ITR0; // 根据连接选择ITR0或ITR1等 sSlaveConfig.TriggerPolarity = TIM_TRIGGERPOLARITY_RISING; sSlaveConfig.TriggerFilter = 0; HAL_TIM_SlaveConfigSynchro(&hpwm.htim8, &sSlaveConfig); // 5. 配置PWM通道(例如通道1-4) TIM_OC_InitTypeDef sConfigOC = {0}; sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 500; // 初始占空比50% sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(&hpwm.htim1, &sConfigOC, TIM_CHANNEL_1); // 重复配置通道2-4... // 6. 配置DMA(突发传输,循环模式) hpwm.hdma_tim1_up.Instance = DMA1_Stream2; hpwm.hdma_tim1_up.Init.Channel = DMA_CHANNEL_6; // 根据映射 hpwm.hdma_tim1_up.Init.Direction = DMA_MEMORY_TO_PERIPH; hpwm.hdma_tim1_up.Init.PeriphInc = DMA_PINC_DISABLE; hpwm.hdma_tim1_up.Init.MemInc = DMA_MINC_ENABLE; hpwm.hdma_tim1_up.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD; hpwm.hdma_tim1_up.Init.MemDataAlignment = DMA_MDATAALIGN_WORD; hpwm.hdma_tim1_up.Init.Mode = DMA_CIRCULAR; hpwm.hdma_tim1_up.Init.Priority = DMA_PRIORITY_HIGH; HAL_DMA_Init(&hpwm.hdma_tim1_up); __HAL_LINKDMA(&hpwm.htim1, hdma[TIM_DMA_ID_UPDATE], hpwm.hdma_tim1_up); // 7. 启动DMA传输(从dutyData到TIM1->CCR1,突发4次) HAL_DMA_Start(&hpwm.hdma_tim1_up, (uint32_t)dutyData, (uint32_t)&TIM1->CCR1, 4); // 8. 启动PWM和从定时器 HAL_TIM_PWM_Start(&hpwm.htim1, TIM_CHANNEL_1); // 启动其他通道... HAL_TIM_Base_Start(&hpwm.htim8); } // 主循环中无需处理中断,只需更新dutyData数组 void UpdateDuty(uint32_t* newData, uint8_t len) { memcpy(dutyData, newData, len * sizeof(uint32_t)); } ``` ## 注意事项 - **DMA突发长度**:必须与CCR寄存器数量匹配,且数据宽度为32位(因为CCR是32位寄存器)。 - **内存对齐**:dutyData数组建议4字节对齐,避免DMA访问错误。 - **从定时器触发源**:需根据STM32型号的ITR映射表选择正确的ITR编号(如TIM8的ITR0对应TIM1)。 - **禁用DMA中断**:本方案无需DMA中断,确保不产生竞争。 - **调试技巧**:使用逻辑分析仪观察波形,若仍有抖动,检查DMA优先级和从定时器配置。 ## 总结 通过定时器级联和DMA突发传输,将DMA传输与PWM更新事件解耦,彻底避免中断竞争,实现多路高精度方波输出。此方法适用于对波形质量要求高的场景,如多轴电机控制。