准备把SG90舵机接到STM32上时,我特意数了数踩过的坑:信号接了、程序编译过了、舵机不动;动是动了,抖得跟筛子一样;不抖了,角度又跑偏……干过这活的都知道,问题百分之九十不在代码能不能编译,而在PWM参数和初始化细节上。这篇东西就围绕“用HAL库控制SG90”这条主线,把定时器怎么配、预分频系数和自动重载值怎么算、角度函数怎么写、出了问题怎么查,一口气讲完。后面提到的计算方法和调试思路都是我实测验证过的,按这个流程做,四十分钟内应该能转起来。
1. 先读懂SG90的“脾气”:20ms周期与脉宽角度的底层逻辑
1.1 舵机内部在做什么
SG90这种模拟舵机的内部结构其实很简单:一个直流电机、一组减速齿轮、一块控制板,再加上一个电位器。控制板每隔20ms读取一次输入信号的高电平脉宽,和电位器反馈回来的当前位置做比较,误差不为零就去驱动电机往误差方向转。
关键点在于,它对“频率”并不敏感,真正在意的是每一周期内的高电平持续时间,也就是脉宽。业界约定俗成把周期固定为20ms,所以对应50Hz。你给它40ms周期、同样的1.5ms脉宽,它大概率也认,但抖动和噪音都会变大,所以尽量按20ms来。理解了这一点,后面配置定时器时就不会被网上的各种说法绕晕。
1.2 脉宽与角度的两种标定方式
市面上的SG90资料里,角度和脉宽的对应关系通常写两种:
| 角度 | 常见标定A | 常见标定B |
|---|---|---|
| 0° | 0.5ms | 1.0ms |
| 90° | 1.5ms | 1.5ms |
| 180° | 2.5ms | 2.0ms |
两种标定在90°附近重合,差别在两端的极限。遇到舵机“极限位置异响”或者“想转到0°却只到中间偏右”的时候,问题八成就在这里。不同批次、不同厂商的SG90参数真的有差别,照抄网上别人发出来的映射公式不一定管用。
我的习惯是:第一次拿到舵机,先不管标称,直接送1.5ms脉宽让它回中,确认机械零位和安装位置,然后以50°为单位往两端扫脉宽,看实际能转的范围。通过串口或显示屏把当前脉宽打出来,记下“到达极限发出嗡嗡声前的最大脉宽”,作为软件限位。这比直接套公式靠谱得多,而且整个过程也就两三分钟。
1.3 供电红线:信号只是“告诉它”,能量另算
很多人第一次接线直接把舵机信号、5V、GND全往开发板上一插,结果舵机一启动,板子复位,屏幕乱码。原因很直接:SG90正常运转电流几十毫安,堵转时能冲到700毫安甚至更高,而开发板上的稳压器承载不了这种瞬态冲击。
我常用的做法是:信号线接单片机的GPIO,舵机电源单独接一个5V/1A以上的外部电源,舵机多的话按总电流估算上不封顶,然后务必把外部电源的地和单片机的地接在一起。共地是舵机稳定控制的基本前提,这点经常被新手忽略,抖动排查里有一半问题都能追溯到没共地。
2. CubeMX定时器配置:PSC和ARR要用算的,不要瞎填
2.1 时钟树上的第一个坑:定时器时钟不等于主频
STM32的通用定时器通常挂在APB1总线上,而APB1预分频器的设置会直接影响定时器时钟。在CubeMX默认配置里,如果系统主频是72MHz,那么APB1分频系数为2时,定时器时钟会被倍频到72MHz;分频系数不为1的时候,定时器输入时钟等于APB1时钟乘以2。很多教程只告诉你“填PSC=71,ARR=19999”,却不解释为什么,换一块主频48MHz的板子照着填,舵机自然不动。
所以配置前先到CubeMX的Clock Configuration页面看一眼Timer Clocks,确认你选的定时器输入时钟具体是多少,再去算分频系数。这个页面很多人从来不打开,等出了问题才发现主频和定时器时钟压根不是一回事。
2.2 PWM频率和分辨率怎么同时满足
定时器的PWM输出频率公式是:
PWM频率 = TimerClock / ((PSC + 1) * (ARR + 1))我们需要的是50Hz,也就是周期20ms。但光频率对还不行,脉宽分辨率也要够用。0.5ms到2.5ms之间要映射180°角度,哪怕按每度细分,至少也得有几十微秒的调节能力。
我推荐一个通用组合:在72MHz定时器时钟下,设 PSC = 71,ARR = 19999。
- 计数时钟 = 72MHz / (71+1) = 1MHz,即每个计数单位 = 1us
- PWM周期 = (19999+1) × 1us = 20ms,正好50Hz
- 高电平脉宽 = CCR值 × 1us
这个组合的好处是CCR值和脉宽微秒数直接对应:0.5ms就是CCR=500,1.5ms就是CCR=1500,2.5ms就是CCR=2500,调试时心算非常方便。你当然可以用PSC=719,ARR=1999,也能得到50Hz,但那时每个CCR单位是10us,角度调节粗很多,舵机可能“一格一格”地跳,接近目标位置时很难调平。
2.3 Channel模式与引脚复用
CubeMX里选好定时器后,在TIMx的Channel1下拉框里选“PWM Generation CH1”。这里有两个容易漏的地方。
第一,GPIO配置页要确认该引脚的工作模式是Alternate Function Push-Pull,而不是普通的GPIO Output。有些时候CubeMX会帮你自动设置好,但当你手动改过引脚功能,它可能保留成Output,导致PWM信号根本出不来。
第二,通道模式建议用PWM Generation CHx而不是PWM Generation CHxN。CHxN是互补输出,主要配高级定时器用的,普通通用定时器上用不到,选错了还会多出一堆初始化代码。
配置完成后,CubeMX生成的MX_TIM2_Init()里会看到类似这样的代码:
static void MX_TIM2_Init(void) { TIM_OC_InitTypeDef sConfigOC = {0}; htim2.Instance = TIM2; htim2.Init.Prescaler = 71; htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 19999; 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 = 500; sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.QuickDisable = TIM_OCFASTDISABLE_DISABLE; HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1); }注意Period默认是65535,Prescaler默认0,不做修改直接生成的代码是输出不了一个20ms周期的PWM的。第一次用HAL库的人最容易在这上面栽跟头。
3. HAL库代码链路:别漏了Start,角度换算建议封装成函数
3.1 为什么CubeMX生成了代码,舵机还是不转
这是新手问得最多的问题,答案通常只有一个:CubeMX只帮你配置了PWM外设,没有帮你启动输出。
HAL库里面,初始化函数MX_TIM2_Init()负责配置寄存器,但要让引脚真正输出波形,还得手动调用:
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);这句一般放在while(1)之前,或者某个初始化函数里。漏了它,你用示波器测引脚是平的,舵机当然不动。记住这个特点,HAL库很多外设都有类似逻辑:Init负责“摆好姿势”,Start才真正“开始干活”。这算是我见过最多的低级错误,没有之一。
3.2 SetAngle角度换算函数怎么写
既然CCR和脉宽是1us对1的关系,角度换算就变成小学数学了。从0.5ms到2.5ms一共2ms,对应0°到180°,所以每度的脉宽增加值是:
pulse = 500 + (uint32_t)(((uint32_t)angle * 2000) / 180);完整的封装可以写成这样:
void Servo_SetAngle(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t angle) { uint32_t pulse; if (angle > 180) angle = 180; pulse = 500 + (uint32_t)(((uint32_t)angle * 2000) / 180); __HAL_TIM_SET_COMPARE(htim, channel, pulse); }写这个函数的时候我特意加了angle的上限保护,避免不小心传入一个200,脉宽算出来超过2.5ms,舵机就往机械极限方向硬顶。实际调用时,先启动PWM,再传角度就行:
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM2_Init(); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); Servo_SetAngle(&htim2, TIM_CHANNEL_1, 0); HAL_Delay(1000); while (1) { Servo_SetAngle(&htim2, TIM_CHANNEL_1, 90); HAL_Delay(2000); Servo_SetAngle(&htim2, TIM_CHANNEL_1, 180); HAL_Delay(2000); } }如果用到的舵机按标定B(1ms到2ms)工作,把函数里两个系数改一下就行,500改成1000,2000改成1000。我更建议把系数集中写成宏,方便不同批次舵机快速切换,而不是在每个调用点手动改。
3.3 关于死区:别把目标值设到齿轮卡死的位置
SG90这类模拟舵机在靠近极限位置时,如果脉宽对应的位置已经超出了机械行程,电机会持续堵转,发出嗡嗡声,电流居高不下。前文标定B就是典型场景:你用标定A的公式给它发0°,它实际到了标定B的0°会继续使劲顶。
所以我的SetAngle里除了角度上限180,还常留一个软限位:比如实际可用的脉宽下限定成520而不是500,上限2480而不是2500。留出20us的余量,能在保证转到位的同时避免傻转。这属于细节层面,平时不显眼,但批量跑耐久时很关键。
4. 三类高频故障的完整排查链路:不动、抖动、角度不对
4.1 舵机完全不动:按信号、电源、程序的顺序查
遇到舵机完全不动,先别急着改代码,脑子里过一遍信号链路。
首先确认信号线有没有接对引脚,很多人用错通道,把PWM输出到TIM2_CH1对应的引脚,程序却启动的是TIM2_CH2。其次用示波器或逻辑分析仪看信号引脚有无50Hz方波,没有示波器时,把该引脚临时配置成普通IO输出,循环翻转,用LED判断引脚本身是否正常。再确认舵机供电是否正常,用万用表测舵机两端电压,看启动瞬间电压有没有被拉低,如果低于4.5V,转向无力甚至不转很正常。最后才回头看代码:HAL_TIM_PWM_Start有没有调用,通道号对不对,ARR/PSC有没有被其他地方覆盖。
多数情况下,前两步就能定位问题。我见过最多的组合是:代码没错、引脚没错,就错在供电被拉垮了。
4.2 舵机抖动或啸叫:先从频率下手
抖动和啸叫的原因很多,但出现频率最高的是PWM频率完全不对。有些人配置的时候把PSC、ARR算成周期1ms甚至更短,舵机内部电路刚采到半个脉宽就被下一周期打断,自然无法稳定锁定角度。
排查办法是把示波器接上,数一下两个上升沿之间的时间。如果不是20ms左右,回去改ARR。算的时候注意公式里的+1,PSC=72和PSC=71差一个计数单位,不影响大局,但ARR差一个单位,周期就差一个计数时钟,偏差被放大后就明显了。
除此以外,供电电压纹波大、共地不良,也会表现为舵机在目标位置附近小幅来回抖。给舵机电源并一个470uF以上的电解电容,再用万用表确认单片机和舵机电源的GND连通,绝大多数抖动都能解决。
4.3 角度能转但跑偏:校准优先于改代码
角度能转、但始终差角度,通常是映射关系不准,不一定是程序错误。举个例子,某批舵机在脉宽0.9ms时是0°,2.1ms时是180°,用0.5~2.5ms的公式,就会发现从0°开始就偏了。
我的处理流程是:先发中间值1.5ms,确认舵机是否在几何中位;再发理论0°和180°对应的脉宽,用角度尺或目测记录实际停留位置;然后把实际可用的最小脉宽和最大脉宽代回角度公式。也就是把:
pulse = min_pulse + (angle * (max_pulse - min_pulse) / 180);当成校准公式,min_pulse和max_pulse是两个实测出来的宏。顺带一句,舵机负载较重或减速齿轮有回差时,会出现“从0°转到90°和从180°转到90°最终停在略微不同的位置”的现象,这是机械回差,靠软件只能减轻不能消除,要求高就换数字舵机,那是另一个话题了。
5. 同一定时器带多路舵机:频率共享与供电规划
5.1 原理:周期共用,占空比独立
通用定时器一般有4个通道,每个通道可以独立设置CCR值,但ARR和PSC是共用的。由于SG90只认脉宽不认具体通道,只要保证所有通道的PWM周期都是20ms,就能用同一定时器的多个通道同时控制多个舵机,互不干扰。
实际使用上,每路舵机占一个GPIO引脚,代码里一个函数已经带了通道参数,所以调用方式极其简单:
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3); Servo_SetAngle(&htim2, TIM_CHANNEL_1, 45); Servo_SetAngle(&htim2, TIM_CHANNEL_2, 90); Servo_SetAngle(&htim2, TIM_CHANNEL_3, 135);注意CubeMX里要把用到的每个通道都先配置成PWM Generation CHx,生成代码里会多出几个HAL_TIM_PWM_ConfigChannel调用,而HAL_TIM_PWM_Start仍然要对你实际用到的通道逐个调用,这个没有偷懒的捷径。
5.2 多路舵机更考验供电:算总账再决定电源方案
4路SG90同时堵转,瞬时总电流可能到2.8A以上;就算两个舵机同方向联动、负载不大,启动瞬间的电流冲击也不小。全交给开发板的5V引脚带,大概率出问题。
如果只是临时demo,我会用一个5V/3A的电源适配器单独给舵机供电,共地之后把信号线分别接到单片机引脚。如果是长时间运行的项目,还会在舵机电源输入端并一个大容量电容,吸收启动瞬间的电流毛刺。供电这块宁可富余,不要抠门,烧一块板子的成本够买好几个电源了。
5.3 定时器不够用时的替代思路
一个定时器的4个通道都用完了,还有更多舵机,思路一般是两条。
一是换更多通道的定时器,或者在CubeMX里把多个定时器都用上。这是最省事的做法,代价是重新算一组PSC/ARR,但公式完全一样,把上一组参数复制过去改个定时器句柄就行。
二是在一个更新周期内,用普通IO引脚软件模拟多路PWM输出。做法是在定时器更新中断里维护一张表,按时间点翻转多个引脚。这条路能省定时器资源,但中断里要做的事变多,系统负载上升,实时性要求高时不太推荐。若只是演示场景,倒也能跑,只是后期维护麻烦。
6. 写在最后:我先说三件小事
如果看到这里已经能把舵机转起来,那我的目标就达成一半了。最后按个人习惯分享三件小事,算是我这几年调试舵机系统沉淀下来的经验。
第一,调试SG90时后备一个已知正常的舵机。排错时最怕“这块板子本来就只有这个舵机”,一旦怀疑舵机本身有问题,手头没有替代品就无法交叉验证。
第二,角度函数里的数值尽量用宏集中定义。min_pulse、max_pulse、angle_max这几个常量写死在函数各处,后期换舵机型号时要改的地方太多,容易漏。我把它们集中放在一个servo_config.h里,每次换舵机只改这个文件。
第三,项目里永远亮出一路调试通道。比如把当前正在发送的脉宽值通过串口打印出来,和舵机实际位置对照,能省下大量“看起来转到了但好像还差一点”的猜测时间。这个习惯在后续接更多传感器和复杂机械结构时,价值会越来越明显。