1. 为什么我要折腾六路互补PWM这个实验
如果你正在做无刷电机驱动、数字电源、逆变器或者任何需要三相桥式拓扑的项目,六路互补PWM几乎是绕不开的一道坎。我最近在基于STM32G431做一块无刷电机驱动板,板子打样回来之后第一件事就是验证TIM1能不能正常输出六路互补波形,死区时间能不能按预期插入,刹车保护能不能及时封波。这个实验看起来简单,但实际调起来坑不少,尤其是中心对齐模式下更新中断的触发时机、死区时间的计算、互补通道的极性配置这几个地方,稍不注意就会翻车。
所谓六路互补PWM,说白了就是三对互补信号,每对包含一个高边驱动和一个低边驱动,两者电平相反,中间插入一段死区时间防止上下管直通。STM32G431的高级定时器TIM1自带三对互补通道(CH1/CH1N、CH2/CH2N、CH3/CH3N),加上刹车输入和死区发生器,硬件层面就是为电机控制量身定做的。但硬件有归有,配置不对照样出不来波形,或者波形出来了但死区不对、极性反了、中断不触发,这些问题我在调试过程中基本都遇到了。
这篇文章适合谁看?如果你刚接触STM32的PWM配置,或者之前只用过普通PWM输出没碰过互补模式,又或者你已经配了互补PWM但波形总是不对,那这篇内容应该能帮你省下不少时间。我会从整体设计思路讲起,然后拆解关键配置细节,再给出完整的实操流程和参数计算过程,最后把我踩过的坑整理成排查表。所有代码基于STM32CubeMX生成的HAL库工程,芯片型号STM32G431CBU6,主频170MHz,定时器时钟170MHz。
2. 整体设计思路与方案选型
2.1 为什么选TIM1而不是其他定时器
STM32G431属于G4系列,定位就是电机控制和高精度电源,它内部有三个高级定时器:TIM1、TIM8和TIM20。这三个都支持互补输出、死区插入和刹车功能。我选TIM1的原因很简单:它是挂在APB2上的,时钟频率最高可以到170MHz,而且CubeMX对TIM1的支持最成熟,社区资料也最多。TIM8和TIM20虽然功能一样,但在某些封装上引脚复用不如TIM1灵活。
这里有个细节值得注意:G431的TIM1时钟源来自APB2总线,但并不是直接等于APB2频率。APB2的预分频器如果设为1,TIM1时钟就是170MHz;如果APB2预分频不为1,TIM1时钟会是APB2频率的2倍。我在CubeMX里把APB2预分频设为1,所以TIM1直接跑170MHz,这样PWM分辨率最高。
2.2 中心对齐模式的选择理由
PWM模式有边沿对齐和中心对齐两种。边沿对齐就是计数器从0数到ARR然后归零,波形不对称;中心对齐是计数器从0数到ARR再倒数回0,波形关于中心对称。做电机FOC控制必须用中心对齐,因为只有中心对齐才能在PWM周期中间触发ADC采样,采到的才是相电流的真实值。边沿对齐模式下,PWM周期内电流是变化的,采样时刻不同结果差很多。
中心对齐还有一个好处是谐波特性更好。边沿对齐的PWM频谱里除了基频和开关频率,还有很多低次谐波,中心对齐可以把这些谐波推高到开关频率附近,对电机噪音和EMI都有改善。代价是同样的ARR值下,中心对齐的实际PWM频率是边沿对齐的一半,因为一个完整周期需要上下各数一次。
2.3 死区时间的计算逻辑
死区时间是六路互补PWM最核心的参数之一。设得太小,上下管可能直通烧管子;设得太大,输出波形畸变严重,电机低速时尤其明显。死区时间的具体数值取决于你用的MOS管或IGBT的开关特性,包括开通延迟、关断延迟和驱动电路的传输延迟。
STM32的死区时间通过BDTR寄存器的DTG位配置,计算公式分四段:
| DTG[7:5] | 死区时间计算公式 | 适用范围 |
|---|---|---|
| 0xx | DT = DTG[7:0] × tDTS | 0~127×tDTS |
| 10x | DT = (64 + DTG[5:0]) × 2×tDTS | 128~254×tDTS |
| 110 | DT = (32 + DTG[4:0]) × 8×tDTS | 256~504×tDTS |
| 111 | DT = (32 + DTG[4:0]) × 16×tDTS | 512~1008×tDTS |
其中tDTS是死区时间基准时钟周期,由TIM1的时钟分频决定。我用的MOS管是IRF3205配IR2104半桥驱动,查手册开通延迟约50ns,关断延迟约80ns,驱动芯片传输延迟约200ns。保守起见死区时间取500ns左右。tDTS我设为1/170MHz≈5.88ns,500ns/5.88ns≈85,落在第一段范围内,所以DTG直接填85即可。
2.4 刹车功能的预留设计
刹车输入(BKIN)是高级定时器的安全特性,当检测到刹车信号时,硬件自动把所有输出通道拉到预设的安全电平,不需要软件干预。我在设计时把BKIN引脚引出来接到过流比较器的输出,一旦电流超过阈值,比较器翻转,TIM1立即封波。这个响应速度是纳秒级的,比软件中断快得多。
刹车后的恢复策略有两种:自动恢复和手动恢复。自动恢复是刹车信号消失后PWM自动恢复输出,手动恢复需要软件清除刹车标志再重新使能输出。电机驱动场景我建议用手动恢复,因为过流往往是故障状态,自动恢复可能导致反复过流烧管。CubeMX里把Automatic Output State设为Disable就是手动恢复。
3. 核心配置细节与CubeMX实操要点
3.1 时钟树配置与定时器时钟确认
打开CubeMX后第一件事是配时钟树。G431的外部晶振我用的24MHz,经过PLL倍频到170MHz作为SYSCLK。关键一步是确认APB2预分频器设为1,这样TIM1的时钟就是170MHz。如果你把APB2分频设成2,TIM1时钟会变成170MHz(因为定时器时钟是APB2的2倍),但其他外设频率会变,容易搞混。
配置完成后在Clock Configuration页面可以看到TIM1的时钟标注,确认是170MHz。这个数值直接决定后续PWM频率和死区时间的计算精度,一定要先确认再往下走。
3.2 TIM1参数设置:中心对齐模式1
在Timers选项卡里选TIM1,Clock Source选Internal Clock。然后配置以下参数:
- Prescaler:0,不分频,计数器时钟170MHz
- Counter Mode:Center Aligned Mode 1
- Counter Period (ARR):4250
- Internal Clock Division:No Division
- Repetition Counter:1
- auto-reload preload:Enable
ARR为什么取4250?中心对齐模式下PWM频率 = 170MHz / (2 × ARR) = 170M / 8500 = 20kHz。20kHz是电机驱动的常用开关频率,高于人耳听觉范围,同时开关损耗可以接受。如果你做数字电源可能需要更高的频率,那就减小ARR值。
Repetition Counter设为1意味着每两次计数溢出才产生一次更新事件。中心对齐模式下计数器先向上溢出一次再向下溢出一次,Repetition Counter=1让更新中断在每个完整PWM周期结束时触发一次,而不是上下各触发一次。这个细节很关键,设错了中断频率会翻倍。
3.3 通道配置:PWM模式1与互补输出
TIM1的CH1、CH2、CH3配置为PWM Generation CH1 CH1N这种互补模式。每个通道的具体参数:
- Mode:PWM mode 1
- Pulse (CCR):初始值设ARR的一半,即2125,占空比50%
- Output compare preload:Enable
- Fast Mode:Disable
- CH Polarity:High
- CHN Polarity:High
- Idle State:Reset
PWM模式1和模式2的区别在于有效电平的定义。模式1是计数器小于CCR时输出高电平,模式2是计数器小于CCR时输出低电平。互补通道CH1N的输出逻辑与CH1相反,但具体极性还受CCR和极性位共同影响。我统一用模式1加高极性,这样CH1高电平时CH1N低电平,逻辑直观。
这里有个容易搞混的地方:CH Polarity和CHN Polarity都设为High时,CH1和CH1N是严格互补的。如果把CHN Polarity设为Low,CH1N的极性会反转,导致CH1和CH1N可能同时为高,那就危险了。所以互补输出的两个极性位必须设成一样的。
3.4 死区配置与BDTR寄存器
在Break and Dead Time配置页里:
- Dead Time:85
- Automatic Output:Disable
- Break Polarity:High
- Break Filter:0
- OSSR State:Enable
- OSSI State:Enable
- Lock Configuration:Off
OSSR和OSSI这两个位控制运行模式和空闲模式下输出引脚的状态。OSSR=Enable表示运行模式下当输出被禁用时,引脚由定时器控制而不是释放为高阻;OSSI=Enable表示空闲模式下同样保持控制。对于电机驱动,这两个都使能比较安全,避免引脚悬空导致误触发。
Lock Configuration我设为Off,方便调试阶段随时改寄存器。量产时可以设成Level 1或更高,防止寄存器被意外修改。
3.5 更新中断与NVIC配置
在NVIC Settings里勾选TIM1 update interrupt。这个中断在每个PWM周期结束时触发,用来做FOC算法计算或者更新CCR值。中断优先级我设为1,比SysTick低一点,避免影响系统时基。
中心对齐模式下更新中断的触发时刻是在计数器从ARR倒数到0的时候,也就是PWM周期的中点。这个时刻正好对应相电流采样的最佳时机,所以ADC触发通常也配在这个点。如果你用TIM1的TRGO触发ADC,TRGO源选Update Event,ADC就会在更新中断的同时启动转换。
4. 完整实操流程与代码实现
4.1 CubeMX工程生成与初始化代码
配置完成后生成MDK或STM32CubeIDE工程。生成的代码里MX_TIM1_Init函数包含了所有配置。我习惯在main函数里先调用HAL_TIM_PWM_Start和HAL_TIMEx_PWMN_Start分别启动主通道和互补通道:
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2); HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_2); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_3); HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_3);注意互补通道的启动函数是HAL_TIMEx_PWMN_Start而不是HAL_TIM_PWM_Start,这个容易写错。如果只启动了主通道没启动互补通道,CH1N引脚不会有输出。
然后启动更新中断:
HAL_TIM_Base_Start_IT(&htim1);4.2 死区时间的实测验证方法
代码跑起来之后怎么验证死区时间对不对?最直接的方法是用示波器双通道同时抓CH1和CH1N的波形。把两个通道的探头分别接到PA8和PB13(TIM1_CH1和CH1N的默认引脚),触发方式设为CH1上升沿,时基调到100ns/div左右。
正常的波形应该是:CH1上升沿之后经过一段死区时间,CH1N才下降;CH1下降沿之后同样经过死区时间,CH1N才上升。两段死区时间应该相等,实测值应该接近500ns。如果死区时间明显不对,检查DTG值是否算错,或者tDTS是否按预期分频。
我实测下来DTG=85时死区时间约490ns,和理论值500ns差10ns左右,这个偏差来自引脚输出延迟和探头误差,完全可以接受。
4.3 占空比动态调整与CCR更新
电机控制需要实时调整占空比,也就是动态改CCR值。由于我使能了Output compare preload,写入CCR的新值不会立即生效,而是在下一个更新事件时才加载到影子寄存器。这样做的好处是避免PWM周期中间改CCR导致波形出现毛刺。
更新CCR的代码:
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, new_ccr1); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, new_ccr2); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, new_ccr3);这三个值通常来自FOC算法的SVPWM计算输出。注意CCR值不能超过ARR,否则占空比会饱和到100%。我在代码里加了限幅保护:
if (new_ccr1 > ARR_VALUE) new_ccr1 = ARR_VALUE; if (new_ccr1 < 0) new_ccr1 = 0;4.4 刹车功能的测试与恢复流程
刹车功能测试很简单:把BKIN引脚(TIM1_BKIN默认是PB12)短接到地或拉高(取决于Break Polarity设置),观察PWM输出是否立即被封。我设的是High极性,所以给BKIN一个高电平,六路输出应该全部变成Idle State设定的电平。
刹车触发后会在SR寄存器里置位BIF标志,恢复流程是:
__HAL_TIM_CLEAR_FLAG(&htim1, TIM_FLAG_BREAK); __HAL_TIM_MOE_ENABLE(&htim1);第一句清刹车标志,第二句重新使能主输出。注意MOE位(Main Output Enable)在刹车触发时会被硬件自动清除,必须软件重新置位才能恢复输出。如果你用的是自动恢复模式,这两句就不需要了,但我不建议自动恢复。
4.5 更新中断服务函数的编写
更新中断里做两件事:一是触发ADC采样,二是执行FOC计算。简化版的ISR:
void TIM1_UP_TIM16_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(&htim1, TIM_FLAG_UPDATE) != RESET) { __HAL_TIM_CLEAR_FLAG(&htim1, TIM_FLAG_UPDATE); // 触发ADC或执行FOC foc_calculation(); } }注意G431的TIM1更新中断和TIM16共用中断向量,所以函数名是TIM1_UP_TIM16_IRQHandler。这个在启动文件里可以查到,写错了中断不会进。
5. 常见问题与排查技巧实录
5.1 互补通道没有输出怎么办
这是最常见的问题。排查顺序如下:
| 排查项 | 检查方法 | 解决方法 |
|---|---|---|
| MOE位是否使能 | 读BDTR寄存器bit15 | 调用__HAL_TIM_MOE_ENABLE |
| 互补通道是否启动 | 检查是否调用了HAL_TIMEx_PWMN_Start | 补上启动代码 |
| 引脚复用是否正确 | CubeMX里看引脚颜色 | 重新配置GPIO |
| 刹车是否触发 | 读SR寄存器BIF位 | 清标志并重新使能MOE |
| OSSR/OSSI设置 | 检查BDTR配置 | 都设为Enable |
我遇到过一次互补通道没输出,查了半天发现是刹车引脚悬空导致BIF被置位,MOE被硬件清除了。把BKIN引脚加上下拉电阻之后就正常了。
5.2 死区时间实测与理论值偏差大
如果实测死区时间和计算值差很多,先确认tDTS的分频设置。CubeMX里Internal Clock Division如果设成了2或4,tDTS会相应变大,死区时间就跟着变大。我建议设成No Division,让tDTS=1/170MHz,计算最直观。
另一个可能的原因是探头和示波器带宽不够。测量500ns级别的死区时间,示波器带宽至少100MHz,探头要用1x档而不是10x档,否则上升沿被拉缓,测出来的死区时间偏大。
5.3 更新中断频率不对
中心对齐模式下更新中断的频率应该是PWM频率,即20kHz。如果你发现中断频率是40kHz,那说明Repetition Counter设成了0。设成1之后,每两个计数溢出才产生一次更新事件,中断频率就对了。
还有一种情况是中断根本不进。检查NVIC里TIM1 update interrupt有没有使能,中断优先级有没有被其他中断屏蔽,以及启动文件里的中断函数名是否匹配。
5.4 PWM波形有毛刺或抖动
波形毛刺通常来自几个方面:一是CCR更新时没有使能preload,新值立即生效导致占空比突变;二是死区时间太小,上下管切换时出现直通;三是电源去耦不足,开关瞬间电源跌落导致输出异常。
我的经验是CCR preload必须开,死区时间宁大勿小,电源引脚旁边至少放100nF加10uF的组合去耦。如果毛刺还在,用示波器的单次触发模式抓一下毛刺出现的时刻,看是否和更新中断或刹车信号相关。
5.5 刹车后无法恢复输出
刹车后恢复不了,八成是BIF标志没清或者MOE没重新使能。还有一个小概率情况是Lock Configuration设成了Level 1以上,BDTR寄存器被锁定,MOE写不进去。调试阶段把Lock设为Off,量产再根据需求锁定。
另外注意,如果刹车信号一直存在,即使清了BIF,MOE使能后硬件会立即再次触发刹车。所以恢复前要确认刹车源已经消失。
6. 实测波形分析与参数优化建议
6.1 六路波形的相位关系验证
用四通道示波器抓CH1、CH1N、CH2、CH2N的波形,可以看到CH1和CH2之间相差120度,CH1N和CH1互补,CH2N和CH2互补。中心对齐模式下,三路PWM的相位关系是关于周期中心对称的,这和边沿对齐的相位分布不一样。
验证相位关系的方法是测量CH1上升沿到CH2上升沿的时间差。20kHz下周期是50us,120度对应50us×120/360≈16.67us。实测值在这个附近就说明相位正确。
6.2 死区时间对输出波形的影响
死区时间会略微降低有效占空比。高边导通时间实际是CCR值减去死区时间,低边导通时间也是CCR值减去死区时间。在50%占空比时影响不大,但在接近0%或100%占空比时,死区时间会导致输出无法达到极限值。
如果应用需要接近100%的占空比,可以考虑在极端区域动态减小死区时间,或者用死区补偿算法在CCR里加上死区偏移。我在低速电机驱动里用了简单的补偿:高边CCR加死区值,低边CCR减死区值,实测波形对称性明显改善。
6.3 开关频率与死区时间的权衡
开关频率越高,死区时间占PWM周期的比例越大,波形畸变越严重。20kHz下500ns死区占周期的1%,影响很小;如果开关频率提到100kHz,500ns就占5%,低速时电机转矩脉动会很明显。
所以高频应用要选开关速度更快的MOS管,把死区时间压到100ns以内。但死区时间太小又有直通风险,需要根据管子的实际开关特性仔细权衡。我的建议是先用示波器测出管子的实际开通和关断延迟,然后死区时间取关断延迟的1.5倍左右,留足裕量。
6.4 中心对齐模式下的ADC采样时机
中心对齐模式下,计数器在ARR处向下溢出时触发更新中断,这个时刻PWM输出处于周期中点,三相下管全部导通,相电流在采样电阻上产生的压降最稳定。所以ADC采样应该紧跟在更新中断之后启动,转换时间要远小于死区时间,否则采到的值已经变了。
我用的ADC采样时间设为2.5个ADC时钟周期,总转换时间约1us,远小于500ns死区时间加PWM周期,采样精度足够。如果ADC转换太慢,可以降低分辨率或者提高ADC时钟。
7. 个人实操体会与后续扩展方向
这个实验做完之后我最大的体会是:六路互补PWM的配置本身不复杂,但细节特别多,任何一个参数设错都可能导致波形异常甚至烧管。死区时间的计算、MOE位的管理、刹车标志的清除这三件事必须搞清楚,否则调试时会很痛苦。
后续我打算在这个基础上加三电阻电流采样,配合TIM1的TRGO触发ADC,实现完整的FOC电流环。再往后是加上过流比较器的硬件刹车通路,把保护响应时间压到最短。如果你也在做类似的项目,建议先把六路互补波形调稳,确认死区时间和相位关系都正确,再往上叠算法,这样出问题的时候排查范围小很多。
最后分享一个小技巧:调试PWM的时候把示波器的余辉模式打开,可以很直观地看到波形的稳定性和抖动情况。如果余辉模式下波形边缘模糊,说明有抖动,需要查时钟源或者CCR更新逻辑;如果边缘清晰锐利,说明PWM输出很干净,可以放心往下做。