STM32驱动MG90S数字舵机:从PWM原理到多舵机稳定控制实战 1. 从一次舵机抖动说起为什么MG90S值得单独写一篇驱动笔记如果你玩过STM32大概率绕不开舵机。而在所有舵机里MG90S几乎是入门和进阶之间那道绕不过去的坎——它比SG90扭矩大、金属齿轮、响应快价格又没贵到让人心疼。但真正上手之后你会发现这颗舵机并没有想象中那么“听话”上电瞬间乱转、角度对不上、堵转发热、多舵机一起动就抖……这些问题我在不同项目里反复踩过最后发现根源往往不在舵机本身而在驱动信号和供电设计上。这篇内容就是围绕“STM32驱动MG90S数字舵机”这件事把从原理到代码、从供电到调参的完整链路讲清楚。适合已经会点灯、会配GPIO和定时器但一碰到舵机就各种玄学问题的朋友。我会把PWM频率为什么必须是50Hz、脉宽和角度的换算关系、为什么TIM的预分频和重装载值要那样算、以及实际调试中那些文档里不会写的坑全部摊开讲。看完之后你应该能做到单舵机角度精准可控多舵机同步不抖堵转能保护代码可以直接搬到自己的工程里。MG90S本质上是一颗数字舵机内部有控制IC接收的是标准PWM信号不是模拟舵机那种简单比较。它的标称参数是工作电压4.8V到6V扭矩在4.8V时约1.8kg·cm、6V时约2.2kg·cm速度约0.1秒每60度转动范围0到180度。这些数字看着简单但每一个都直接影响你的驱动设计后面会逐个拆开。2. MG90S的控制信号到底长什么样50Hz背后的硬约束2.1 为什么频率必须是50Hz而不是随便给很多人第一次驱动舵机直觉是“给个PWM就行”于是拿个1kHz的PWM接上去结果舵机要么不动要么疯狂抖。原因在于舵机的控制协议是基于周期的脉宽编码不是占空比编码。MG90S要求的外部信号是周期20ms也就是50Hz高电平持续时间在0.5ms到2.5ms之间对应0到180度。注意这里的关键是高电平的绝对时间而不是占空比。如果你把频率改成100Hz周期变成10ms哪怕占空比一样舵机内部IC识别到的高电平时间窗口就全乱了轻则角度错乱重则内部保护直接不响应。那为什么是20ms这是早期模拟舵机的历史遗留标准数字舵机为了兼容整个生态沿用了下来。MG90S作为数字舵机内部其实有更高的处理频率但外部信号依然认这个20ms周期。所以第一条铁律驱动MG90SPWM周期锁定20ms频率锁定50Hz不要自作聪明改。2.2 脉宽与角度的线性关系及死区标准对应关系是这样的高电平时间对应角度说明0.5ms0度最小位置1.0ms45度1.5ms90度中位2.0ms135度2.5ms180度最大位置理论上这是线性的每度对应(2.5-0.5)/180 ≈ 0.0111ms也就是约11.1微秒每度。但实测下来MG90S在两端各有一点点死区大概0度附近和180度附近各有2到3度的非线性区。也就是说你给0.5ms它可能停在2度给2.5ms可能停在178度。这不是质量问题是机械限位和内部电位器的一致性导致的。所以实际写代码时我通常不会把可用范围拉满到0.5ms到2.5ms而是留一点余量用0.6ms到2.4ms对应0到180度或者干脆在软件里做一次校准。这个后面调参章节会详细说。2.3 数字舵机与模拟舵机在驱动上的差异虽然信号协议一样但MG90S作为数字舵机有两个明显不同。第一是死区更小模拟舵机死区可能到10微秒数字舵机能做到2到3微秒所以角度更精准但也意味着信号稍有抖动它就会跟着动对PWM的稳定性要求更高。第二是响应更快、保持力更强它会持续高频修正位置所以静态时电流并不为零这也是为什么它比模拟舵机更容易发热。理解这一点很重要你不能像对待模拟舵机那样随便给个粗糙PWM就完事数字舵机对信号质量更敏感这也是很多人觉得MG90S“难搞”的根本原因。3. 用STM32定时器生成舵机PWM参数计算的完整推导3.1 定时器选型与通道分配思路STM32上生成PWM通用定时器TIM2到TIM5都可以高级定时器TIM1、TIM8也行。选择原则很简单哪个定时器方便引出引脚、不和其他功能冲突就用哪个。比如TIM2的CH1到CH4对应PA0到PA3四个通道正好能独立控制四个舵机这在做机械臂或者四足的时候非常实用。这里有个容易忽略的点一个定时器的所有通道共享同一个周期也就是ARR和PSC是共用的但每个通道的CCR可以独立设置。这正好符合舵机需求——所有舵机都是50Hz周期只是脉宽不同。所以一个TIM带四个舵机是完全可行的不需要每个舵机一个定时器。我一般这样分配TIM2负责4个舵机TIM3再负责4个做八自由度机械臂绰绰有余。选定时器的时候还要注意引脚有没有被其他外设占用比如串口、SPI提前在数据手册的复用功能表里确认好。3.2 预分频PSC和重装载ARR到底怎么算这是整个驱动里最核心的计算也是最容易算错的地方。目标很明确PWM周期等于20ms。STM32定时器的PWM周期公式是T (ARR 1) × (PSC 1) / F_clk其中F_clk是定时器的时钟频率。假设你的STM32主频72MHzTIM2挂在APB1上APB1预分频系数不为1时定时器时钟是72MHz这点很多人会搞错APB1是36MHz但定时器倍频后还是72MHz具体看时钟树。我们要T 20ms 0.02sF_clk 72MHz 72,000,000Hz。先定PSC。为了让CCR有足够的分辨率PSC不能太大。如果PSC 71那么定时器计数频率 72MHz / 72 1MHz也就是每个计数1微秒。这样ARR 20000 - 1 19999周期正好20ms。而CCR的值直接就是微秒数比如要1.5ms就写1500非常直观。这个配置我强烈推荐PSC 71ARR 19999计数频率1MHz1个计数等于1微秒。好处是脉宽计算不用换算CCR写多少就是多少微秒调试的时候脑子清爽。如果你主频是168MHz比如某些F4那PSC要重新算。168MHz下PSC 167得到1MHzARR还是19999。总之核心思路就是先把计数频率降到1MHz再让ARR对应20ms。3.3 角度到CCR的换算与代码封装有了1微秒每计数的前提角度换算就简单了。假设0度对应500微秒180度对应2500微秒CCR 500 angle × (2500 - 500) / 180 即 CCR 500 angle × 2000 / 180 简化 CCR 500 angle × 100 / 9为了避免浮点运算可以写成整数形式// angle范围0到180 uint16_t angle_to_ccr(uint8_t angle) { if (angle 180) angle 180; return 500 (uint16_t)((uint32_t)angle * 2000 / 180); }这个函数返回的就是直接写进CCR寄存器的值。实际用的时候我会再加一层校准偏移比如return 500 offset ...offset根据实测调整。封装成函数的好处是业务代码里只关心角度不关心底层脉宽。做动作序列的时候直接调set_servo_angle(1, 90)这种可读性高后期换舵机型号也只需要改这一个换算函数。4. 供电与共地比代码更容易翻车的地方4.1 为什么舵机不能直接从STM32的3.3V或5V引脚取电这是新手最常犯的致命错误。MG90S堵转电流可以轻松超过1A正常工作电流也有100到200mA而STM32板子上的5V引脚如果是通过USB供电通常只能提供500mA左右还要分给板子本身。结果就是舵机一动电压被拉低STM32复位或者舵机抖动无力。更严重的是舵机启动瞬间的浪涌电流会在电源线上产生尖峰通过共用的电源影响MCU导致程序跑飞。我见过最惨的一次是舵机一转整个板子反复重启查了半天以为是代码问题最后发现是供电。正确做法舵机用独立电源供电比如4节镍氢电池4.8V或者稳压到5V到6V的独立电源模块电流能力至少2A。STM32和舵机电源共地但电源线分开走。4.2 共地为什么必须做以及电源去耦的实操共地是必须的因为PWM信号是相对于地的电平。如果STM32和舵机不共地信号就没有统一的参考点舵机根本识别不了。很多人用独立电源后忘了把GND连起来结果舵机完全不动还以为是代码问题。去耦方面我习惯在舵机电源输入端并一个470μF到1000μF的电解电容再并一个0.1μF的陶瓷电容。大电容吸收浪涌小电容滤高频。这个组合成本几毛钱但能解决大部分因为电源波动导致的抖动和复位问题。另外如果舵机离MCU比较远信号线最好用杜邦线拧一下或者加个100欧姆的串联电阻减少反射和干扰。信号线不要和电源线捆在一起走能分开就分开。4.3 多舵机系统的供电设计做多舵机项目时供电要按最坏情况算。比如6个MG90S每个堵转1A理论上要6A。实际不会同时堵转但保守按每个0.5A到0.8A算6个也要3A到5A。这时候普通稳压模块就不够了要用开关电源或者大容量电池。我的做法是多舵机系统用独立的5V开关电源电流按舵机数量×0.8A预留。电源线用粗一点的每个舵机的电源线尽量短必要时星型接地避免一个舵机的电流波动影响另一个。这些细节在单舵机时无所谓但舵机一多供电设计的好坏直接决定项目能不能跑起来。5. 从零到跑通单舵机驱动的完整实操步骤5.1 硬件连接清单与接线顺序先把硬件理清楚。你需要一块STM32开发板F1、F4都行这里以常见的F103为例一个MG90S舵机独立5V电源电池盒或稳压模块若干杜邦线接线对应关系MG90S线色连接目标说明棕色/黑色电源GND和STM32 GND共地必须接红色独立5V电源正极不要接STM32的5V橙色/黄色STM32 PWM引脚如PA0信号线接线顺序建议先接好电源和地确认舵机电源正常最后接信号线。上电前用万用表量一下电源电压确认在4.8V到6V之间别超6VMG90S虽然标称能到6V但长期超压会缩短寿命。5.2 定时器PWM初始化的关键寄存器配置下面这段是基于标准外设库的初始化用HAL库思路一样只是函数名不同。核心是PSC、ARR、CCR和PWM模式1的配置。void servo_pwm_init(void) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tim; TIM_OCInitTypeDef oc; // 1. 开时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 2. 配置PA0为复用推挽输出 gpio.GPIO_Pin GPIO_Pin_0; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); // 3. 时基配置72MHz下PSC71得1MHzARR19999得20ms tim.TIM_Prescaler 71; tim.TIM_Period 19999; tim.TIM_ClockDivision TIM_CKD_DIV1; tim.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, tim); // 4. PWM模式1初始1.5ms对应90度 oc.TIM_OCMode TIM_OCMode_PWM1; oc.TIM_OutputState TIM_OutputState_Enable; oc.TIM_Pulse 1500; oc.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM2, oc); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); // 5. 开自动重装载预装载启动 TIM_ARRPreloadConfig(TIM2, ENABLE); TIM_Cmd(TIM2, ENABLE); }几个关键点解释一下。TIM_OCMode_PWM1表示计数器小于CCR时输出高电平这正是我们需要的——CCR就是高电平时间。TIM_OCPreload_Enable让CCR的修改在下一个周期生效避免当前周期被截断产生毛刺。TIM_ARRPreloadConfig同理保证周期稳定。5.3 角度控制函数的调用与实测验证初始化之后控制角度就是改CCRvoid set_servo_angle(uint8_t angle) { uint16_t ccr 500 (uint16_t)((uint32_t)angle * 2000 / 180); TIM_SetCompare1(TIM2, ccr); }实测验证步骤先上电舵机应该停在90度中位。然后依次调用0度、45度、90度、135度、180度观察舵机是否平滑转动到对应位置。如果方向反了比如0度跑到180度那边把CCR换算反过来即可或者机械上把舵机臂换个方向装。这里有个实测经验第一次上电前先把舵机臂拆下来让它自由转动到中位后再装臂。否则如果舵机臂装的位置不对上电瞬间它会强行转到中位可能撞到机械限位导致堵转发热。6. 调参与避坑那些文档里不会写的实测经验6.1 角度不准的校准方法前面说过MG90S两端有死区加上每个舵体电位器有差异所以标准换算出来的角度可能偏几度。校准方法很简单写一个串口命令能手动输入CCR值然后观察舵机实际位置。具体操作先给500看舵机停在哪如果没到0度慢慢加比如加到530才到0度那你的下限就是530。再给2500看是否到180度如果超了或者没到同样微调。把实测的下限和上限记下来替换换算函数里的500和2500。我一般会把校准值存在Flash里这样换舵机不用重新编译。对于精度要求高的项目这一步不能省。6.2 舵机抖动的三类原因与排查顺序舵机抖动是最常见的问题原因分三类排查要有顺序第一类信号问题。PWM不稳定、周期不对、CCR被频繁改。排查方法是用示波器看信号没有示波器就用逻辑分析仪。如果CCR在业务代码里被高频修改舵机会一直追看起来就是抖。第二类电源问题。电压不足、电流不够、去耦不够。排查方法是万用表测舵机电源在动作时的电压如果掉到4.5V以下基本就是供电问题。加电容、换电源。第三类机械问题。舵机臂卡住、负载过重、齿轮磨损。排查方法是空载测试如果空载不抖带载抖就是机械或负载问题。排查顺序建议从电源开始因为最容易测然后信号最后机械。我踩过的坑里电源问题占一半以上。6.3 堵转保护与寿命延长技巧MG90S堵转时会持续大电流很快发热长期会烧。软件上可以做堵转保护如果某个角度指令发出后舵机在合理时间内没到位通过外部反馈或者电流检测就切断PWM或者回到安全位置。没有反馈的情况下至少要做到不要长时间给超出机械范围的角度。另外数字舵机静态时也在高频修正所以不用的时候可以关闭PWM输出让它彻底放松减少发热和齿轮磨损。我的做法是动作完成后延时几百毫秒如果没新指令就把定时器通道关掉。还有个小技巧舵机供电电压不要一味求高。6V虽然扭矩大但发热也快。如果负载不重用5V反而更稳定、寿命更长。这个要根据实际负载权衡。6.4 多舵机同步的时序处理多舵机一起动的时候如果一个个改CCR会有微小的时间差看起来不整齐。更好的做法是先算好所有通道的CCR值然后一次性更新。因为同一个定时器的多个通道共享周期你在一个周期内改完所有CCR下一个周期它们就同时生效。代码上就是先算好数组再循环TIM_SetCompareX。虽然还是逐个写寄存器但都在同一个20ms周期内完成实际效果就是同步的。如果跨定时器那就没法完全同步只能尽量把更新放在相近的时间点。做动作序列时我习惯用一个定时器中断每20ms更新一次所有舵机目标做插值平滑。这样动作连贯也不会因为频繁改CCR导致抖动。7. 把驱动封装成可复用模块的思路7.1 面向对象式的结构体封装裸写寄存器虽然直接但项目一大就乱。我习惯把每个舵机封装成一个结构体typedef struct { TIM_TypeDef *tim; uint8_t channel; uint16_t min_ccr; uint16_t max_ccr; uint8_t current_angle; uint8_t target_angle; } Servo_t;这样每个舵机的校准值、当前角度、目标角度都在一起管理多个舵机很清晰。初始化的时候填好tim和channel控制的时候传结构体指针。7.2 平滑运动与速度控制的实现直接改CCR舵机会瞬间跳过去机械冲击大。要平滑运动可以用定时器中断做插值每20ms把current_angle往target_angle靠近一步步长决定速度。void servo_update(Servo_t *s) { if (s-current_angle s-target_angle) { s-current_angle; } else if (s-current_angle s-target_angle) { s-current_angle--; } uint16_t ccr s-min_ccr (uint32_t)s-current_angle * (s-max_ccr - s-min_ccr) / 180; TIM_SetCompareX(s-tim, s-channel, ccr); }在20ms中断里调用所有舵机的update就能实现匀速平滑运动。步长改成2就是双倍速度很灵活。这个思路做机械臂轨迹规划特别有用。7.3 移植到不同STM32系列的注意事项这套代码从F1移植到F4或者G0主要改三个地方时钟频率变了要重算PSC、库函数从标准库换HAL、引脚复用配置方式不同。核心的PWM原理和换算逻辑完全不变。HAL库下用__HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, ccr)替代TIM_SetCompare1初始化用CubeMX生成更省事。但CubeMX生成的PSC和ARR要自己核对它默认可能不是1MHz计数频率需要手动改。不管哪个系列记住那个核心公式先降计数频率到1MHz再让ARR对应20ms。抓住这一点换什么芯片都不慌。8. 写在最后的一点个人体会MG90S这颗舵机说难不难说简单也不简单。它的驱动代码可能就几十行但真正让它在项目里稳定工作靠的是对信号协议的理解、对供电的重视、对细节的打磨。我见过太多人代码抄过去跑不通最后发现是电源没共地或者PSC算错了一位。如果你正准备用STM32做舵机项目我的建议是先把单舵机在独立供电下跑稳把角度校准做好再去扩展多舵机。不要一上来就搭六足那样出了问题你根本不知道是代码、供电还是机械的锅。一步步来每个环节都验证过后面才会顺。还有示波器或者逻辑分析仪真的值得买一个哪怕是最便宜的那种。看一次实际波形比猜十次都管用。舵机调试里那些玄学问题百分之八十都能在波形和电压上找到答案。