先别急着敲代码。
PWM电机开环控制,听起来是嵌入式里最人畜无害的方向之一:一个定时器,输出一路方波,占空比大点电机转快点,占空比小点转慢点,似乎没什么可讲的。但说句实话,这个题目我做了不下四五回,每次以为自己闭着眼都能搞定的时候,总能在某个细节上翻车。最近一次帮朋友调试一台两轮小车底盘,又踩了一遍“上电瞬间电机猛转一下”“占空比加到100%反而停转”“电机一转MCU就复位”这三个经典大坑,查了一圈下来,全都是最基础的东西,恰恰印证了一件事:PWM开环控制的门槛不在“让电机转起来”,而在“让电机稳定、可控、不烧东西地转起来”。
这篇文章就把整个项目从需求拆解、硬件选型、电路分析、STM32定时器配置到实测调试完整过一遍。内容面向正在做智能车、机器人底盘、云台、泵阀控制的读者,也适合刚接触电机驱动、想搞明白“开环控制和闭环控制到底差在哪”的初学者。所有关键计算、参数选择、代码配置、问题排查都会按实际调试的顺序写清楚。
1. 项目思路拆解:PWM开环控制的定位与整体方案
1.1 P0的含义:先让电机转起来,再谈优化
项目编号里带着“P0”,这个优先级不是随便定的。P0意味着这件事是整个电机控制链路的最底层地基,不把它跑通,后面所有东西都免谈:PID闭环、编码器测速、FOC、机器人运动学解算,全都建立在“PWM能稳定驱动电机”这个前提上。
实际做项目时,很多人一上来就想上PID、上FOC、上无刷电机,觉得开环PWM“太低级”。但我的经验恰恰相反:开环PWM控制是整个电机控制体系里最值得先做扎实的一步。因为它的变量最少、逻辑最直观、出问题最好排查。先把开环跑稳,你会对电机的脾气有非常具体的感知——比如同样的占空比,空载和带载转速差多少;电压跌落多少电机就开始抖动;频率设到多少噪音最刺耳。这些体感数据是后面调闭环时必不可少的参照基准。
说白了,开环PWM是“摸着电机的脾气走路”,闭环是“看着电机的脸色走路”。前者都没走稳,后者只会摔得更惨。
1.2 开环控制的边界:能做什么,不能做什么
开环控制,就是不给电机任何反馈,只按照预设值输出PWM信号,期望电机达到某个转速。它没有“我实际转得多快”这个概念,也没有“我偏了多少需要纠正”的机制。
举个例子,给一个直流减速电机50%占空比,空载时转速可能稳定在300转/分钟,但用手捏住输出轴,转速立刻掉到100转/分钟,甚至堵转。开环系统对此是无感知的——PWM还是那个PWM,电压还是那个电压,但转速就是变了。这就是开环的根本局限:无法对抗负载扰动。
那开环还有什么用?当然有,而且用处不小。对于负载相对恒定的场景,比如风扇、水泵、加热器、灯带,开环PWM完全够用,成本低、实现简单、可靠性高。对于机器人底盘这种负载变化大的场景,开环也是第一步的必由之路:先把电机转起来、把方向搞对、把PWM频率和占空比范围摸清楚,然后再加编码器做闭环。
所以这个项目的定位就是:让你在半小时内获得对电机、驱动板和PWM信号的完全掌控,为后续所有高级控制打底。
1.3 硬件选型思路:MCU、驱动板与电机的搭配
这次项目用的主控是STM32F103ZET6,选它的原因就一个字:熟。这款芯片的定时器资源非常丰富,TIM1到TIM8加在一起足够驱动十几个电机通道,而且TIM1和TIM8还是高级定时器,自带互补输出和刹车功能,做电机控制天然合适。这回调用的是TIM3,属于通用定时器,配置简单,上手友好。
驱动板的选择上,我推荐TB6612。这个模块在智能车圈子里几乎成了标配,它的优势在于:逻辑电压和电机电压分离,3.3V单片机可以直接驱动逻辑引脚;内部是MOSFET H桥,饱和压降小,发热低;体积小,引脚直插友好。相比另一款常见驱动L298N,TB6612在压降上优势巨大——L298N用三极管H桥,大电流时压降能到2V以上,电机根本吃不到多少电压,而且发热严重。TB6612的MOSFET压降通常在0.5V以内,对于12V供电的小型电机来说,这两个方案的电机实际功率能差出一大截。
电机的选择上,我用的是一台12V直流减速电机,带霍尔编码器(编码器这次不接,留给后期闭环用)。减速比1:30,空载转速约400转/分钟,额定电流约800mA,堵转电流约3A。这个参数范围对于TB6612来说非常安全,也给后期闭环留了余量。如果你手里是额定电流超过2A的电机,建议把驱动换成大功率模块,TB6612的持续电流上限是3A左右,超了就随时可能冒烟。
2. PWM与驱动电路:几个搞不懂就必然翻车的细节
2.1 占空比、频率与电机转速的真实关系
先明确一个基本模型:对于直流有刷电机,在稳态下,转速和电枢电压近似成正比。用PWM驱动时,电枢电压的平均值约等于“电源电压 × 占空比”,所以转速也近似和占空比成正比。注意“近似”两个字,实际转速还会受到电枢电阻压降、电刷摩擦、负载转矩等因素影响,所以拿万用表测到的平均电压和转速并不是完美的线性关系。
频率的选择同样关键。PWM频率过低会带来两个问题:一是电机运转时噪音明显,这个噪音其实是定子线圈在“叽叽叫”,频率落在人耳可听范围内(20Hz到20kHz)就会非常烦人;二是电流纹波变大,电机的转矩脉动加剧,转速不平稳。频率过高也有问题:MOSFET或驱动芯片的开关损耗随频率直线上升,芯片发热加剧,甚至触发过热保护。工程上常用的折中区间是10kHz到20kHz,这个范围既超出了人耳听阈(大多数成年人听不到10kHz以上的纯音),开关损耗又在可控范围内。
我这次选择10kHz,具体原因后面在定时器计算部分说明。总之记住一个结论:PWM频率的选择不是越大越好也不是越小越好,而是要让电机工作在“无噪音、低纹波、低损耗”的平衡点。
2.2 H桥驱动的选型对比:TB6612与L298N的差别
很多初学者第一次接触电机驱动都是从一个L298N模块开始的,我也一样。但用过TB6612之后,回头再看L298N,真的回不去了。这两者的核心区别在于内部器件的类型。
L298N内部是双极性三极管组成的H桥,三极管饱和导通时有比较高的集电极-发射极压降,典型值在1.5V到2.5V之间。这意味着如果电源电压是12V,电机实际能拿到的电压只有不到10V。而且压降在满载电流下会带来严重的功率损耗,公式是 P = I × U,2V压降 × 2A电流就是4W的发热,全烧在芯片上。
TB6612内部是功率MOSFET组成的H桥,MOSFET导通时是纯电阻特性,导通电阻典型值在0.5Ω以内,同样2A电流下的压降只有不到1V,损耗小了一个数量级。发热自然少很多,效率也高得多。
| 对比项 | TB6612 | L298N |
|---|---|---|
| 内部开关器件 | 功率MOSFET | 三极管 |
| 典型导通压降 | 0.5V以内 | 1.5V-2.5V |
| 逻辑电平兼容 | 3.3V/5V均支持 | 建议5V |
| 持续输出电流 | 约2A-3A | 约2A(实际易发热) |
| 发热表现 | 低 | 高,必须加散热片 |
| 典型应用 | 智能车、机器人、云台 | 老式教学实验板 |
当然,L298N也不是一无是处,它的抗造能力确实强,耐压高,不容易烧。但对于现代低功耗设计,TB6612基本是更合理的选择。
2.3 死区、续流与100%占空比异常排查
这个话题值得单独拎出来讲,因为几乎所有人在调试过程中都会遇到,而且不止一次。
H桥驱动一个电机需要四个开关管,任何时刻都不能让同一桥臂的上下两个管子同时导通,否则电源直接短路,瞬间大电流足以烧毁驱动芯片和电机。防止这种“直通”的最常用手段就是设置死区时间——让上下管在切换时有一段极短的“同时关闭”时间,确保一个管子完全关断后另一个才导通。
对于TB6612这类集成驱动芯片,死区在内部已经处理好了,用户不需要关心。但如果你用的是分立元件做H桥(比如用两颗IR2104半桥驱动芯片加MOSFET),死区的设置就变成了必须认真设计的环节。这也是为什么热词里会出现“28335配置的PWM死区DBFED和DBRED”“死区主源选择有什么要求”这类问题——高级定时器或者专用PWM控制器里,死区设置直接决定驱动的可靠性。
另一个特别容易被忽视的问题是续流。电机是感性负载,当PWM关断时,电机电流不能突变,如果没有续流路径,会产生很高的感应电动势,轻则产生尖峰干扰MCU,重则击穿MOSFET。TB6612内部已经集成了续流二极管,这也是选集成驱动的重要原因。如果你自己做分立H桥,必须在外围加上快恢复二极管或者肖特基二极管。
再来说100%占空比异常。细心的开发者在测试时可能遇到:占空比调到100%(比如CCR值等于ARR值)时,电机反而停了,或者输出变得异常。这个现象的原因通常是:在某个占空比下,PWM占空比达到了极值,一些驱动芯片会进入特殊的保护逻辑或者状态翻转异常。另外,如果用STM32的PWM1模式,CCR=ARR时输出应该是恒高电平,但如果定时器的极性配置反了,100%占空比对应的反而是恒低电平,电机自然不转。遇到这种问题,先检查极性配置,再用示波器或逻辑分析仪看真实波形,不要凭代码推测。
2.4 电源与地线处理:为什么电机一转MCU就复位
这是我踩过最多次的坑,也是排查起来最容易走偏的问题。电机一转,MCU就复位,换什么软件逻辑都没用,最后发现问题出在电源上。
电机启动和堵转时的电流是额定电流的好几倍,如果MCU和电机共用一个电源,电机的瞬态大电流会把电源电压拉低。电压一旦跌到MCU复位电压以下,MCU立即重启。更隐蔽的是,即使电源本身的电压没跌破,电机电流通过导线产生的压降,加上地线上的噪声,也可能干扰MCU的复位电路或者JTAG调试接口。
解决办法有几条,都是实测有效的:
第一,MCU和电机使用独立的电源轨道。电机用电池或者大功率电源供电,MCU用单独的降压模块供电,两者之间只在参考地端单点相连。
第二,在电机电源输入端并联大容量电解电容。我习惯在电机驱动板的电源入口并联两个电容:一个470uF的电解电容用于吸收低频大电流波动,一个0.1uF的陶瓷电容用于吸收高频噪声。有条件的话,还可以在电机两端直接并联一个102或103的陶瓷电容,直接吸收电机换向时的火花噪声。
第三,保证共地。MCU的地和驱动板的地必须可靠连接,否则逻辑信号的电平参考点不一致,会出现随机误触发现象。地线尽量粗、尽量短。
3. STM32F103 TIM3输出PWM的完整配置流程
3.1 引脚选择与重映射:PA6还是PB4
在开始配置代码之前,先把引脚定下来。STM32F103ZET6的TIM3有四个通道,默认引脚如下:
- TIM3_CH1:PA6
- TIM3_CH2:PA7
- TIM3_CH3:PB0
- TIM3_CH4:PB1
如果这些引脚被其他外设占用,TIM3还支持部分重映射。比如完全重映射后,CH1会从PA6跑到PB4,CH2跑到PB5,CH3跑到PB0不变,CH4跑到PB1不变。需要注意的是,PB4默认用作JTAG的NJTRST引脚,如果要把它当作普通GPIO使用,必须先禁用JTAG功能,否则引脚电平会被JTAG逻辑拉住,PWM波形根本出不来。
这次项目里我用的是TIM3_CH1的默认引脚PA6,同时把重映射的操作也做了说明,因为很多情况下默认引脚确实不够用。比如在做双电机驱动时,如果两个电机的PWM通道都占了默认引脚,就可能需要动用重映射。
3.2 定时器参数计算:从72MHz到10kHz
这是整个配置里最核心的数学部分。STM32F103的系统时钟最高是72MHz,TIM3挂接在APB1总线上,当APB1预分频系数为2时,定时器时钟同样是72MHz。
我需要输出10kHz的PWM,那么定时器的整个计数周期就是“1/10kHz = 100微秒”。这里有两个参数可以调:预分频器PSC和自动重装载值ARR。
转速10kHz,计数周期100us,使用72MHz时钟源,若PSC=71,则计数器时钟频率为 72MHz / (71+1) = 1MHz,计数周期为 1us。若ARR=99,则整个计数周期为 100 × 1us = 100us,对应PWM频率为10kHz。这样ARR的物理意义就变成了“占空比的分辨率”——ARR=99意味着占空比可以从1%到100%,以1%为步进,共有100级。100级的占空比分辨率对于电机调速来说完全够用了。
如果你想获得更高的占空比分辨率,比如0.1%步进,可以把PSC减小、ARR增大,但这样PWM频率会变低,或者需要更高的时钟频率。所以PSC和ARR的搭配本质上是在“频率”和“分辨率”之间做权衡,需要根据应用场景取舍。对于电机调速,10kHz + 100级是我用着最舒服的配置。
3.3 代码实现与占空比调节
配置代码用标准外设库写,核心逻辑就是四步:开启时钟、配置引脚复用、配置定时器、配置PWM通道。
// 定时器和GPIO时钟使能 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); // 配置PA6为复用推挽输出,作为TIM3_CH1引脚 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 定时器基本配置:1MHz计数频率,100次溢出 = 10kHz TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1; // PSC = 71 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period = 100 - 1; // ARR = 99 TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); // 配置PWM1模式,输出极性为高 TIM_OCInitTypeDef TIM_OCInitStructure; TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 50; // CCR = 50(初始50%占空比) TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM3, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); // 启动定时器和PWM输出 TIM_Cmd(TIM3, ENABLE);PWM模式1的含义是:计数器CNT小于CCR时输出有效电平(高电平),CNT大于等于CCR时输出无效电平(低电平)。所以CCR = 50、ARR = 99时,一个周期内有50个计数单位是高电平,占空比约50%。
后面要改变转速,只需要修改CCR的值:
void motor_set_speed(int duty_percent) { if (duty_percent < 0) duty_percent = 0; if (duty_percent > 100) duty_percent = 100; TIM_SetCompare1(TIM3, duty_percent); }这里连续调用TIM_SetCompare1时有个细节值得注意:我开了预装载功能,也就是CCR的新值会先锁存到影子寄存器,等当前PWM周期结束后才更新到实际寄存器。这样做的目的是避免在PWM周期中间修改CCR导致一个周期内出现异常波形,对电机来说就是瞬时电流突变。预装载功能默认可以通过TIM_OC1PreloadConfig开启,在实时调速时非常重要。
3.4 波形验证:Keil仿真与逻辑分析仪
代码写完,先别急着接电机,我强烈建议先验证波形。原因很简单:电机转不转受到太多因素影响,如果波形本身就不对,后面全是在浪费生命。
常用的验证手段有两个。第一个是Keil MDK内置的逻辑分析仪仿真,在Debug模式下打开View → Analysis Windows → Logic Analyzer,添加TIM3_CH1对应的GPIO引脚(这里就是PA6),然后运行代码,就能看到PWM波形。设置里把Display Type改成Bit,波形会更清晰。这种方式不需要任何额外硬件,适合快速验证定时器配置是否正确。
第二个是逻辑分析仪。这个比Keil仿真更真实,因为它测的是芯片引脚上真实的电平变化。国产十几块钱的24MHz逻辑分析仪就够用,接上PA6和GND,打开配套软件抓波形,直接就能看到PWM的频率、占空比和波形完整性。我遇到过Keil仿真显示波形正常,但实际引脚输出全是高电平的情况——后来发现是PB4重映射的JTAG冲突问题,只有真用仪器测试才能暴露出来。
实测时,建议用占空比10%、50%、90%三组分别抓波形,确认波形的周期始终是100us(对应10kHz),高电平宽度分别是10us、50us、90us。如果有示波器,顺便看一下波形边沿是否陡峭,有没有过冲或者振铃。波形干净了再接电机,心里才踏实。
提示:改完代码后如果波形频率不对,优先检查时钟树:系统时钟是不是72MHz、APB1预分频是多少、定时器时钟是从哪条总线来的。这块最容易因为库函数的默认配置和预期不一致而出错。
4. 开环调速实测:从接线到转动的完整流程
4.1 六个步骤把电机从静止转到目标转速
接线和配置都完成后,开始实际通电测试。以下是我惯用的六个步骤,每个环节都有明确的“过不过”判据,避免稀里糊涂地把所有问题堆到最后一起爆炸。
第一步,静态检查:断电状态下,用万用表蜂鸣档确认TB6612的电源正负极没有短路,VM和GND之间有没有焊锡桥接,逻辑引脚和电机输出之间有没有误连。
第二步,逻辑供电测试:只给MCU供电不下电机的电源,用万用表量TB6612的VCC引脚,确认有3.3V或5V。再量一下PWM输入引脚的静态电平——如果驱动芯片支持逻辑引脚悬空保持高电平,那这一步就会暴露出上电瞬间电机自转的风险。
第三步,方向测试:给方向引脚一个确定的电平(比如高电平),然后设置占空比为0,给驱动板上电。此时电机应该不转。如果电机突然转起来,说明驱动器逻辑引脚在MCU初始化之前已经被拉到了异常电平,需要在硬件上增加下拉电阻,或者在MCU初始化代码里先把方向引脚设置为确定输出。
第四步,低占空比试转:占空比设置为10%,期望电机以低速平滑旋转。如果电机不转,先检查方向引脚是否正常、PWM波形是否到达驱动板、驱动板工作电压是否正常。低速下电机不转特别常见,原因通常是启动转矩不够,或者占空比太低导致电机的平均电压无法客服静摩擦。可以先把占空比提到20%试一下。
第五步,中高占空比调速:从20%到80%,每次+10%,记录每个占空比下的电机转速(用手感或者非接触转速计估测)。这一步的目的有两个:确认占空比变化时电机转速同步变化,没有跳变或者失速区间;确认电机和驱动板的温升是否正常。
第六步,满载和动态测试:给电机加上实际负载(比如装上轮子放在地面上),测试相同占空比下转速的变化。这一步会让你直观地感受开环的局限——负载一变,转速就变,负载越大,转速跌落越明显。
4.2 常见问题速查表
把我在调试过程中反复遇到过的现象和解决方案整理成一个速查表,方便后面遇到问题时直接对号入座。
| 故障现象 | 可能原因 | 排查方案 |
|---|---|---|
| 电机完全不动 | 方向引脚悬空/电平错误 | 检查方向引脚电平是否符合驱动板逻辑 |
| 电机完全不动 | PWM信号未到达驱动板 | 用逻辑分析仪抓驱动板PWM输入引脚 |
| 电机完全不动 | 驱动板电源未接入 | 检查VM和GND电压 |
| 上电瞬间电机猛转一下 | MCU复位期间GPIO电平不确定 | 硬件加下拉电阻,或初始化GPIO后再上驱动电源 |
| 电机转速抖动/异响 | PWM频率过低 | 把PWM频率提高到10kHz以上 |
| 占空比100%时电机停转 | PWM输出极性配置反了 | 检查TIM_OCPolarity配置,必要时换PWM2模式 |
| 电机一转MCU就复位 | 共电源导致电压跌落 | 独立电源或加大电容 |
| 驱动芯片严重发烫 | PWM频率过高/负载过大 | 降低频率或更换更大功率驱动 |
4.3 开环控制的天花板
实测下来,开环PWM在稳定负载下表现称职:电机转速平稳,噪音可接受,调速手感线性。但你一旦给它加上会变的负载,问题立刻就暴露出来。还是那个最简单的场景:小车在地板上跑,地板稍微有点坡度,同样的占空比,转速立刻往下掉。这就是开环控制的天花板——它永远不知道电机实际转速和目标转速之间的差距有多大,更谈不上纠正。
理解这个天花板很重要,它决定了你什么时候应该从开环切换到闭环。判断标准很简单:如果负载基本恒定,转速微微波动无所谓,那开环足够了,省成本、省代码、省心。如果负载变化剧烈,或者转速必须严格稳定,那就必须上编码器、上PID闭环。不要为了追求“高级感”盲目上闭环,也不要为了省事在必须闭环的场景硬撑开环。工具没有高下之分,只有合不合适。
5. 下一步进化:从开环PWM到闭环控制
5.1 编码器反馈与PID转速闭环
一旦开环跑顺了,下一步自然是上闭环。最常见的做法是给电机加装编码器,用MCU的定时器捕获功能读取编码器脉冲,计算实际转速,再通过PID控制器调节PWM的占空比,让实际转速始终逼近目标转速。
STM32的定时器做编码器接口模式非常方便——TIM3和TIM4都支持编码器模式,直接把编码器的A相和B相接到定时器的两个通道上,硬件自动处理正交解码和计数方向,CPU完全不操心计数的问题。测速周期建议在10ms到20ms之间,太短了脉冲数太少测不准,太长了实时性不够。
PID参数整定我的个人经验是:先调比例项P,让它快速响应但不过冲;再调积分项I,消除稳态误差;最后调微分项D,抑制超调和振荡,但D对噪声敏感,优先级最低。如果发现电机在目标转速附近来回振荡,通常是P太大;如果稳态误差一直消不掉,是I不够或者积分限幅设得太小。
5.2 从方波控制到FOC的扩展路径
如果你用的不是直流有刷电机而是无刷电机(BLDC或者PMSM),那路线图会稍有不同。无刷电机不能简单地“通个PWM就转”,必须知道转子位置,按照特定顺序给三相绕组通电,这就是方波控制(六步换相),也是很多航模电调的做法。更进一步的是FOC(磁场定向控制),通过Clarke变换和Park变换把三相电流解耦成转矩和磁场两个独立分量,实现对转矩的精准控制。
FOC通常需要一个电流采样电阻、三路PWM输出(高级定时器的互补PWM加上死区控制正好可以满足)和性能足够的控制芯片。STM32F103ZET6做FOC性能偏紧但并非不行,更推荐F4系列,可以加载官方的电机控制SDK,自带速度环和电流环,把很多底层的坑都填平了。
但这些都是后面的事了。当下这个“P0:PWM电机开环控制”的项目,到电机能稳定、可靠地跟随占空比旋转,就已经圆满完成了任务。地基打得越牢,后面盖楼才越安心。
最后说一个我个人的操作习惯:每次给电机控制系统上电之前,我都会先在代码里把PWM输出禁用、方向引脚设置为确定电平,然后再初始化定时器,最后才上电机电源。这一套“先软件后硬件”的顺序,帮我避免过无数次上电瞬间的电机乱转。听起来微不足道,但做过电机驱动的人都知道,电机乱转一下,轻则吓得人一激灵,重则齿轮崩齿、烧毁驱动。安全第一,永远不嫌啰嗦。