
1. 从零拆解为什么电机驱动离不开互补PWM1.1 一个被低估的硬件细节很多人第一次接触电机驱动脑子里想的都是“给个PWM波调个占空比电机就转起来了”。这个理解不能说错但只对了一半。如果你驱动的是那种小小的直流有刷电机一个MOS管加一个续流二极管确实能凑合。但一旦涉及到H桥拓扑——也就是让电机能正反转、能刹车、能调速的标准电路——事情就变得微妙起来了。H桥有四个开关管分成左右两组桥臂。每个桥臂由上下两个MOS管串联组成中间引出线接电机。问题来了同一个桥臂的上下两个管子绝对不能同时导通。一旦同时导通电源正极直接通过两个管子短路到地瞬间电流可以飙到几十甚至上百安培管子当场炸给你看。这个现象在行业里叫“直通”或者“shoot-through”。那怎么保证它们不同时导通靠软件延时靠死循环里写个delay都不靠谱。你永远不知道中断什么时候来也永远不知道编译器会把你的代码优化成什么样。真正可靠的做法是让硬件自己来处理这件事——这就是互补PWM输出和死区时间存在的意义。1.2 互补输出到底“互补”在哪里STM32的高级定时器比如TIM1、TIM8有一个非常实用的功能每个通道都可以配置一对输出——CH1和CH1N也就是主输出和互补输出。这两个信号在逻辑上是反相的CH1为高时CH1N为低CH1为低时CH1N为高。你把CH1接到H桥左桥臂的上管CH1N接到左桥臂的下管。这样当CH1输出高电平时上管导通、下管截止当CH1输出低电平时上管截止、下管导通。理论上它们永远不会同时导通。但“理论上”和“实际上”之间还差着一个死区时间。MOS管不是理想开关它关断需要时间通常几十到几百纳秒。如果CH1从高变低的同时CH1N从低变高上管还没完全关断下管就已经开始导通了那一瞬间两个管子都处于半导通状态等效电阻很小电流照样会冲上去。所以STM32的互补输出还配了一个死区时间发生器。你可以设置一个时间段比如500ns在这段时间内CH1和CH1N都输出低电平确保上下管都处于关断状态等过了这段时间再让另一个管子导通。这个死区时间由硬件自动插入不需要CPU干预精度和可靠性远超软件延时。1.3 这套方案适合谁如果你正在做以下事情这篇文章的内容可以直接拿去用用STM32驱动直流有刷电机需要正反转和调速做BLDC电机的方波驱动六步换相需要三对互补PWM设计数字电源、逆变器、音频功放等需要H桥或半桥拓扑的场合学习STM32高级定时器的互补输出和死区配置我下面会以STM32G431为例用STM32CubeMX从零配置一对互补PWM驱动一个H桥来控电机。其他型号的STM32操作逻辑基本一致只是引脚和时钟树略有差异。2. 工具链准备与CubeMX工程创建2.1 软件安装的坑我先替你踩了STM32CubeMX的安装本身不复杂但有几个地方容易卡住。首先它依赖Java运行环境如果你电脑上没装Java或者版本太老CubeMX启动时会直接闪退连报错都不给。建议去官网下载最新的CubeMX安装包它通常会自带一个JRE但如果你之前装过其他版本的Java可能会冲突。我的做法是先卸载所有旧版Java再装CubeMX让它用自己的JRE省心。安装路径不要有中文和空格。这个不是CubeMX矫情而是它内部调用的一些命令行工具对路径解析有问题。我见过有人装在“D:\学习资料\STM32开发\”下面结果生成代码时各种报错换成“D:\STM32\”就一切正常。另外CubeMX需要下载对应的芯片固件包Firmware Package。如果你用的是G4系列第一次创建工程时它会提示你下载G4的固件包大概几百兆。网络不好的话可能会失败可以手动去官网下载离线包然后通过“Help - Manage embedded software packages”导入。2.2 新建工程与芯片选型打开CubeMX点击“New Project”在搜索框里输入你的芯片型号。我手头用的是STM32G431CBU6所以直接搜“G431CB”。选中芯片后右边会显示它的资源概览多少Flash、多少RAM、有哪些定时器、哪些引脚支持PWM。这里有个细节值得注意不是所有定时器都支持互补输出。在STM32G4系列里TIM1、TIM8、TIM20是高级定时器支持互补PWM和死区插入TIM2、TIM3、TIM4是通用定时器只能输出普通PWM没有互补功能。所以你选芯片的时候如果要做互补输出必须确认至少有一个高级定时器可用。选好芯片后点击“Start Project”进入配置界面。2.3 时钟树配置别让定时器跑错频率时钟树是CubeMX里最容易让人头晕的部分但也是最重要的。PWM的频率和死区时间都直接依赖于定时器的时钟源。我的板子上有一颗24MHz的外部晶振HSE。在“System Core - RCC”里把High Speed Clock设为“Crystal/Ceramic Resonator”。然后进入Clock Configuration页面把PLL的输入源选为HSE经过分频和倍频后让系统时钟SYSCLK跑到170MHz——这是G431的最高主频。定时器的时钟来源是APB2总线。在G4系列里如果APB2的分频系数不为1定时器时钟会额外倍频。我这里APB2不分频所以TIM1的时钟就是170MHz。记住这个数字后面算PWM频率和死区时间都要用到。注意不同系列的STM32时钟树结构不同F1和F4的定时器倍频规则也不一样。配置完后一定要在Clock Configuration页面确认一下“Timer clocks”那一栏显示的频率不要凭感觉猜。3. 定时器参数计算与互补PWM配置3.1 PWM频率和分辨率的取舍假设我要驱动一个普通的直流有刷电机PWM频率选多少合适这个问题没有标准答案但有几个约束条件太低比如1kHz以下电机会听到明显的啸叫声因为PWM频率落在了人耳可听范围内。而且电流纹波大电机发热严重。太高比如100kHz以上MOS管的开关损耗会急剧增加驱动电路可能发热甚至烧毁。同时死区时间占整个周期的比例变大有效占空比范围被压缩。常用范围对于有刷直流电机15kHz到25kHz是比较甜的区域。刚好超过人耳听觉上限20kHz左右又不会让开关损耗太大。我选20kHz作为PWM频率。接下来算定时器的参数。STM32的定时器产生PWM的原理是计数器从0数到ARR自动重装载值然后归零重新开始。输出比较寄存器CCR决定占空比。当计数器值小于CCR时输出一种电平大于CCR时输出另一种电平。PWM频率的计算公式是Fpwm Ftim / ((ARR 1) × (PSC 1))其中Ftim是定时器时钟频率PSC是预分频器。我取PSC 0也就是不分频Ftim 170MHz。那么ARR 1 170,000,000 / 20,000 8500ARR 8499这样PWM频率正好是20kHz。分辨率方面ARR 8499意味着占空比可以调节的档位有8500级对于电机调速来说绰绰有余。3.2 死区时间到底设多少死区时间的计算稍微复杂一点因为STM32的死区时间发生器不是直接填纳秒数而是通过几个位域组合来设置。在CubeMX里你只需要填一个以纳秒为单位的数值它会自动帮你算寄存器值。但你需要知道合理的死区时间是多少。这取决于你的MOS管和驱动芯片MOS管的关断延迟时间td(off)通常在几十到几百纳秒驱动芯片的传输延迟也会影响一般取200ns到1μs之间我用的MOS管是IRF3205关断延迟大概100ns左右加上驱动芯片的延迟我设500ns的死区时间。这个值偏保守但安全。如果你追求效率可以用示波器观察桥臂中点电压的上升沿和下降沿逐步减小死区时间直到刚好不出现直通。在CubeMX的TIM1配置里找到“Dead Time”这一项填入500单位是ns。它会自动计算出对应的寄存器值。3.3 CubeMX里的具体操作步骤进入“Timers - TIM1”配置页面Clock Source选“Internal Clock”Channel1选“PWM Generation CH1 CH1N”——注意要选带CH1N的这才是互补输出ModePWM Mode 1Counter SettingsPrescaler 0Counter Period 8499Counter Mode UpAuto-reload preload EnablePWM Generation Channel 1Pulse 0初始占空比为0电机不转Output compare preload EnableFast Mode DisableCH Polarity HighCHN Polarity HighIdle State ResetBreak and Dead TimeDead Time 500nsAutomatic Output EnableOff State Selection for Run Mode DisableOff State Selection for Idle Mode Disable这里解释几个关键选项CH Polarity和CHN Polarity都设为High意味着CH1有效时输出高电平CH1N有效时也输出高电平。但注意CH1N是互补的所以当CH1输出高时CH1N输出低。如果你把CHN Polarity设为Low那CH1N的逻辑就反过来了容易搞混。建议保持默认的High。Automatic Output这个选项很重要。它允许定时器的输出在刹车事件后自动恢复。如果你使能了刹车功能Break当刹车触发时PWM输出会被强制到空闲状态电机停转。Automatic Output使能后刹车条件解除时PWM会自动恢复不需要软件干预。Off State Selection决定刹车时输出是高电平还是低电平。对于H桥驱动通常设为Disable让输出处于高阻态或者由外部下拉电阻决定。具体要看你的驱动电路设计。配置完TIM1后还需要配置GPIO。CubeMX会自动把CH1和CH1N分配到对应的引脚上。G431的TIM1_CH1默认在PA8CH1N在PA7或者PB13取决于复用功能。你可以在Pinout视图里看到这两个引脚被标记为绿色。3.4 生成代码前的最后检查在点击“Generate Code”之前我习惯做几件事在Project Manager里设置工程名称和路径路径同样不要有中文Toolchain/IDE选“MDK-ARM”Keil或者“STM32CubeIDE”看你习惯用哪个在Code Generator里勾选“Generate peripheral initialization as a pair of .c/.h files”这样代码结构更清晰确认时钟树没有红色警告生成代码后CubeMX会创建完整的工程包括HAL库、初始化代码和main函数框架。4. 代码实现从PWM输出到电机转动4.1 启动PWM输出的正确姿势CubeMX生成的代码里MX_TIM1_Init()函数已经帮你配置好了所有寄存器。但PWM不会自动输出你需要在main函数里手动启动。HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); HAL_TIMEx_PWMN_Start(htim1, TIM_CHANNEL_1);注意第二行互补输出需要用HAL_TIMEx_PWMN_Start()单独启动。只调用HAL_TIM_PWM_Start()的话CH1N是没有输出的。这个坑我踩过当时示波器上只看CH1有波形CH1N一直是低电平查了半天才发现少调了一个函数。启动之后通过修改CCR寄存器来调节占空比__HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, 4250);4250对应50%占空比因为ARR是8499。占空比从0到8499变化电机从停止到全速。4.2 死区时间的实测验证代码烧进去之后第一件事不是接电机而是用示波器看波形。把探头接到CH1和CH1N对应的引脚上触发方式设为上升沿触发时间基准调到1μs/格。你应该能看到两路反相的PWM波并且在每次电平翻转的地方有一段两路都是低电平的区域——那就是死区时间。实测下来500ns的死区在示波器上大概是半格多一点。如果你把死区设成0会看到两路波形几乎同时翻转中间没有明显的间隔。这时候如果接上H桥大概率会看到电源电流瞬间飙升。提示没有示波器的话可以用逻辑分析仪代替。几十块钱的USB逻辑分析仪配合开源软件采样率足够看20kHz的PWM和死区时间。4.3 电机正反转的控制逻辑一对互补PWM只能控制一个桥臂。要让电机正反转你需要至少两对互补PWM——左桥臂一对右桥臂一对。STM32的TIM1有CH1/CH1N、CH2/CH2N、CH3/CH3N、CH4/CH4N最多可以驱动两个H桥每个H桥需要两对。控制逻辑是这样的电机状态左桥臂上管左桥臂下管右桥臂上管右桥臂下管正转PWM互补PWM关断导通反转关断导通PWM互补PWM刹车关断导通关断导通停止关断关断关断关断正转时左桥臂输出PWM右桥臂下管常通。电流从左桥臂中点流向电机再经过右桥臂下管回到地。反转时反过来。实现上你可以用TIM1的CH1/CH1N驱动左桥臂CH2/CH2N驱动右桥臂。正转时启动CH1的PWM把CH2的CCR设为0CH2输出低CH2N输出高右桥臂下管导通。反转时反过来。4.4 刹车与惰行两个容易混淆的概念很多新手会把“刹车”和“停止”搞混。在电机驱动里这是两种完全不同的状态惰行Coast所有MOS管都关断电机两端悬空。电机靠惯性继续转直到摩擦力让它停下来。这时候电机不产生制动力矩但反电动势会通过体二极管形成回路产生一定的阻尼。刹车Brake下管全部导通电机两端被短接。电机旋转时产生的反电动势会在短路回路里产生电流这个电流产生的磁场与转子磁场相互作用形成制动力矩。电机会很快停下来同时动能被转化为热能消耗在电机绕组和MOS管上。在H桥电路里刹车就是把两个下管都导通上管都关断。用互补PWM实现的话就是把CH1和CH2的CCR都设为0这样CH1N和CH2N都输出高电平下管导通。注意频繁刹车会让电机和MOS管发热。如果电机负载惯量大刹车电流可能超过MOS管的额定值。必要时要加刹车电阻或者用能耗制动的方式。5. 常见问题排查与实战避坑指南5.1 上电就炸管先查这几个地方这是最让人心痛的问题一颗MOS管几块钱炸了还得查半天。根据我的经验上电直通通常有以下几个原因第一死区时间没生效。检查CubeMX里Dead Time是否真的填了值生成的代码里htim1.Init.DeadTime是否非零。有时候CubeMX的界面会骗你你填了500但它可能因为时钟配置问题算出来的实际死区是0。第二互补输出没启动。只调了HAL_TIM_PWM_Start()没调HAL_TIMEx_PWMN_Start()。结果CH1有PWMCH1N一直是低电平下管一直关断——这其实不会直通但电机会不转。反过来如果你把CH1N的极性设反了CH1高的时候CH1N也高那就直通了。第三GPIO复用没配对。有些引脚有多个复用功能CubeMX可能把CH1N分配到了错误的引脚上。生成代码后去tim.c里检查一下HAL_TIM_MspPostInit()函数看看CH1N对应的GPIO是否正确配置为复用推挽输出。第四外部驱动电路的问题。如果你用的是独立的栅极驱动芯片比如IR2104它本身也有死区要求。STM32的死区加上驱动芯片的死区总死区可能过大导致有效占空比范围很小。或者驱动芯片的输入逻辑和STM32的输出极性不匹配也会出问题。5.2 电机不转或者转速异常PWM波形正常但电机不转先别怀疑代码。用万用表量一下电机两端的电压。如果电压为0说明H桥没有正确导通。检查使能引脚如果有的话是否拉高驱动芯片的电源是否正常MOS管的栅极电压是否达到导通阈值如果电机转但转速不对检查PWM频率和占空比。用示波器量CH1的占空比和你设置的CCR值是否一致。有时候ARR的值被其他代码修改了导致频率偏移。还有一种情况电机低速时抖动严重。这通常是因为PWM频率太低或者死区时间占空比的比例太大。20kHz下500ns死区只占周期的1%影响很小。但如果你把频率降到1kHz500ns就占0.05%也还好。真正的问题是低速时占空比很小比如1%这时候死区的影响就相对明显了。5.3 常见问题速查表现象可能原因排查方法解决方案上电炸管死区未生效示波器看CH1和CH1N是否有间隔检查DeadTime配置确认互补输出已启动CH1N无输出未调用PWMN_Start示波器量CH1N引脚添加HAL_TIMEx_PWMN_Start()电机不转使能引脚未拉高万用表量驱动芯片使能脚拉高使能引脚或检查硬件连接转速不稳PWM频率漂移示波器量PWM周期检查ARR是否被意外修改低速抖动死区占比过大计算死区时间占周期比例降低PWM频率或减小死区时间MOS管发热开关损耗大摸MOS管温度看PWM频率降低PWM频率或换更低Qg的MOS管刹车无力下管未导通量电机两端电阻确认刹车时CCR设为0下管导通5.4 几个让我印象深刻的坑坑一CubeMX生成的代码里TIM1的刹车功能默认是使能的。如果你没有配置刹车引脚它可能会因为引脚悬空而误触发刹车导致PWM输出被强制关断。我的做法是如果不用刹车功能在CubeMX里把Break Input设为Disable或者把刹车引脚配置为内部上拉。坑二HAL库的__HAL_TIM_SET_COMPARE()宏在CCR预装载使能时新值不会立即生效。它会在下一个更新事件时才写入影子寄存器。如果你在中断里频繁修改CCR可能会发现占空比变化有延迟。解决办法是调用__HAL_TIM_SET_COMPARE()之后手动触发一次更新事件__HAL_TIM_GENERATE_EVENT(htim1, TIM_EVENTSOURCE_UPDATE)。坑三不同批次的MOS管关断时间可能差很多。我有一批IRF3205死区设300ns没问题换了一批同型号的300ns就直通了。后来统一设500ns再也没出过问题。所以死区时间要留余量不要卡着临界值设。坑四电机引线太长会引入振铃。电机离驱动板超过20厘米时PWM的快速边沿会在引线上产生振铃峰值可能超过MOS管的耐压。解决办法是在电机两端并联一个RC吸收电路或者在驱动板上加TVS管钳位。6. 进阶扩展从一对PWM到完整电机控制6.1 用TIM1的CH1和CH2驱动完整H桥前面只配置了CH1/CH1N现在把CH2/CH2N也加上。在CubeMX里把Channel2也设为“PWM Generation CH2 CH2N”其他配置和CH1一样。生成代码后启动四路输出HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); HAL_TIMEx_PWMN_Start(htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_2); HAL_TIMEx_PWMN_Start(htim1, TIM_CHANNEL_2);正转时__HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, duty); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_2, 0);反转时__HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, 0); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_2, duty);刹车时__HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, 0); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_2, 0);停止时先设CCR为0然后关闭PWM输出HAL_TIM_PWM_Stop(htim1, TIM_CHANNEL_1); HAL_TIMEx_PWMN_Stop(htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Stop(htim1, TIM_CHANNEL_2); HAL_TIMEx_PWMN_Stop(htim1, TIM_CHANNEL_2);6.2 加入电流采样和闭环控制开环调速只能让电机转起来但负载变化时转速会波动。要稳速需要闭环。最简单的方案是用一个采样电阻测电机电流用ADC读取然后做PI调节。STM32G4的ADC和定时器可以联动定时器在计数器归零时触发ADC采样这样每次采样都发生在PWM周期的固定位置避免开关噪声干扰。CubeMX里可以配置ADC的External Trigger为“TIM1 TRGO”触发源选“Update Event”。电流环的控制频率就是PWM频率20kHz。PI调节器的输出更新CCR值。代码框架void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { int current HAL_ADC_GetValue(hadc); int error target_current - current; integral error; int output Kp * error Ki * integral; if (output MAX_DUTY) output MAX_DUTY; if (output 0) output 0; __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, output); }注意PI参数需要根据实际电机和负载整定。Kp太大会振荡Ki太大会超调。我一般先用纯比例控制调Kp到临界振荡然后取一半再加积分。6.3 用DMA自动更新CCR实现正弦波驱动如果你要做BLDC的方波驱动或者PMSM的正弦波驱动需要三对互补PWM并且CCR值要按正弦规律变化。这时候可以用DMA把预先算好的正弦表自动搬运到CCR寄存器CPU完全不参与。在CubeMX里给TIM1的CH1、CH2、CH3都配置DMA请求方向是Memory to Peripheral。然后在代码里准备一个正弦表uint16_t sine_table[256]; for (int i 0; i 256; i) { sine_table[i] (uint16_t)(4249.5 * (1 sin(2 * 3.1415926 * i / 256))); }启动DMAHAL_TIM_PWM_Start_DMA(htim1, TIM_CHANNEL_1, (uint32_t*)sine_table, 256);这样每个PWM周期结束后DMA自动把下一个CCR值写入输出就是正弦波调制的PWM。三相各差120度就能驱动BLDC电机了。6.4 保护机制让电机驱动更可靠工业场合的电机驱动保护比功能更重要。STM32的TIM1自带刹车输入Break Input可以接一个比较器或者外部故障信号。一旦刹车触发PWM输出立即被强制到预设状态电机停转。配置方法在CubeMX的TIM1配置里使能“Break Input”选择触发极性。然后在代码里写刹车回调void HAL_TIMEx_BreakCallback(TIM_HandleTypeDef *htim) { // 刹车触发记录故障等待恢复 fault_flag 1; }除了硬件刹车软件上还要做过流保护ADC采样电流超过阈值时立即关闭PWM过温保护如果板子上有NTC定期采样温度欠压保护ADC监测母线电压低于阈值时禁止启动堵转保护电流大但转速为零持续超过一定时间就停机这些保护逻辑放在主循环或者定时器中断里优先级要高于正常的控制逻辑。7. 我的实操心得与最后几句这套互补PWM驱动电机的方案我从G431用到F407再到G474核心逻辑没变过。CubeMX把寄存器配置图形化了确实省了很多查手册的时间但也容易让人忽略底层细节。我的建议是第一次配置时把生成的代码和参考手册对照着看一遍搞清楚每个参数对应哪个寄存器位。这样出了问题才知道往哪里查。死区时间不要照抄别人的值。你的MOS管、驱动芯片、PCB布局都会影响最佳死区。有条件的话用示波器看桥臂中点的电压波形从大往小调死区直到刚好不出现直通尖峰然后再加50%的余量。PWM频率也不是越高越好。20kHz对大多数有刷电机够用了。如果你驱动的是空心杯电机或者超声波电机可能需要更高的频率但那时候MOS管的开关损耗会成为主要矛盾要选Qg更小的管子或者用GaN器件。最后电机驱动板的第一版PCB一定要留够测试点CH1、CH1N、桥臂中点、母线电压、电流采样输出。调试的时候示波器的探头往上一搭就能看波形比用万用表一根一根量效率高十倍。