
做移相全桥电源的人多半都绕不过“怎么生成四路带死区、相位可调的PWM”这道坎。以前我玩数字电源用的还是TI DSP的ePWM模块后来因为项目要换国产化方案全部逻辑迁移到STM32上当时第一反应是“定时器够不够用、移相角度能不能调得准”。折腾完一轮之后我想把完整的实现思路和踩坑记录整理出来给正打算用STM32做全桥电源的人一个参考。先说清楚这篇文章能解决什么问题不是讲移相全桥的功率拓扑怎么设计也不是讲闭环算法而是纯粹聚焦在“STM32如何生成任意角度移相的4路PWM驱动波形”。我会把定时器配置、角度计算、死区插入、实测波形、常见坑全部铺开讲。适合正在做数字电源、全桥LLC、DAB、逆变器或者单纯想把STM32高级定时器玩明白的工程师阅读。1. 移相全桥的驱动需求以及STM32方案选型1.1 移相全桥驱动波形的基本逻辑先简单回顾一下移相全桥Phase-Shifted Full BridgePSFB在驱动层面的需求。全桥电路有4个开关管习惯上分成超前桥臂Leading Leg和滞后桥臂Lagging Leg每个桥臂上下两个管子互补导通上下管之间要留死区时间防止直通短路。关键点在于两个桥臂之间并不是同时导通的而是存在一个相位差这个相位差就是所谓的“移相角”。通过调节移相角的大小可以改变原边施加到变压器上的等效电压占空比从而调节输出功率。传统的模拟方案用移相芯片比如UCC3895来实现而数字方案则是通过MCU的定时器产生4路PWM利用软件动态调整两路PWM之间的相位关系。驱动波形上我们通常期望4路PWM的占空比都接近50%实际每路留一点死区在这个前提下任意改变两个桥臂之间的相位差。这个“占空比固定但相位可调”的需求恰恰是STM32中心对齐模式最擅长的事情。1.2 为什么要用STM32而不是专用芯片或DSP先说说我为什么从DSP转到STM32。专用移相芯片如UCC3895当然省心但它的问题在于灵活性差很多参数被硬件固定无法适应需要动态调整、多模式切换的数字电源方案。DSP的ePWM模块确实非常强但价格和供货在近两年的环境下都是问题。STM32的优势在于生态成熟、参考资料多、价格便宜、高中低端产品线齐全而且高级定时器TIM1/TIM8具备互补PWM输出和硬件死区基本上是为电机控制和电源变换量身定做的。配合HAL库或者标准外设库可以在很短时间内搭建一个数字电源的PWM核心。我用的是STM32F103主频72MHz虽然性能不算强但生成4路20kHz~100kHz的移相PWM完全够用。如果做更高端的双向DC-DC或者更复杂的控制算法可以考虑STM32G4系列它的定时器资源更丰富还内置了高速比较器、DAC等模拟外设做数字电源更合适。1.3 “任意角度”在工程上意味着什么标题里“任意角度”听起来很玄其实工程上就是指移相角可以0~360°连续可调能往前移也能往后移并且分辨率要足够细。为什么强调这一点因为在移相全桥的实际应用中移相角不仅用于正常调节输出电压还可能用于实现软开关ZVS/ZCS的优化甚至在不同负载条件下动态调整。如果角度只能粗粒度调节比如1°一步就可能造成输出电压纹波偏大或者环路控制精度不足。用STM32的中心对齐模式理论上角度分辨率可以达到0.1°甚至更高完全能满足模拟芯片的精度水平甚至是超越。2. 核心原理中心对齐模式下为什么CCR只移相位、不变占空比2.1 中心对齐模式与边沿对齐模式的差异很多人在用STM32输出PWM时习惯用边沿对齐模式也就是定时器从0向上计数到ARR然后立刻归零重新计数。在这种模式下改变比较值CCR会同时改变占空比和相位两者耦合在一起很难实现“只移相不改变占空比”。而中心对齐模式Center-Aligned Mode是定时器从0向上计数到ARR然后再从ARR向下计数到0如此反复。一个完整的PWM周期对应两次计数过程计数波形是一个三角波。中心对齐模式1下配合PWM模式1向上计数时CNTCCR输出有效电平向下计数时CNTCCR输出有效电平会得到一个非常有意思的结论无论CCR取什么值输出波形的占空比恒定为50%变化的只是高电平区间在周期内的位置——也就是相位。我来推导一下假设ARR 100看两组CCR值当CCR 25时向上计数阶段0~25输出有效25~100输出无效向下计数阶段100~25输出有效因为CNT CCR25~0输出无效。累加有效时间为25 (100-25) 100个计数步总周期为2 × 100 200个计数步占空比50%。当CCR 75时同样计算有效时间为75 (100-75) 100个计数步占空比也是50%。可以看到改变CCR并没有改变占空比只是把高电平区间的“中心点”在周期内移动了。这就是中心对齐模式实现移相的数学基础。2.2 四路PWM通道的分工与角度映射在STM32的高级定时器TIM1中有4个通道CH1、CH2、CH3、CH4以及互补输出CH1N、CH2N、CH3N。我们实现移相全桥一般只用CH1和CH2两组TIM1_CH1滞后桥臂上管TIM1_CH1N滞后桥臂下管TIM1_CH2超前桥臂上管TIM1_CH2N超前桥臂下管CH1和CH1N是硬件互补输出之间的死区由定时器的死区寄存器自动插入。同理CH2和CH2N。这样一来上下管直通的风险由硬件保证我们只需要关注CH1和CH2之间的相位差。CH1的比较值设为CCR1CH2的比较值设为CCR2。方向与角度定义如下当CCR1 CCR2时两个桥臂同相移相角为0此时相当于全桥没有移相输出功率为零理想情况下。当CCR1 CCR2时CH1滞后于CH2一个角度这个角度对应CCR1 - CCR2个计数步。当CCR1 CCR2时CH1超前于CH2一个角度。角度的绝对大小换算公式为移相角 (CCR1 - CCR2) / (2 × ARR) × 360°注意这里的单位是度数并且因为一个完整周期对应2 × ARR个计数步三角波来回一趟所以分母是2 × ARR而不是ARR。这一点新手很容易算错。2.3 为什么这个方案能覆盖0~360°任意角度从公式可以看出CCR1和CCR2的取值范围都是0~ARR但二者差值可以从负数到正数跨越2 × ARR的区间也就是说理论上可以实现0~360°全范围移相。这里有一个容易出问题的细节如果CCR1设定为900CCR2设定为300二者差值600对应的相位差是600 / 3600 × 360° 60°这是正确的。但是如果想实现300°的移相角直接用CCR1 900CCR2 -300是不行的因为CCR不能为负。解决方法是利用角度的周期性把300°等效为-60°即让CH1相对于CH2滞后300°等价于超前60°。所以在配置CCR时把负的差值折算成对应的正差值即可。具体做法是先计算目标角度对应的计数步数Δ 角度 / 360° × 2 × ARR然后如果CCR2 - Δ 0就在结果上加上2 × ARR这样保证比较值落在有效范围内。当然还有一个更简单的思路你不用苛求CCR本身落在0ARR之间因为当CCR超出ARR时定时器的行为会变得不可控。所以工程上我一般会用函数对CCR做一次模运算把目标比较值限定在0(2 × ARR - 1)之间然后根据实际定时器行为做映射。这个在后面的代码部分会详细说明。3. 从零开始配置以20kHz、30°移相为例的完整实操3.1 目标参数设定在做任何配置之前先把目标明确下来开关频率20kHz输出PWM路数4路CH1、CH1N、CH2、CH2N桥臂内死区时间200ns移相角30°CH2滞后CH1也就是传统全桥里滞后桥臂相对超前桥臂滞后30°MCU主频72MHz先算定时器时钟。STM32F103的TIM1挂在APB2总线上当APB2预分频为1时定时器时钟等于系统主频72MHz。这里我不对定时器再做内部分频直接使用72MHz作为计数时钟这样可以获得最高的角度分辨率。3.2 参数计算全过程第一步确定ARR。中心对齐模式下PWM频率计算公式为f_PWM 定时器时钟 / (2 × (ARR 1))注意这里与边沿对齐模式不同边沿对齐是 f 定时器时钟 / (ARR 1)中心对齐因为一个周期要来回各一趟所以要多除一个2。代入目标频率20kHz20000 72,000,000 / (2 × (ARR 1))解得 ARR 1 1800所以 ARR 1799。第二步计算一个计数步对应的角度分辨率。整个周期有2 × (ARR 1) 3600个计数步对应360°电角度所以一个计数步对应0.1°。这个分辨率相当高了做高精度数字电源完全够用。第三步计算30°移相角需要多少个计数步Δ 30° / 360° × 3600 300也就是说如果我们希望CH2滞后CH1 30°那么CCR2要比CCR1的基准值小300。注意方向问题中心对齐模式下因为波形在周期中间翻转CCR越大输出的相位越靠前或者靠后取决于你定义的正方向这个需要通过实际波形确认。我在第一次调试时就是因为搞反了方向导致移相角越大输出反而越小排查了半天。第四步设置死区时间。STM32死区时间由TIM1的BDTR寄存器DTG[7:0]控制具体延时和定时器时钟、死区发生器时钟有关。当DTG[7:5] 0时死区时间 DTG[7:0] × T_DTS其中T_DTS 1 / 72MHz ≈ 13.89ns。200ns对应的计数值为DTG 200 / 13.89 ≈ 14.4取14实测死区约为194ns接近目标值如果对死区精度敏感可以再用更细的DTG档位。3.3 CubeMX图形化配置步骤如果使用STM32CubeMX配置TIM1的步骤很直观打开TIM1的Clock Source选择Internal Clock。在PWM Generation Channel1和Channel2处打勾同时要启用CH1N和CH2N这会在Pinout视图中自动分配PA8、PA7、PB13、PB12等引脚。然后在Parameter Settings里做如下配置Counter PeriodARR设为1799Counter Mode选择Center Aligned Mode 1Repetition Counter设为0这个先不管进阶可以配合更新事件使用Auto-reload preload设为Enable这样修改占空比时不会立即生效而是等更新事件后才更新避免运行中产生不全的PWM周期CH1的Pulse值设为900也就是50%占空比对应的CCRCH2的Pulse值设为600900 - 300对应移相30°Output Compare Preload设为EnableCH1和CH2的Polarity都设为High互补输出CH1N、CH2N的Polarity根据实际驱动电路决定我这里用高有效死区时间在Break and Dead-Time settings里配置把Dead-Time值设为14单位是纳秒级别对应上面计算的200ns左右。这里有一个非常关键的点在中心对齐模式下CH1和CH2的Pulse值决定的是整个波形的相位以及“恰好为50%”的占空比。如果想让占空比严格小于50%因为要留死区不能靠修改CH1的CCR来实现那样会同时移动相位。正确的做法是在互补通道上配置死区让上下管实测占空比自然缩短到50%减去死区比例。也就是说CH1和CH2本身的CCR设定值不因死区而改变死区完全由硬件插入。3.4 代码实现与动态移相调整CubeMX配置好后初始化代码已经自动生成主要工作在main函数里启动PWMint main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM1_Init(); // 启动TIM1的4路PWM输出 HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_2); // 注意启动互补通道不是用HAL_TIM_PWM_Start而是用下面这对函数 HAL_TIMEx_PWMN_Start(htim1, TIM_CHANNEL_1); HAL_TIMEx_PWMN_Start(htim1, TIM_CHANNEL_2); while (1) { // 主循环中可以动态调整移相角 } }动态调整移相角的核心函数是修改CCR值void set_phase_shift_degree(float degree) { static uint16_t base_ccr 900; // CH1基准比较值占空比50% uint16_t ccr1, ccr2; int32_t delta; // 计算角度对应的计数步数 delta (int32_t)(degree / 360.0f * 7200.0f); // 注意这里乘以7200而不是3600因为我们要处理的是可能超过一个周期的差值 ccr1 (uint16_t)base_ccr; // 防止CCR2超出范围这里做一个周期取模 ccr2 (uint16_t)((base_ccr delta 7200) % 7200); // 如果CCR2超过了ARR范围需要截断处理 if (ccr2 1799) { ccr2 - 1800; } __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, ccr1); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_2, ccr2); }这个代码里有几个关键细节我要强调一下。第一CH1的基准CCR我固定在900因为移相全桥里超前桥臂的占空比通常恒定在50%通过调节滞后桥臂的相位来调节功率。如果你需要两路都动原理类似。第二角度到计数步的换算里我用了7200而不是3600这是为了让负数角度也能正确处理避免int32强制转换时出现符号混乱。最后对7200取模把结果归一到正数范围。第三截断处理ccr2 1799这一段很多人不理解为什么去掉1800而不是3600。原因在于中心对齐模式下CCR超过ARR其实等效于反相我们不想让波形反相所以要把超过ARR的部分折回去。这个逻辑我调试了很久才彻底理顺直接看手册相关的定时器行为描述比较难懂建议用示波器实际打一下输出波形辅助理解。3.5 现场实测30°移相的输出波形记录配置完成后我用示波器同时抓取CH1和CH2的输出波形探头分别接在TIM1_CH1PA8和TIM1_CH2PA9引脚上示波器触发电平设为1.65V时基20μs/div。实测结果和预期一致两路波形都是20kHz、50%占空比方波CH2的上升沿比CH1的上升沿滞后了约4.17μs。验证一下计算20kHz的周期是50μs30°移相对应的时间为 50μs × 30/360 ≈ 4.17μs实测基本吻合。然后用差分探头测CH1和CH1N之间、CH2和CH2N之间的互补波形死区时间检查出来约190ns左右在目标范围以内。这里有一个经验之谈测死区时间时不能只看上升沿和下降沿的简单间隔因为在带容性负载的情况下管子关断后电压波形会有一点拖尾示波器测出来的死区往往比实际死区稍大。最好在栅极驱动IC的输出端测量而不是直接测MCU引脚。接着我做了全范围移角测试从0°到360°以10°步进改变相位用示波器记录了每个角度的输出波形。结果在0°附近因为两路PWM完全同相输出变压器原边电压接近于零这是移相全桥特有的现象在180°附近输出变压器原边电压达到最大值。实测角度线性度很好未出现跳变或突变。4. 进一步优化死区设置、中心对齐模式的选择与电流采样配合4.1 死区寄存器的高级用法前面提到用DTG[7:0]直接设置死区适用于较粗粒度需求。STM32的死区发生器还有一种更细的档位利用DTG[7:5]组合可以实现最小约13.89ns的步进DTG[7:5] 000T_DTS 1 / 72MHz死区 DTG × T_DTSDTG[7:5] 001或010T_DTS 2 / 72MHzDTG[7:5] 011T_DTS 8 / 72MHz实际还和DTG[4:0]组合这里不展开DTG[7:5] 10x等进入更长死区范围适合IGBT驱动对于普通的MOSFET全桥200~500ns死区比较常见。死区太小关断慢的管子来不及完全关断可能直通烧管死区太大又会损失有效占空比降低效率。我通常的做法是先按功率管的Qg和驱动电阻计算关断时间再留出30%~50%的裕量来设定死区。例如一个Qg 60nC的管子关断电流0.5A关断时间约120ns那么死区至少设200ns。4.2 中心对齐模式1还是模式2STM32中心对齐模式有1、2、3三种区别在于比较中断标志的置位时机。对于PWM波形本身模式1和模式2的主要区别是输出有效电平的极性正好相反也就是CCR相同的情况下同样配置下输出的占空比互为镜像。对移相全桥来说我推荐使用中心对齐模式1。原因有两个第一模式1在向上计数和向下计数时都在CCR处翻转输出逻辑简单相位和CCR的映射关系清晰方便计算。第二模式1的有效电平区间对称分布在计数周期的两端这样与电流采样配合时采样窗口的居中位置比较固定便于在定时器更新中断或特定比较事件中触发ADC采样。4.3 电流采样时刻与PWM相位的关系不要忽略一个关键问题数字电源闭环控制时电流采样时刻点必须选准。移相全桥的原边电流波形不是标准的正弦或方波而是带有一个电流平台和斜坡上升段。如果想采样原边电流的平均值或者峰值采样时刻必须落在PWM波形的特定区间内。在中心对齐模式下采样时刻通常选择在计数周期的中心点也就是CNT 0或CNT ARR的时刻。此时所有PWM输出处于一种相对“平衡”的位置电流纹波最小采样值最能代表该周期的平均电流。如果你用的是边沿对齐模式采样窗口往往要避开开关管的开关瞬间否则采样值会被毛刺干扰。我这里推荐一个做法把ADC的触发源设置为TIM1的更新事件Update Event然后在更新中断服务函数里启动一次ADC转换。因为中心对齐模式的更新事件发生在CNT向上到达ARR后归零以及向下到达0后归零的瞬间对应波形的正负峰值附近采样噪声最低。4.4 多定时器协调的扩展思路如果项目只需要一路移相全桥单定时器的方案已经足够。但有些场合需要两路甚至多路全桥并联或者需要实现更复杂的PWM组合单定时器就不够用了。我的建议方案是用TIM1做主定时器输出主桥PWMTIM8做从定时器输出副桥PWM二者通过ITRx信号实现硬件同步。具体配置思路是TIM1的主输出触发TRGO选择Enable主模式选择UpdateTIM8的从模式选择外部时钟模式1或门控模式触发源选择ITR0对应TIM1。这样TIM8可以跟随TIM1的周期自动同步即使调整开关频率副桥也能自动跟进不会出现相位漂移。这样的多定时器方案特别适合双有源电桥DAB变换器因为它需要两个全桥之间独立控制移相角而DAB的功率传输方向和大小都取决于两个全桥之间相位差的方向和大小。5. 实测中的常见问题与排查技巧实录5.1 频率正常但示波器看不到PWM波形这种问题大多出在引脚复用或引脚初始化上。CubeMX生成的代码会自动配置GPIO复用但如果你手动在初始化函数里调用了HAL_GPIO_WritePin把这个引脚拉低或拉高就会覆盖掉复用功能导致PWM信号出不来。另外还有一种情况启动互补输出方式的顺序不对。必须先调用HAL_TIM_PWM_Start启动主输出再调用HAL_TIMEx_PWMN_Start启动互补输出。如果顺序颠倒有时会出现只有主输出有波形、互补输出始终为低的现象。5.2 两路PWM已有正确移相角但互补通道的死区时间不对先检查DTG寄存器的配置。如果死区设得太小示波器上看起来上下管波形几乎完全重叠时间差只有几纳秒这是很危险的。你可以把死区临时设为一个很大的值比如1μs验证一下波形变化确认死区寄存器是否真的生效。如果死区时间寄存器配置没问题但实际波形仍然出现直通就要检查外部驱动电路确认驱动芯片的上拉/下拉电阻是否合理是否有RC延时导致管子开通延迟不一致。5.3 移相角越大输出电压反而越小这通常不是PWM生成的问题而是移相方向搞反了。在移相全桥中原边电压占空比随移相角先增大后减小而且存在一个最大点。如果你一开始就设了比较大的移相角可能已经越过最大点输出自然下降。调试建议从一个很小的角度比如5°开始逐渐加大角度观察输出电压是否上升如果下降就反向调整设置。也可以把两个通道的CCR互换试试看看输出波形相位关系是否反过来。5.4 PWM在运行中突然出现毛刺可能原因是更新事件和CCR更新的时序冲突。我建议所有对CCR的修改都放在更新中断或定时器中断里做不要在普通中断或主循环里随意修改比较值。虽然STM32的预装载寄存器可以保障更新瞬间的原子性但如果在更新事件发生的同时又写入CCR某些异常时序下仍可能出现一个不受控的完整PWM周期。解决方法是开启预装载功能并让所有CCR写操作都在更新事件之后的固定时间点完成比如在更新中断服务函数里调用__HAL_TIM_SET_COMPARE。5.5 常见问题速查表现象可能原因排查思路波形完全没有输出GPIO复用未配置/MO主输出未使能检查引脚复位功能检查BDTR寄存器的MOE位只有主输出有波形互补输出恒低未启动互补输出通道调用HAL_TIMEx_PWMN_Start移相角不随CCR变化使用了边沿对齐模式而非中心对齐确认Counter Mode为Center Aligned Mode 1死区时间严重不准T_DTS计算错误/外部RC延时大用示波器测驱动IC栅极引脚调整DTG输出电压控制非线性移相角超过180°或方向搞反从小到大扫角度画出输出趋势判断方向运行中偶发一个宽脉冲CCR更新与更新事件竞争开启预装载在更新中断里改CCR6. 我的实操心得与后续扩展思考这套方案我最终用在了两台样机上一台是2kW的AC-DC全桥电源另一台是3.3kW的双向DC-DC。整体跑下来很稳定20kHz开关频率下的角度分辨率0.1°足够闭环使用。不过有几点心得值得说。第一个体会是中心对齐模式虽然相位控制精准但在轻载时容易出现不对称。尤其是CCR设置接近0或接近ARR时三角波的顶点附近可能会看到一点点毛刺这是因为计数器的更新和比较事件在时间上几乎重合。我在实际项目里做了限制把CCR的可用范围限定在50到1750之间也就是移相角约90%的范围极端角度不影响实际使用。第二个体会是STM32的定时器做移相PWM确实不如DSP的ePWM直观特别是交互触发、相位加载这些机制需要花时间读参考手册。但如果只是实现“任意角度移相”这一个核心功能单定时器的方案已经非常成熟完全不需要上DSP。第三个建议如果做的是大功率电源强烈建议加入硬件级的故障保护。STM32定时器的刹车输入Break Input可以用来做硬件过流保护一旦触发PWM输出立即进入高阻或低电平状态不需要MCU软件干预。我在样机上把过流信号接到了BKIN引脚上实测保护响应时间在百纳秒级别比软件中断保护靠谱得多。后续如果想扩展可以在这个基础上继续做软开关控制策略通过检测原边电流过零点来动态调节移相角实现滞后桥臂的ZVS。这部分控制逻辑不涉及PWM生成本身但依赖我们上面搭建的这套灵活可调的PWM框架。STM32G4系列的定时器还支持硬件死区再加PWM移相的组合更容易做出高性能的数字电源。最后再分享一个小技巧调试移相角度时如果手头没有示波器可以通过改变移相角观察输出功率或输出电压的变化趋势大致判断相位是否按照预期方向移动。第一种情况是输出电压随设置角度增大而增大第二种情况是减小这说明方向反了。这个方法在简陋的办公桌上救了我好几次。