1. 从"六路互补PWM"这个实验说起
如果你手上有STM32G431这类带高级定时器的芯片,又恰好想驱动一个三相无刷电机或者做一套数字电源,那"六路互补PWM"这个词你迟早会撞上。我第一次接触这个概念是在调一块无刷电机驱动板的时候,当时天真地以为"互补"就是两路反相输出,结果波形一上示波器就傻眼了——上下桥臂直通,管子瞬间发烫。后来才明白,互补PWM远不止"反相"这么简单,它背后牵扯到死区时间、中心对齐模式、刹车保护、通道极性、寄存器影子加载等一整套机制。
这篇内容就是围绕"验证六路互补PWM"这个实验展开的。我会把TIM1这个高级定时器怎么配置出三对互补通道、死区时间到底怎么算、为什么一定要用中心对齐模式、怎么用示波器验证波形、以及实测中那些文档里不会写的坑,全部摊开讲清楚。适合正在做电机驱动、数字电源、逆变器,或者单纯想搞懂高级定时器互补输出机制的嵌入式开发者。不管你是刚上手CubeMX的新手,还是已经能写寄存器但没系统验证过的老手,应该都能从里面找到点有用的东西。
先说清楚这个实验的目标:用STM32G431的TIM1,在三个通道上各输出一对互补PWM(CH1/CH1N、CH2/CH2N、CH3/CH3N),共六路,带可调死区时间,工作在中心对齐模式,最后用示波器或者逻辑分析仪验证六路波形的互补关系和死区是否符合预期。这个实验看起来简单,但它是所有三相驱动的基础,验证不透彻,后面调电机一定出问题。
2. 六路互补PWM到底在解决什么问题
2.1 三相桥臂的开关逻辑与直通风险
要理解六路互补PWM,得先理解它服务的对象——三相全桥。一个三相逆变桥有六个开关管,分成三个桥臂,每个桥臂有上下两个管子。以A相为例,上管接母线正极,下管接母线负极,中间那个点就是A相输出。正常工作时,上管开下管必须关,下管开上管必须关,否则母线正负极通过这两个管子直接短路,这就是"直通",电流瞬间能到几百安培,管子基本当场报废。
那为什么不能简单地给上下管相反的电平就完事?因为真实的MOSFET或者IGBT不是理想开关,它有开通延迟和关断延迟。你给上管发关断信号、同时给下管发开通信号,理论上一个关一个开,但实际上下管可能在上管还没完全关断的时候就已经导通了,这中间有个短暂的重叠期,直通就发生在这几纳秒到几百纳秒之间。死区时间就是插在这个切换瞬间的一段"两个管子都关断"的空白期,用来躲开这个重叠。
所以六路互补PWM的核心价值就两点:一是保证同一桥臂上下管的开关状态严格互补,二是通过死区时间保证切换过程中不会直通。这两点缺一不可,只做互补不做死区,等于没做。
2.2 为什么必须用高级定时器而不是通用定时器
STM32的定时器分好几档,基本定时器、通用定时器、高级定时器。TIM1和TIM8属于高级定时器,它们比通用定时器多了几个关键能力:互补输出通道(CHxN)、死区时间发生器(BDTR寄存器)、刹车输入(BKIN)、以及中心对齐模式下的特殊行为。
通用定时器比如TIM2、TIM3,它们也能输出PWM,但只有单路输出,没有硬件互补通道,也没有硬件死区发生器。你要用通用定时器做互补,只能靠软件在两个通道上分别设置相反的占空比,死区也得靠软件延时或者额外的逻辑门电路来插。软件方案的问题在于:延时不准、CPU占用高、切换瞬间容易出问题,而且一旦中断被打断,死区就可能丢失。硬件方案则是定时器内部自动完成互补和死区插入,CPU只需要设置一次,之后完全不用管,可靠性高一个数量级。
STM32G431用的是Cortex-M4内核,TIM1挂载在APB2总线上,时钟频率可以到170MHz。这个频率对做电机驱动来说很关键,因为死区时间的精度直接取决于定时器时钟。170MHz下一个计数周期约5.88纳秒,死区时间可以精确到十几纳秒级别,足够覆盖大多数MOSFET和IGBT的开关延迟需求。
2.3 中心对齐模式在电机驱动里的意义
PWM有两种基本对齐方式:边沿对齐和中心对齐。边沿对齐就是计数器从0数到ARR然后归零,PWM的跳变都发生在计数器的起点或终点。中心对齐则是计数器从0数到ARR再数回0,形成一个三角波,PWM的跳变发生在三角波的对称位置。
为什么电机驱动偏爱中心对齐?因为中心对齐模式下,PWM的谐波频谱更干净。边沿对齐会在开关频率及其整数倍上产生较强的谐波,而中心对齐能把很多低次谐波抵消掉,电机电流波形更接近正弦,转矩脉动更小,噪音也更低。另外中心对齐模式下,每个PWM周期内计数器会经过两次比较点,这意味着同样的ARR值下,实际PWM频率是边沿对齐的一半,但分辨率翻倍,对需要精细占空比调节的FOC控制很友好。
代价是中心对齐模式下中断触发点更多,如果你在更新中断里做电流采样,得注意采样时刻和PWM中心点的对齐关系,这个后面会细说。
3. TIM1互补通道的配置逻辑与寄存器拆解
3.1 通道、互补通道与输出使能的层级关系
TIM1有四个通道,CH1到CH4,其中CH1、CH2、CH3各有一个互补通道CH1N、CH2N、CH3N,CH4没有互补。每个通道的输出要真正到达引脚,需要经过好几层开关,这是很多人配置完发现没波形的根本原因。
第一层是通道配置寄存器CCMR1/CCMR2里的OCxM位,决定这个通道是PWM模式1还是模式2,还是强制输出高/低。第二层是捕获比较使能寄存器CCER里的CCxE和CCxNE位,分别控制主通道和互补通道的输出使能。第三层是刹车和死区寄存器BDTR里的主输出使能位MOE,这个位是总闸,MOE不置1,所有输出通道全部无效。
我见过太多人卡在MOE上。CubeMX生成的代码里,HAL_TIM_PWM_Start()只负责启动定时器和设置CCxE,但MOE位需要额外调用__HAL_TIM_MOE_ENABLE()或者HAL_TIMEx_ConfigBreakDeadTime()来使能。如果你只调了PWM_Start,示波器上什么都看不到,别怀疑硬件,先查MOE。
3.2 死区时间的计算:从寄存器值到纳秒
死区时间由BDTR寄存器的DTG[7:0]这8位控制,但它的编码不是线性的,分四段:
| DTG[7:5] | 死区时间计算式 | 范围(170MHz时钟) |
|---|---|---|
| 0xx | DT = DTG[7:0] × Tdtg | 0 ~ 127×5.88ns ≈ 0 ~ 747ns |
| 10x | DT = (64 + DTG[5:0]) × 2×Tdtg | 约 753ns ~ 1.5μs |
| 110 | DT = (32 + DTG[4:0]) × 8×Tdtg | 约 1.5μs ~ 3μs |
| 111 | DT = (32 + DTG[4:0]) × 16×Tdtg | 约 3μs ~ 6μs |
其中Tdtg就是定时器时钟周期,170MHz下约5.88ns。实际配置时不用手算,CubeMX里直接填纳秒值它会自动算DTG,但你要知道它算出来的实际值可能有舍入误差。比如你填500ns,它可能给你配成503ns或者497ns,这个误差在大多数应用里可以接受,但如果你的管子开关特别快,就得手动核对一下。
死区时间设多大合适?经验值是管子关断延迟时间的1.5到2倍。比如你的MOSFET关断延迟是100ns,死区设150到200ns比较稳妥。设太小有直通风险,设太大则输出波形畸变严重,尤其在大占空比和小占空比时,死区会明显吃掉有效占空比,导致电机低速时转矩不足。这个权衡在做FOC的时候特别明显,我一般会先用示波器测管子实际开关波形,再定死区值。
3.3 中心对齐模式下ARR与PWM频率的关系
中心对齐模式下,PWM频率的计算公式是:
Fpwm = Ftim / (2 × ARR)
注意这个2,很多人第一次配中心对齐会算错频率。比如TIM1时钟170MHz,你想要20kHz的PWM,ARR应该是170000000 / (2 × 20000) = 4250。如果你按边沿对齐的公式算成8500,实际频率就只有10kHz了。
ARR值还决定了PWM分辨率。中心对齐下,有效计数范围是0到ARR再回0,一个完整周期有2×ARR个计数步。ARR越大分辨率越高,但频率越低。20kHz下ARR=4250,分辨率约12位,对FOC来说够用。如果你要更高的频率比如40kHz,ARR降到2125,分辨率约11位,也还能接受。但再高就要考虑换更快的芯片或者用边沿对齐了。
4. 从CubeMX到示波器的完整验证流程
4.1 CubeMX里的关键配置项逐个说明
打开CubeMX,选好STM32G431,进Timers配置TIM1。Clock Source选Internal Clock,Channel1到Channel3的Mode都选PWM Generation CHx CHxN,这样才会同时使能主通道和互补通道。CH4不用管。
下面Parameter Settings里几个关键项:
- Counter Mode:选Center Aligned mode 1、2或3。mode 1最简单,只在计数器从0向上数到ARR时产生比较事件,向下数时不产生。mode 2和3会在上下都产生,用于特定场景。一般选mode 1。
- Prescaler:170MHz直接跑太快,先不分频,Prescaler=0。
- Counter Period (ARR):按上面公式算,20kHz填4250。
- Pulse:初始占空比,填ARR的一半即2125,对应50%。
- CH Polarity:主通道和互补通道的极性。默认主通道High、互补通道High,意味着主通道有效时输出高,互补通道有效时也输出高。但互补通道的"有效"是相对于主通道反相的,所以实际波形是主高互补低。这个极性设置要和你的驱动电路匹配,如果你的栅极驱动芯片是低有效,就得改。
- Dead Time:填你算好的值,比如200(单位是纳秒,CubeMX会转成DTG)。
- Automatic Output:建议开,这样MOE会在需要时自动置位。
- Off State:刹车时的输出状态,一般选Disable,即刹车时输出高阻。
配置完生成代码,HAL库会帮你把大部分寄存器配好,但MOE和启动PWM还需要你自己在main里加。
4.2 启动代码里最容易漏掉的两行
生成的代码里,MX_TIM1_Init()会配置好所有参数,但不会启动PWM输出。你需要在main函数的初始化部分加上:
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_3); HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_1); HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_2); HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_3);注意互补通道要用HAL_TIMEx_PWMN_Start,不是HAL_TIM_PWM_Start。这两个函数内部操作的是CCER寄存器的不同位,用错了互补通道不会输出。
还有MOE。如果你在CubeMX里勾了Automatic Output,HAL_TIM_PWM_Start内部会调__HAL_TIM_MOE_ENABLE()。但如果你没勾,就得手动加:
__HAL_TIM_MOE_ENABLE(&htim1);我建议不管勾没勾,都在启动后手动确认一下MOE位,用调试器看BDTR寄存器的bit15是不是1。这个位是总开关,忘了它你会浪费半天时间查硬件。
4.3 用示波器验证六路波形的具体步骤
代码跑起来后,把示波器探头接到对应的引脚上。STM32G431的TIM1通道引脚分布:CH1是PA8,CH1N是PB13(或者PA7,取决于复用),CH2是PA9,CH2N是PB14,CH3是PA10,CH3N是PB15。具体看你的芯片封装和CubeMX的引脚分配。
验证分几步:
第一步,单独看CH1和CH1N。两路波形应该频率相同、占空比互补(一个高时另一个低),并且在切换点有明显的死区——两路都是低电平的那段时间。把时基放大到死区时间量级,比如200ns死区就把时基设到50ns/div,应该能看到清晰的死区平台。
第二步,看CH1和CH2的相位关系。中心对齐模式下,三路PWM的相位差应该是120度。但注意,TIM1的三个通道共用同一个计数器,它们的相位差不是靠计数器偏移实现的,而是靠比较值CCR的不同。如果你三路CCR都设成一样,三路波形是完全同相的。要做三相驱动,你得在运行中动态改CCR值来实现正弦调制,静态验证时三路同相是正常的。
第三步,测死区实际值。用示波器的光标功能量一下两路都低的那段时间,和你的设定值对比。如果偏差超过20%,检查一下定时器时钟是不是真的170MHz,有时候CubeMX里时钟树没配好,TIM1实际跑在别的频率上。
第四步,验证互补性。把CH1和CH1N同时显示,确认在任何时刻都不会同时为高。这个用示波器的逻辑触发或者数学运算功能都能看。
5. 实测中那些文档不会告诉你的坑
5.1 死区时间在高低占空比下的不对称表现
这是我在调电机时踩得最狠的一个坑。理论上死区时间是个固定值,但实际输出波形里,死区对上升沿和下降沿的影响是不对称的。原因在于死区插入的位置:它是在主通道关断和互补通道开通之间插入的,但主通道开通和互补通道关断之间是否也插死区,取决于你的配置。
具体表现是:当占空比接近0%或100%时,有效占空比会被死区明显压缩。比如你设了500ns死区,PWM周期50μs,占空比设1%,理论高电平时间500ns,但实际因为死区吃掉一部分,可能只剩几十纳秒甚至没有。这在电机低速启动时会导致某一相完全没输出,电机抖动或者启动失败。
解决办法有两个:一是死区尽量设小,在保证不直通的前提下取最小值;二是在软件里做死区补偿,根据当前占空比和电流方向动态调整CCR值。FOC算法里通常都有这个补偿环节,但如果你只是做简单的六步换相,就得自己注意低速时的占空比下限。
5.2 中心对齐模式下更新中断的触发时刻
中心对齐模式下,更新事件(UEV)的触发时刻和边沿对齐不一样。边沿对齐时,UEV在计数器归零时触发,也就是PWM周期的起点。中心对齐时,UEV在计数器到达ARR(波峰)和到达0(波谷)时都可能触发,具体取决于你设置的重复计数器RCR。
这个差异直接影响电流采样。FOC控制里,电流采样通常安排在PWM周期的特定时刻,以避开开关噪声。边沿对齐时你在UEV中断里采样,采的是周期起点的电流。中心对齐时如果你还在UEV里采样,可能采到的是波峰或波谷的电流,和你的算法假设不符。
正确的做法是:中心对齐模式下,把ADC触发源配成TIM1的CH4比较事件或者TRGO,让ADC在计数器经过某个特定值时自动采样。STM32G431的ADC支持定时器触发,这个硬件联动比在中断里软件触发要准得多,也不会被中断延迟影响。
5.3 刹车输入BKIN的意外触发
BKIN是高级定时器的刹车输入,用来在故障时快速关断所有PWM输出。这个功能很好用,但它的引脚如果没配置好,可能被外部噪声误触发,导致PWM莫名其妙停掉。
我遇到过的情况是:BKIN引脚悬空,周围有电机驱动的强电磁干扰,结果PWM时不时就停一下。后来把BKIN配置成内部上拉,并且加了RC滤波,问题才解决。如果你暂时不用刹车功能,建议在CubeMX里把BKIN的引脚功能关掉,或者配置成普通GPIO,别让它悬空。
另外BKIN的触发极性也要注意,默认是高电平触发还是低电平触发,要和你的故障检测电路匹配。配反了要么一直触发,要么永远不触发。
5.4 互补通道引脚复用冲突
STM32G431的引脚复用很灵活,但TIM1的互补通道引脚往往和别的外设冲突。比如PB13既是TIM1_CH1N,也可能是SPI2_SCK或者I2C2_SCL。如果你同时用了这些外设,就得重新分配引脚或者换通道。
CubeMX会在引脚配置界面用颜色标出冲突,但有时候冲突是隐性的——比如你用了某个外设,它的默认引脚恰好和TIM1_CH1N重合,但CubeMX没报错,因为那个外设的引脚可以重映射到别处。这种情况下生成的代码里,TIM1_CH1N可能没有正确映射到引脚上,示波器上自然没波形。
排查方法是:生成代码后,检查GPIO的初始化部分,确认TIM1_CH1N对应的引脚被配置成了AF模式,并且AF编号正确。STM32G431的TIM1_CH1N在PB13上是AF6,在PA7上是AF6,别配错了。
6. 从验证实验到实际驱动的延伸思考
6.1 六路互补PWM只是起点,不是终点
验证完六路互补PWM,你只是拿到了三相驱动的人场券。真正做电机驱动,后面还有一堆事:六步换相的换相逻辑、FOC的坐标变换和PID调节、电流环和速度环的级联、弱磁控制、无感观测器等等。但所有这些高级算法,底层都依赖这六路PWM的稳定输出。如果PWM本身有问题,上层算法再精妙也是空中楼阁。
我建议在做完这个验证实验后,先别急着上电机。拿六个LED或者六个小功率MOSFET加电阻,搭一个假负载,把六路波形在各种占空比、各种频率下都跑一遍,确认死区、互补性、相位关系都正确。这个步骤花不了多少时间,但能帮你排除掉大部分底层问题。
6.2 不同芯片平台的互补PWM差异
STM32的TIM1配置逻辑在G4、F4、H7系列上基本一致,但细节有差异。比如G431的TIM1时钟可以到170MHz,F407的TIM1最高168MHz,H743的TIM1能到240MHz。死区时间的计算式一样,但Tdtg不同,同样的DTG值在不同芯片上对应的死区时间不一样。
如果你从STM32换到别的平台,比如英飞凌的CCU6或者国产HC32F460,互补PWM的配置思路类似但寄存器完全不同。CCU6的死区配置更复杂,有专门的死区控制寄存器;HC32F460的定时器互补输出需要配置专门的互补通道使能位。但核心概念——互补、死区、中心对齐、刹车——是通用的。理解了STM32这套,换平台时只需要查对应的寄存器手册,概念上不会有障碍。
6.3 用逻辑分析仪做批量验证的技巧
示波器看两路波形很直观,但要同时看六路,示波器就不够用了。这时候逻辑分析仪更合适。我用的是8通道的逻辑分析仪,把六路PWM全接上,采样率设到100MHz以上,能同时看到六路的时序关系。
逻辑分析仪的软件通常有协议解码功能,虽然PWM不是标准协议,但你可以用它的测量功能自动量死区、频率、占空比。更高级的用法是导出数据到CSV,用Python脚本批量分析。我写过一个脚本,自动检查六路波形里有没有同时为高的时刻,有就报警。这个脚本在批量测试不同死区配置时特别有用,比人眼看示波器快得多。
如果你没有逻辑分析仪,也可以用STM32自己的定时器输入捕获功能来测。把CH1和CH1N分别接到另外两个定时器的输入捕获引脚上,用捕获中断记录边沿时刻,算出死区。这个方法精度受中断延迟影响,但胜在不用额外设备。
6.4 死区时间与系统效率的权衡
最后说一个实际产品里才会考虑的问题:死区时间对效率的影响。死区期间,电机电流通过续流二极管流动,这部分能量不产生有效转矩,纯粹是损耗。死区越大,损耗越大,效率越低。但死区太小又有直通风险。
在低压小功率场合,比如24V以下的电机驱动,死区可以设得很小,100ns甚至50ns都行,因为低压下直通电流有限,管子能扛住。但在高压大功率场合,比如400V母线,死区必须设大一些,200ns到500ns,因为高压直通瞬间电流极大,管子扛不住。
还有一个技巧是自适应死区:根据负载电流大小动态调整死区时间。轻载时死区设小,重载时设大。这个在STM32上可以通过运行时改BDTR寄存器实现,但要注意改的时候PWM不能正在输出,否则可能产生异常波形。一般是在PWM停止时改,改完再启动。
这个实验看起来只是配置几个寄存器,但真正做透,涉及到定时器架构、功率电子、控制算法、测试方法多个层面。我自己的经验是,每次换芯片或者换功率等级,都要重新验证一遍六路互补PWM,因为死区、极性、时钟这些参数一变,之前调好的东西可能就不适用了。把这个验证流程固化下来,做成一个checklist,能省掉很多调试时间。