PWM电机开环控制实战:STM32+TB6612从原理到调试全解析 1. 为什么开环控制能排到P0先搞清它的边界和用途接到这个任务的时候团队里其实有过一轮争论——有人觉得开环控制四个字本身就带着一股不靠谱的气息既然最终目标是精准控速、控位为什么不直接上闭环我的态度一直很明确P0是最高优先级意味着它是后续所有工作的地基。如果连开环都跑不稳闭环就是空中楼阁。开环控制的核心逻辑并不复杂你给PWM一个占空比电机就转至于实际转速是多少、扭矩输出多大系统不关心也不反馈。这在控制理论上属于前馈没有误差校正环节。很多人一听不反馈就觉得这方案太糙但在实际工程项目里开环的价值恰恰体现在两个地方第一它能以最少的代码和最简的硬件把电机驱动链路完整点亮用来验证电源、驱动板、MCU引脚、PWM定时器这一整条通路是否正常工作第二很多应用场景本身对精度要求就没那么高比如风扇、水泵、破壁机、玩具车PWM开环调速完全够用硬上PID反而是过度设计。具体到机械臂、AGV小车、舵机云台这类项目P0阶段用开环还有一个隐蔽的好处调参前置。你可以在开环模式下摸清电机的死区电压、启动占空比、空载转速和负载特性这些数据后面做闭环PID时全都会用到尤其是积分限幅和输出饱和的初始值基本都参考开环跑出来的曲线。没有这层数据闭环参数就是一通瞎猜。所以这篇文章的定位是以STM32F103ZET6为主控用定时器的PWM输出配合TB6612驱动模块实现一个完整的电机开环调速系统。从硬件选型、电路接线到定时器配置、占空比调节再到实际调试中踩过的坑和规避方案全部摊开来讲。适合刚接触电机控制的嵌入式新手也适合那些能用就行但不想踩坑的项目老手。2. 驱动链路选型TB6612和L298N之间我为什么选了前者2.1 从H桥原理看驱动芯片的本质无论你用哪款驱动芯片核心都绕不开H桥。H桥的名字很形象四个开关管排成H形电机接在中间横杠上。对角导通时电流方向相反电机就正转或反转同侧导通则相当于刹车全部关断则是滑行。PWM调速的本质就是通过高频开关电源电压改变施加在电机两端的平均电压。重点说一下这个平均电压——很多人误以为占空比50%就是电机电压减半实际并不完全准确因为电机绕组有电感电流会续流实际的电压电流关系还要考虑电感储能和续流路径。但工程上我们可以先按中值等效来估算U_avg U_bus × duty。这个公式后面调试时会反复用到。TB6612和L298N是最常见的两款电机驱动模块。L298N是老一辈工程师的最爱驱动能力强单个H桥能到2A还可以并联提高电流但它的压降实在太大用的是达林顿管饱和压降随电流上升能到2V以上供电6V给电机实际到电机两端可能只剩4V出头。而TB6612用的是MOSFET H桥内部压降小很多同样工况下压降只有0.5V左右而且模块体积只有L298N的三分之一PWM频率响应也更高。对比项TB6612L298N供电电压范围2.7V~5.5V逻辑/ 2.5V~15V电机5V~35V电机最大持续电流1.2A/通道2A/通道内部开关器件MOSFET达林顿管导通压降约0.5V1.8V~2.5VPWM频率上限100kHz以上较受限空闲电流约30uA约几十mA2.2 TB6612的逻辑真值表和接线陷阱TB6612的每个通道有四个输入管脚AIN1/AIN2控制方向PWMA控制速度STBY是总使能。这里有个新手最容易忽略的坑——STBY如果接到低电平模块整个休眠电机怎么转都不动。很多人的电机没反应先查代码半天最后发现是STBY没拉高这种坑我踩过一次后就长记性了。方向逻辑其实很直观AIN1AIN2电机状态高低正转低高反转高高短路刹车低低惰性停止另一个接线要点是逻辑电源和电机电源必须分开。TB6612模块上的VCC接MCU的3.3V或5V逻辑电平参考用VM接电机供电两者共地是必须的。如果你用STM32F103ZET63.3V逻辑去控制TB6612AIN1/AIN2/PWMA/STBY全部可以直接连GPIO不需要电平转换前提是VM不要超过15V。驱动电路的滤波电容别省。VM和GND之间至少并一个100uF电解电容加一个0.1uF陶瓷电容电解电容负责储能缓冲陶瓷电容负责吸收高频噪声。电机启动瞬间的电流冲击很猛没有这个电容电源电压会被拉出很大的纹波严重时MCU会直接复位。3. PWM生成的工程细节从定时器配置到占空比调节3.1 定时器与引脚重映射的关系STM32F103ZET6的PWM输出本质上就是定时器的比较寄存器CCR与计数寄存器CNT不断比较的过程。当CNT小于CCR时输出高电平大于等于CCR时输出低电平这样就产生了一个频率和占空比都可控的方波。F103系列有好几个定时器但输出PWM的能力并不完全一样。TIM1和TIM8是高级定时器除了PWM输出还支持死区插入、刹车输入Break、互补输出这些是专门的电机控制功能。TIM2/3/4/5是通用定时器输出PWM没问题但没死区功能。做有刷电机开环控制通用定时器足够了。我这次用的是TIM3的通道1和通道2分别控制两个电机如果你做的是双电机差速小车最好用同一个定时器的两个通道保证PWM频率和相位完全一致避免拍频噪声。引脚重映射是很多人第一次容易绕进去的地方。F103的定时器通道引脚并不固定比如TIM3_CH1默认在PA6但AFIO重映射后可以挪到PB4或PC6。这事的本质是IO复用——同一个引脚可以连接多个外设需要由GPIO_AFIO寄存器决定把哪个外设信号引到物理引脚上。做电机驱动时如果你手上正好有一块板子PA6、PA7已经被别的功能占用了就可以通过重映射把TIM3的通道挪到别的引脚灵活性就在这里。3.2 PWM频率和占空比的计算思路PWM频率由定时器的时钟源、预分频器PSC和自动重装载值ARR共同决定公式是f_PWM f_CLK / (PSC 1) / (ARR 1)以我的板子为例系统主频72MHzTIM3挂载在APB1总线上分频系数为1时实际时钟就是72MHz。如果我想输出20kHz的PWM取PSC71那么f定时器时钟 72MHz / 72 1MHzARR 1MHz / 20kHz - 1 49。这样CNT从0计到49正好50个计数点一个周期20us50 × 1us输出频率就是20kHz。占空比则由CCR决定duty CCR / (ARR 1)。还是上面的例子里ARR49那么CCR25时占空比就是25/5050%CCR50时占空比100%——注意这里有个临界点当CCR大于ARR时输出恒为高电平同样是100%占空比但PWM就变成直流输出了。这个现象后面会讲到实际项目里的影响先知道有这么回事。关于PWM频率的选择有刷电机的常见工作区间是10kHz到20kHz。为什么是10kHz以上主要是为了避开人耳听觉最敏感的频段2kHz到5kHz不然电机转动时你会听到持续的尖锐啸叫。20kHz以上理论上更安静但驱动芯片和电机电感的开关损耗会同步上升。我常用的是一律先设15kHz到20kHz无感无刷那套后面再说那边频率逻辑完全不同。3.3 基于标准库的完整配置代码这里用标准库写一个TIM3双通道PWM初始化函数。注意这段代码做了一件事把TIM3的CH1重映射到了PC6CH2重映射到了PC7因为PA6/PA7在项目里被留给传感器了void TIM3_PWM_Init(u16 arr, u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 开启时钟AFIO时钟必须有否则重映射不生效 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 部分重映射TIM3_CH1 - PC6, TIM3_CH2 - PC7 GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE); // 配置PC6、PC7为复用推挽输出 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOC, GPIO_InitStructure); // 时基结构体预分频71计数模式向上ARRarr TIM_TimeBaseStructure.TIM_Period arr; TIM_TimeBaseStructure.TIM_Prescaler psc; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); // PWM模式1CNT CCR 时为有效电平即高电平 TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM3, TIM_OCInitStructure); TIM_OC2Init(TIM3, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM3, ENABLE); TIM_Cmd(TIM3, ENABLE); }调用时传入TIM3_PWM_Init(49, 71)就得到了20kHz的PWM。之后想调节占空比直接改CCRvoid Motor_SetSpeed(uint8_t motor_id, int speed_percent) { u16 ccr 0; if (speed_percent 100) speed_percent 100; if (speed_percent -100) speed_percent -100; ccr (u16)((speed_percent * (49 1)) / 100); if (motor_id 0) { if (speed_percent 0) { // 方向A AIN1_0_Set(1); AIN2_0_Set(0); } else { AIN1_0_Set(0); AIN2_0_Set(1); } TIM_SetCompare1(TIM3, ccr); } else { // 同理处理另一个电机 } }这里值得多说一句为什么用TIM_SetCompare1而不是直接写TIM3-CCR1因为预加载打开了直接改寄存器会等下一个更新事件才生效也可以写TIM3-CCR1效果一样只是寄存器访问方式不同。官方封装函数的好处是自动处理了预加载和立即更新两种模式代码可读性也更好。4. 从占空比到电机转速几个极易翻车的工程细节4.1 100%占空比时的PWM消失现象先说我实际踩过的一个坑。当时做整机联调程序逻辑是当目标速度达到最大值时直接把占空比设成100%。结果发现电机全速转的时候系统有间歇性的咔哒声而且MCU的3.3V供电偶尔会抖一下。排查到最后原因并不是电源带不动而是PWM在某些时刻变成了纯直流输出。当你设置的CCR等于ARR或者CCR大于ARR的时候PWM波的有和无的切换行为会改变定时器输出基本恒定在高电平。这时候PWM信号不再有边沿MCU的时钟边沿触发中断如果你开了的话、隔离器件、电平转换电路都可能出现无法预期的行为。更关键的是如果电机用的是软启动占空比逐渐爬升你最终目标是100%那么从99%到100%这个临界点系统会丢掉最后一个PWM边沿。如下措施可以规避代码里给占空比设置上限比如DUTY_MAX 98%或99%避免跑到绝对的100%或者反过来如果你确实需要全速就把ARR和CCR的边界设计成CCR ARR - 1。为什么要留这个余量因为PWM的边沿不仅用于功率传输在很多驱动芯片上还兼任逻辑时钟。保持一个最低的翻转脉宽有时反而比绝对的最高占空比跑得更稳。4.2 快衰减和慢衰减一个被很多人忽略的续流问题有刷电机是感性负载PWM关断瞬间绕组里的电流不能突变必须通过续流二极管或MOSFET体二极管释放。这里就引出了快衰减和慢衰减的概念听起来很高深其实说的就是续流阶段电流下降的速率。TB6612的数据手册里把这两种模式叫作Fast Decay和Slow Decay。快衰减模式是当PWM为低电平时把H桥的下管同时导通让电机两端短路这时电流通过两个下管形成回路电流衰减很快但扭矩波动大噪音也大慢衰减模式是让电机两端接在电源上电流通过体二极管回灌到电源衰减慢扭矩更平滑效率更高。对开环控制来说这个选择会直接影响电机转速的线性度。低速小占空比情况下如果用了快衰减电机容易出现一顿一顿的现象就是占空比低上电时间短续流太快扭矩跟上不。调试时如果你的电机在20%~30%占空比区间有明显顿挫大概率是衰减模式没配对。方法很简单换一种衰减模式TB6612的AO1/AO2接法会影响衰减方式部分模块会引出衰减控制脚要么降低PWM频率让每个周期的续流时间更宽裕。4.3 死区只做开环也需要提前明白的问题严格来说用TB6612这样的集成驱动模块死区已经被芯片内部处理好了你不需要自己在PWM里插入死区时间。但如果你用的是分立器件搭H桥NP MOS或全N MOS加自举就必须在上下管切换时插入死区否则同一桥臂的上下管会同时导通直接电源短路。STM32F103的高级定时器TIM1/TIM8对死区有专门的寄存器配置。例如你要设置2us的死区在72MHz时钟下死区计数周期为 1/72MHz ≈ 13.89ns2us约等于144个计数周期直接在BDTR寄存器里写入对应的DTG值。配置好之后互补PWM输出CH1和CH1N在切换时会自动插入这段死区时间确保上下管不会直通。开环控制项目如果需要自己搭分立式H桥死区时间记得不要吝啬。MOS管的栅极寄生电容决定了开关时间你选的栅极驱动电阻越大开关越慢需要的死区时间越长。选小了会炸管选大了占空比精度受影响折中方案是死区取1us~2us驱动电阻选10Ω~22Ω。这里的计算逻辑先查MOS管数据手册里的Qg栅极总电荷然后用 t Qg / Idrive 估算开关时间死区至少取开关时间的两倍。5. 实测中最容易蒙圈的三个问题无输出、转速不稳、方向反5.1 排查链路PWM引脚波形和驱动模块电平匹配遇到过好几次这类问题电机彻底没反应代码看着完全没问题最后定位却花了大半天。我总结了一套固定排查路径照着走能节省大量时间。第一步先用示波器量MCU侧PWM引脚看有没有波形。如果没有波形检查定时器有没有使能、GPIO是否配置成了复用推挽、AFIO重映射有没有生效。这里有个非常经典的坑部分重映射TIM3到PC6/PC7时如果忘了开AFIO时钟GPIO_PinRemapConfig函数调用无效但代码不会报错这是典型的静默失败。第二步量驱动模块输入端PWMA看MCU的波形是否无损到达。如果波形变形检查公共地。MCU和驱动模块不共地信号到达不了轻则无输出重则烧引脚。第三步量VM引脚电压是否足够。如果VM持续掉压大概率是电源限流了。之前测过一个小功率电源标称12V 2A但接上电机启动瞬间拉低到6V以下电机的启动电流实测能到1.5A以上普通电源根本扛不住。换电源或者加启动电容问题立刻消失。第四步STBY引脚有没有拉高这个前面提过不重复了。5.2 转速不稳的隐藏原因PWM频率和机械谐振耦合开环控制下转速不稳很多人第一反应是电源不行但还有一个经常被忽视的源头机械谐振。电机通过联轴器带负载时整个系统就是一个弹簧-质量系统。如果PWM信号的工作频率恰好落在机械共振频率附近即使占空比不变电机也能出现明显的转速波动和噪音。这种问题用示波器看PWM波形是完全正常的因为波动发生在机械环节而不是电路环节。解决方法是改变PWM频率往高调一档15kHz到20kHz往往能明显改善如果还不行就要考虑在程序中加入简单的梯形加减速避免电机从一个占空比直接跳到另一个占空比时激发共振。这一条纯属实践经验数据手册上不会写。5.3 方向控反的锅逻辑电平定义而非硬件接反电机方向接反第一反应是交换电机两根线。这个操作没问题但如果你做的是可以软件定义方向的产品更合理的做法是建立逻辑方向的概念——在头文件里定义MOTOR_FORWARD_LEVEL和MOTOR_REVERSE_LEVEL接线时按默认方向来软件层面定义统一的方向语义。后期维护的时候机械结构变更导致电机安装方向翻转只需要改头文件里的一个宏定义不用动任何硬件接线。我在实际项目里遇到过前后端扯皮的问题——机械说电机朝这个方向转才对软件说我代码里明明写的是正转最后发现是通信协议里方向字节的定义两边理解不一致。电平定义这层封装虽然不能直接解决协议歧义但至少能让每个环节都通过同一个方向常量来判断减少扯皮空间。6. 开环控制的下一步往PID、BLDC和FOC延伸前的准备开环控制稳定跑通后就该思考往哪个方向升级了。这里给出三个明确的路径每条的入口和前提条件都不一样。6.1 路径一加编码器做闭环PID如果目标是精确控速或控位那么下一步就是在电机尾部加编码器。增量式编码器、霍尔编码器、磁编码器都可以但接口不一样代码量也不一样。STM32F103自带正交编码器接口Encoder Interface Mode可以直接读TIM的CNT来获取编码器计数不用外部中断很省事。PID闭环的核心思想和开环并没有本质区别变化的是目标开环只管我给的占空比是50%闭环管的是我让转速稳定在1000转/分钟。但这里有很重要的一点在写PID之前最好先把开环控制的占空比-转速曲线粗测出来哪怕测五个点都行。这组数据有什么价值它决定了PID输出限幅的初始范围假设目标转速是1000转/分钟开环需要40%占空比那你的PID输出上限完全可以先设50%下限30%然后再精细调节。6.2 路径二换成BLDC无刷电机开环逻辑全变有刷电机的开环控制你已经理解了给电压就转但BLDC电机完全不同它靠电子换相驱动必须知道转子的位置才能正确换相。所以BLDC的开环控制比有刷电机多了一个步骤强制换相。也就是说你按固定的时序给三相绕组通电转子跟着磁场的旋转方向走如果负载太大跟不上电机就会失步。BLDC的开环控制有两个前提一是换相时序必须是正确的通常120°换相六步换相法二是启动阶段要控制好加速斜率不能直接跳到一个很高的换相频率。这些和PWM占空比的关系是PWM只负责调压控速换相频率决定电机转速上限两者要联合调。很多BLDC开环方案其实就是调试期的过渡方案最终还是要引入霍尔传感器或反电动势检测来获取转子位置才能真正上量。6.3 路径三FOC的高端局先搞清楚概念再入FOC磁场定向控制现在被讲得很玄乎但它的核心目标很朴素让电机的转矩控制像直流电机一样线性。它把定子电流矢量分解为产生磁场的d轴分量和产生转矩的q轴分量再通过坐标变换和PI调节实现解耦控制。从工程角度看FOC不是一个入门方案它需要你理解Clarke变换、Park变换、SVPWM、母线电流采样等一系列概念还要有一块跑得足够快的MCU带浮点运算单元最佳或者定点运算优化过的和电流采样电路。如果项目对精度、效率、噪音有硬性要求比如无人机、机器人关节、伺服系统FOC值得投入如果只是普通调速直接上FOC属于杀鸡用牛刀且会让项目周期拉长很多。我在机械臂关节的控制里就吃过亏开始想着直接上FOC结果连电流采样都没调试明白后来退回到PWM开环加编码器闭环反而先把机械结构和运动模型跑通了再回头上FOC这时理解完全不一样只剩下往代码里填PI参数的工作量。路径这件事有时候慢就是快。写在最后的一点实际体会P0定为PWM电机开环控制真正做完之后的感受是它看起来是最简单的一环但它把所有累的底层问题都兜住了——电源怎么配、GPIO怎么复用、PWM频率怎么选、驱动芯片怎么选、衰减模式怎么调、启动和刹车逻辑怎么划分。后面所有更高阶的工作都是在这套链路上做增量而不是另起炉灶。一个小建议在开环阶段就有意识地设计接口边界。比如把电机控制封装成独立的接口文件不和应用层代码混在一起。后面换驱动芯片、加编码器、上闭环你只需要替换底层实现应用层代码一行都不用动。这算是我在多次项目里被逼出来的习惯也是P0这个优先级真正该有的产出。