1. 互补PWM到底解决的是什么问题
如果你之前只用过STM32定时器输出单路PWM——比如点个LED、调个电机转速、驱动个舵机——那你可能会觉得PWM这东西没什么难的:配好预分频器和自动重装载值,选个PWM模式,使能通道,搞定。但一旦你开始接触半桥驱动、H桥电机驱动、逆变器、LLC谐振变换器这类拓扑,单路PWM就完全不够用了。你需要的是两路频率完全相同、占空比互补、并且在切换瞬间留有一段"谁都不导通"的空白时间的信号。这就是互补PWM输出,那段空白时间就是死区时间。
为什么非要互补?拿最常见的半桥电路来说,上管和下管串联在电源和地之间,中间那个节点就是输出。如果上管和下管同时导通,电源直接对地短路,瞬间大电流就能把两个MOS管一起送走——这就是常说的"炸管"。所以控制逻辑上必须保证:上管开的时候下管一定关,下管开的时候上管一定关。但现实中的MOS管不是理想开关,它有开通延迟和关断延迟,栅极电荷需要时间泄放。如果上管"关"的信号刚发出去、它还没完全关断,下管就"开"了,那还是会有一小段时间两管同时导通,这个现象叫直通(shoot-through)。死区时间就是插在"一个管子关断"和"另一个管子开通"之间的安全间隔,确保先关的那个彻底关死了,再开另一个。
STM32的高级定时器(TIM1、TIM8,以及部分型号的TIM15/16/17)硬件层面就支持互补输出和死区插入,你不需要在软件里手动翻转两个GPIO,也不需要自己算延时。硬件自动帮你生成带死区的互补波形,CPU几乎零负担。这一点非常关键——如果你用软件去翻转IO做互补,一旦进了中断或者被高优先级任务抢占,时序就乱了,轻则效率下降,重则直接炸管。所以只要涉及半桥以上的拓扑,优先用高级定时器的互补输出,这是行业里的共识。
这篇文章我会以STM32CubeMX为工具,从定时器选型、CubeMX参数配置、死区时间计算、代码生成后的关键函数、到实际调试中容易踩的坑,完整走一遍。目标读者是已经会用CubeMX配置基本外设、但对互补输出和死区还没实操过的朋友。看完你应该能独立配出一对安全的互补PWM,并且知道每个参数背后的含义,而不是照着别人的截图抄一遍。
2. 定时器选型与CubeMX里的关键参数
2.1 为什么必须是高级定时器
STM32的定时器分三类:基本定时器(TIM6、TIM7)、通用定时器(TIM2~TIM5、TIM9~TIM14等)、高级定时器(TIM1、TIM8,部分型号还有TIM15/16/17、TIM20)。互补输出这个功能,只有高级定时器才有。通用定时器虽然也能输出多路PWM,但它的通道之间是独立的,没有互补关系,也没有硬件死区插入。所以你在CubeMX里选定时器的时候,第一件事就是确认你用的是TIM1或TIM8这类高级定时器。
以最常见的STM32F103C8T6为例,它有一个高级定时器TIM1,四个通道,每个通道都可以输出一对互补信号(CH1/CH1N、CH2/CH2N、CH3/CH3N、CH4/CH4N)。注意CH4比较特殊,它只有CH4没有CH4N,也就是说第四通道不能输出互补对。如果你需要三对互补PWM(比如驱动三相无刷电机),TIM1的CH1/CH2/CH3刚好够用。如果需要更多,就得看你的芯片有没有TIM8。
选定时器还有一点要考虑:引脚复用。TIM1的通道引脚分布在特定的GPIO上,比如CH1在PA8、CH1N在PB13(不同封装可能不同)。你在CubeMX里点开TIM1的某个通道,它会自动把可用引脚高亮出来,你选一个没被占用的就行。这里有个经验:尽量把互补对的两个引脚选在同一个端口或者相邻位置,布线的时候方便,尤其是做PCB的时候,互补对走线要尽量等长,减少寄生参数差异。
2.2 CubeMX中Mode配置的取舍
打开CubeMX,在Timers里选中TIM1,Combined Channels那一栏有几个选项:PWM Generation CH1、PWM Generation CH1 CH1N、PWM Generation CH2 CH2N等等。你要输出互补对,就选带"CHxN"的那个。选完之后,Channel1和Channel1N的引脚会自动分配。
接下来是Clock Source,选Internal Clock。然后到下面的Configuration里,Parameter Settings有几个关键参数:
Prescaler(预分频器):决定定时器的计数频率。假设你的系统时钟是72MHz,定时器时钟也是72MHz,Prescaler设为0,那计数频率就是72MHz。如果设为71,那就是72MHz/(71+1)=1MHz。这个值直接影响PWM的频率和死区时间的分辨率。
Counter Mode:一般选Up(向上计数)。Center-aligned模式(中央对齐)在某些电机控制场景下有用,因为它生成的PWM是对称的,谐波特性更好,但配置起来稍微复杂一点,死区插入的行为也略有不同。初学者先用Up模式。
Counter Period(ARR,自动重装载值):和Prescaler一起决定PWM频率。公式是:PWM频率 = 定时器时钟 / ((Prescaler+1) × (Counter Period+1))。比如72MHz时钟,Prescaler=0,ARR=999,那PWM频率就是72MHz/1000=72kHz。这个频率怎么选?后面会详细说。
Pulse(CCR,捕获比较值):决定占空比。占空比 = Pulse / (Counter Period+1)。比如ARR=999,Pulse=500,占空比就是50%。注意在互补模式下,CH1的占空比和CH1N的占空比是互补的——CH1高的时候CH1N低,反之亦然。但死区时间会在两者之间插入,所以实际波形不是严格的"你高我低",而是"你高、我低、中间有一段都低"。
2.3 死区时间的计算不是拍脑袋
死区时间(Dead Time)是互补PWM配置里最需要认真对待的参数。CubeMX里有一个Dead Time的输入框,你填一个数值,但它背后的单位不是纳秒,而是定时器时钟周期数。这一点很多人第一次配的时候会搞错。
STM32的死区时间生成逻辑是这样的:你填的那个值会被写入BDTR寄存器的DTG位段,硬件根据DTG的值和定时器时钟频率算出一个实际延时。DTG的编码方式有点特殊,分几档:
| DTG值范围 | 死区时间计算方式 | 适用场景 |
|---|---|---|
| 0~127 | DT = DTG × Tdtg | 短死区,分辨率高 |
| 128~191 | DT = (64 + DTG[5:0]) × 2 × Tdtg | 中等死区 |
| 192~255 | DT = (32 + DTG[5:0]) × 8 × Tdtg | 长死区 |
| 248~255 | DT = (32 + DTG[5:0]) × 16 × Tdtg | 最长死区 |
其中Tdtg = 1/定时器时钟频率(如果时钟分频系数为1)。举个例子,定时器时钟72MHz,Tdtg≈13.89ns。如果你需要500ns的死区时间,500/13.89≈36,那DTG填36就行,实际死区约500ns。
但CubeMX的Dead Time输入框,你填的值和DTG的关系取决于你选的时钟分频。CubeMX界面上会显示一个"Dead Time"的数值,单位是纳秒或者微秒,你直接填你想要的死区时间,它会自动帮你算DTG。但你要知道它算得对不对,所以最好自己心里有个数。
死区时间选多大?这取决于你的MOS管和驱动电路。一般来说,MOS管的关断延迟加上驱动芯片的传输延迟,加起来就是最小死区时间。太小了会直通,太大了会失真、效率下降。常见的取值在200ns到1μs之间。如果你用的是低速光耦驱动,可能要到1~2μs;如果是专用栅极驱动芯片,200~500ns就够了。我个人的习惯是先用示波器看实际波形,从500ns开始调,慢慢往下压,直到波形边缘刚好不重叠为止,然后再留20%的余量。
注意:死区时间是在CH1和CH1N之间插入的,不影响CH2/CH2N。每个通道的死区时间是一样的,由BDTR寄存器统一控制。
3. 从CubeMX到Keil:生成代码后的关键操作
3.1 生成代码后必须手动加的那几行
CubeMX帮你把定时器的初始化代码都生成好了,包括时钟使能、GPIO配置、时基单元、输出比较单元、BDTR寄存器(死区、刹车、使能输出)。但有一个地方CubeMX不会自动帮你做:启动PWM输出。你需要在main函数里手动调用HAL库的启动函数。
对于互补输出,要用的是HAL_TIM_PWM_Start和HAL_TIMEx_PWMN_Start。前者启动主通道,后者启动互补通道。两个都要调,少一个都不行。代码大概长这样:
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_1);如果你用的是三个通道,那就把CH1、CH2、CH3都启动一遍。注意HAL_TIMEx_PWMN_Start这个函数在stm32f1xx_hal_tim_ex.h里,不是标准tim.h,别找错了。
还有一个容易忘的点:MOE(Main Output Enable)位。高级定时器的输出有一个总开关,叫MOE,在BDTR寄存器里。如果MOE没置位,即使你启动了PWM,引脚上也不会有波形。CubeMX在配置界面里有一个"Automatic Output"选项,勾上之后,MOE会在需要的时候自动置位。但如果你用了刹车功能(Break),MOE可能会被硬件清零,这时候你需要手动重新置位。所以保险起见,在启动PWM之前,可以手动调一下:
__HAL_TIM_MOE_ENABLE(&htim1);这行代码不是必须的,但加上去不亏,尤其是调试阶段。
3.2 占空比动态调整的正确姿势
很多应用需要实时调整占空比,比如电机调速、LED调光。HAL库提供了__HAL_TIM_SET_COMPARE宏来修改CCR值:
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, new_pulse);这个宏直接写CCR寄存器,立即生效。但要注意:在互补模式下,修改CH1的CCR值,CH1N的占空比会自动跟着变,因为它们是互补关系。你不需要单独去改CH1N的CCR。实际上CH1N没有独立的CCR寄存器,它的输出逻辑是CH1的反相加上死区。
还有一个细节:如果你在PWM输出过程中修改ARR(频率),要小心。修改ARR会影响所有通道的周期,而且如果新ARR值小于当前计数值,可能会产生一个异常的长周期或短周期。正确的做法是等计数器溢出后再改,或者用预装载功能(ARPE)。CubeMX里有一个"auto-reload preload"选项,勾上之后ARR的修改会在下一个更新事件生效,比较安全。
3.3 刹车功能:不是可选,是保命
高级定时器有一个刹车(Break)输入功能,专门用来在故障时快速关断PWM输出。比如你的半桥电路上串了一个采样电阻,过流的时候比较器输出一个高电平,接到定时器的BKIN引脚,硬件会立刻把MOE清零,所有PWM输出进入安全状态(根据你配置的极性,要么全低要么全高)。这个过程是纯硬件完成的,不经过CPU,响应时间在纳秒级。
CubeMX里配置刹车很简单:在Break and Dead Time那一栏,使能Break Input,选极性,选滤波。但很多人配的时候会忽略一个选项:Automatic Output。如果勾了这个,刹车事件结束后,MOE会自动恢复,PWM继续输出。如果不勾,刹车后MOE保持清零,你需要软件手动恢复。哪种好?看你的应用。如果是过流保护,一般不希望自动恢复,因为故障可能还在,自动恢复会反复炸管。所以过流保护场景下不要勾Automatic Output,让软件来判断故障是否排除,再决定要不要恢复输出。
刹车引脚一般是BKIN,对应特定的GPIO。以TIM1为例,BKIN在PB12(不同封装可能不同)。这个引脚要配置成复用功能,并且使能上拉或下拉,取决于你的故障信号极性。
4. 实测中那些让人抓狂的坑
4.1 波形出不来?先查这五个地方
配置完CubeMX,烧录进去,示波器一搭,发现引脚上啥也没有。这种情况太常见了,我按排查优先级列一下:
第一,MOE位没置位。这是最高频的原因。用调试器看一下BDTR寄存器的bit15(MOE),如果是0,那肯定没输出。解决办法就是前面说的,手动调__HAL_TIM_MOE_ENABLE,或者检查CubeMX里Automatic Output有没有勾。
第二,启动函数调错了。只调了HAL_TIM_PWM_Start没调HAL_TIMEx_PWMN_Start,结果主通道有波形,互补通道没有。或者反过来。两个都要调。
第三,GPIO复用没配对。CubeMX有时候会自动分配引脚,但如果你手动改过引脚,可能复用功能没选对。检查GPIO的Alternate Function设置,确保选的是正确的AF编号。STM32F1的AF编号和F4不一样,F1是直接配到对应的外设,F4要选AF1~AF15。
第四,时钟没使能。TIM1的时钟在APB2上,GPIO的时钟在APB2上,都要使能。CubeMX一般会自动生成,但如果你手动改过代码,可能漏了。
第五,ARR或Pulse设成了0。如果ARR=0,定时器根本不计数;如果Pulse=0,占空比0%,输出一直低。检查一下参数。
4.2 死区时间看起来不对?
有时候你填了500ns的死区,示波器测出来却是1μs或者200ns。原因通常是定时器时钟频率和你以为的不一样。STM32的定时器时钟不一定等于系统时钟。APB2的定时器时钟有一个倍频规则:如果APB2预分频系数是1,定时器时钟等于APB2时钟;如果APB2预分频系数大于1,定时器时钟等于APB2时钟×2。比如系统72MHz,APB2预分频=2,那APB2时钟=36MHz,但定时器时钟=72MHz。这个规则很容易搞混。
所以你在算死区时间之前,先确认一下定时器的实际时钟频率。可以在CubeMX的Clock Configuration界面看,它会显示每个定时器的时钟。或者用示波器测一下PWM频率,反推定时器时钟:定时器时钟 = PWM频率 × (Prescaler+1) × (ARR+1)。
4.3 互补波形有重叠?可能是极性搞反了
互补输出的两个通道,极性是可以配置的。CH1可以设成高有效或低有效,CH1N也可以。如果两个都设成高有效,那CH1高的时候CH1N也高,就重叠了。正确的配置是:CH1高有效,CH1N低有效(或者反过来),这样它们才是互补的。CubeMX里每个通道的Polarity选项要单独设置,别只改一个。
还有一种情况:你用了外部驱动芯片,比如IR2110,它的输入逻辑可能是反相的。这时候你要根据驱动芯片的真值表来配置STM32的极性,确保最终加到MOS管栅极上的信号是对的。这个没有统一答案,看你的硬件设计。
4.4 电机飞车:一个真实的教训
我之前调一个H桥驱动直流电机的项目,互补PWM配好了,死区也加了,空载测试波形完美。结果一接电机,电机直接全速飞车,不受控。查了半天,发现是上电瞬间的默认电平问题。STM32复位后,GPIO是浮空输入状态,在PWM启动之前,MOS管的栅极可能处于不确定状态,导致上下管同时导通了一下,电机就冲出去了。
解决办法有两个:一是硬件上在栅极加下拉电阻,确保默认关断;二是软件上在初始化GPIO之后、启动PWM之前,先把输出电平设成安全状态。CubeMX里可以配置GPIO的初始输出电平,但复用功能下这个选项可能不起作用。更可靠的做法是在HAL_TIM_MspInit里,先把引脚设成普通推挽输出并输出低电平,等定时器配置完了再切回复用功能。这个操作有点绕,但能有效避免上电飞车。
提示:任何涉及功率管的项目,上电顺序和默认电平都要反复确认。示波器看波形是一回事,接上负载是另一回事。
4.5 死区时间与效率的权衡
死区时间不是越小越好。太小了,直通风险高;太大了,输出波形失真,谐波增加,效率下降。尤其是在低频PWM下(比如1kHz),死区时间占周期的比例很小,影响不大;但在高频PWM下(比如100kHz),500ns的死区可能占周期的5%,那就很可观了。
我一般会做一个折中:先根据MOS管的数据手册算出理论最小死区(关断延迟+驱动延迟),然后在这个基础上加50%的余量。比如算出来最小300ns,那就设450ns。然后用示波器看实际波形,确认没有重叠,再试着往下压,直到波形边缘刚好不重叠,再回调一点。这个过程需要耐心,但值得。
5. 进阶:三相互补与中心对齐模式
5.1 三相无刷电机的六路PWM
如果你要驱动三相无刷电机,需要三对互补PWM,共六路信号。TIM1的CH1/CH1N、CH2/CH2N、CH3/CH3N刚好够用。配置方法和单对一样,只是要启动三个通道:
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);三相互补PWM的死区时间是一样的,由BDTR统一控制。但要注意,三相的占空比是独立变化的,你需要根据电机控制算法(比如SVPWM)实时计算三个CCR值。这个计算量不小,一般用查表法或者定点数运算来加速。
5.2 中心对齐模式为什么更适合电机
前面提到Counter Mode可以选Center-aligned。在中心对齐模式下,定时器先向上计数到ARR,再向下计数到0,形成一个对称的三角波。PWM输出在计数器的上升沿和下降沿都会翻转,所以生成的PWM脉冲是居中的。这种模式有两个好处:一是谐波特性更好,电机噪音小;二是ADC采样时机更灵活,可以在计数器过零或者峰值时触发采样,避开开关噪声。
但中心对齐模式下,死区时间的插入位置和边沿对齐模式略有不同。具体来说,死区是在两个互补信号都翻转的时候插入的,所以实际死区时间可能看起来是设定值的两倍。这个细节在调试的时候要注意,别以为配置错了。
5.3 互补PWM在逆变器中的应用
逆变器的本质就是把直流变成交流。全桥逆变器有四个开关管,需要两对互补PWM。通过改变正弦调制波的幅度和频率,就可以控制输出电压和频率。这就是SPWM(正弦脉宽调制)。STM32的高级定时器配合DMA,可以把预先算好的正弦表自动搬运到CCR寄存器,实现硬件级的SPWM生成,CPU只需要在过零时更新一下频率和相位。
做逆变器的时候,死区时间尤其关键。因为逆变器的母线电压通常很高(比如300V以上),直通瞬间的电流非常大,MOS管根本扛不住。我见过有人死区设了100ns,结果一上电就炸了。后来改成500ns,稳了。所以高压场景下,死区宁大勿小。
6. 几个容易被忽略的寄存器细节
6.1 BDTR寄存器的位段含义
BDTR(Break and Dead-Time Register)是高级定时器互补输出的核心寄存器。它的位段分布如下:
| 位段 | 名称 | 含义 |
|---|---|---|
| bit15 | MOE | 主输出使能,0=关闭所有输出,1=使能 |
| bit14 | AOE | 自动输出使能,刹车后是否自动恢复MOE |
| bit13 | BKP | 刹车输入极性,0=低有效,1=高有效 |
| bit12 | BKE | 刹车输入使能 |
| bit11 | OSSR | 运行模式下关闭状态选择 |
| bit10 | OSSI | 空闲模式下关闭状态选择 |
| bit9:8 | LOCK | 锁定配置,防止意外修改 |
| bit7:0 | DTG | 死区时间发生器配置 |
LOCK位段值得说一下。它可以锁定BDTR寄存器的部分位,防止软件意外修改导致输出异常。比如你设了LOCK=1,那DTG、BKE、BKP、AOE、MOE这些位就不能再改了,直到复位。这个功能在安全关键的应用里很有用,防止程序跑飞了乱改配置。但调试阶段别开,不然改不了参数很麻烦。
6.2 OSSR和OSSI:空闲和运行时的关闭状态
当MOE=0的时候(比如刹车触发),PWM输出引脚是什么状态?这由OSSR和OSSI决定。OSSR是运行模式下的关闭状态,OSSI是空闲模式下的关闭状态。如果OSSR=0,那MOE=0时输出引脚是高阻态;如果OSSR=1,输出引脚是无效电平(由极性决定)。对于驱动MOS管的应用,一般希望MOE=0时输出低电平(关断MOS),所以OSSR和OSSI都设成1,并且极性配置成低有效关断。
这个细节在CubeMX里对应的是"Off State Selection for Run Mode"和"Off State Selection for Idle Mode"两个选项。默认可能是高阻,你要根据硬件改成无效电平。
6.3 预装载寄存器:什么时候需要开
高级定时器的CCR寄存器有预装载功能(OCxPE)。如果开了预装载,你写CCR的值不会立即生效,而是等到下一个更新事件(UEV)才生效。这样做的好处是避免在PWM周期中间修改占空比导致波形出现毛刺。对于电机控制这种需要平滑调速的应用,建议开启预装载。CubeMX里对应的选项是"PWM Mode"下面的"CH Polarity"旁边的"Preload"。
但开了预装载之后,你修改CCR后要等一个周期才能看到变化。如果你需要立即生效,可以手动触发一个更新事件:
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, new_pulse); // 如果需要立即生效,可以调用: // HAL_TIM_GenerateEvent(&htim1, TIM_EVENTSOURCE_UPDATE);不过手动触发更新事件要小心,它会影响所有通道,包括计数器和预分频器。一般不建议频繁调用。
7. 调试工具与实测方法
7.1 示波器怎么接
测互补PWM,最好用双通道示波器,一个探头接CH1,一个接CH1N。触发方式选CH1的上升沿,时间基准调到能看到一个完整周期。然后放大到切换瞬间,测量CH1下降沿到CH1N上升沿之间的时间,那就是死区时间。反过来,CH1N下降沿到CH1上升沿之间也是死区时间,两者应该相等。
如果示波器有数学运算功能,可以用CH1-CH1N看差分信号,更直观。但注意探头的地要接对,别把两个探头的地夹到不同电位上,会短路。
7.2 用定时器测频率和占空比
如果你手头没有示波器,可以用另一个定时器来测PWM的频率和占空比。STM32的定时器有输入捕获功能,可以捕获上升沿和下降沿的时间戳,从而算出频率和占空比。具体做法是:用一个通用定时器(比如TIM2)的CH1配置成PWM输入模式,把待测信号接到这个引脚上。PWM输入模式会自动测量周期和占空比,结果存在CCR1和CCR2里。这个方法精度取决于定时器时钟,一般能到纳秒级。
7.3 实际项目中的验证清单
每次配完互补PWM,我一般会按这个清单过一遍:
- 示波器确认两路波形都有,频率正确。
- 确认两路波形互补,没有重叠。
- 测量死区时间,和设定值对比,误差在20%以内。
- 修改占空比,确认两路都跟着变。
- 触发刹车,确认输出立即关断。
- 解除刹车,确认输出恢复(如果配了自动恢复)。
- 接上负载,观察波形是否畸变,MOS管是否发热。
- 长时间运行,确认没有异常。
这个清单看起来简单,但每一步都能查出不同的问题。尤其是第7步,空载和带载的波形可能完全不一样,一定要带载测。
8. 写在最后的一些个人体会
互补PWM和死区时间这个东西,理论上一页纸就能讲完,但实际调试中遇到的问题千奇百怪。我踩过的坑包括但不限于:MOE没开、启动函数漏调、极性配反、死区算错、上电飞车、刹车不恢复。每一个坑都花了不少时间才定位到。
我的建议是,第一次配的时候,不要急着接功率管。先用LED或者示波器看波形,确认时序完全正确了,再接驱动芯片,最后接MOS管。每一步都确认无误再往下走。功率电路炸一次,损失的不只是管子,还有时间和心情。
另外,CubeMX虽然方便,但它生成的代码只是起点。BDTR寄存器的很多细节,CubeMX的界面并没有完全暴露出来。你需要去看参考手册的定时器章节,把每个位段的含义搞清楚。尤其是刹车和死区部分,手册里的描述很详细,值得反复读几遍。
最后说一个实用技巧:如果你用的是Keil,可以在调试模式下直接查看TIM1的寄存器。在Watch窗口里输入TIM1->BDTR、TIM1->CCR1、TIM1->ARR,就能看到实时值。比盲猜快多了。配合示波器,软硬结合,定位问题的效率会高很多。