做电机驱动、逆变器、UPS这类功率变换项目,六路互补PWM这关是绕不过去的。不管你是刚接触FOC的小白,还是已经在调BLDC的老手,这路波形如果处理不好,轻则电机抖动噪音大,重则上下桥臂直通炸管子。我这篇文章就围绕“验证六路互补PWM”这个实验,把完整的原理、配置、实测方法和踩坑实录都梳理出来,手把手带你过一遍这套调试流程。
这篇文章适合正在做三相逆变器、无刷直流电机(BLDC)或永磁同步电机(PMSM)驱动方案的工程师,也适合学校实验室里刚接触STM32高级定时器的学生。通过这篇内容,你会搞明白互补PWM为什么需要死区、六路信号之间的相位关系怎么验证、STM32的寄存器该怎么配,以及示波器上哪些现象是正常的、哪些是危险的。
1. 六路互补PWM的整体设计思路
1.1 从H桥到三相全桥的进化
先说清楚六路互补PWM是从哪来的。单相H桥有四个开关管,一对桥臂输出一个交流电;三相逆变器是把三个H桥拼在一起,共用母线,这就是三相全桥拓扑。三个桥臂各有一对上下管,总共六个开关管。
上管打开时下管必须关断,下管打开时上管必须关断,这就是“互补”关系的来源。而所谓“六路”PWM,就是三个桥臂的上管和下管各一路控制信号,总计六路。在整个PWM周期里,任意时刻某个桥臂只能有一只管子导通,否则就构成母线短路。
真正的电机控制场景下,这三对互补PWM可不是简单复制成三份。三相定子绕组需要相位差120°的正弦波电流,对应到PWM上就是三对互补波形的占空比按正弦规律变化,并且三对波形之间相差120°电角度。验证这个相位关系是实验的关键环节。
1.2 为什么一定要加死区时间
理想情况下,上下管互补切换是瞬间完成的。实际功率器件不是理想开关,MOS管有关断延迟时间(td_off),栅极驱动器也有传输延迟差异。
如果上管还没完全关断,下管就已经开始导通,这时候母线正负极就通过两管直连了。实测中,哪怕只有几百纳秒的重叠导通,母线电容储存的能量也能瞬间烧毁MOS管。死区时间的本质就是在互补信号切换之间插入一段“双方都关断”的空白期,让上管可靠关断后再让下管导通。
这一点必须实测确认,不能只看逻辑。示波器上看互补波形,切换沿处有一段水平空档,那就是死区。关于死区时间的计算和设定,后面会专门展开讲。
1.3 实验验证的目标拆解
这个实验要验证的东西,我总结成四个检查点:
- 互补关系:每个桥臂的上下管波形在任意时刻电平相反(死区期间除外)。
- 死区时间:切换沿处的高电平空缺宽度与寄存器设定值一致。
- 频率与占空比:六路输出频率相同,占空比跟随配置值变化。
- 相位关系:三对波形依次延迟120°,这是三相控制的核心。
这四个点通过示波器逐项确认后,才算真正验证通过。
2. 实验环境搭建与工具选型
2.1 主控芯片选型的关注点
并不是任意一个单片机都能输出带死区的互补PWM。普通定时器PWM输出引脚只有一个,要产生互补输出,必须选用带“高级定时器”的芯片,典型代表是STM32的TIM1和TIM8。
高级定时器内置了死区发生器、互补输出通道和刹车保护功能。这意味着死区时间、互补极性、故障关断这些逻辑全在硬件里完成,不需要CPU实时干预。如果你用的MCU没有高级定时器,只能软件做死区,那又别论了,但做工业级产品我不会用这种方案。
我实验时用的板子是STM32F103C8T6最小系统板,几块钱一片,TIM1就是高级定时器,完全够用。如果你手头有GD32、MM32这类国产替系列芯片,同样有高级定时器,操作逻辑大同小异。
2.2 功率级与栅极驱动电路的组合方案
主控输出的3.3V PWM信号根本没能力直驱大功率MOS管。标准方案是主控接栅极驱动芯片,再驱动MOS管或IGBT。
栅极驱动我常用IR2101S或EG2131这类半桥驱动芯片,一路驱动芯片正好驱动一个桥臂的上下管。这种芯片内部的死区逻辑配合外部电阻,还可以再加一道硬件死区保障。三相全桥需要三颗这样的驱动芯片。母线电压我这套实验平台用的24V,MOS管选的额定电压100V的N-MOS,余量留了四倍,实验安全性高很多。
2.3 示波器是这实验的关键工具
验证PWM波形绕不开示波器。关于示波器的选择,我想多说两句,因为这块的经验太多人踩坑了。
至少要有两个通道,最好四通道。两通道意味着你一次只能看一个桥臂的上下管波形,然后轮换着看另外两相;四通道可以一次性把三对波形的相位关系全抓到,效率一下子就上来了。
带宽至少要50MHz,实测20kHz PWM是绰绰有余的。但是如果你想细看死区几十纳秒的沿细节,带宽越高失真越小。我用的四通道100MHz数字示波器,测死区精度足够。
有个容易忽略的点是探头补偿。示波器探头上有补偿电容调节孔,先把探头接到示波器自带的1kHz方波校准端,调成标准方波再测。不然你测出来的死区时间误差会很大,甚至波形边缘出现假振铃。
3. 核心参数计算与寄存器配置
3.1 理解定时器时钟树的来龙去脉
参数计算之前,先搞明白PWM的底层来源。STM32F103的TIM1挂在APB2总线上,来自主时钟的72MHz。这个时钟先经过预分频器(PSC),再经过计数器计数(ARR),最终决定PWM频率。
PWM输出频率的计算公式是这样的:
F_PWM = 主频 / ((PSC + 1) × (ARR + 1))主频=72MHz时,如果我想要20kHz的PWM频率,可以取PSC=0,那么(ARR+1)=72000000/20000=3600,即ARR=3599。
为什么选20kHz?这里有一个理论支撑:20kHz已超出人耳可听范围(20Hz~20kHz)。电机开关频率低于这个值,运行时会发出刺耳的电磁噪音。20kHz意味着每个PWM周期是50μs,死区几μs只占周期百分之几,效率损失非常小。如果在实验初期调低到10kHz,会明显听到电机有“咝咝”的啸叫,那其实是PWM频率落在可听范围内,不是电机故障。
3.2 死区时间的计算与寄存器写入
死区时间设置的依据是功率器件的开关特性。芯片数据手册上会给出典型开通延迟(td_on)和关断延迟(td_off),两者之差就是直通风险窗口。栅极驱动电阻也会影响这个时间。
保守的做法是死区时间取器件开关延迟之和的3~5倍。比如我用的MOS管关断延迟约120ns,开通延迟约80ns,那么理想死区至少200ns,安全起见取1μs。留足裕量之后,1μs的代价是PWM周期50μs里占2%的效率损失,换来的是炸管风险大幅降低。
STM32 TIM1死区时间寄存器是TIMx_BDTR,其中DTG位域的控制逻辑并不直接写入纳秒数,而是分段映射。我的实验以72MHz主频为例,逆推了一下死区值,实际效果很理想:
- Tdts对应的是定时器时钟周期,72MHz下约为13.89ns。
- DTG位域高三位是000,属于模式0,直接死区时间就是DTG值乘以Tdts。
- 需要1μs死区,就要写入DTG=72,72×13.89ns≈1μs。
我习惯在初始化代码里把死区值写明确并加注释,防止换一批芯片后主频变了,死区时间在不知情的情况下跟着变。
3.3 寄存器级别初始化代码解析
下面是我在实验里实际用过的TIM1初始化代码,我贴的是简化版,屏蔽了中断处理部分,只保留核心配置逻辑。
void TIM1_PWM_Init(uint16_t arr, uint16_t psc, uint16_t dtg) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_BDTRInitTypeDef TIM_BDTRInitStructure; TIM_TimeBaseStructure.TIM_Prescaler = psc; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period = arr; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); /* 三对互补通道的模式都设置为PWM1,比较值与手动配置 */ TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Enable; TIM_OCInitStructure.TIM_Pulse = 0; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_High; TIM_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Set; TIM_OCInitStructure.TIM_OCNIdleState = TIM_OCNIdleState_Reset; TIM_OC1Init(TIM1, &TIM_OCInitStructure); TIM_OC2Init(TIM1, &TIM_OCInitStructure); TIM_OC3Init(TIM1, &TIM_OCInitStructure); /* 六路主输出使能,必须写,否则高级定时器不输出 */ TIM_CtrlPWMOutputs(TIM1, ENABLE); /* 死区时间写入BDTR寄存器 */ TIM_BDTRInitStructure.TIM_OSSRState = TIM_OSSRState_Enable; TIM_BDTRInitStructure.TIM_OSSIState = TIM_OSSIState_Disable; TIM_BDTRInitStructure.TIM_LOCKLevel = TIM_LOCKLevel_OFF; TIM_BDTRInitStructure.TIM_DeadTime = 72; TIM_BDTRInitStructure.TIM_Break = TIM_Break_Disable; TIM_BDTRInitStructure.TIM_BreakPolarity = TIM_BreakPolarity_High; TIM_BDTRInitStructure.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable; TIM_BDTRInit(TIM1, &TIM_BDTRInitStructure); TIM_ARRPreloadConfig(TIM1, ENABLE); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC3PreloadConfig(TIM1, TIM_OCPreload_Enable); }注意TIM_CtrlPWMOutputs这一行,高级定时器区别于普通定时器的一个坑就在这里。即使前面都配好了,不调用这个函数,主输出就是不出来,波形一直是低电平。
补一句比较值怎么设置。上面代码里TIM_Pulse先置为0,实际运行中通过TIM_SetCompare1(TIM1, 300)这类函数动态更新占空比。比如ARR=3599时,比较值300表示占空比约8.3%,比较值3300表示占空比约91.7%。
3.4 利用STM32CubeMX快速生成工程
有些朋友喜欢用STM32CubeMX图形化配置,我实验初期也用它快速搭过框架。在CubeMX里选择TIM1,把Channel1、Channel2、Channel3的模式都设为PWM Generation CHx and CHxN(互补输出),然后在Break and Dead Time settings里设置死区时间。
注意CubeMX里死区时间单位是ns,界面框会让你直接输入数字,底层自动帮你换算成BDTR值。输入1000代表1μs,生成代码后再核对一下寄存器值,确认换算逻辑没问题。
我个人最终倾向于寄存器方式,因为经验上调试中修改比较值以及分析波形时,寄存器方式逻辑更透明,出了诡异问题能少绕弯子。CubeMX适合快速起步,寄存器适合深度排错,两者不矛盾。
4. 实操过程与六路波形验证
4.1 硬件接线与上电检查
我的实验用板是STM32F103C8T6,选用的管脚对应TIM1通道,实验具体接线如下:
| 信号 | MCU引脚 | 去往 | 备注 |
|---|---|---|---|
| TIM1_CH1 | PA8 | IR2101S的高边输入HIN | 桥臂A上管 |
| TIM1_CH1N | PB13 | IR2101S的低边输入LIN | 桥臂A下管 |
| TIM1_CH2 | PA9 | IR2101S的高边输入HIN | 桥臂B上管 |
| TIM1_CH2N | PB14 | IR2101S的低边输入LIN | 桥臂B下管 |
| TIM1_CH3 | PA10 | IR2101S的高边输入HIN | 桥臂C上管 |
| TIM1_CH3N | PB15 | IR2101S的低边输入LIN | 桥臂C下管 |
| 母线电源 | - | 桥臂MOS管直流母线 | 24V |
| 示波器夹子 | - | GND | 接地要就近接 |
上电顺序有讲究。先给MCU烧录程序,确认主控板工作正常;再接通24V母线。第一次上电时,我习惯把母线电源限流调小,比如限流1A,即使有错误也不至于炸管,这个习惯帮我避免了两次事故。
栅极驱动芯片的地必须和MCU共地,很多初学者漏了这一步,导致高低边逻辑乱跳,波形完全不可控。
4.2 示波器上验证互补关系
示波器通道1接TIM1_CH1引脚,通道2接TIM1_CH1N引脚。触发源选在通道1,触发方式设置为下降沿,时基先放到50μs/格。
正常情况下,你会看到上下两个波形接近镜像,但不是完全对称。高电平宽度之差正好是死区时间。通道1从高变低后,通道2还要保持一段低电平才变高;通道2从高变低后,通道1也要保持一段低电平。这个双双低电平的区间,就是死区。
给一个我更推荐的观察方式:示波器打开“数学运算”功能,用通道1加通道2的“与”逻辑或用“下降沿延时”测量功能,直接读出死区时间数值。如果算出来的死区在0.9μs到1.1μs之间,和设定的1μs一致,这组波形就验证通过了。
4.3 三对波形的相位差验证
三相电机的核心是相位差120°。验证PWM的时候,不能只看单独一个桥臂,还得看三对波形之间的相位关系。
示波器四通道分别接CH1、CH2、CH3,同时观察三个上管波形。设置时基50μs/格,你会看到三个波形形状相同但互相错开。PWM周期是50μs,120°相位差对应的时间偏移是50μs除以3,约16.67μs。也就是说,通道1的上升沿出现后,通道2的对应上升沿要晚约16.67μs,通道3再晚16.67μs。
如果这里测出来时间偏移不对,说明比较值更新时机有误,通常是对齐/边沿对齐模式选择出了问题。做FOC算法的大忌是六路PWM虽然都有输出,但相位关系全是乱的。
4.4 上管和下管不能简单看同一路信号
有些学习板为了简化接线,直接把互补输出的N通道悬空,用上管信号逻辑取反来控制下管。这种做法虽然能让电机转起来,但丢失了硬件死区的意义。
我建议实验验证时一定要把CH1和CH1N都接上,不光是看波形,还要接功率级的输入实测。有些问题只在带载情况下显现,空载时示波器看着教科书级完美,一带电机就出毛刺。
5. 调试过程实录与常见问题排查
5.1 上电立即炸管:典型的主输出未关断问题
我调试初期最惨烈的一次经历是上电以后开始配置寄存器,程序还没运行到主输出使能那段,MOS管就冒烟了。
原因很简单:STM32刚上电时IO引脚默认是浮空输入状态,对于栅极驱动芯片来说,输入悬空就等于随机电平。IR2101S的LIN输入一旦悬空被拉高,下管直接导通,再叠加母线电源一开,若上管也被意外拉导通,瞬间就直通了。
解决方式有两个必须同时做:第一,在栅极驱动输入前对地加下拉电阻(10kΩ),保证MCU还没初始化的时间段里栅极驱动低电平输入;第二,程序一启动先把刹车信号拉起来,或者在主函数最开始就配置TIM1的MOE主输出使能为0,全部初始化完成后再置1。
炸管的问题是整个实验中最危险最花钱的问题,这一条务必重视。
5.2 只有三路输出:被忽略的N通道使能位
有次实验六路波形只出来三路,CH1、CH2、CH3都有,CH1N、CH2N、CH3N全是低电平。查来查去,最终发现是结构体里TIM_OutputNState一直没配置,默认输出状态被关掉了。
对比代码就一目了然:
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Enable; // 这一行漏了就只输出三路TIM_OCInitStructure是库函数方式的老黄历写法,但原理是通用的。如果你用寄存器方式初始化,检查CCER寄存器里CC1NE位有没有置1,低电平和这个位是强关联的。
5.3 死区时间偏大的影响
有段时间我把死区设成5μs,电机低速转动时明显能听到“嗒嗒”的异响,但波形看着没问题。效率测量对比后发现输出功率比预期低了约10%。
这就是死区时间过大的副作用。死区越大,每个周期里两个管子都关断的时间越长,实际输出电压相对理想值的损失就越大。这个现象在低电压大电流的电机驱动上尤其明显。
要做死区补偿的话,原理上是在软件算法里对电压矢量做修正,根据电流方向前移或者后退PWM前沿。这个属于比较进阶的内容,实验初期没必要上,先把死区值控制在合理范围内更重要。
5.4 波形毛刺和振铃不是芯片问题
示波器上看到波形上升沿有振铃,很多初学者误以为是芯片时序有问题。我排查下来,大部分情况来自两个物理因素:探头接地线太长,或者开关管快速切换时回路寄生电感与电容产生谐振。
第一个因素最容易验证。把示波器探头换成弹簧地线,接地端紧贴测试点附近,如果振铃明显变小,就是探头带来的假象。第二个因素需要改善PCB布局,让母线电容尽可能靠近MOS桥臂。实验中用的面包板由于寄生电感大,毛刺严重是正常的,放宽心态。
5.5 互补PWM常见问题速查表
把调试中容易碰到的问题和排查方向整理成一个表格,方便你对照定位。
| 故障现象 | 优先怀疑方向 | 检查手段 |
|---|---|---|
| 上电即短路或炸管 | IO上电默认电平、未使能MOE | 栅极驱动输入对地下拉;主循环最先关主输出 |
| 只有三路低电平输出 | N通道使能未配置 | 检查CCER的CCxNE位,或结构体OutputNState |
| 互补波形没有死区 | BDTR死区寄存器值为0 | 读取TIMx_BDTR寄存器,确认DTG非零 |
| 波形振铃大 | 探头地线过长、PCB寄生电感大 | 换弹簧接地探针;优化功率回路布局 |
| 某一路占空比异常 | 比较值更新时机问题 | 检查是否在需要使用影子寄存器时开了预装载 |
| 死区值与设定不一致 | 主频估计错误、分频配置有误 | 用示波器频率功能实测PWM频率反推主频 |
6. 从实验台到产品级的几个改进方向
实验波形验证通过,说明你的PWM配置基础已经扎实了,但距离能上产线的固件还有几个坎要过。
第一,增加刹车保护功能。高级定时器上的刹车输入(BKIN)并不是摆设。产品上得过流保护、过压保护、电机驱动故障信号,直接接到刹车输入脚。一旦故障信号触发,芯片硬件直接将PWM输出全部置为安全状态,响应时间在硬件级纳秒级别,而软件中断响应最快也要几微秒。这个时间差就能区分是管子保住了还是冒烟了。
第二,开启定时器中断做逐周期电流保护。母线电流通过采样电阻采集后送入比较器,比较器输出接BKIN。也可以直接用ADC采样,在定时器更新中断里做限流判断,超出阈值就关闭PWM输出。
第三,考虑自适应死区时间。如果项目对效率极致追求,可以根据母线电压和负载电流动态调整死区时间。小型电机上差异不明显,大功率驱动上死区损耗不容忽视。
实验做到这一步,离实际的驱动器产品就比较接近了。剩下FOC、SVPWM那些算法层面的东西,是基于这套可靠的PWM底层逐个叠加的。
我个人在实际操作中的体会是:六路互补PWM验证虽然步骤琐碎,但一次扎实的验证能让你后续调FOC算法时少走好多弯路。PWM底层如果没有踩踏实,算法跑得再花哨,最终都会在各种奇怪的现象里翻车。建议你将实验记录数据保存成模板,以后换主控平台直接照这个流程做验证,省下的时间非常可观。