STM32六路互补PWM验证:TIM1死区与中心对齐配置实战
1. 为什么我要专门搭一套六路互补PWM的验证环境六路互补PWM这个词第一次接触的人容易把它想复杂。说白了它就是三对波形每一对里上下两个通道的占空比互补一个高的时候另一个低中间还要插入一段谁都不导通的时间也就是死区。三对加起来正好六路驱动三相桥臂无刷电机、三相逆变、数字电源这些场景都绕不开它。我这次做验证的起因很朴素手上有一块STM32G431的开发板想确认TIM1在中心对齐模式下输出的六路互补波形到底对不对死区有没有真正插进去互补关系在边界条件下会不会翻车。别看CubeMX里勾几个选项就能生成代码真到示波器上看波形问题往往出在你没想到的地方——比如死区时间算错了、极性反了、刹车功能误触发、中心对齐和边沿对齐搞混了。这篇内容适合三类人刚上手STM32高级定时器、想搞清楚互补输出到底怎么配的初学者已经在做电机驱动或电源、需要一套可复现验证流程的工程师以及被CubeMX自动生成的代码绕晕、想弄明白每一行配置背后含义的人。我会从TIM1的硬件特性讲起把CubeMX的配置逐项拆开给出完整的验证代码再把我实测中遇到的坑一个个摆出来。你照着做基本能在一两个小时内把六路互补波形在示波器上跑通。需要提前说明的是我用的芯片是STM32G431CB主频170MHzTIM1挂在APB2上。不同型号的定时器时钟源和引脚可能不一样但互补PWM的配置逻辑是通用的换芯片主要改时钟树和引脚映射。2. TIM1的互补输出通道到底是怎么组织的2.1 三对通道与CHxN的对应关系STM32的高级定时器TIM1、TIM8、TIM20这类和通用定时器最大的区别就是每个通道都带一个互补输出。以TIM1为例它有四个通道其中CH1/CH2/CH3各自带一个互补通道CH1N/CH2N/CH3NCH4没有互补。所以严格说TIM1能提供的是三路互补加一路独立共七路输出。我们说的六路互补用的就是CH1/CH1N、CH2/CH2N、CH3/CH3N这三对。这里有个容易混淆的点CHx和CHxN不是简单的反相器关系。它们各自有独立的使能位CCxE和CCxNE、独立的极性控制位CCxP和CCxNP还有共享的死区发生器。也就是说你可以让CH1和CH1N同时为高也可以让它们极性都反过来硬件不会拦着你。互补只是通常这么用不是硬件强制。理解这一点很重要因为很多人的波形不对就是因为极性位配反了却以为是死区的问题。在G431上TIM1的CH1默认映射到PA8CH1N到PA7或PB13取决于复用CH2到PA9CH2N到PB0或PB14CH3到PA10CH3N到PB1或PB15。具体用哪组引脚CubeMX里点一下就知道但要注意有些引脚在不同封装上可能没引出。2.2 死区发生器互补PWM的灵魂死区时间是为了防止上下桥臂直通。MOSFET关断需要时间如果上管还没完全关断下管就导通了瞬间大电流会把管子烧掉。死区发生器的作用就是在CHx关断和CHxN导通之间、以及CHxN关断和CHx导通之间插入一段延迟。STM32的死区发生器有个特点它不是在软件层面延迟而是硬件根据你写入BDTR寄存器的DTG位段自动生成。DTG是一个8位字段但它的编码不是线性的分四段DTG位段范围死区时间计算方式说明0x00-0x7FDTG × tDTS步进为1个tDTS0x80-0xBF(64 DTG[5:0]) × 2 × tDTS步进为2个tDTS0xC0-0xDF(32 DTG[4:0]) × 8 × tDTS步进为8个tDTS0xE0-0xFF(32 DTG[4:0]) × 16 × tDTS步进为16个tDTS其中tDTS是死区时钟周期由TIM1的时钟分频决定。G431上TIM1时钟170MHz如果tDTS取1/170MHz≈5.88ns那么DTG0x7F时死区约745ns。实际选死区要看你的MOSFET和驱动芯片的开关速度一般几百纳秒到一两微秒。我这次验证用的是DTG0x60算下来约564ns够用且不会太夸张。注意死区时间只在互补输出都使能、且处于互补模式时才生效。如果你只使能了CHx没使能CHxN死区发生器不工作。2.3 中心对齐模式为什么更适合电机驱动TIM1支持三种计数模式向上、向下、中心对齐上下交替。六路互补PWM在电机驱动里几乎都用中心对齐原因有两个。第一中心对齐时PWM脉冲关于周期中心对称谐波频谱更干净电机噪音和转矩脉动都更小。第二中心对齐模式下每个周期会产生两次比较匹配一次在向上计数时一次在向下计数时配合ADC可以更灵活地在PWM周期中间触发采样避开开关噪声。代价是中心对齐模式下实际PWM频率是定时器时钟除以2×ARR。比如ARR8499时钟170MHz那么PWM频率170M/(2×8500)10kHz。如果你按边沿对齐的公式算会得到20kHz差一倍。这个坑我在第一次配置时就踩了示波器上频率对不上查了半天才发现是计数模式的问题。3. CubeMX里配置TIM1的每一步都在做什么3.1 时钟树与引脚分配打开CubeMX先确认时钟树。G431的TIM1挂在APB2总线上如果APB2预分频不为1定时器时钟会倍频。我这次SYSCLK设170MHzAPB2不分频所以TIM1时钟就是170MHz。这一步很关键因为后面算死区和PWM频率都依赖这个值。引脚分配上我在Pinout视图里把PA8、PA7、PA9、PB0、PA10、PB1分别设为TIM1_CH1、CH1N、CH2、CH2N、CH3、CH3N。CubeMX会自动把复用功能配好但你要检查一下有没有引脚冲突——比如PA9/PA10有时候被USB或串口占用。3.2 TIM1的参数设置逐项拆解进入TIM1的配置界面我按下面的值设置Clock SourceInternal ClockPrescaler0不分频保持170MHzCounter ModeCenter Aligned mode 1Counter Period (ARR)8499Internal Clock DivisionNo DivisionRepetition Counter0Auto-reload preloadEnable这里解释几个关键选择。Prescaler0是为了让死区分辨率最高因为tDTS直接等于时钟周期。ARR8499配合中心对齐得到10kHz的PWM频率这个频率对大多数中小功率电机驱动够用。Repetition Counter0表示每个周期都更新如果设成1就是每两个周期才产生一次更新事件一般用不到。Auto-reload preload使能后ARR的修改会在下一个更新事件生效避免波形撕裂。3.3 PWM通道与死区的具体配置在Channel1到Channel3的配置里我统一设置ModePWM Generation CHx CHxNPulse4250初始占空比50%Output compare preloadEnableFast ModeDisableCH PolarityHighCHN PolarityHighIdle StateResetPulse4250是因为ARR8499中心对齐下占空比Pulse/ARR4250/8499≈50%。CH和CHN极性都设High意味着有效电平为高。如果你用的是低边驱动的MOSFET可能需要把CHN极性设成Low具体看驱动电路。死区配置在Break and Dead Time management标签页Dead Time96对应DTG0x60Automatic OutputEnableOff State Selection for Run ModeDisableOff State Selection for Idle ModeDisableBreak InputDisableBreak PolarityHighAutomatic Output使能后MOE位会在更新事件时自动置位不用软件干预。Break Input我这次先关掉因为验证阶段不需要刹车功能等波形跑通再单独测。3.4 生成代码后必须检查的几处CubeMX生成代码后别急着编译下载。先打开tim.c确认这几处htim1.Instance TIM1; htim1.Init.Prescaler 0; htim1.Init.CounterMode TIM_COUNTERMODE_CENTERALIGNED1; htim1.Init.Period 8499; htim1.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim1.Init.RepetitionCounter 0; htim1.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE;以及通道配置sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 4250; sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCNPolarity TIM_OCNPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; sConfigOC.OCIdleState TIM_OCIDLESTATE_RESET; sConfigOC.OCNIdleState TIM_OCNIDLESTATE_RESET;还有BDTRsBreakDeadTimeConfig.OffStateRunMode TIM_OSSR_DISABLE; sBreakDeadTimeConfig.OffStateIDLEMode TIM_OSSI_DISABLE; sBreakDeadTimeConfig.LockLevel TIM_LOCKLEVEL_OFF; sBreakDeadTimeConfig.DeadTime 96; sBreakDeadTimeConfig.BreakState TIM_BREAK_DISABLE; sBreakDeadTimeConfig.BreakPolarity TIM_BREAKPOLARITY_HIGH; sBreakDeadTimeConfig.AutomaticOutput TIM_AUTOMATICOUTPUT_ENABLE;如果这几处和你的预期一致说明CubeMX配置没问题。不一致的话回去检查是不是哪个选项点错了。4. 让六路波形真正跑起来的验证代码4.1 启动顺序MOE位不能忘很多人代码写完PWM死活不出来八成是忘了使能MOEMain Output Enable。高级定时器的输出默认是关闭的必须软件置位MOECHx和CHxN才会真正驱动引脚。CubeMX生成的HAL_TIM_PWM_Start函数内部会调用__HAL_TIM_MOE_ENABLE但如果你用的是LL库或者自己写寄存器就得手动加。我的启动代码是这样的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);注意CHxN要用HAL_TIMEx_PWMN_Start不是普通的HAL_TIM_PWM_Start。这两个函数操作的是不同的使能位CCxE和CCxNE用错了对应的引脚就没输出。4.2 动态改占空比验证互补关系光看静态波形不够我写了个简单的循环让占空比从10%扫到90%同时用示波器观察CH1和CH1N的关系for (uint32_t duty 850; duty 7650; duty 850) { __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, duty); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_2, duty); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_3, duty); HAL_Delay(500); }这里duty从850到7650对应占空比10%到90%。每500ms变一次方便示波器抓。实测下来CH1高电平时间随duty线性变化CH1N正好相反两者之间始终隔着约560ns的死区没有重叠。这说明互补逻辑和死区都正常工作。4.3 用更新中断确认中心对齐的对称性为了确认中心对齐模式真的对称我使能了TIM1的更新中断在中断里翻转一个GPIO然后用示波器看这个GPIO和PWM波形的相位关系void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM1) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0); } }中心对齐模式下更新事件发生在计数器到达ARR和到达0的时候各一次所以中断频率是PWM频率的两倍。示波器上看到PA0的翻转频率是20kHz而PWM是10kHz且翻转点正好在PWM波形的中心位置验证了对称性。5. 实测中踩到的四个坑和排查过程5.1 波形出不来MOE位和引脚复用双重问题第一次下载代码后示波器上六路全是低电平一点动静都没有。我先查MOE位在调试器里读TIM1-BDTR发现MOE确实是1说明不是使能问题。然后查引脚读GPIOA和GPIOB的MODER寄存器发现PA8、PA9、PA10配成了复用模式但PA7、PB0、PB1还是默认的输入模式。原因是CubeMX里我只在TIM1的通道配置里选了CH1N/CH2N/CH3N但没有在Pinout视图里确认这三个引脚的复用功能被正确分配。重新在CubeMX里点了一遍引脚生成代码后PA7/PB0/PB1的MODER变成了复用模式波形立刻出来了。经验CubeMX有时候不会自动帮你分配互补通道的引脚尤其是当默认引脚被其他外设占用时。生成代码前一定要在Pinout视图里肉眼确认六个引脚都标成了TIM1的复用功能。5.2 死区时间对不上tDTS被时钟分频影响了我设的DTG96按公式算应该是(6432)×2×tDTS192×5.88ns≈1.13μs。但示波器上量出来死区只有560ns左右正好差一倍。查了半天发现是ClockDivision这个参数在作怪。CubeMX里Internal Clock Division我选了No Division对应tDTStCK_INT。但如果选了DIV2tDTS就是2倍的时钟周期死区时间会翻倍。我确认自己选的是No Division那为什么算出来不对再仔细看参考手册发现DTG96落在0x80-0xBF区间公式是(64DTG[5:0])×2×tDTS。96的二进制是01100000DTG[5:0]10000032所以是(6432)×2×tDTS192×tDTS。192×5.88ns1.13μs。但实测560ns说明tDTS实际是2.94ns也就是时钟被二分频了。最后发现是CubeMX在生成代码时把ClockDivision设成了TIM_CLOCKDIVISION_DIV2而不是我选的No Division。可能是界面选项和生成代码不一致的bug。手动改成TIM_CLOCKDIVISION_DIV1后死区变成1.13μs和计算值吻合。5.3 互补波形在占空比接近0和100%时异常当占空比调到5%以下时CH1N的高电平时间变得极短几乎看不到。这是正常的因为死区时间已经和有效脉宽相当了。但占空比到0%时CH1应该完全为低CH1N完全为高实测却发现CH1N有窄脉冲。原因是PWM模式1下当比较值大于ARR时输出强制为有效电平。我把duty设成0__HAL_TIM_SET_COMPARE写入0此时CNT永远大于0所以CH1一直有效高CH1N一直无效低和预期相反。要得到0%占空比应该把duty设成ARR1或者用强制输出模式。这个坑提醒我PWM模式1和模式2的边界行为不一样调占空比时要注意比较值的范围。5.4 刹车功能误触发导致输出突然关闭验证完基本波形后我顺手把Break Input使能了想试试刹车功能。结果PWM跑着跑着突然全没了BDTR的MOE位被硬件清零。查了手册才知道Break输入一旦触发MOE自动清零且需要软件重新置位才能恢复。我的Break引脚悬空容易受干扰误触发。解决办法有两个要么给Break引脚加下拉电阻要么在NVIC里使能Break中断在中断里处理。我选了后者在中断回调里重新使能MOE同时记录触发次数。实际产品里刹车功能要配合故障检测电路用不能悬空。6. 从验证环境到实际驱动还差什么6.1 死区时间要根据MOSFET参数重新算我验证用的564ns死区是基于手头驱动芯片的典型值。实际做电机驱动时死区要按MOSFET的关断延迟、驱动电阻、栅极电荷来算。一般来说死区时间要大于上下管的关断时间之和再留20%余量。比如上管关断需要200ns下管关断需要150ns那死区至少420ns取500ns比较稳。死区太大也有问题会导致输出波形畸变电机低速时转矩脉动增加。所以要在安全和性能之间找平衡。我的经验是先用示波器量实际关断波形再定死区值别照搬手册的典型值。6.2 互补PWM和ADC采样的配合电机驱动里PWM和ADC要同步。中心对齐模式下可以在计数器到达0或者ARR时触发ADC这样采样点正好在PWM周期中间避开开关噪声。TIM1的TRGO可以配置成更新事件触发连到ADC的外部触发源。我这次验证没接ADC但如果你要做电流环这一步不能省。配置方法是TIM1的Trigger Output选Update EventADC的External Trigger选TIM1_TRGO然后使能ADC的连续转换或单次转换。6.3 实际驱动板上的隔离与保护验证环境是开发板直接输出实际驱动板要加隔离。光耦或者数字隔离器把MCU的PWM信号传到栅极驱动芯片中间还要加死区有些驱动芯片自带死区和MCU的死区叠加。保护方面过流检测要连到Break输入一旦过流硬件立刻封波比软件响应快得多。我见过有人把Break输入直接接比较器输出比较器阈值设错导致正常工作时也触发刹车。调试时先用示波器看比较器输出确认阈值合理再连到TIM1。7. 几个容易被忽略的寄存器细节7.1 CCER寄存器里的极性位和使能位CCER寄存器控制每个通道的使能和极性。CH1的使能位是CC1Ebit0极性位是CC1Pbit1CH1N的使能位是CC1NEbit2极性位是CC1NPbit3。这四个位要配合设置缺一不可。我习惯在初始化后直接读一次CCER确认值符合预期。比如六路互补、极性都High的情况下CCER应该是0x0555CH1/CH2/CH3的E和NE都置1P和NP都清0。如果读出来不对说明HAL库的配置没生效要查是不是在别处被覆盖了。7.2 BDTR寄存器的LOCK位BDTR有个LOCK位段可以锁定DTG、OSSR、OSSI等位的写入。LOCKLEVEL设成1后这些位就不能再改直到复位。这是为了防止软件跑飞后误改死区导致炸管。实际产品里建议设成LOCKLEVEL_1或更高验证阶段可以设OFF方便调试。7.3 重复计数器的实际用途Repetition Counter在电机驱动里有个巧妙用法设成1可以让更新事件每两个PWM周期才发生一次这样ADC采样率减半适合低速场合。但要注意中心对齐模式下重复计数器的行为和边沿对齐不同具体看手册的时序图。8. 我个人的验证清单和常用参数每次换芯片或者改配置我都会按这个清单过一遍检查项预期值实测方法TIM1时钟170MHz读RCC寄存器或示波器量MCOPWM频率10kHz示波器量CH1周期死区时间约560ns示波器量CH1和CH1N间隔互补关系严格反相双通道示波器对比MOE位1调试器读BDTR引脚复用AF模式读GPIOx_MODER占空比线性度10%-90%线性扫duty看脉宽常用参数我整理成了一张表换项目时直接改这几个值ARR决定频率中心对齐下f170M/(2×(ARR1))Pulse决定占空比dutyPulse/(ARR1)DTG决定死区按公式反算Prescaler一般设0除非需要更低频率这套验证环境搭完之后我把它固化成了一个模板工程以后做任何三相驱动都从这个模板改省去了重复配置的时间。如果你也在做类似的东西建议先把六路波形在示波器上确认无误再往下做功率级不然出了问题很难判断是控制的问题还是硬件的问题。