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频率响应也更高。
| 对比项 | TB6612 | L298N |
|---|---|---|
| 供电电压范围 | 2.7V~5.5V(逻辑)/ 2.5V~15V(电机) | 5V~35V(电机) |
| 最大持续电流 | 1.2A/通道 | 2A/通道 |
| 内部开关器件 | MOSFET | 达林顿管 |
| 导通压降 | 约0.5V | 1.8V~2.5V |
| PWM频率上限 | 100kHz以上 | 较受限 |
| 空闲电流 | 约30uA | 约几十mA |
2.2 TB6612的逻辑真值表和接线陷阱
TB6612的每个通道有四个输入管脚:AIN1/AIN2控制方向,PWMA控制速度,STBY是总使能。这里有个新手最容易忽略的坑——STBY如果接到低电平,模块整个休眠,电机怎么转都不动。很多人的电机"没反应",先查代码半天,最后发现是STBY没拉高,这种坑我踩过一次后就长记性了。
方向逻辑其实很直观:
| AIN1 | AIN2 | 电机状态 |
|---|---|---|
| 高 | 低 | 正转 |
| 低 | 高 | 反转 |
| 高 | 高 | 短路刹车 |
| 低 | 低 | 惰性停止 |
另一个接线要点是逻辑电源和电机电源必须分开。TB6612模块上的VCC接MCU的3.3V或5V(逻辑电平参考用),VM接电机供电,两者共地是必须的。如果你用STM32F103ZET6(3.3V逻辑)去控制TB6612,AIN1/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)
以我的板子为例,系统主频72MHz,TIM3挂载在APB1总线上,分频系数为1时实际时钟就是72MHz。如果我想输出20kHz的PWM,取PSC=71,那么f定时器时钟 = 72MHz / 72 = 1MHz,ARR = 1MHz / 20kHz - 1 = 49。这样CNT从0计到49,正好50个计数点,一个周期20us(50 × 1us),输出频率就是20kHz。
占空比则由CCR决定:duty = CCR / (ARR + 1)。还是上面的例子里,ARR=49,那么CCR=25时占空比就是25/50=50%,CCR=50时占空比100%——注意这里有个临界点,当CCR大于ARR时,输出恒为高电平,同样是100%占空比,但PWM就变成直流输出了。这个现象后面会讲到实际项目里的影响,先知道有这么回事。
关于PWM频率的选择,有刷电机的常见工作区间是10kHz到20kHz。为什么是10kHz以上?主要是为了避开人耳听觉最敏感的频段(2kHz到5kHz),不然电机转动时你会听到持续的尖锐啸叫。20kHz以上理论上更安静,但驱动芯片和电机电感的开关损耗会同步上升。我常用的是一律先设15kHz到20kHz,无感无刷那套后面再说,那边频率逻辑完全不同。
3.3 基于标准库的完整配置代码
这里用标准库写一个TIM3双通道PWM初始化函数。注意,这段代码做了一件事,把TIM3的CH1重映射到了PC6,CH2重映射到了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,计数模式向上,ARR=arr 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模式1:CNT < 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。之后想调节占空比,直接改CCR:
void 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桥(N+P MOS或全N MOS加自举),就必须在上下管切换时插入死区,否则同一桥臂的上下管会同时导通,直接电源短路。
STM32F103的高级定时器TIM1/TIM8对死区有专门的寄存器配置。例如你要设置2us的死区,在72MHz时钟下,死区计数周期为 1/72MHz ≈ 13.89ns,2us约等于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"这个优先级真正该有的产出。