STM32两路PWM互补输出死区详解:从原理到上板实测 前阵子我在调一块半桥驱动板两个低压MOSFET轮换导通用来驱动一台12V直流电机。一开始图省事直接用两路普通GPIO在主循环里软切换结果板子跑了几分钟其中一个功率管温度异常升高拆下来一量已经击穿。把两路波形抓出来一看问题很清楚上下桥在切换沿有一段同时导通的时间这就是直通。解决这类问题的标准做法就是让STM32输出两路PWM互补信号并在一路切换到另一路之间插入死区。这篇文章把“基于STM32的两路PWM互补输出带死区”这件事从头到尾讲一遍包括直通原理、硬件死区机制、CubeMX与HAL库的配置方法、仿真验证以及上板实测中会踩的坑。适合刚开始调电机驱动、想做半桥或全桥的开发者也适合已经能输出PWM但没认真处理死区的工程师参考。1. 上下桥直通才是真问题死区到底在防什么1.1 一次烧管经历引出的直通分析半桥驱动的原理是两个功率管交替导通上管开通时下管关断下管开通时上管关断。理想情况下这是两路完全反相的方波但实际功率管不是理想开关。驱动芯片有传播延迟MOSFET本身有开通延迟和关断延迟而且这两类延迟往往不一样更关键的是两路驱动信号的边沿不可能做到严格意义上的“上管刚关下管就开”。如果两路有效电平在几百纳秒内重叠电源正极会通过上管和下管直接到地形成一条短路通路电流只受回路寄生电阻和电感限制远大于正常工作电流。这个电流哪怕持续几百纳秒也足以让管子结温急剧上升反复出现就离烧管不远了。我当时就是只看了静态电平觉得“程序里已经让一路先关、再开另一路了”但物理世界里的延迟和程序顺序是两回事最终只能靠波形说话。后来把两路栅极波形叠加在示波器上清清楚楚看到两路高电平有一段重叠窗口真相就在眼前。1.2 死区的时间尺度不能太小也不能太大死区就是在两路互补输出之间强行插入的一小段“两路都无效”的时间。以高电平有效为例上管PWM的下降沿到来后先让两个管子都保持关断一小段时间这段时间就是死区随后下管PWM再给出上升沿。同理下管切回上管时也有一小段死区。常见功率器件的死区参考值差别不小低压MOSFET通常在几百纳秒到2微秒IGBT一般要2到10微秒。如果死区太小防不住直通如果死区太大占空比被白白吃掉输出波形畸变电机噪声和发热都会上来。所以死区不是“设一个意思意思”而是要根据器件开关参数、驱动芯片传播延迟、开关频率一起算出来的。后面第6章我会给一个工程取值思路这里先把概念对齐。2. 高级定时器的死区发生器把“防直通”做进硬件2.1 为什么不能靠软件延时来插死区很多人第一反应是切换的时候两个通道都拉低软件延时几微秒再拉高另一路。这条路在低速、低可靠性要求的场合勉强能跑但在电机驱动这种场景有几个根本问题。第一软件延时严重依赖CPU执行路径一个中断进来死区时长就跟着飘无法保证每次都一样而直通恰恰是“偶尔一次”就会烧管。第二CPU被死区延时占用主循环和中断响应都会变慢占空比刷新不及时还容易引入抖动。第三如果程序因为看门狗复位或跑飞GPIO状态可能恰好停在两路都导通的电平上没有硬件保护结果只能再烧一块板。STM32高级定时器里的死区发生器就是专门解决这件事的它完全由硬件逻辑生成互补波形CPU只负责在恰当的时候修改比较值死区和互补关系不依赖代码循环。这才是真正可靠的实现方式。2.2 从时基到互补输出的信号链具体看一下STM32里的实现。以常用的F1系列为例TIM1和TIM8属于高级控制定时器它们才是带死区发生器和刹车输入的那一类通用定时器通常不具备互补输出能力配了也没有OCxN。常用的互补通道是CH1、CH2、CH3CH4一般只做单路输出。整个信号链大致是这样的定时器计数器CNT持续计数ARR寄存器决定周期CCR寄存器决定翻转位置。在PWM模式1下CNT小于CCR时输出有效CNT大于等于CCR时输出无效。原来的有效信号经过输出比较电路之后分成了两路一路直接作为OCx输出另一路经死区发生器延迟后作为OCxN输出。OCx和OCxN本来来自同一个比较事件但死区发生器会在OCx的边沿和OCxN的边沿之间插入一段可配置的延迟保证其中一路还没完全无效之前另一路不会变成有效。从波形上看两条曲线的相邻翻转沿之间就有了一个间隙这就是我们要的死区。2.3 DTG寄存器死区时间到底怎么算死区长度由TIMx_BDTR寄存器里的DTG[7:0]字段控制但DTG值和实际时间不是简单的线性关系要分段看公式。死区时间步长Tdts默认等于一个定时器时钟周期也就是fDTSfCK_INTTIMx_CR1的CKD位可以把步长变成2倍或4倍用于获得更粗的时间分辨率。设DTG[7:0]的二进制高三位为DTG[7:5]分段公式如下DTG[7:5]0xx时DT DTG[7:0] × TdtsDTG[7:5]10x时DT (64 DTG[4:0]) × Tdts × 2DTG[7:5]110时DT (32 DTG[4:0]) × Tdts × 8DTG[7:5]111时DT (32 DTG[4:0]) × Tdts × 16。举个例子定时器时钟72MHzTdts约13.89ns。想要约500ns死区在0xx段需要DTG≈36写0x24实际死区500ns。如果想用2us死区0xx段理论上是144但144的二进制是0b10010000高三位已经变成100会自动落到10x段实际算出来是(6416)×27.8≈2.22us。这说明填寄存器时不能只按十进制大小想当然一定要先看落在哪个区间。实际开发中CubeMX会帮我们算但看波形对不上时回来手算一遍往往能发现问题。3. 从CubeMX到HAL库把互补PWM配出来3.1 CubeMX里的关键配置项如果你用STM32CubeMX生成工程整个过程其实不难。下面是我在一颗F103C8T6上跑通的配置思路其他型号的TIM1/TIM8逻辑类似。首先在Pinout视图中选TIM1把Channel1的工作模式设为“PWM Generation CH1 CH1N”这样CH1和CH1N会被同时启用对应引脚自动变成复用输出。然后在Parameters配置里做几件事把Clock Source设成Internal Clock把Prescaler设为71这样72MHz输入被分频成1MHz计数时钟Counter Period设为999得到1kHz的PWM周期Pulse初始值设500占空比50%在Dead Time一栏直接填你想要的死区时间单位是纳秒CubeMX会帮你翻译成DTG寄存器值输出极性一般选High后面再根据驱动芯片需求调整。为了方便对照我把这次实验的关键配置整理成了下面这张表。配置项本次实验取值说明Clock SourceInternal Clock使用内部时钟在F1上即APB2总线时钟Channel1 ModePWM Generation CH1 CH1N同时启用主输出与互补输出Prescaler7172MHz/(711)1MHz计数频率Counter Period9991MHz/(9991)1kHz PWMPulse500初始占空比50%后续可动态改Dead Time500~1000nsCubeMX中按ns填写自动换算DTGCKD00保持默认时Tdts等于定时器时钟周期3.2 生成代码后必须补的三行CubeMX生成的MX_TIM1_Init会把上面的参数写成寄存器但它默认不会启动PWM输出尤其不会启动互补通道。实际使用中至少要在初始化完成之后补上这样几行HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); HAL_TIMEx_PWMN_Start(htim1, TIM_CHANNEL_1); /* 运行中修改占空比 */ __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, 300);第二行HAL_TIMEx_PWMN_Start是启动互补通道的和第一行的HAL_TIM_PWM_Start是完全分开的函数。即便在CubeMX里已经勾选了CH1N不调用它PB13那个引脚就是没有波形。这是我见过的最高频问题也是新手最容易卡半天的点。建议每次写完驱动代码后先用示波器确认两个引脚都有信号再往下接负载。3.3 预装载与动态更新为什么改了占空比没反应运行中改PWM参数最常用的是修改CCR和ARR。上面代码里用了__HAL_TIM_SET_COMPARE宏去改CCR它改的是CCR的影子寄存器。如果CCR的预装载位使能了这个新值不会立刻生效而是等到定时器更新事件发生时才真正写入硬件比较器。CubeMX默认生成的代码通常把预装载打开所以改完Pulse后不会立刻看到波形变化而是等到下一周期才生效。这本身是好事能避免在PWM周期中途突然跳变带来的尖峰但如果你是为了调试想立刻看到变化就会觉得“改了没反应”。需要立刻生效时可以临时把预装载关掉或者手动触发一次更新事件。我个人不太建议频繁手动改计数器因为那样定时器的时基会乱。更好的做法是设计状态机在特定的更新中断里改CCR和ARR保证每次修改都发生在一个周期的边界。另外要小心把ARR改得比CCR还小这会导致输出逻辑不稳定写代码前先算好范围。4. 仿真探索先用软件把互补逻辑跑通4.1 用Proteus搭一个PWM仿真工程代码跑通之前先用仿真把逻辑验证一遍成本最低。我用的是常见的Proteus仿真环境步骤很简单。先在CubeMX里按第3章的参数生成工程编译出HEX文件。然后在Proteus里放一个STM32F103C8芯片把PA8和PB13两个引脚用导线引出来连接到虚拟示波器的两个输入通道。加载HEX文件后运行示波器上应该能看到两路方波。需要注意的是虚拟示波器的时基要匹配PWM频率1kHz的PWM建议把每格时间先设成500us能看到几个完整周期后面想看死区再把时基放大到几十微秒甚至几微秒。有个小细节Proteus里芯片型号和CubeMX里最好保持一致引脚封装不同会影响你拖出来的引脚位置。如果放了一个LQFP48封装PA8和PB13的实际坐标可能和原理图库不一样运行前先确认引脚的网络标签。4.2 在仿真波形上识别死区互补PWM带死区在示波器上看起来并不复杂两路波形基本反相但在每个翻转沿附近有一段两路都是低电平的间隙。测量方法是用示波器的标尺量一下PA8下降沿到PB13上升沿的时间差这段间隔就是死区。如果配置了500ns理论测量结果应该在500ns附近。注意不要只测一边另一边的边沿间隔也要看因为在有些极性配置下两个方向的实际死区可能略有差别。这里要提醒一句Proteus对STM32外设的建模并不是把所有寄存器时序都百分百还原不同版本对死区发生器的支持程度也不一样。如果仿真波形里两路看起来完全互补、没有任何间隙先别急着怀疑代码去查一下仿真模型版本和软件设置。仿真最重要的价值是验证通道配对、引脚连接、PWM频率和占空比这些宏观逻辑而不是验证纳秒级的时序细节。4.3 仿真能发现和不能发现的问题我用仿真实际验证下来能发现的问题集中在几类通道有没有选对、引脚是不是被其他外设占用、占空比修改后波形有没有随Pulse变化、两路是否真的互补。这些问题如果直接上板排查往往要花不少时间。仿真看不出来的则是真实世界的问题MOSFET的开关延迟、栅极驱动芯片的传播延迟、PCB寄生电感引起的振铃、负载突变带来的电压尖峰。所以我的建议是仿真跑通是第一步但死区参数是否真的合适最终必须以板级实测波形为准。5. 上板实测波形和死区值到底准不准5.1 逻辑分析仪采样率的选择与接线从仿真切到真实板子我用的是通用USB逻辑分析仪加开源分析软件。接线非常简单把通道A接PA8通道B接PB13地线夹住板子的GND共地必须一致。采样率要根据死区宽度来选不能拍脑袋。比如目标死区是500ns逻辑分析仪一个采样点如果是62.5ns16MS/s理论上还能看出大致趋势但边沿定位误差会比较大建议至少用50MS/s这样死区测量误差能控制在几十纳秒量级。如果手头工具采样率上不去就把死区临时调大到几微秒先验证逻辑再回到正常工作值。还有接线方式的问题如果你直接把逻辑分析仪夹在MOSFET栅极上探头引线越长引入的振铃越明显测出来的边沿和死区可能被噪声干扰。我更建议先测驱动芯片输入端的两路PWM信号确认定时器输出没问题后再测栅极波形看看驱动芯片有没有引入额外的延迟。5.2 现象一CH1N没有输出把整个链路查一遍这个现象出现频率最高而且很容易一踩就是半天。我的排查顺序是先看PA8有没有PWM。如果PA8也没有基本可以断定定时器没启动或时钟配置有问题回到CubeMX检查TIM1的Clock Source和引脚复用。如果PA8有、PB13没有下一步查GPIO配置PB13是不是被另一个初始化函数覆盖成了普通GPIO或者别的外设功能很多开发板的PB13还连着板载LED或按键代码里一旦初始化了GPIO就会把复用功能冲掉。如果GPIO没问题再看启动函数。这里再强调一次HAL_TIMEx_PWMN_Start和HAL_TIM_PWM_Start是两回事漏掉任何一个对应通道就是死的。最后一步检查CubeMX生成的引脚冲突表看看有没有其他外设也在用PB13。按这个顺序查基本都能定位。5.3 现象二两路高电平重叠死区没有生效看到两路高电平重叠时第一反应不是“器件坏了”而是先确认死区寄存器是不是真的写进去了。建议调试时直接在代码里读一遍TIM1-BDTR寄存器看看DTG字段是不是你期望的值。如果读出来是0说明配置过程被某个函数覆盖了比如初始化之后又调用了HAL_TIMEx_ConfigBreakDeadTime而这个传入的DeadTime参数是0那就等于把死区清掉了。还有一种隐蔽情况CubeMX生成的初始化代码里如果Break和DeadTime配置在某个条件编译块里实际没执行你看到的寄存器值自然不对。检查完寄存器再检查CKD位。有人把APB2配置成36MHz忘记定时器时钟是2倍关系以为fDTS36MHz实际是72MHz算出来的DTG就会差一倍。这种时钟理解问题在死区排查里很容易被忽略。5.4 现象三死区值与预期偏差很大用反推法定位如果死区值不对最快的定位方法是反推把DTG设成0xFF用逻辑分析仪测实际死区反推出当前的Tdts。比如在72MHz、CKD00时0xFF落在111段计算值是(3231)×16×13.89≈14us。如果你实测出来接近28us说明Tdts比预期大一倍问题基本锁定在时钟配置或者CKD的选择上。定出Tdts之后再按公式算回想要的死区对应的DTG重新配置。这个反推法在实际调试里特别有用因为很少有人能一眼看出寄存器配置问题但波形不会说谎。我第一次调的时候就是靠这个办法发现APB2分频设置和预期不同的。6. 死区参数的工程取值与调试心得6.1 不同器件类型的死区参考范围死区取值没有通用标准答案但可以按器件类型给一个经验范围低压MOSFET比如40V以内的电机驱动通常在0.2到1微秒高压MOSFET100V以上因为开关时间更长通常0.5到2微秒IGBT一般需要2到10微秒具体要看模块的开关损耗和驱动电阻。选择时至少要考虑三部分时间功率管的关断延迟、驱动芯片的传播延迟、以及安全余量。总死区应大于上管完全关断到下管开始开通之间的实际间隔否则直通保护就是假的。我把常用参考整理在下面供初选时用但肯定要拿实际波形复核。器件类型常见死区范围主要考虑因素低压MOSFET0.2~1us开关时间短死区可以小注意寄生振荡高压MOSFET0.5~2us米勒平台时间长需要更大间隔IGBT2~10us拖尾电流明显死区必须留够6.2 死区过大带来的占空比损失死区不是越大越安全。死区期间两路都关断负载两端的有效电压会减少等效为输出占空比被吃掉了一块。对于电机驱动这意味着实际施加到电机的平均电压低于理论值电流波形可能产生畸变电机噪声和损耗都会增加。如果系统里有电流闭环或速度闭环死区还容易在过零附近引起电流波动。所以在满足防直通的前提下死区应该尽量小。等到你开始做高性能电机控制时还会碰到死区补偿的问题但基础是先能把死区做对。6.3 上板后我建议的验证流程每次调完新板子我固定走一遍这个流程第一步空载状态下测定时器输出端的两路PWM确认频率、占空比、互补关系、死区时间都和配置一致。第二步接上驱动芯片测驱动芯片输出端波形确认驱动芯片的传播延迟没有把死区缩小到危险水平。第三步接上功率级先低压限流测栅极波形重复检查死区同时观察负载有没有异常噪声。第四步满载运行一段时间用温度数据验证死区余量是否合理。这套流程看起来繁琐但能省下很多返工时间。尤其是第一步很多板子最终烧管问题根本不在功率部分而在定时器配置。6.4 扩展方向刹车输入与硬件故障保护最后说一个和死区紧密相关的扩展点高级定时器的刹车输入BKIN。它的作用是可以让一个外部信号直接触发定时器封锁PWM输出把所有输出引脚拉到安全状态。这个过程由硬件完成不需要CPU响应比软件关输出快得多。做电机驱动的时候我强烈建议把过流保护信号接到刹车输入上。这次文章主题集中在互补PWM和死区刹车功能以后有机会再单独展开但原理上它是死区保护的天然搭档。最后分享一点个人体会。刚接触互补PWM时我也觉得死区只是个“为了稳妥而存在”的细节结果第一次烧管就狠狠补了一课。现在我调这类板子固定三步先按器件手册和驱动芯片传播延迟选一个初值再上板用逻辑分析仪抓真实栅极波形核实死区到底有多少最后用满载温升来验证余量合不合理。仿真能帮你快速跑通逻辑但真实死区值最终要以板级波形为准。每一次看到波形上清清楚楚地出现那段两路都低的时间我心里才算踏实。建议你也把这个流程养成习惯先把死区问题想透再去谈效率和补偿能少走很多弯路。