移相全桥这四个字,做电源的兄弟一听就知道是硬骨头:四个开关管,时序精度要跑到纳秒级,上下桥臂之间还得留死区防止炸管。而用一颗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_CH1N,TIM1_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走PA8,TIM1_CH1N走PA7,TIM1_CH2走PA9,TIM1_CH2N走PA10,这组默认映射在CubeMX里一眼就能看到。如果你用的封装不一样,记得在CubeMX的Pinout视图里确认Alternate Function,G0系列的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页面里配置。这里直接填你想要的时间,比如200ns,CubeMX会根据TIM1时钟自动换算成DTG寄存器的值。千万别自己手工换算,DTG的编码分段逻辑有好几种区间,手工算容易出错,而且调试时看不出问题在哪。配置完成后生成代码,但是要注意:CubeMX生成的只是基础PWM,移相角需要自己额外写代码去修改CCR2,这也是整个项目里唯一需要手动干预的地方。
3.2 关键参数计算:100kHz、30度移相角怎么算
假设系统时钟64MHz,APB预分频为1,TIM1时钟就是64MHz,一个定时器计数周期是15.625ns。目标是100kHz的PWM频率,那么在向上计数模式下:
- ARR = 64MHz / 100kHz - 1 = 640 - 1 = 639
- CCR1 = 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μs,200ns就是周期的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 = 0,TIM1时钟=64MHz htim1.Init.CounterMode = TIM_COUNTERMODE_UP; htim1.Init.Period = 639; // ARR = 639,100kHz 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 = 319,50%占空比 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_count,PWM的移相角就会平滑地变化。
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极性相反 |