
先别急着敲代码。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桥饱和压降小发热低体积小引脚直插友好。相比另一款常见驱动L298NTB6612在压降上优势巨大——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 × U2V压降 × 2A电流就是4W的发热全烧在芯片上。TB6612内部是功率MOSFET组成的H桥MOSFET导通时是纯电阻特性导通电阻典型值在0.5Ω以内同样2A电流下的压降只有不到1V损耗小了一个数量级。发热自然少很多效率也高得多。对比项TB6612L298N内部开关器件功率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模式CCRARR时输出应该是恒高电平但如果定时器的极性配置反了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_CH1PA6TIM3_CH2PA7TIM3_CH3PB0TIM3_CH4PB1如果这些引脚被其他外设占用TIM3还支持部分重映射。比如完全重映射后CH1会从PA6跑到PB4CH2跑到PB5CH3跑到PB0不变CH4跑到PB1不变。需要注意的是PB4默认用作JTAG的NJTRST引脚如果要把它当作普通GPIO使用必须先禁用JTAG功能否则引脚电平会被JTAG逻辑拉住PWM波形根本出不来。这次项目里我用的是TIM3_CH1的默认引脚PA6同时把重映射的操作也做了说明因为很多情况下默认引脚确实不够用。比如在做双电机驱动时如果两个电机的PWM通道都占了默认引脚就可能需要动用重映射。3.2 定时器参数计算从72MHz到10kHz这是整个配置里最核心的数学部分。STM32F103的系统时钟最高是72MHzTIM3挂接在APB1总线上当APB1预分频系数为2时定时器时钟同样是72MHz。我需要输出10kHz的PWM那么定时器的整个计数周期就是“1/10kHz 100微秒”。这里有两个参数可以调预分频器PSC和自动重装载值ARR。转速10kHz计数周期100us使用72MHz时钟源若PSC71则计数器时钟频率为 72MHz / (711) 1MHz计数周期为 1us。若ARR99则整个计数周期为 100 × 1us 100us对应PWM频率为10kHz。这样ARR的物理意义就变成了“占空比的分辨率”——ARR99意味着占空比可以从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自带速度环和电流环把很多底层的坑都填平了。但这些都是后面的事了。当下这个“P0PWM电机开环控制”的项目到电机能稳定、可靠地跟随占空比旋转就已经圆满完成了任务。地基打得越牢后面盖楼才越安心。最后说一个我个人的操作习惯每次给电机控制系统上电之前我都会先在代码里把PWM输出禁用、方向引脚设置为确定电平然后再初始化定时器最后才上电机电源。这一套“先软件后硬件”的顺序帮我避免过无数次上电瞬间的电机乱转。听起来微不足道但做过电机驱动的人都知道电机乱转一下轻则吓得人一激灵重则齿轮崩齿、烧毁驱动。安全第一永远不嫌啰嗦。