拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32驱动MG996R舵机:从PWM控制到闭环位置控制实战

直接玩过MG996R的朋友应该都有体会这舵机便宜、力气大但真要让它老老实实转到你想要的任意角度尤其是做机械臂、云台这类项目时光靠STM32输出一路PWM波形是远远不够的。标题里PWM控制与闭环原理听起来像教科书目录但实际上从波形产生、角度标定、供电设计到位置反馈每一步都有能让人折腾到半夜的隐性坑。这篇文章就把我从零开始驱动MG996R、最终做成闭环位置控制的完整经历拆开来讲包括定时器配置、中位校准、死区实测以及为什么单纯发PWM控制角度这件事本身就有天花板。1. MG996R的真实脾气先弄清楚你要驱动的到底是什么MG996R是MG995的升级版标称扭矩在6V供电下能到9.4kg·cm左右听起来很适合做机械臂关节。但实际用起来你会发现它和那些几百块钱的数字舵机在控制逻辑上完全是两种东西。它本质上是模拟舵机内部由一个直流电机、减速齿轮组、电位器反馈和一块简单的控制板组成控制方式是标准的50Hz PWM信号——也就是每20ms刷新一次脉冲宽度脉冲宽度决定角度。1.1 标称参数与实际表现的差距先看一组我实测的数据避免大家被规格书带偏参数标称值实测表现备注工作电压4.8V~6.6V5V供电时扭矩明显缩水建议5.5V~6V独立供电空载电流约10mA静止时低但启动瞬间可达数百mA堵转电流可达1.2A以上失控保护有脉冲信号丢失后保持最后位置但只对2ms以内信号有效死区手册未标注实测约5μs~8μs意味着0.25°到0.4°分辨率齿轮金属齿轮实际是粉末冶金强度一般堵转太久还是会扫齿这些数据说明什么问题它的扭矩数字看着吓人但那是在6V供电、舵机处于堵转极限时测得的理论值。你要是真的用STM32开发板的3.3V引脚去给舵机供电或者用同一个5V电源同时给STM32和舵机供电大概率会遇到两种情况一是舵机一转屏幕串口全部乱掉电流跌落二是舵机抖个不停角度永远定不住电压纹波太大。1.2 电位器反馈和控制板的局限MG996R内部其实是有闭环的——它本身就带一个位置传感器也就是那个电位器控制板会把电位器电压和输入PWM对应到目标位置电压做比较电压有差值就驱动电机往目标方向转。这是舵机自带的闭环也是很多人觉得舵机直接给PWM就应该准的根源所在。但问题在于它里面的控制器是模拟比较器不是数字PID死区比较大。我实测过同一个PWM脉宽下从不同方向靠近目标位置最终停下来的位置能差出将近1度。对0.1°精度级别的项目来说这是不可接受的但更关键的是——它的电位器反馈信号不可能从外部读出来所以外部的人永远不知道舵机实际转到哪了。这一条就直接决定了你如果想做高精度位置控制一定得额外加外部编码器或角度传感器绕开它内部那套反馈系统。1.3 360度版本是完全不同的生物买MG996R的时候经常会在参数里看到360°舵机这个变体。这里必须强调360°版不是能转360度的舵机而是把内部电位器拆掉、换成固定电阻、让电机无限旋转的版本。这种舵机对PWM的响应逻辑不再是脉宽对应角度而是脉宽对应转速和方向1.5ms 脉宽停止1.0ms~1.3ms全速逆时针旋转1.7ms~2.0ms全速顺时针旋转如果你项目里要控制的是一个普通角度舵机千万别买带360°标识的版本。之前有个朋友做云台买了标注360° MG996R的型号结果发现给PWM它不停转还以为是代码问题最后换回标准角度版才正常。标题里写的闭环原理如果要做位置闭环同样得用标准版舵机加外部反馈360°版只能做轮式移动这类速度控制场景。2. 用STM32定时器产生PWM为什么选TIMx而不是软件延时既然要控制舵机第一关就是怎么让STM32产生50Hz、脉宽可在500μs到2500μs之间微调的PWM信号。很多人刚上手时会尝试用HAL_Delay()或者空循环翻转GPIO来模拟PWM低速时候也能转但实际测下来角度抖动明显而且一加中断处理逻辑就卡顿。2.1 软件模拟PWM的致命伤软件模拟PWM核心思路是这样while (1) { HAL_GPIO_WritePin(SERVO_GPIO_Port, SERVO_Pin, GPIO_PIN_SET); delay_us(pulseWidth); HAL_GPIO_WritePin(SERVO_GPIO_Port, SERVO_Pin, GPIO_PIN_RESET); delay_us(20000 - pulseWidth); }这套代码看起来想当然没问题但实测会遇到两件恶心事延时精度受中断影响只要系统里开了定时器中断、串口中断、ADC中断任何一个中断抢占CPUdelay_us实际执行时间就会拉长脉宽变得忽大忽小舵机就会抖。更新率不稳定舵机对50Hz敏感如果刷新率降到30Hz以下力矩会明显下降甚至发出高频叫声。所以结论很直接用STM32驱动PWM控制舵机必须用定时器的硬件PWM输出功能让芯片周期性地自动翻转GPIO电平完全不占CPU时间。2.2 定时器配置的完整参数计算STM32产生PWM的核心思路是用定时器的计数器和一个比较寄存器做匹配。比如我用的是STM32F103C8T6主频72MHzTIM2挂在APB1总线上时钟也是72MHz。目标是50Hz PWM即周期20ms。计算公式定时器时钟 72MHz 预分频 PSC 71 → 计数频率 72MHz / (71 1) 1MHz 自动重载 ARR 19999 → PWM周期 20000 / 1MHz 20ms 50Hz代码如下HAL库标准写法TIM_OC_InitTypeDef sConfigOC {0}; htim2.Instance TIM2; htim2.Init.Prescaler 71; // 72MHz/72 1MHz每个计数1μs htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 19999; // 20ms周期50Hz htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_PWM_Init(htim2); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 1500; // 初始1.5ms中位对应0° sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim2, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1);这段配置里几个容易犯错的地方PSC和ARR一定都要-1因为定时器寄存器从0开始计数这个细节搞错整个频率就全乱了。Pulse 1500对应的是计数器值1500也就是1.5ms脉宽。因为预分频后每1μs计一次数Pulse值直接等于微秒数这样后面做角度映射时特别直观。TIM2是16位定时器ARR最大只能到65535所以50Hz完全没问题。但如果你要驱动总线舵机那种高刷新率或者用TIM1做36MHz PWM就得重新算PSC和ARR不能让ARR超过65535。2.3 频率要不要听网上说的330Hz网上关于MG996R频率的说法很混乱有人用50Hz有人用330Hz还有人用1kHz。我的实测结论标准MG996R老老实实用50Hz。因为它的控制板内部是模拟电路RC充放电时间常数就是为20ms周期设计的。你给它330Hz的PWM脉宽对应的电压平均值会上升舵机不仅会偏转到一个错误角度还可能持续抖动发热。DG601这类数字舵机才支持高刷新率模拟舵机请勿尝试。如果想验证这个说法可以把频率从50Hz调到100Hz保持脉宽1.5ms舵机会咔咔响然后往一个方向慢慢漂这就是内部RC充放电和外部信号周期不匹配的典型症状。3. 角度映射与实机标定从零到180度需要哪些准备定时器能输出PWM后下一步是把脉宽和角度对应起来。MG996R的规格一般是脉宽500μs对应0°1000μs对应45°1500μs对应90°2000μs对应135°2500μs对应180°。理论线性关系为角度 (脉宽 - 500) / 2000 * 180。但实测偏差往往超过20°。3.1 手动标定别信线性对应关系我手里的MG996R实测0°对应脉宽大约530μs90°对应约1520μs180°对应约2420μs。也就是说同一个1.5ms脉宽写进去这台舵机实际转到了约88°不是理想的90°。这说明量产舵机电位器有个体差异批次不同差异还不一样。所以我每次换新舵机都会做一次标定流程很简单用代码发1.5ms脉宽让舵机转到中位用卡尺或者直角尺记录摇臂位置。逐步增加脉宽每次加5μs延时200ms记录舵机刚发生移动时的脉宽。找到最大脉宽角度不再增大仍会明显堵转的位置记作上限。反向重复找下限。这样一组数据就能修正你的映射函数。我的做法是改用两段线性插值拟合而不是全局一个线性公式typedef struct { uint16_t pulse; // μs uint16_t angle; // 0.1° } CalibrationPoint; CalibrationPoint cal_table[] { {530, 0}, {1520, 900}, {2420, 1800}, }; uint16_t angle_to_pulse(uint16_t angle_tenths) { uint16_t i; for (i 1; i 3; i) { if (angle_tenths cal_table[i].angle) { uint32_t t cal_table[i-1].pulse * (cal_table[i].angle - angle_tenths) cal_table[i].pulse * (angle_tenths - cal_table[i-1].angle); return t / (cal_table[i].angle - cal_table[i-1].angle); } } return cal_table[2].pulse; }标定完成后误差可以控制在±0.2°以内——但这仅限静态定位。如果要连续运动动态误差和机械回差又会冒出来。3.2 转速限制与波形斜坡函数MG996R空载转速大约是0.17s/60°6V时也就是每毫秒能转0.59°。如果代码里直接把脉宽从1500μs跳到2000μs舵机臂会啪一下甩过去不仅会撞到限位块还会因为惯性造成过冲和机械冲击。时间长了齿轮就容易扫齿。我后来加了一个斜坡函数让角度目标值一步步逼近相当于把硬阶跃变成了梯形速度曲线uint16_t current_pulse 1500; uint16_t target_pulse angle_to_pulse(1200); // 目标120.0° while (current_pulse ! target_pulse) { if (current_pulse target_pulse) current_pulse 5; // 每次加5μs else current_pulse - 5; __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, current_pulse); HAL_Delay(10); // 每10ms更新一次 }每次步进5μs对应0.25°左右10ms更新一次那么转速大约是25°/s。如果觉得太慢可以加大步长但要注意步长超过舵机内部闭环能达到的最大速度时舵机就会一直追不上目标出现一种类似跟不上的滞后抖动。这个逻辑在后面的闭环设计里同样适用。3.3 限位保护的硬件哲学MG996R的0°和180°其实是物理限位但很多卖家所谓180°版本因为电机和控制板差异实际机械结构能超过180°吗实测结果是脉冲超过2500μs后舵机会坚持往限位位置顶持续堵转。堵转时电流能到1A两分钟之内齿轮箱就能闻到塑料焦味。所以代码里务必做限幅#define SERVO_PULSE_MIN 500 #define SERVO_PULSE_MAX 2500 uint16_t servo_apply_limit(uint16_t pulse) { if (pulse SERVO_PULSE_MIN) return SERVO_PULSE_MIN; if (pulse SERVO_PULSE_MAX) return SERVO_PULSE_MAX; return pulse; }不要试图使用2.5ms以上的脉宽——极限位置附近舵机没有闭环能力反馈电位器在这个区间已经进入死区和饱和区你加再多脉宽它也不转只会白白增加堵转功耗。__HAL_TIM_SET_COMPARE()前一定过这个函数这是改装机械臂项目的基本素养。4. 从开环到闭环为什么单纯输出PWM注定有误差把所有参数标定好、斜坡函数做好之后你会发现在空载、稳定、缓慢运动的条件下MG996R确实能保持在差不多±1°的精度上。但再往下走比如给舵机轴加上负载、推一下它的摇臂或者改变供电电压位置就绷不住了。4.1 开环控制的三个盲区在完全没有外部反馈的控制方式下实际舵机位置与预期位置之间存在三个系统性偏差来源供电波动导致内部比较电压漂移舵机控制板里对PWM信号做低通滤波比较器阈值由5V基准分压决定。当供电从6V降到4.8V时峰值检测电压、阈值电压都会变电机输出扭矩也下降最终定位位置就会出现几个毫秒级的偏差。负载引起的位置滞回给舵机输出轴施加反向力矩时由于齿轮箱内摩擦力、间隙和电位器触点电阻的存在舵机即便收到同样的PWM信号最终的机械位置也会比空载状态偏移1°~3°。齿轮箱回差与机械间隙减速齿轮组中齿与齿之间存在间隙当舵机从小角度方向逼近某个位置和从大角度方向逼近时输出轴最终停止的位置会有明显差异。这种误差无法通过标定来解决因为它是方向相关的。简单说开环PWM控制的问题是你只知道命令它去哪不知道它到底在哪。如果要构建真正稳定可控的机械臂或云台必须绕开舵机内部不可见的电位器在外部增加一个能反馈输出轴绝对位置的角度传感器形成一个完整的外环闭环系统。4.2 闭环系统的典型构成与选型我采用的是STM32 外部角度传感器 串级控制思想的结构目标角度 → 控制器PID → PWM脉宽 → MG996R → 输出轴角度 ↑ 外部角度传感器AS5600 / 电位器选型角度传感器时我对比过三个方案方案精度接口成本适中适配度旋转电位器0.5°左右模拟量/ADC几块钱需机械同轴连接磨损后不稳定AS5600磁编码器0.1°以内I2C十几块钱磁铁对心安装非接触无磨损12位光电编码器0.088°SPI/并口百元级别精度高但贵占IO多MG996R自身的机械回差就接近0.5°所以没必要上12位光电编码器用了也发挥不出来。AS5600的12位I2C接口输出配上便宜的小磁铁和PCB夹持件是最合适的选择——它的分辨率远高于舵机自身精度而且非接触式安装不会给舵机轴增加摩擦阻力。4.3 增量式PID控制器的实现细节有了外部角度反馈后控制器就能闭环运行。我用的增量式PID只记录误差的变化量输出一个脉宽修正增量叠加到上一次的PWM脉宽上避免直接更新绝对值导致的跳变typedef struct { float kp, ki, kd; float prev_err, integral; } PID_t; uint16_t servo_pid_control(PID_t *pid, float target_angle, float current_angle, uint16_t last_pulse) { float err target_angle - current_angle; float output pid-kp * err; pid-integral err; output pid-ki * pid-integral; output pid-kd * (err - prev_err); prev_err err; int16_t pulse_delta (int16_t)(output * 11.11); // 度数到脉宽转换系数 uint16_t new_pulse last_pulse pulse_delta; return servo_apply_limit(new_pulse); }其中11.11μs/度这个系数对应的是2000μs跨度对应180°的线性映射。但你肯定注意到了如果直接拿degree到pulse做线性转换前面说到的非线性、个体标定又全部被绕过去了。所以在实际使用中我是先通过calibration_table把target_angle转换成目标脉宽再把实际角度传感器返回的角度也反转成当前等效脉宽代入PID做脉宽域的误差计算。这样标定函数和PID控制就统一了不会出现误差1度和脉宽差15μs这种单位混乱问题。4.4 整定一个合适PID参数的土办法PID整定是最容易劝退新手的环节。网上标准步骤是只保留P把Ki、Kd设0然后手动加大kp直到系统出现等幅振荡记录此时kp为Ku振荡周期为Tu再用齐格勒-尼科尔斯经验公式算出其他参数。实际操作中我建议按这四步来kp从小到大试从0.1开始加每次给一个100°阶跃指令观察超调量。MG996R本身速度有限kp太小会慢吞吞kp太大时会听到嗡嗡声甚至直接甩到限位来回弹跳。找到一个抖动临界值后再把kp回调到临界值的60%~70%。加kd抑制过冲MG996R齿轮组间隙大微分项对测量噪声和机械回差都很敏感。kd过大反而会在中位附近产生高频振荡建议从0.01这个数量级开始微调。ki处理稳态误差机械臂在某个角度停住时会因为重力或负载产生静态力矩误差只靠kp和kd无法归零这时加入ki。但要设限幅防止积分饱和——我会把累计误差限制在±30°以内。做斜坡响应和阶跃响应实测把目标角度每2秒改变一遍同时记录实际角度曲线。满足无明显超调、收敛时间小于1s、稳态误差小于0.5°就算调通了。这一步调好后你会发现MG996R的表现会有质的提升你推它一下、干扰它一下它都会自动回到设定的位置——这就是闭环的意义。5. 供电、抖动与机械安装三个能毁掉一切方案的隐形杀手控制策略再完美供电和机械设计出了问题舵机还是会在物理世界里露出獠牙。这一节讲三个我在实践中踩过最深的坑全部都有可复现的排查思路。5.1 供电架构舵机电源和主控电源要彻底分开MG996R在正常转动时电流大约200mA~300mA但启动瞬间或负载增大时峰值电流很容易冲上1A以上。假如这个电流和STM32共用同一个5V电源那么内部走线上的压降和电磁干扰会直接导致单片机复位、ADC采集跳变、串口乱码。很多人拍完板子发现STM32反复重启第一反应是查代码其实罪魁祸首是舵机电流倒灌。我的设计习惯是舵机供电用5V/3A以上的开关电源或电池单独一路。STM32用AMS1117-3.3从同一路5V或者独立5V取电但必须在STM32电源入口处加一个100μF电解电容 0.1μF陶瓷电容去耦。舵机地线在主电源处单点接地不要和STM32的模拟地混在一起走。舵机信号线串一个300Ω~1kΩ电阻再接STM32输出引脚防止舵机电源到信号线之间的干扰直接灌进MCU。如果手头实在没有独立电源退而求其次要用同一个5V电源那至少做软启动——上电后先让舵机停在当前角度等待电压稳定再执行指令防止启动瞬间的大电流把CPU拉进欠压复位。5.2 舵机抖动问题排查链路抖动是所有舵机项目都会遇到的顽疾排查顺序非常重要不要一上来就改代码。我一般按这个顺序来看电源波形用示波器看舵机电源端子如果电压在启动瞬间跌到4.5V以下那抖动根源在电源内阻或线缆太细。尤其用面包板或杜邦线时线阻和接触电阻会放大压降。查PWM波形质量霍尔探头测STM32引脚上的PWM波形确认电平是干净的0~3.3V方波。如果波形边缘有严重的毛刺或振铃多半是共地不良或者信号线过长。检查频率和脉宽匹配重新确认定时器参数是不是正确的50Hz、脉宽范围是否在死区附近。舵机在中位附近最容易抖如果伺服代码让脉宽在死区边缘反复震荡抖动会非常明显。去掉PID再看如果已经上了闭环把kd设成0、kp设小看是否还抖。抖动可能是控制器积分振荡带来的这时要重新整定PID参数而不是怀疑舵机。机械共振排查舵机臂末端如果固定了一根长螺丝或轻型杆舵机的位置保持振动可能会与结构共振。此时无论怎么调PID都压不住得从机械刚度入手给舵机臂加配重或缩短力臂。实测下来80%的随机抖动其实都是供电问题而不是算法问题。先把供电弄干净再动代码能省一天时间。5.3 舵机臂连接与齿轮间隙机械回差不是算法能补回来的MG996R的塑料舵盘和输出轴之间是花键配合拧紧螺丝后一般不会松脱但如果反复堵转冲击花键会被磨圆之后就会出现舵盘在轴上打滑的现象表现为怎么发指令都卡在某个角度。这种问题很难通过代码补偿只能换舵盘或者整个舵机。还有一点很多人忽略输出轴上即便装得很紧齿轮箱内部的间隙依然存在。实测我手里这台MG996R正转到位和反转到位之间的回差约有0.6°。做闭环时如果只读外部角度传感器的值直接反馈这种回差会导致PID在目标位置两侧不断做微小修正出现电流声不断的现象。解决办法是在YPID里加死区判断#define ANGLE_DEADBAND 0.3f if (fabsf(err) ANGLE_DEADBAND) { // 误差在死区内保持当前脉宽不动 return new_pulse 0; }0.3°死区略小于回差足够让舵机安静下来又不会对最终精度造成可感知的影响。这个数值要实测调整死区太大控制器就会变得呆滞。6. 一把电烙铁和一万次测试换来的经验清单整个调试过程下来如果说只能留几条最有价值的建议给后来者我会列出下面这一版。它不是教程里能查到的内容每一条都是真金白银换来的。买舵机别只看MG996R三个字不同店铺、不同批次从舵盘精度到内部电位器线性度差异都很大甚至同批次都有明显个体差异。固定一个渠道备几台做统一标定项目一致性会好很多。标定数据不要写死在代码里把标定参数放进Flash或EEPROM每台设备出厂前自动标定一次方便量产配置。预留一个调试串口把目标角度、实际角度、PID输出、供电电压都打出来很多灵异现象一看实时曲线就明白了。我用过最顺手的方式是串口输出CSV格式直接拉进Excel里画折线。给舵机加心跳PWMMG996R内部失控保护只对丢失信号超过50ms有效。如果系统进入待机模式记得让定时器持续输出一个保持脉宽防止下电瞬间舵机抽搐。防堵转要有硬时限所有伺服指令执行前都做一个期望到位时间预估超过这个时间还没有到位就立刻停止输出并报警。比如舵机从0°转到180°需要约0.5秒如果1秒后检测角度变化小于2°多半是卡死堵转立刻切输出。个人实际体验最深的一幕是整套闭环系统断电后在桌面上放了一周再上电时PID参数没变、舵机位置没动角度传感器的值却偏移了3°。原因是机械结构长期受力导致舵机轴与支架位置发生了微小的塑性形变而AS5600读到的绝对角度和输出轴真实角度之间的耦合关系已经变了。这给我的教训是机械系统一定和电气系统一样都要定期重新标定和巡检。闭环能解决控制器内部的误差但解决不了物理世界里连接件的老化和变形。如果你打算把这套方案放到长期运行的设备上——机械臂也好、云台也好——一定要在系统里预留定期自检和重新校准的机制。这比任何高级的控制算法都更能保证系统在三个月后依然稳定可靠。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门