STM32六路互补PWM实测:死区配置与波形验证全流程
做电机驱动、逆变器这类项目六路互补 PWM 基本是绕不开的一关。标题里这个实验看起来只是“验证”一下波形实际上验证的是整套定时器配置逻辑死区有没有生效、上下管会不会重叠导通、三相互补信号之间的相位关系对不对。我见过不少人代码写得漂亮示波器一接上去全露馅要么上管下管直通要么死区大到波形直接畸变。这篇文章就把我实测六路互补 PWM 的完整过程、判定标准和踩过的坑一次讲清楚适合刚写完驱动代码但心里没底、以及准备把 PWM 从逻辑电平搬到功率桥之前的开发者。1. 实验目标与理论底子为什么是“六路”为什么要“互补”1.1 三相全桥为什么必须用互补 PWM先理清一个基础概念电机驱动或者三相逆变器的功率级几乎都是三相全桥结构。每一相就是一个桥臂桥臂上有两个开关管上管接母线正极下管接母线负极两个管子中间引出去接电机绕组。三个桥臂一共六个开关管这就是“六路”的物理来源。三相全桥的关键约束是同一桥臂的上下两个管子绝对不能同时导通。一旦同时导通母线正极直接通过两个管子短接到负极电流瞬间飙升轻则跳闸重则炸管。这个逻辑不复杂难的是怎么在高速开关时确保万无一失。互补 PWM 就是解决方案。同一条桥臂的两路驱动信号电平永远相反上管为高时下管为低上管为低时下管为高。但电子元件有个要命的物理特性——开关管的开通和关断都需要时间而且关断通常比开通慢。如果上下管信号严格反相、零间隔切换就可能出现上管还没完全关断、下管已经开始导通的危险窗口。所以互补 PWM 必须在两路信号之间强行插入一段“两边都不导通”的时间这就是死区。死区时间的底线计算不复杂。查手册拿到开关管的关断延迟 t_off(max) 和开通延迟 t_on(min)死区至少要有 t_off(max) - t_on(min) 的余量再乘上 1.5 到 2 倍的安全系数。比如某 IGBT 关断延迟 500ns开通延迟 100ns差值是 400ns工程上留出 1μs 比较稳。我给实验选的也是 1μs公式和实测值后面表格里会对比。1.2 六路互补信号的组合关系与验证维度六路互补 PWM 不是六路独立的占空比信号而是三组“主输出 互补输出”的配对组合。以常见的 STM32 高级定时器为例TIM1 的 CH1/CH1N 是一对CH2/CH2N 是第二对CH3/CH3N 是第三对。每一对内部电平互补且有死区三对之间还要满足三相系统的相位关系。做电机控制时三路调制波相位相差 120°做三相逆变器时也是同样的电气角度关系。这就引出了验证实验的核心问题到底要验证什么我把它拆成四个维度验证维度判定标准测试工具频率精度实测频率与配置理论值一致示波器频率测量占空比精度实测占空比与 CCR/ARR 计算值一致示波器占空比测量死区正确性主输出下降沿到互补输出上升沿有可见间隔示波器游标互补极性同桥臂两路任何时候不得同时为高示波器双通道叠加查看四个维度缺一不可。只看频率和占空比容易漏掉死区问题只看单对互补波形又容易忽略三对之间相位关系。完整跑一遍才算真正“验证”了六路互补 PWM。1.3 为什么我推荐用中心对齐模式做实验有些开发者习惯用边沿对齐模式因为配置简单、计数器只增不减。但做三相全桥验证我更推荐中心对齐模式。原因有二第一中心对齐模式下 PWM 脉冲在周期内居中对称死区造成的伏秒不平衡在正半周期和负半周期相互抵消输出谐波更小第二SVPWM 和大多数 SPWM 软件实现都基于三角形载波和中心对齐模式的计数方式天然匹配。代价是中心对齐模式的频率计算比边沿对齐多一步。计数器从 0 加到 ARR再从 ARR 减到 0一个完整的 PWM 周期需要 2 倍的计数时间。同样一批参数边沿对齐输出 20kHz中心对齐就只有 10kHz。这个减半效应是新手最容易翻车的地方后面配置章节我会把具体算例展开。2. 搭建实验环境硬件接线与定时器配置怎么才算“对”2.1 实验硬件准备与测量安全做这个验证实验硬件门槛其实不高。主控板我用的是 STM32F103 系列原因是 TIM1 直接带三对互补输出通道引脚也固定不用做重映射的额外配置。示波器要求至少双通道能做数学运算、有游标测量最好。如果只有两通道验证六路也不麻烦分三次观察三组桥臂就行只是效率低一点。接线方式要强调一下。TIM1 主输出通道分别引出到 PA8、PA9、PA10互补输出通道在 PB13、PB14、PB15。如果板子引脚被其他外设占用可以用重映射功能挪到其他引脚但建议第一轮实验先按默认引脚来排除不必要的变量。示波器探头的接地是重灾区。测六路 PWM 时六个通道的信号地最终都回到主控板的地理论上可以把示波器探头的地线夹全夹在同一块地。但现实是地线夹本身有电感长地线在开关瞬间会拾取噪声导致测出来的波形沿口上带着毛刺。我的做法是把探头换成短地弹簧或者每个通道只夹最近的地引脚。注意如果后面要接功率桥实测示波器探头地线夹绝不能在浮地系统上随意乱夹。验证阶段只测控制板的逻辑电平输出风险不大一旦牵涉强电隔离探头和差分探头才是正确选择。2.2 定时器参数的计算与配置细节以 72MHz 主频的 STM32F103 为例目标是输出 10kHz 的六路互补 PWM死区 1μs。先算定时器计数参数。中心对齐模式下单次计数周期等于 2 × (ARR1) 个时钟周期。反推 ARRf_pwm f_clk / (2 × (PSC1) × (ARR1))设 PSC0f_clk72MHz目标 10kHzARR 72MHz / (2 × 10kHz) - 1 3599所以 ARR 设为 3599PSC 设为 0。占空比 50% 对应 CCR (ARR1) × 0.5 1800。这里犯过的错是直接把 ARR 按边沿对齐方式算成 7199结果输出只有 5kHz频率直接减半。老手一看波形周期就知道计数器模式设错了新手可能对着参数表反复怀疑人生。死区时间由 BDTR 寄存器控制。F103 的 DTG 位段是 8 位主频 72MHz 时死区步长为 1/72MHz ≈ 13.9ns。1μs 死区换算下来约需要 72 个步长也就是 0x48。这个值落在 DTG[7:5]010 的线性区间死区时间直接等于 DTG × tDTS不需要额外的换算公式。还有两个容易漏的寄存器位。第一个是 BDTR 的 MOE 位主输出使能不置 1 的话所有 PWM 输出引脚全是空闲电平但代码配置流程又不会报错。第二个是 CCER 里的 CCxE 和 CCxNE 位主输出和互补输出的通道使能要分别打开只开一个就会出现“一路有波形一路是死人”的诡异现象。2.3 可复现的代码骨架用 HAL 库来做初始化整体思路分成五步开时钟、配 GPIO 复用、配定时器时基、配 PWM 通道和死区、最后按顺序启动输出。代码用的是 CH1 这一对做示范CH2 和 CH3 完全同理复制改通道号就行。/* TIM1 GPIO 配置 */ void MX_GPIO_PWM_Pins_Init(void) { GPIO_InitTypeDef gpio {0}; __HAL_RCC_TIM1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); /* 主输出通道 PA8, PA9, PA10 */ gpio.Pin GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10; gpio.Mode GPIO_MODE_AF_PP; gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, gpio); /* 互补输出通道 PB13, PB14, PB15 */ gpio.Pin GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15; HAL_GPIO_Init(GPIOB, gpio); } /* 定时器时基配置中心对齐模式周期 10kHz */ static void PWM_TimerBase_Init(TIM_HandleTypeDef *htim) { htim-Instance TIM1; htim-Init.Prescaler 0; htim-Init.CounterMode TIM_COUNTERMODE_CENTERALIGNED1; htim-Init.Period 3599; /* ARR见 2.2 计算过程 */ htim-Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim-Init.RepetitionCounter 0; HAL_TIM_PWM_Init(htim); } /* PWM 通道与死区配置50% 占空比1us 死区 */ static void PWM_Channel_Init(TIM_HandleTypeDef *htim) { TIM_OC_InitTypeDef oc {0}; TIM_BreakDeadTimeConfigTypeDef bd {0}; oc.OCMode TIM_OCMODE_PWM1; oc.Pulse 1800; /* CCR50% 占空比 */ oc.OCPolarity TIM_OCPOLARITY_HIGH; oc.OCNPolarity TIM_OCNPOLARITY_HIGH; oc.OCFastMode TIM_OCFAST_DISABLE; oc.OCIdleState TIM_OCIDLESTATE_RESET; oc.OCNIdleState TIM_OCNIDLESTATE_RESET; HAL_TIM_PWM_ConfigChannel(htim, oc, TIM_CHANNEL_1); HAL_TIM_PWM_ConfigChannel(htim, oc, TIM_CHANNEL_2); HAL_TIM_PWM_ConfigChannel(htim, oc, TIM_CHANNEL_3); bd.OffStateRunMode TIM_OSSR_ENABLE; bd.OffStateIDLEMode TIM_OSSI_ENABLE; bd.LockLevel TIM_LOCKLEVEL_OFF; bd.DeadTime 0x48; /* 1us见 2.2 计算过程 */ bd.BreakState TIM_BREAK_DISABLE; bd.BreakPolarity TIM_BREAKPOLARITY_HIGH; bd.AutomaticOutput TIM_AUTOMATICOUTPUT_DISABLE; HAL_TIMEx_ConfigBreakDeadTime(htim, bd); } /* 启动输出主通道 互补通道 MOE */ void PWM_Start_All(TIM_HandleTypeDef *htim) { HAL_TIM_PWM_Start(htim, TIM_CHANNEL_1); HAL_TIMEx_PWMN_Start(htim, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim, TIM_CHANNEL_2); HAL_TIMEx_PWMN_Start(htim, TIM_CHANNEL_2); HAL_TIM_PWM_Start(htim, TIM_CHANNEL_3); HAL_TIMEx_PWMN_Start(htim, TIM_CHANNEL_3); TIM_CtrlPWMOutputs(htim, ENABLE); /* 置位 MOE漏了就没波形 */ }启动顺序有个细节互补输出必须在主输出启动之后再开或者同步开。如果先开了主输出、互补通道还悬空那一瞬间桥臂上管有驱动、下管处于不确定态虽然逻辑电平实验问题不大但养成的坏习惯带到功率桥就会出大事。正确姿势是初始化完成后一次性把六路全部启动。主循环里如果要动态改占空比直接改捕获比较寄存器。中心对齐模式下要注意更新 CCR 后要到下一个计数周期顶点才会生效所以用示波器看占空比变化会有一个周期的延迟这是正常现象不是 Bug。3. 波形实测与关键指标校核怎么读示波器才算数3.1 频率与占空比基线校准代码烧进去以后第一步不是急着看死区先校准最基本的频率和占空比。示波器探头打到 10× 档通道耦合设为直流带宽限制打开。把 CH1 接 PA8测上管输出。用示波器的 Measure 功能直接读频率和占空比。按前面的配置理论值是 10kHz、50%。实测出来的频率应该是 10.00kHz 左右占空比 50.1% 以内都算正常。占空比有零点几个百分点的偏差主要来自计数器重载时机和测量噪声不要一看到 49.8% 就觉得代码错了。如果频率不是 10kHz 而是 5kHz回到中心对齐模式 ARR 计算那里重新查一遍如果占空比完全不对检查 CCR 和 ARR 的比值。CH1 测完把探头换到 PB13 测互补输出。此时应该看到反相波形占空比理论值依然是 50%。如果 CH1N 的占空比和 CH1 相同而不是互补说明极性配置或者死区逻辑有问题后面单独说。3.2 死区时间实测那段“双低”才是安全的关键死区验证是整个实验的精华也是最容易被误判的环节。把 CH1 和 CH1N 同时接到示波器两个通道时基先放到 20μs/div 看整体然后切换到上升沿触发慢慢把时基压缩到 500ns/div这样能清晰看到 CH1 下降沿和 CH1N 上升沿之间的双低区间。测量方法是用游标分别卡在两个沿上读绝对时间差。理想情况下实测死区应该略大于等于设定的 1μs。为什么说“略大于”因为定时器内部死区插入点的分辨率是 tDTS实际插入值是一个量化结果1.0000μs 只是一个理论换算值实测 1.04μs 很正常关键是不能小于设定的安全阈值。另一个容易被忽略的现象是死区的对称性。中心对齐模式下PWM 脉冲在一个周期内有两次切换——一次在计数器上坡一次在下坡。理论上两个切换点都应该插入 1μs 死区但由于计数器重载和输出比较事件的触发时刻差异两个切换点的实际死区可能有几十纳秒的差别。这是硬件行为不是配置错误。用示波器分别测上升沿死区和下降沿死区两者之差在 100ns 以内都属于正常。3.3 三对通道之间的相位与逻辑关系验证单对互补波形验证通过后进入最后一步确认三个桥臂的组合逻辑。把三路主输出 CH1、CH2、CH3 分别接到示波器 CH1、CH2、CH3 通道以 CH1 的上升沿做触发源看另外两路的电平关系。固定 50% 占空比输出的情况下三路主输出看起来是三路同频方波但相对相位取决于计数器配置。用 SPWM 或 SVPWM 调制时三路调制波本来就应该有 120° 相移这在静态 50% 占空比下很难直观验证。我的做法是先改成不对称占空比测试保持 CH1 的 CCR1800把 CH2 的 CCR 改成 600CH3 改成 3000。如果三路都能输出对应的占空比且互不串扰说明三个通道的更新逻辑正常相位关系后续由软件调制算法决定不在定时器硬件层面出问题。提示有条件的话这一步用四通道示波器效率最高。只有双通道就用分组法先看 CH1/CH1N再看 CH2/CH2N最后看 CH3/CH3N每组都验证频率、占空比、死区三项指标。六路全部达标验证结论才站得住。4. 常见问题与排查技巧实录4.1 六路全无输出问题多半不在定时器现象代码编译烧录都正常示波器探头点到输出引脚上却一条平线。这种“全无输出”的情况我排查的顺序基本固定先查 MOE再查通道使能最后查 GPIO 复用。MOE 位是最阴险的坑。HAL_TIM_PWM_Start 成功返回代码流程没报错但 MOE 没置 1 时 PWM 输出被强制拉到无效电平。遇到过不少开发者最后定位到只差一行 TIM_CtrlPWMOutputs。其次是 CCER 的 CCxE 和 CCxNE两个位必须同时为 1。如果只使能了 CCxE主通道有输出、互补通道始终为低就有可能出现“看起来波形正常但桥臂直通”的隐患——因为互补管一直关断上管正常斩波电机却能动只是性能极其拉胯。GPIO 复用配置也值得复查。STM32F103 的默认引脚上PA8、PA9、PA10 本身就是 TIM1 的通道输出复用设置一旦写成普通推挽输出定时器事件到了引脚也出不来。可以用寄存器回读的方式确认读 GPIOA 的 CRH 寄存器看对应位是不是复用推挽模式。4.2 上下管同时导通最危险的现象怎么排查如果示波器上清晰看到 CH1 和 CH1N 的高电平区间有重叠这不是配置瑕疵是直接往炸管方向走的问题。第一时间确认两件事。第一件CCER 的 CCxP 和 CCxNP 极性位是否被改过。默认配置下主输出高电平有效、互补输出也是高电平有效互补通道硬件上会生成反相且带死区的信号。如果有人为了某种原因把 CCxNP 也设成了反极性两路就会变成同相加死区的伪互补实际输出高电平区间部分重叠。第二件死区寄存器换算是不是出了低级错误。0x48 这个值落在 DTG 的线性区间死区 1μs。如果改成其他 8 位值要特别注意 DTG[7:5] 落在哪一段不同段位的换算公式完全不同。比如 DTG[7:5]100 时计算公式变成 (64 DTG[4:0]) × 16 × tDTS哪怕 DTG 低 5 位没变死区也可能跳变到十几微秒。查表不仔细死区就会和预期差一个数量级。排查重叠问题还有个技巧把示波器时基拉到 100ns/div开峰值检测模式。普通采样模式下示波器可能漏掉极窄的重叠尖峰峰值检测能抓住每个采样窗口内的最大最小值把异常沿口暴露出来。这个方法也适用于开关电源调试通用性很强。4.3 部分通道无输出的定位速查表实际项目里最常见的是“一路好、一路坏”这种局部故障排查起来比全无输出更费时间。整理一个速查表现象可能原因排查动作主输出有波互补输出无未使能 CCxNE 或未调用 HAL_TIMEx_PWMN_Start检查该通道的 NE 位某一路主输出无波通道 CCR 为 0 或被误写读寄存器确认两路波形同相捕获比较模式设成 PWM2 或极性位错误复核 OC 配置频率为设定值一半中心对齐 ARR 计算未考虑减半重算 ARR占空比偏大固定值死区被计入占空比改用 CCR 直接测占空比波形上沿有毛刺探头地线过长或接地不良换短地弹簧4.4 两个实测效率翻倍的技巧第一个技巧是用示波器的数学运算功能把死区“看得更清楚”。把 CH1 接主输出、CH2 接互补输出调出 Math CH1 - CH2。当两路同时为低时差分结果是一条基线当一路为高一路为低时差分是正负两个方向的台阶。死区区间在差分波形上表现为一个极窄的“台阶缺失区”。肉眼在普通双通道波形上分辨 1μs 死区容易疲劳看差分波形会直观很多。第二个技巧是动态改占空比来暴露相位错误。静态 50% 占空比下三对桥臂如果同步更新波形看不出互相之间的相对关系。写一个简单的主循环每隔 1ms 将三路 CCR 设置为 40%、50%、60% 的错落值再用示波器持续观察。如果某一路 CCR 更新没有生效或者更新到了错误的通道这种错落测试能立刻暴露问题。做这个验证实验花在示波器上的时间至少是写代码的两倍。我个人体会是六路互补 PWM 的配置流程并不复杂真正的门槛在于“确认每一个参数都真实作用到了引脚上”。寄存器配置只是逻辑上的“我认为”示波器上的波形才是物理上的“事实”。养成每次改参数都实测一次的习惯后面调试真正的三相电机驱动时能省下大量时间。最后再分享一个小技巧验证通过后建议把这组配置参数和波形截图整理成一份自己的调参基准记录下次换 MCU 平台或者换开关管时拿基准记录对照着改比从零开始从头理一遍要快得多。