STM32定时器主从模式实现两路同步PWM输出实战指南 1. 项目缘起为什么需要两路同步PWM在嵌入式开发特别是电机控制、数字电源、LED调光这些领域我们经常会遇到一个看似简单但实现起来有点“讲究”的需求需要两路或多路PWM信号它们不仅频率和占空比要能独立调节最关键的是它们的脉冲边沿必须严格对齐或者说它们的相位关系必须是确定且稳定的。举个例子你正在做一个H桥电机驱动。H桥的上下桥臂需要互补的PWM信号来驱动并且为了防止上下管直通烧毁中间还需要插入死区时间。这时候如果你用两个独立的定时器分别产生这两路PWM即使你把它们的频率配置得一模一样由于定时器是独立启动的它们的计数器初始值、重装载时机都存在微小的随机性。这会导致两路PWM的上升沿无法精确对齐死区时间会飘忽不定轻则电机运行有噪音、效率低下重则瞬间发生直通炸管没商量。再比如做多路LED的混光控制或者需要两个执行机构严格同步动作。如果PWM不同步就会导致颜色闪烁或者动作不同步体验很差。所以“同步”这个要求不是锦上添花而是很多应用场景下的硬性需求。STM32的定时器功能非常强大其中一个杀手锏级的功能就是“主从模式”。它能让一个定时器主定时器去控制另一个定时器从定时器的启动、停止甚至计数节奏从而实现多个定时器输出的PWM信号在硬件层面上的绝对同步。这比用软件去同步要可靠、精确得多也几乎不占用CPU资源。今天我就以STM32F103系列为例手把手带你实现一个频率、占空比均可独立调节但脉冲严格同步的两路PWM输出系统。2. 核心武器库STM32定时器的主从模式解析在动手写代码之前我们必须先吃透STM32定时器主从模式的工作原理。这不是简单的API调用理解了底层机制你才能灵活应对各种复杂场景。2.1 定时器的内部架构与同步信号STM32的通用/高级定时器如TIM1, TIM2, TIM3, TIM4等不仅仅是几个计数器。它们内部有一套精密的“联动”机制核心是几个关键的内部触发信号TRGO (Trigger Output): 主定时器可以配置在特定事件如更新事件UEV、比较匹配等发生时通过这个引脚向其他定时器发送一个硬件触发信号。ITRx (Internal Trigger input): 从定时器可以通过这个输入来接收来自其他定时器的TRGO信号。ITR0, ITR1, ITR2, ITR3分别对应着不同的内部连接路径具体哪个定时器能接收到哪个定时器的信号需要查芯片的参考手册例如TIM2的ITR0可能连接的是TIM1的TRGO。主从模式的本质就是将一个定时器的TRGO输出连接到另一个定时器的ITRx输入并配置从定时器以特定的方式响应这个触发信号。2.2 主从模式的几种关键工作模式对于实现同步PWM我们最常用的是以下两种从模式从模式触发模式 (Slave Mode: Trigger Mode)工作方式当从定时器检测到有效的触发信号TRGI的上升沿时其计数器立即复位并开始从0开始计数。效果这保证了从定时器的计数器启动时刻与主定时器的某个特定事件如更新事件完全同步。此后两个定时器依据各自预分频器PSC和自动重装载值ARR独立运行。只要PSC和ARR设置相同它们的计数周期就完全一致PWM自然同步。如果ARR不同则频率不同但每个周期的起始点依然是同步的。适用场景实现多路同频同相或同相不同频的PWM输出。这是我们本次项目的核心模式。从模式门控模式 (Slave Mode: Gated Mode)工作方式从定时器的计数使能CEN直接由触发信号的电平控制。高电平时计数低电平时停止。效果可以实现更复杂的同步比如让从定时器只在主定时器输出的PWM为高电平时才计数。这常用于需要严格时间窗口的应用。适用场景本次项目不主要使用但了解有助于拓宽思路。2.3 同步PWM的物理连接与信号流我们以TIM1作为主定时器TIM2作为从定时器为例规划信号流主定时器 (TIM1) 配置配置为PWM输出模式产生我们需要的PWM波形比如从CH1输出。配置主模式设置TIMx-CR2寄存器中的MMS位让它在更新事件(UEV)发生时产生一个触发输出TRGO。这是关键一步意味着每次TIM1的计数器溢出归零时都会发出一个同步脉冲。内部硬件连接根据芯片数据手册TIM1的TRGO内部连接到了TIM2的ITR0或其他ITRx需查表确认。这个连接是芯片内部固定的我们只需要在配置从定时器时选择正确的触发源TS即可。从定时器 (TIM2) 配置同样配置为PWM输出模式。配置从模式控制器设置触发源TS为“内部触发0ITR0”对应TIM1的TRGO。设置从模式SMS为“触发模式Trigger Mode”。这样当TIM2检测到TIM1发来的TRGO信号即TIM1的更新事件时它的计数器会立刻被复位并开始计数。最终效果TIM1和TIM2的计数器在每一个PWM周期开始时都被强制同步到同一个起点尽管TIM2可能稍晚几个时钟周期但在硬件层面这个延迟是固定且极小的可视为同步。两路PWM的周期频率由各自的ARR和PSC决定占空比由各自的CCR捕获/比较寄存器决定。但由于起点同步它们的相位关系是锁定的。注意选择哪两个定时器做主从不是随意的。必须查阅你所用STM32型号的《参考手册》中“定时器内部触发连接”表格。例如在STM32F103中TIM1的TRGO可以连接到TIM2的ITR0。如果你选错了组合比如TIM2和TIM3没有内部连接这个方案就无法实现。3. 实战配置从CubeMX配置到代码详解理论清楚了我们进入实战环节。我会使用STM32CubeMX进行图形化配置并解释生成的关键代码。这里假设我们需要两路同步PWM一路由TIM1_CH1PA8引脚输出另一路由TIM2_CH1PA0引脚输出。目标基础频率10kHz占空比50%且严格同步。3.1 STM32CubeMX工程配置步骤系统与时钟选择合适的STM32系列如F103C8T6。配置系统时钟SYSCLK比如72MHz。确保给定时器的时钟APB1, APB2是使能的。配置主定时器 TIM1在Pinout Configuration标签页找到TIM1。时钟源选择Internal Clock。通道1选择PWM Generation CH1。这会自动将PA8引脚配置为复用推挽输出。参数设置Prescaler (PSC - 16 bits value)预分频器。我们的定时器时钟APB2为72MHz。若要产生10kHz PWM计数周期为1/10k 0.1ms 100us。72MHz时钟下一个时钟周期约13.9ns。设定PSC71则定时器时钟为72M/(711)1MHz一个计数周期为1us。Counter ModeUp向上计数。Counter Period (ARR - 16 bits value)自动重装载值。定时器时钟1MHz周期100us则ARR应设置为100-1 99。auto-reload preloadEnable使能ARR预装载防止修改ARR时产生毛刺。PWM生成配置在下方PWM Generation Channel 1部分设置Pulse (CCR1 - 16 bits value)为50占空比50%即高电平时间为50us。CH PolarityHigh有效电平为高。主模式配置最关键的一步在Parameter Settings选项卡最下方找到Trigger Output (TRGO) Parameters。将Master/Slave Mode (MSM bit)选择为Enable。将Trigger Event Selection选择为Update Event。这意味着每次TIM1发生更新事件计数器溢出时都会在其TRGO输出一个脉冲。配置从定时器 TIM2找到TIM2。时钟源选择Internal Clock。通道1选择PWM Generation CH1PA0引脚。参数设置PSC和ARR的设置必须和TIM1完全一致才能保证同频。即PSC71ARR99Counter Mode为Up。PWM生成配置设置Pulse为50极性为High。从模式配置另一个关键步骤在Parameter Settings选项卡中找到Slave Mode区域。将Slave Mode选择为Trigger Mode。将Trigger Source选择为ITR0。这里的ITR0对应的是TIM1的TRGO输出对于STM32F103TIM2的ITR0连接源就是TIM1。这一点务必根据芯片手册确认。生成代码配置好项目名称、IDE如MDK-ARM后生成代码。3.2 生成代码的关键点解析与用户代码补充CubeMX生成的代码搭建好了框架但我们还需要添加一些用户代码来启动PWM。// 在 main.c 的 /* USER CODE BEGIN 2 */ 部分添加 HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); // 启动TIM1的PWM输出 HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1); // 启动TIM2的PWM输出 // 注意不需要手动启动从定时器的计数器 // 因为TIM2配置为触发从模式当TIM1启动并产生第一个更新事件TRGO时TIM2的计数器会自动被复位并启动。关键代码逻辑解读我们只显式地启动了主定时器TIM1的PWM输出HAL_TIM_PWM_Start。这个函数内部会启动定时器的计数器。TIM1的计数器开始运行当它计数到ARR99时发生更新事件UEV。这个事件会做三件事将预装载寄存器ARR, CCR等的值更新到影子寄存器。产生更新中断如果使能了。因为配置了主模式所以同时会在TRGO上输出一个触发脉冲。这个TRGO脉冲通过内部线路送到TIM2的ITR0。TIM2被配置为“触发从模式”一旦检测到ITR0上的有效边沿其计数器立即被硬件清零并开始计数。同时TIM2的PWM输出通道也随之开始工作。从此两个定时器基于各自的时钟独立计数但因为每个周期开始时都被TIM1的更新事件强行“对齐”一次所以它们的PWM波形始终保持同步。3.3 动态调节频率与占空比项目要求频率和占空比可调。在同步框架下调节方法如下// 动态修改TIM1主定时器的频率和占空比TIM2会随之同步启动新周期 void Adjust_Master_PWM(uint32_t frequency_hz, uint8_t duty_cycle_percent) { uint32_t tim_clock 72000000; // APB2时钟72MHz uint32_t psc 71; // 保持预分频不变简化计算。也可动态计算PSC。 uint32_t arr (tim_clock / (psc 1) / frequency_hz) - 1; uint32_t ccr (arr 1) * duty_cycle_percent / 100; __HAL_TIM_SET_AUTORELOAD(htim1, arr); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, ccr); } // 动态修改TIM2从定时器的占空比频率跟随主定时器不建议单独改频率 void Adjust_Slave_Duty(uint8_t duty_cycle_percent) { uint32_t arr __HAL_TIM_GET_AUTORELOAD(htim2); // 获取当前周期值 uint32_t ccr (arr 1) * duty_cycle_percent / 100; __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, ccr); }重要原则频率调节原则上只应调节主定时器的ARR值。因为从定时器TIM2的计数器复位和启动是由主定时器的更新事件触发的。改变主定时器的频率从定时器会在下一个同步脉冲到来时自动适应新的周期。如果单独修改从定时器的ARR会导致其与主定时器周期不同破坏严格的“每个周期起点对齐”的同步关系虽然可能仍有同步信号但波形会混乱。占空比调节两路PWM的占空比可以完全独立调节互不影响。只需修改各自定时器的CCRx寄存器即可。这是同步PWM的巨大优势。修改时机为了波形平滑最好在计数器运行时启用预装载功能CubeMX默认已使能这样修改ARR或CCR的值会在下一个更新事件时才生效避免当前周期出现毛刺。4. 示波器实测与常见问题深度排坑配置完了代码也写了用示波器一看可能问题就来了。下面是我在实际调试中踩过的坑和解决方案。4.1 问题一完全没有PWM波形输出排查步骤检查时钟树首先确认HAL_RCC_ClockConfig函数是否被正确调用APB1和APB2的定时器时钟是否使能。可以用__HAL_RCC_TIM1_CLK_ENABLE()这类宏在代码里再确认一下。检查引脚复用确认GPIO引脚是否被正确初始化为复用推挽输出模式GPIO_MODE_AF_PP。CubeMX通常会做好但手动移植代码时容易出错。检查定时器使能顺序务必先启动主定时器HAL_TIM_PWM_Start(htim1, ...)。从定时器千万不要用HAL_TIM_Base_Start或HAL_TIM_PWM_Start来启动其计数器否则它会脱离主定时器的控制独立运行。从定时器只需要配置好PWM通道其计数器由主定时器的TRGO触发启动。检查主从模式配置寄存器单步调试查看TIM1-CR2寄存器的MMS位是否为010更新事件作为TRGO以及TIM2-SMCR寄存器的TS位触发源选择和SMS位从模式选择是否正确。4.2 问题二两路PWM不同步有固定或随机相位差现象两路PWM频率相同但上升沿没有对齐有一个固定的时间偏移或者这个偏移每次上电还不一样。根因分析固定相位差最常见的原因是GPIO输出速度配置不一致。如果TIM1的输出引脚配置为低速如2MHz而TIM2的输出引脚配置为高速如50MHz信号从芯片内部到达引脚的时间会有差异在示波器上就会看到几十纳秒的固定偏移。解决方案在CubeMX的GPIO配置中将两个PWM输出引脚的速度GPIO Speed设置为相同的等级通常设为High或Very High。随机相位差如果从定时器意外地被软件启动了。比如错误地调用了HAL_TIM_Base_Start(htim2)那么TIM2的计数器会在程序运行到该语句时立即启动而不是等待TIM1的触发信号。这样两个定时器的启动时间就是随机的相位差自然不固定。解决方案确保只启动主定时器的计数器从定时器仅配置PWM通道。验证同步性使用示波器的双通道触发模式设为Slope边沿触发触发源选其中一路PWM。然后观察另一路PWM的上升沿是否与触发点完美重合。放大时间轴到纳秒级别可以看到极小的固定延迟源于信号路径差异这属于正常物理现象通常可以接受。4.3 问题三修改参数时波形出现毛刺或跳动现象在运行过程中动态修改ARR调频或CCR调占空比时当前周期或下一个周期的PWM波形出现异常脉冲。根因与解决未使用预装载寄存器这是最主要的原因。直接写入ARR或CCR寄存器新值会立即生效可能会打断当前计数周期。解决方案在CubeMX配置中确保auto-reload preload和PWM generation中的Fast Mode/Preload选项是使能的默认通常是使能的。在代码中使用__HAL_TIM_SET_AUTORELOAD和__HAL_TIM_SET_COMPARE宏它们会操作预装载寄存器新值在下一个更新事件时才生效。在中断中修改不当如果在更新中断UEV中断服务函数中修改参数要非常小心。因为更新中断发生时硬件正在处理更新事件包括产生TRGO。此时修改寄存器可能引发不可预知的行为。安全做法设置一个标志位在中断中在主循环中检查并修改参数。4.4 问题四高级需求——带死区的互补PWM同步场景如果需要同步的两路PWM是互补带死区的如H桥的上臂和下臂并且需要另一组同样的互补PWM同步如另一个电机。进阶方案STM32的高级定时器如TIM1, TIM8本身支持互补输出和死区插入这可以通过配置BDTR寄存器轻松实现一对互补PWM。要实现两组互补PWM的同步可以将一个高级定时器如TIM1设为主产生第一组互补PWMCH1/CH1N和同步TRGO。另一个定时器可以是通用定时器如TIM2或另一个高级定时器设为从模式产生第二路PWM。但这路PWM需要软件或外部逻辑来生成其互补信号和死区因为通用定时器没有硬件死区发生器。更优雅的方案使用两个都支持互补输出的高级定时器如TIM1和TIM8。将TIM1设为主TIM8设为从。同时配置TIM1和TIM8的通道为互补输出模式并插入死区。这样两对带死区的互补PWM就能实现完美同步。配置的关键点与基础同步类似只是PWM模式需要选择PWM mode 1或2并配置CH Polarity和CH Complementary Polarity以及在Break and Dead Time部分设置死区时间。通过以上从原理到配置再到调试排坑的完整流程你应该能够牢牢掌握STM32主从模式实现同步PWM的精髓。这个技术的核心在于理解硬件内部的触发网络并让CubeMX或寄存器配置正确地建立起这条“控制链”。一旦配置成功其同步的稳定性和精确度是纯软件无法比拟的可以广泛应用于对时序要求苛刻的工业控制场景。