公司动态

基于STM32G0 TIM1的移相全桥PWM实现:CH1/CH2互补输出方案

📅 2026/8/30 1:46:28
基于STM32G0 TIM1的移相全桥PWM实现:CH1/CH2互补输出方案
移相全桥这四个字做电源的兄弟一听就知道是硬骨头四个开关管时序精度要跑到纳秒级上下桥臂之间还得留死区防止炸管。而用一颗MCU去生成带相位差的四路PWM更是很多人被卡住的第一道坎。这篇文章我直接用STM32G0B1CBT6的TIM1高级定时器只用CH1和CH2两个通道配合各自的互补输出CH1N和CH2N完整跑通了Phase shifted full bridge PWM。移相角连续可调死区由硬件自动插入全程不占用CPU中断资源。这个方案做数字电源、车载充电器、通信电源的副边控制都够用尤其适合刚从传统模拟PWM方案转数字控制、想在一颗常规MCU上验证移相全桥思路的工程师。整个实现过程里最让我觉得值钱的不是“把PWM点着了”而是把TIM1的计数结构、比较寄存器偏移、死区编码这些细节彻底捋了一遍。这篇文章就从拓扑原理、定时器资源、参数计算到调试踩坑一次性写透看完应该能直接在你的板子上复现。1. 移相全桥PWM的核心思路与方案拆解1.1 移相全桥为什么靠“移相”就能调压移相全桥的功率级长什么样这里不画图了说文字全桥由四个开关管组成左边两个构成一个桥臂右边两个构成另一个桥臂每个桥臂的上下管互补导通各占半个周期。传统硬开关全桥通过改变PWM占空比来调节输出电压两个桥臂同时动作原边电压波形就是简单的方波。而移相全桥不一样四个管子都固定工作在50%占空比状态但左右两个桥臂的驱动信号之间存在一个相位差这个角度通常写作移相角也就是项目标题里“Phase shifted”这个核心动作的物理含义。移相角的作用非常直观当两个桥臂的驱动完全同步时原边电压正负半周各占半周期输出电压达到最大当滞后桥臂相对超前桥臂往后移一个角度时原边电压中间就会出现一段零电平区间这段零电平期间变压器原边没有能量传递输出电压随之下降。移相角越大零电平区间越宽输出越低。这相当于把原来固定宽度的方波变成了带死区的“准方波”调压的手段也从“改宽度”变成了“改相位”。这个拓扑在数字电源领域流行的原因很实在固定50%占空比让每个开关管的工作状态非常规整配合谐振电感滞后桥臂的开关管有机会实现零电压开通ZVS开关损耗显著下降。做通信电源48V输出的场景开关频率跑到100kHz甚至更高ZVS省下来的损耗相当可观。所以“移相全桥PWM”这套信号本质上是在为ZVS创造条件而不仅仅是生成一组PWM那么简单。1.2 用TIM1的CH1与CH2是怎么覆盖四路驱动的关键问题来了全桥需要四路驱动信号为什么标题里只说“using tim1 (ch1 ch2)”因为TIM1是高级定时器它的每个通道都带一路互补输出。TIM1_CH1的互补通道是TIM1_CH1NTIM1_CH2的互补通道是TIM1_CH2N二者在硬件上自动反向并且可以插入死区时间。这样一来CH1和CH1N天然就是一组带死区的互补PWM可以直推一个桥臂的上下管CH2和CH2N同理推另一个桥臂。实际接线逻辑是这样的CH1输出的PWM接超前桥臂的开关管CH1N接同一桥臂的另一个开关管CH2输出的PWM接滞后桥臂的开关管CH2N接滞后桥臂的另一个开关管。四路驱动波形全部由TIM1一个外设产生频率天然一致相位关系只取决于CH1和CH2两个比较寄存器之间的偏移。这比用两个定时器靠中断去对齐要稳定太多也比用4个独立PWM通道去凑相位差要省事。为什么不在同一颗MCU上用CH1和CH3或者干脆用CH1/CH2/CH3/CH4四路独立输出原因在于移相全桥要求两对信号之间不仅频率相同占空比相同还要求相位差精确可控。TIM1的CH1和CH2共用同一个时基同一个ARR和PSC它们的占空比和相位天然同步演算你在代码里只需要改某一个通道的CCR值整个移相角就变了。用CH3虽然也能做到但CH1和CH2在引脚映射、PWM启动流程、以及跟刹车保护配合上更顺手这也是很多参考设计默认选CH1/CH2的原因。1.3 为什么STM32G0B1CBT6这颗料适合干这个活选STM32G0B1CBT6首先是成本考虑。G0系列定位入门级但它的TIM1高级定时器并没有被砍功能互补输出、死区、刹车、重复计数器全都在。这意味着你用一颗很便宜的MCU就能实现以前需要专用PWM控制器才能完成的移相全桥驱动。其次是主频架构这颗料跑64MHz做100kHz级别的PWM开关频率分频下来有640个计数点相位分辨率能做到0.5625度绝大多数移相全桥应用都绰绰有余。另外STM32G0B1CBT6的系统时钟树比较简单内部HSI是16MHz外部晶振可以倍频到64MHz没有F4那种复杂的多级总线锁相环配置。我实际测试下来TIM1的时钟就是APB时钟当APB预分频为1时TIM1直接就是64MHz周期计算非常干净。这颗料还自带多个运放和比较器后面如果想做原边电流采样、过流保护可以直接用内部比较器接到TIM1的刹车引脚实现硬件级关断板子可以省掉一颗比较器芯片。2. 定时器资源盘点与硬件电路准备2.1 TIM1高级定时器的关键寄存器与特性STM32G0B1CBT6的TIM1是16位高级定时器最大计数周期65535支持4个独立通道其中前三个通道带互补输出。对我们这个项目来说最核心的寄存器有这么几块一是时基寄存器PSC和ARR决定PWM频率二是捕获/比较寄存器CCMR1和CCR1/CCR2决定每个通道的占空比和相位位置三是输出使能寄存器CCER决定四路输出是否真正接到GPIO引脚四是死区与刹车寄存器BDTR里面既要设置死区时间还要置位主输出使能位MOE。这里特别提一下MOE这是TIM1最容易坑人的地方。TIM1的所有输出通道最终都要经过MOE这个总开关MOE为0时即使CCER、CCMR全部配置正确引脚上也不会有任何波形输出。这个门控机制是高级定时器的安全设计目的是让硬件故障时能一键切断所有PWM输出。在调试初期如果发现“寄存器配了但示波器没波形”90%的可能是MOE没置1。用HAL库的HAL_TIM_PWM_Start会帮你自动拉高MOE但如果你在写寄存器版代码千万别漏掉TIM1-BDTR | TIM_BDTR_MOE这一句。另一个实际用得上的特性是重复计数器RCR。它可以在N个更新事件之后才产生一次更新中断/DMA请求对移相全桥这种高频PWM场景如果要在每个PWM周期都做ADC采样或更新移相角可以设置RCR减少中断频率降低CPU负担。我在做软启动的时候就是用RCR每64个PWM周期触发一次中断逐步调整CCR2实现平滑移相CPU占用率几乎可以忽略。2.2 引脚映射与最小驱动电路规划STM32G0B1CBT6是LQFP48封装引脚数量不算多。我的板子上TIM1_CH1走PA8TIM1_CH1N走PA7TIM1_CH2走PA9TIM1_CH2N走PA10这组默认映射在CubeMX里一眼就能看到。如果你用的封装不一样记得在CubeMX的Pinout视图里确认Alternate FunctionG0系列的AF表比F1稍微复杂选错复用功能会导致引脚一直输出高阻。从TIM1引脚出来到功率管的栅极中间必须加一级栅极驱动电路。MCU的GPIO驱动能力扛不住大电流MOSFET的栅极充放电尤其是开关频率上了100kHz之后驱动源和灌电流不够波形上升沿会变得很缓开关损耗剧增严重时管子直接进入线性区发热。我用的是半桥栅极驱动芯片CH1和CH1N接驱动芯片的HIN和LIN自举电容选择100nF到220nF之间具体容量看开关频率和MOSFET栅极电荷量保证高端管在每一周期都能被可靠开通。CH2和CH2N同样接另一颗驱动芯片。如果要求隔离则用带隔离的驱动芯片或变压器驱动方案成本高一些但安全性好。硬件上还有一个容易被低估的点TIM1的刹车引脚BKIN建议预留出来。它可以接一个过流比较器的输出当原边电流超过阈值时硬件电平直接让TIM1进入刹车状态所有输出全部钳位到安全电平响应时间在几个时钟周期内比任何软件中断都快。这个习惯我后来在每一版电源板上都坚持保留确实救过我一次板子。3. TIM1移相PWM的配置过程与核心代码3.1 在STM32CubeMX里把基础工程搭起来打开STM32CubeMX芯片选STM32G0B1CBT6在Pinout视图里把PA7、PA8、PA9、PA10全部选中并分别分配为TIM1_CH1N、TIM1_CH1、TIM1_CH2、TIM1_CH2N。时钟树部分建议用外部晶振倍频到64MHz如果板子上没有晶振用HSI 16MHz再PLL到64MHz也完全可行但要注意HSI精度一般如果之后要做USB通信还是老老实实加外部晶振。TIM1的参数配置里先设置预分频PSC再设置自动重载ARR计数器模式选择Up。这里我特别说明一下CubeMX里PWM频率的计算公式是fPWM TIM1_CLK / (PSC 1) / (ARR 1)因为是向上计数模式一个完整PWM周期只经历一次从0到ARR的计数过程。如果你在CubeMX里输入目标频率100kHz它会自动算出ARR的值。然后Channel1和Channel2的模式都选择“PWM Generation CHx”和“PWM Generation CHxN”这样CubeMX会自动把CCER里的互补输出使能位设置好不用手动补。死区时间在Break and Dead-Time settings页面里配置。这里直接填你想要的时间比如200nsCubeMX会根据TIM1时钟自动换算成DTG寄存器的值。千万别自己手工换算DTG的编码分段逻辑有好几种区间手工算容易出错而且调试时看不出问题在哪。配置完成后生成代码但是要注意CubeMX生成的只是基础PWM移相角需要自己额外写代码去修改CCR2这也是整个项目里唯一需要手动干预的地方。3.2 关键参数计算100kHz、30度移相角怎么算假设系统时钟64MHzAPB预分频为1TIM1时钟就是64MHz一个定时器计数周期是15.625ns。目标是100kHz的PWM频率那么在向上计数模式下ARR 64MHz / 100kHz - 1 640 - 1 639CCR1 319占空比约50%CCR2 319 移相计数值移相计数的计算很直接一个PWM周期共640个计数点对应360°。如果要移相30°那么移相计数值 640 × 30 / 360 53.33取整为53。也就是说CCR2 319 53 372。当CNT计数器从0增长到CCR1时CH1翻转输出继续增长到CCR2时CH2翻转输出。两条通道的上升沿之间差了53个计数周期对应的时间是53 × 15.625ns 828ns。由于每个计数周期固定PWM频率恒定所以这个相位差在时间上也是恒定的不会因为温度或电压波动而变化。关于死区200ns对应多少计数200ns / 15.625ns 12.8取13个计数周期实际死区约203ns。这个时间对于常见MOSFET完全够用但要注意你实际选用的栅极驱动芯片的传输延迟死区时间必须大于“MCU输出的死区 驱动芯片的延迟误差”否则依然存在直通风险。我的经验是死区设在开关周期的1%到2%之间比如100kHz周期10μs200ns就是周期的2%是一个比较稳妥的起点。再强调一次CCR2的计算结果必须小于ARR。如果移相角加过头比如45°对应CCR2 319 80 399还在范围内但如果是120°CCR2 319 213 532也还在范围内如果设定150°CCR2 319 266 585也还在。真正危险的是接近180°CCR2可能超过639这时PWM输出会让该通道的占空比不再是50%逻辑混乱。实际移相全桥的正常移相范围一般不超过60°所以这个问题在常规应用里不会碰到但代码里最好做一个边界判断。3.3 核心代码HAL库下的PWM初始化和移相修改CubeMX生成工程后核心初始化在MX_TIM1_Init函数里。我用HAL库做框架代码里最关键的部分如下配合注释说明每一步的作用static void MX_TIM1_Init(void) { TIM_MasterConfigTypeDef sMasterConfig {0}; TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig {0}; TIM_OC_InitTypeDef sConfigOC {0}; htim1.Instance TIM1; htim1.Init.Prescaler 0; // PSC 0TIM1时钟64MHz htim1.Init.CounterMode TIM_COUNTERMODE_UP; htim1.Init.Period 639; // ARR 639100kHz htim1.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim1.Init.RepetitionCounter 0; htim1.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_PWM_Init(htim1); sMasterConfig.MasterOutputTrigger TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode TIM_MASTERSLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(htim1, sMasterConfig); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 319; // CCR1 31950%占空比 sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCNPolarity TIM_OCNPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; sConfigOC.OCIdleState TIM_OCIDLESTATE_RESET; sConfigOC.OCNIdleState TIM_OCNIDLESTATE_RESET; HAL_TIM_PWM_ConfigChannel(htim1, sConfigOC, TIM_CHANNEL_1); sConfigOC.Pulse 372; // CCR2 319 53移相30° HAL_TIM_PWM_ConfigChannel(htim1, sConfigOC, TIM_CHANNEL_2); sBreakDeadTimeConfig.OffStateRunMode TIM_OSSR_DISABLE; sBreakDeadTimeConfig.OffStateIDLEMode TIM_OSSI_DISABLE; sBreakDeadTimeConfig.LockLevel TIM_LOCKLEVEL_OFF; sBreakDeadTimeConfig.DeadTime 13; // 200ns死区CubeMX换算后的值 sBreakDeadTimeConfig.BreakState TIM_BREAK_DISABLE; sBreakDeadTimeConfig.BreakPolarity TIM_BREAKPOLARITY_HIGH; sBreakDeadTimeConfig.BreakFilter 0; sBreakDeadTimeConfig.AutomaticOutput TIM_AUTOMATICOUTPUT_DISABLE; HAL_TIMEx_ConfigBreakDeadTime(htim1, sBreakDeadTimeConfig); }启动PWM时需要分别启动CH1和CH2两个通道。注意这里的细节HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_2);调用HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1)后CH1和CH1N都会输出波形因为CubeMX已经把CC1NE使能了同理HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_2)开启CH2和CH2N。调用一次Start之后MOE会被HAL库自动置1所以不需要额外写__HAL_TIM_MOE_ENABLE(htim1)但了解这个机制对排查问题很有帮助。运行中动态修改移相角只需要一行__HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_2, 319 phase_shift_count);因为CubeMX已经开启了CCR预装载功能这个比较值会在下一次更新事件自动生效不会造成输出波形畸变。这也是为什么我说“软启动很方便”你在主循环里按一定时间间隔逐步增大phase_shift_countPWM的移相角就会平滑地变化。3.4 软启动与动态移相的实现思路移相全桥直接带满载启动原边会出现很大的冲击电流可能直接触发过流保护严重时损毁开关管。所以启动时我会让移相角从一个安全的大角度开始比如从120度逐步减小到目标角度。角度越大原边传能越少变压器承受的电压冲击也越小。软启动本质上就是让移相角按时间步进变化。简化版的软启动代码可以这样写uint32_t target_phase 53; // 30度对应的移相计数 uint32_t current_phase 213; // 120度对应的移相计数启动初值 while (current_phase target_phase) { current_phase--; __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_2, 319 current_phase); HAL_Delay(1); // 每个计数变化间隔1ms }实际项目里我不会用HAL_Delay阻塞主循环而是用一个定时器中断周期性地更新移相角比如每1ms做一次CCR2的递减。这样闭环控制还能在软启动期间同时做母线电压检测任何异常都能快速响应。如果你需要在特定时刻马上停掉PWM输出可以直接调用HAL_TIM_PWM_Stop(htim1, TIM_CHANNEL_1)或者更干脆地把MOE清零。用刹车功能会更专业不过那就需要额外硬件配合了。4. 调试实录与常见问题排查4.1 高频问题排查速查表我把调试移相PWM过程中最常遇到的问题整理成一张表很多问题我自己都踩过照着排除能省不少时间。现象可能原因排查/解决方法引脚完全没有波形MOE主输出使能未置1检查TIM1-BDTR的MOE位或确认调用了HAL_TIM_PWM_Start只有CH1/CH1N有输出CH2没有CCER里CC2E/CC2NE未使能GPIO和PWM通道都配置后检查CubeMX是否勾选了CH2输出CH1N和CH1不是互补关系CC1NP极性配置错误在CCER里检查CC1NP位正常情况下CH1N与CH1极性相反