STM32互补PWM与死区控制:电机驱动安全换相实战
1. 项目缘起与整体设计思路1.1 为什么偏偏要选互补PWM而不是单路PWM加死区芯片很多刚接触电机驱动的朋友第一反应是拿STM32的一个普通定时器通道输出一路PWM再外挂一个IR2104或者EG2133这类半桥驱动芯片靠芯片自带的死区逻辑去推半桥。这个方案能用但做到双极性调制或者需要精确控制上下管切换时序的时候你就会发现外挂死区芯片的延迟一致性很难保证尤其是批量生产时不同批次的驱动芯片死区时间能差出几十纳秒反映到电机上就是低速抖动和MOS管温升不一致。STM32的高级定时器TIM1、TIM8自带互补输出通道和可编程死区发生器死区时间可以从0到几百纳秒精细调节而且上下管的切换由硬件保证不受中断延迟和软件抖动影响。我实测过用TIM1的CH1和CH1N推半桥死区设成200nsMOS管栅极波形干净得很完全没有交叉导通的风险。这就是为什么这个项目值得单独拿出来讲——它解决的是电机驱动里最核心的“安全换相”问题。这个内容适合谁看如果你已经会用STM32CubeMX点灯、会配置基本定时器但一到电机驱动就心里没底不知道死区怎么算、互补输出怎么接、刹车功能怎么用那这篇就是写给你的。我会从CubeMX的配置界面一路讲到示波器实测波形把每个参数背后的“为什么”都掰开揉碎。1.2 整体方案框图和信号流向先把这个项目的硬件链路理清楚不然后面配置的时候容易迷。整个系统分四层控制层STM32F103C8T6或者STM32G431跑CubeMX生成的HAL库代码负责产生互补PWM和接收调速指令。驱动层半桥栅极驱动芯片比如IR2104S、EG2133或者UCC27211把STM32的3.3V逻辑电平抬升到MOS管需要的10-15V驱动电压。功率层N沟道MOS管组成的半桥或全桥比如IRF3205、IRF540N或者更现代的CSD18540负责真正流过电机电流。执行层有刷直流电机、无刷电机的其中一相或者步进电机的某一相绕组。信号流向是这样的STM32的TIM1_CH1输出高侧PWMTIM1_CH1N输出低侧互补PWM两路信号进驱动芯片的HIN和LIN引脚驱动芯片的HO和LO分别推高侧和低侧MOS管的栅极。电机接在半桥中点和高低侧母线之间。如果是全桥驱动那就需要两对互补通道比如CH1/CH1N推左半桥CH2/CH2N推右半桥。注意IR2104这类芯片的HIN和LIN不能同时为高否则它会同时输出两路高电平导致直通。STM32的互补输出模式天然保证CH1和CH1N不会同时有效但前提是你在CubeMX里正确设置了死区时间并且没有在代码里手动去翻转这两个引脚的电平。1.3 关键参数选型背后的计算逻辑PWM频率和死区时间是两个最核心的参数不能拍脑袋定。PWM频率有刷直流电机一般用10kHz到20kHz低于10kHz人耳能听到啸叫高于20kHz开关损耗又会明显上升。我通常取16kHz刚好在听觉范围上限附近MOS管用IRF3205的话开关损耗还能接受。无刷电机FOC控制常用20kHz到40kHz因为电流环带宽要求更高。这个项目以有刷电机为例取16kHz。死区时间死区时间必须大于MOS管的关断延迟时间否则上下管会瞬间直通。IRF3205的关断延迟典型值约30ns加上驱动芯片的传输延迟约100ns再留一倍余量死区时间取200ns到300ns比较稳妥。如果MOS管是IRF540N关断延迟约40ns死区可以取250ns。定时器时钟和预分频STM32F103的TIM1挂在APB2总线上主频72MHz。要得到16kHz的PWM频率计数周期 72MHz / 16kHz 4500。所以ARR寄存器设为4499从0开始计数预分频器PSC设为0。如果你用的是STM32G431主频170MHzARR 170MHz / 16kHz 10625取整后ARR 10624。死区时间的计算死区时间 DTG[7:0] × T_dts其中T_dts是定时器时钟周期。72MHz下T_dts 13.89ns。要得到200ns死区DTG值 200 / 13.89 ≈ 14.4取14实际死区 14 × 13.89 ≈ 194ns。CubeMX里直接填200ns它会自动算但你要知道它算出来的实际值是多少。2. STM32CubeMX配置全流程拆解2.1 时钟树配置与定时器时钟源确认打开CubeMX新建工程选好芯片型号后第一件事是配时钟树。以STM32F103C8T6为例外部晶振选8MHzPLL倍频到72MHzAPB2预分频器设为1这样TIM1的时钟就是72MHz。如果你用的是STM32G431外部晶振选24MHzPLL配置到170MHzAPB2不分频TIM1时钟也是170MHz。这里有个坑很多人配完时钟树直接去配定时器结果发现PWM频率怎么算都不对。原因在于APB2预分频器如果设成了2定时器时钟会自动倍频实际时钟是APB2时钟的2倍。CubeMX的时钟树界面会实时显示各定时器的时钟频率配完一定要看一眼“TIM1 Clock”那一栏显示的是不是72MHz。提示STM32F1系列如果APB2预分频系数为1定时器时钟等于APB2时钟如果预分频系数大于1定时器时钟等于APB2时钟的2倍。这个规则在F4、G4、H7系列上也类似但具体倍频关系要看参考手册的时钟树章节。2.2 TIM1模式配置PWM Generation CH1 CH1N在CubeMX的Pinout视图里找到TIM1把Clock Source设为Internal ClockChannel1设为“PWM Generation CH1 CH1N”。这个选项一选PA8和PB13两个引脚会自动被分配为TIM1_CH1和TIM1_CH1N。PA8是高侧输出PB13是低侧互补输出。接着去Configuration标签页点开TIM1的参数设置Prescaler0Counter ModeUpCounter Period (ARR)4499Internal Clock DivisionNo Divisionauto-reload preloadEnablePWM ModePWM mode 1Pulse (CCR1)2250初始占空比50%CH PolarityHighCHN PolarityHighDead Time200单位是nsCubeMX会自动换算成DTG寄存器值Break State根据你的硬件刹车电路决定没有刹车电路就Disable这里重点说三个参数Counter Period决定了PWM频率前面算过了72MHz / 4500 16kHz。Pulse是占空比2250 / 4500 50%。注意这个值是CCR1寄存器的值不是百分比CubeMX里填的是原始计数值。Dead Time填200CubeMX会在生成的代码里自动计算DTG值。你可以在生成的tim.c文件里看到htim1.Init.DeadTime 14;这样的赋值14就是DTG寄存器的值。2.3 刹车功能和死区时间的联动设置Break功能是高级定时器的一个安全特性当刹车引脚检测到有效电平时定时器会立即强制输出通道进入预设的安全状态通常是高阻或低电平同时可以触发中断。这个功能在电机驱动里非常有用比如过流保护电路检测到电流超标拉低刹车引脚PWM立刻关断比软件中断响应快得多。CubeMX里Break State选Enable的话需要指定刹车引脚通常是PB12或PA6还要设置刹车极性和自动恢复模式。如果你没有过流保护硬件建议先Disable等硬件调通了再开。死区时间和刹车功能在寄存器层面是联动的因为BDTR寄存器同时管这两件事。CubeMX生成的代码里HAL_TIMEx_ConfigBreakDeadTime()这个函数会一次性配置好死区和刹车参数。你如果手动改代码注意不要只改死区而把刹车配置覆盖掉了。2.4 生成代码前的最后检查清单在点“GENERATE CODE”之前对照这个清单过一遍时钟树里TIM1 Clock显示72MHz或170MHzTIM1的Channel1选了“PWM Generation CH1 CH1N”Prescaler 0Counter Period 4499Pulse 2250PWM Mode PWM mode 1CH Polarity HighCHN Polarity HighDead Time 200Break State Disable暂时Toolchain/IDE选MDK-ARM或STM32CubeIDE生成代码后打开tim.c找到MX_TIM1_Init()函数确认里面有以下几行htim1.Instance TIM1; htim1.Init.Prescaler 0; htim1.Init.CounterMode TIM_COUNTERMODE_UP; htim1.Init.Period 4499; htim1.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim1.Init.RepetitionCounter 0; htim1.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE;以及后面的sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 2250; 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;还有死区配置sBreakDeadTimeConfig.OffStateRunMode TIM_OSSR_DISABLE; sBreakDeadTimeConfig.OffStateIDLEMode TIM_OSSI_DISABLE; sBreakDeadTimeConfig.LockLevel TIM_LOCKLEVEL_OFF; sBreakDeadTimeConfig.DeadTime 14; sBreakDeadTimeConfig.BreakState TIM_BREAK_DISABLE; sBreakDeadTimeConfig.BreakPolarity TIM_BREAKPOLARITY_HIGH; sBreakDeadTimeConfig.AutomaticOutput TIM_AUTOMATICOUTPUT_DISABLE;这些都对上了硬件层就稳了。3. 代码实现与电机调速实操3.1 启动PWM输出的正确姿势CubeMX生成的代码里MX_TIM1_Init()只完成了定时器的初始化PWM输出还没有真正启动。你需要在主函数里手动调用两个函数HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); HAL_TIMEx_PWMN_Start(htim1, TIM_CHANNEL_1);第一行启动CH1的高侧PWM第二行启动CH1N的互补PWM。注意第二行用的是HAL_TIMEx_PWMN_Start不是HAL_TIM_PWM_Start这个函数在stm32f1xx_hal_tim_ex.c里别搞混了。如果你只调用了第一行CH1N不会有输出低侧MOS管一直关断电机只能靠高侧MOS管的体二极管续流效率极低而且发热严重。我见过有人调了半天发现电机转是转了但MOS管烫得能煎鸡蛋就是漏了第二行。注意调用HAL_TIMEx_PWMN_Start之前确保BDTR寄存器的MOE位被置位。HAL库的HAL_TIMEx_PWMN_Start内部会自动置位MOE但如果你用的是寄存器操作记得手动TIM1-BDTR | TIM_BDTR_MOE;。3.2 动态调速改CCR寄存器的两种方式调速的本质就是改CCR1寄存器的值。有两种方式方式一直接改寄存器__HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, 3000);这行代码把CCR1改成3000占空比变成3000/4500 66.7%。这种方式最快适合在中断里调用。方式二用HAL库函数__HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, 3000);其实HAL库也是宏定义直接操作寄存器和方式一本质一样。真正用函数调用的是HAL_TIM_PWM_Start这类改占空比没有专门的函数都是用宏。调速范围要注意CCR值不能等于或超过ARR值否则占空比100%就没有开关动作了。也不能太小比如小于死区时间对应的计数值否则实际输出会被死区吃掉。72MHz下200ns死区对应约14个计数所以CCR最小建议不小于50对应占空比约1.1%。3.3 用串口指令实时调速的完整代码光会改CCR还不够实际项目里通常要通过上位机或者遥控器实时调速。这里给一个串口调速的完整实现#include main.h #include usart.h #include tim.h extern TIM_HandleTypeDef htim1; extern UART_HandleTypeDef huart1; uint8_t rx_buffer[1]; uint16_t duty_cycle 2250; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { if (rx_buffer[0] U || rx_buffer[0] u) { if (duty_cycle 4400) duty_cycle 100; } else if (rx_buffer[0] D || rx_buffer[0] d) { if (duty_cycle 100) duty_cycle - 100; } else if (rx_buffer[0] S || rx_buffer[0] s) { duty_cycle 0; } __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, duty_cycle); HAL_UART_Receive_IT(huart1, rx_buffer, 1); } } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); MX_TIM1_Init(); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); HAL_TIMEx_PWMN_Start(htim1, TIM_CHANNEL_1); HAL_UART_Receive_IT(huart1, rx_buffer, 1); while (1) { } }这段代码里串口收到U就加速D就减速S就停机。每次改完CCR立即生效因为ARR预装载和CCR预装载都使能了新值会在下一个计数周期开始时生效不会产生毛刺。3.4 软启动避免电机飞车的保护逻辑直接给电机上50%占空比电流冲击会很大尤其是大功率电机。我习惯加一个软启动逻辑让占空比在500ms内从0线性升到目标值void soft_start(uint16_t target_duty, uint16_t step_delay_ms) { uint16_t current 0; while (current target_duty) { current 50; if (current target_duty) current target_duty; __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, current); HAL_Delay(step_delay_ms); } }调用soft_start(2250, 10)就是每10ms增加50个计数从0升到2250大约需要450ms。这个逻辑放在主循环里上电后先软启动再进入正常调速循环。提示软启动期间不要用HAL_Delay阻塞太久否则串口接收会丢数据。更好的做法是用定时器中断做状态机每10ms更新一次CCR。但作为入门项目HAL_Delay够用了。4. 实测波形分析与常见问题排查4.1 示波器抓互补波形死区到底长什么样配置完成后用示波器双通道抓PA8和PB13的波形。正确的情况下你应该看到两路频率相同、相位相反的PWM但在高低电平切换的瞬间两路都是低电平中间有一段空白时间这就是死区。实测数据72MHz主频ARR4499CCR2250死区设为200ns。示波器上高侧PWM的上升沿到低侧PWM的上升沿之间大约有194ns的间隔因为DTG1414×13.89≈194ns。这个间隔就是死区时间它保证了高侧MOS管完全关断之后低侧MOS管才开通。如果你把死区设成0示波器上两路波形会紧挨着切换看起来“完美互补”但实际电路中上下管会有几十纳秒的同时导通瞬间电流可能达到几十安培MOS管很快就烧了。所以死区宁大勿小200ns到500ns都是安全范围。4.2 MOS管发热严重先查这三个地方电机能转但MOS管烫手按这个顺序排查第一死区时间够不够。把示波器调到上升沿触发放大看高低侧切换的细节。如果两路波形有重叠说明死区太小。加大到500ns试试如果发热明显改善就是死区问题。第二驱动芯片的供电和自举电容。IR2104的高侧驱动靠自举电容供电如果自举电容容量不够或者漏电高侧MOS管会工作在放大区而不是开关区功耗急剧上升。自举电容一般取100nF到1μF耐压要高于母线电压。我遇到过用10nF自举电容导致高侧MOS管发热的案例换成1μF就好了。第三PWM频率是不是太高。16kHz下IRF3205的开关损耗约0.5W如果频率提到40kHz开关损耗会翻倍以上。用手摸散热片如果只是温热问题不大如果烫得不能碰要么降频要么换低Qg的MOS管。4.3 电机不转或者只嗡嗡响的排查流程按这个表逐项检查现象可能原因排查方法电机完全不转MOE位没置位检查是否调用了HAL_TIMEx_PWMN_Start电机嗡嗡响不转占空比太低把CCR调到2250以上试试电机抖动死区太大减小死区到100ns试试电机转但没力驱动电压不足测MOS管栅极电压应该10V以上上电就烧MOS管上下管直通示波器抓波形看有没有重叠调速时电机飞车CCR值超过ARR检查调速代码有没有限幅注意调试时一定要用限流电源把电流限制在电机额定电流的1.5倍以内。一旦发现电流异常立即断电检查不要抱着“再试一次”的心态烧MOS管是小事烧电机或者引发火灾就麻烦了。4.4 互补输出与普通PWM的切换技巧有时候你需要从互补模式切回普通单路PWM比如驱动一个只需要单路PWM的舵机。这时候不能直接改CubeMX重新生成因为会覆盖你写的应用代码。正确做法是在代码里动态切换// 切换到单路PWM HAL_TIMEx_PWMN_Stop(htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); // 切换回互补PWM HAL_TIM_PWM_Stop(htim1, TIM_CHANNEL_1); HAL_TIMEx_PWMN_Start(htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1);注意切换回互补模式时要先启动CH1N再启动CH1顺序反了会有短暂的直通风险。这个细节在参考手册里没有明说是我在实际项目中踩坑总结出来的。5. 进阶扩展与个人实操体会5.1 从半桥到全桥双互补通道的配置要点驱动直流有刷电机做正反转需要全桥。全桥需要两对互补通道CH1/CH1N推左半桥CH2/CH2N推右半桥。CubeMX里把Channel2也设成“PWM Generation CH2 CH2N”对应引脚是PA9和PB14。全桥驱动有个特殊要求左半桥和右半桥不能同时输出高电平否则母线直接短路。所以CH1和CH2的占空比不能同时大于0。正转时CH1给PWMCH2给0反转时CH2给PWMCH1给0。这个逻辑要在应用代码里保证硬件层面没有保护。我通常写一个set_motor(int16_t speed)函数speed范围-1000到1000正值正转负值反转void set_motor(int16_t speed) { if (speed 0) { __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, speed * 4); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_2, 0); } else if (speed 0) { __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, 0); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_2, -speed * 4); } else { __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, 0); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_2, 0); } }5.2 用刹车功能做过流保护的实战配置前面Break State一直Disable现在说说怎么用它做过流保护。硬件上需要一个采样电阻加比较器比较器输出接到STM32的刹车引脚TIM1_BKIN通常是PB12。当电流超过阈值比较器翻转刹车引脚收到有效电平TIM1硬件自动关断所有输出通道响应时间只有几十纳秒比软件中断快两个数量级。CubeMX里Break State选EnableBreak Polarity根据比较器输出极性选High或LowAutomatic Output选Disable这样刹车后不会自动恢复需要软件手动清除。生成的代码里会多出刹车中断的回调函数void HAL_TIMEx_BreakCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM1) { // 刹车触发关闭PWM输出 HAL_TIM_PWM_Stop(htim1, TIM_CHANNEL_1); HAL_TIMEx_PWMN_Stop(htim1, TIM_CHANNEL_1); // 点亮故障指示灯 HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); } }这个回调函数在刹车触发时自动调用你可以在里面做故障记录、报警、停机等操作。恢复时需要手动清除刹车标志位并重新启动PWM。5.3 个人实操中踩过的三个坑第一个坑忘了使能ARR预装载。有一次调速时电机突然抽搐示波器抓波形发现PWM周期偶尔会变长。查了半天发现是ARR预装载没使能改CCR的时候ARR寄存器被意外修改了。CubeMX里Auto-reload preload一定要选Enable这个选项默认是Disable很多人不注意。第二个坑死区时间单位搞错。CubeMX里Dead Time填的是纳秒但生成的代码里DeadTime是DTG寄存器的原始值。我一开始以为填200就是200ns结果生成的代码里DeadTime200实际死区是200×13.89≈2.8μs电机低速时抖动得厉害。后来才搞明白CubeMX会自动换算但如果你手动改代码一定要按DTG的计算公式来。第三个坑互补输出引脚被其他外设占用。STM32F103的PA8和PB13除了TIM1_CH1和CH1N还可以复用为其他功能。有一次我在CubeMX里同时开了SPI1和TIM1结果PA8被SPI1的NSS占用了TIM1_CH1死活没有输出。解决办法是在Pinout视图里检查引脚冲突把冲突的外设重新分配引脚。5.4 这个方案还能怎么扩展互补PWM驱动电机的框架搭好之后可以往几个方向扩展加入电流环用ADC采样母线电流在定时器中断里做PI调节实现恒流驱动。STM32的ADC可以和TIM1同步触发采样时刻精确对准PWM中心点避开开关噪声。换成无刷电机三对互补通道推三相全桥加上霍尔传感器或者无感反电动势检测就是一套完整的BLDC驱动器。加入CAN通信用CAN总线接收上位机的速度和方向指令适合多电机协同的场景比如小车底盘或者机械臂关节。做参数自整定通过串口上位机实时修改PWM频率、死区时间、PID参数不用重新烧录固件就能调参效率提升明显。我个人在实际操作中的体会是STM32CubeMX把互补PWM的配置门槛降得很低但死区时间、刹车功能、预装载这些细节才是决定项目成败的关键。工具帮你省了查寄存器的时间但省不了理解原理的时间。每次调新电机我都会先用示波器把互补波形和死区时间确认一遍再上功率电这个习惯帮我省下了至少五块烧掉的MOS管。