☰
STM32 HAL库驱动SG90舵机:PWM定时器配置与实战调参
2026/10/15 5:10:17 网站建设 项目流程

1. 项目概述:从一块舵机到完整控制链路的思考

如果你刚开始玩STM32,或者已经点过灯、跑过串口,那么用HAL库驱动SG90舵机几乎是每个嵌入式爱好者都会遇到的项目。这个标题看着简单——“【PWM】HAL库控制SG90舵机”——但背后牵扯出的技术点其实相当密集:定时器的PWM输出模式、预分频与自动重载值的计算、占空比与角度的映射关系、舵机供电带来的纹波问题、以及调试过程中常见的抖动和堵转现象。这篇文章会以我实际调试中的真实经历为线索,把一个看似“接三根线就能转”的项目拆成可复现、可排查、可扩展的完整方案。

先说结论:这个项目能做什么?用一颗STM32芯片,通过定时器产生50Hz的PWM信号,控制SG90舵机在0°到180°之间按指定角度转动。什么场景需要它?云台摄像机、机械臂关节、小车转向、门锁驱动、甚至自动喂食器的翻盖机构。如果你正在做DIY机器人或智能硬件原型,这块9g小舵机几乎是性价比最高的“肌肉”单元。适合谁来读?已经熟悉HAL库基本的GPIO操作、正在学习定时器或PWM的开发者,以及被舵机“不听话”折磨到怀疑人生的同学。

我遇到过一个很典型的困惑:代码明明按网上的教程写了,舵机却一动不动;检查了一遍接线、供电、时钟配置,最后发现是定时器重载值设错了,导致PWM频率根本不是50Hz。这一类问题很难通过看代码看出来,因为“表面正确”的配置在硬件上就是不工作。所以这篇博文不打算只贴一段“能用就行”的代码,而是把每一步配置的字面含义、背后的数学关系和硬件约束全部讲透,让你调完这块舵机之后,再看其他PWM设备——无源蜂鸣器、LED呼吸灯、直流电机调速——都能顺藤摸瓜。

我还想说一个容易被忽略的点:SG90舵机和STM32的“共地”问题、供电电压的稳定问题,往往比代码本身更容易让你翻车。这些内容我会放在后面的章节详细展开。如果你手上的板子不是某A品牌,而是其他开发板,也没关系——只要芯片带定时器外设,思路完全一致。

2. 舵机与PWM的基础认知:先弄懂舵机“听”的是什么

2.1 SG90舵机的工作原理与性能边界

SG90属于模拟舵机,内部结构主要由直流电机、减速齿轮组、电位器反馈电路和控制板组成。它的核心逻辑很简单:控制板不断读取电位器反馈的实际角度,与外部输入的PWM信号所代表的目标角度进行比较,误差驱动电机转动,直到反馈角度与目标角度一致。你可以把它理解成一个“带反馈的伺服系统”,而不只是一个“能转的电机”。

参数上,SG90的工作电压通常标称4.8V至6V,推荐5V。无负载时的工作电流约100mA,堵转时能达到650mA甚至更高。力矩方面,在4.8V电压下大约为1.2kg·cm,到6V可以提升到1.6kg·cm左右。虽然数据不大,但用来驱动轻负载的小型云台、轻型机械臂的关节、小车前轮转向等场景完全够用。转速方面,4.8V时约为0.12秒/60度,这个响应速度在普通模型舵机里算是中等水平。

这里有个重点:SG90不是连续旋转电机。它的行程范围约为0°到180°,某些厂商的版本可能略有差异,但绝大多数情况下,不要试图让它转超过这个范围。强行超范围转动会导致电位器卡死,轻则舵机滋滋响,重则齿轮扫齿报废。如果你要做连续旋转的应用——比如驱动小车轮子——那需要的是改装过的360°连续旋转舵机,控制逻辑也不一样。

性能边界方面,还有一个参数容易被忽视:控制信号的刷新频率。SG90的标准PWM信号是50Hz,也就是周期20ms。在这个周期下,控制信号的脉宽范围约为0.5ms到2.5ms,对应0°到180°。严格意义上,0.5ms对应0°,1.5ms对应90°,2.5ms对应180°。这个线性映射关系是整个控制代码的核心尺子,后面所有参数计算都围绕它展开。

2.2 PWM的概念:占空比的“开关灯”类比

PWM(Pulse Width Modulation)的中文叫脉冲宽度调制。从字面看,就是调节脉冲的“宽度”——也就是高电平持续的时间。一个PWM信号由两个核心参数描述:频率(Frequency)和占空比(Duty Cycle)。频率决定了周期长度,占空比决定了高电平在周期中的比例。SG90舵机需要的PWM信号频率是50Hz,占空比则根据目标角度在2.5%到12.5%之间变化——2.5%(0.5ms/20ms)对应0°,12.5%(2.5ms/20ms)对应180°。

为了让你更直观地理解,打个比方:把PWM信号想象成一个人反复开关灯。如果开灯时间占整个周期的10%,那么平均亮度就只有最大亮度的10%;如果开灯时间占90%,平均亮度就是90%。灯的亮度对应PWM输出后的“平均电压效果”。但舵机和灯不一样,它靠的不是“平均电压”,而是“高电平到底持续了多长时间”——即脉宽(Pulse Width)。这个区别非常重要:同样50%的占空比,在50Hz频率下意味着10ms的高电平,在500Hz频率下却只意味着1ms的高电平。后者根本不能让SG90动作。所以对舵机来说,频率是第一决定因素,占空比是在频率确定之后才换算的目标值。

为什么舵机必须用50Hz,而不是更高频率?因为SG90内部的控制电路以20ms周期为基准来解析PWM信号中的脉宽信息。你可以简单地理解为:舵机的“采样周期”就是这么设计的,超出这个频率范围它就不认识。我曾经试过用1kHz的PWM去驱动SG90,结果舵机一顿一顿地抽搐,角度也完全不对。测量信号波形之后才意识到频率完全错了。

2.3 角度与脉宽的换算关系

0°到180°对应0.5ms到2.5ms的脉宽,换算公式可以写成这样:

脉宽脉宽=0.5+角度180×2.0

如果采用50Hz频率,周期为20ms,则占空比为:

占空比占空比=0.5+角度180×2.020×100%

举个例子:想让舵机转到45°,脉宽=0.5 + (45/180)×2.0 = 1.0ms,占空比=(1.0/20)×100%=5%。想让舵机转到135°,脉宽=0.5 + (135/180)×2.0 = 2.0ms,占空比=10%。这个映射关系是所有代码实现的出发点。实际产品中,不同品牌的舵机可能存在微小差异,有的舵机0°对应0.4ms,180°对应2.4ms,但SG90按标准算不会有太大偏差。后面我会讲怎么用代码做“范围校准”,把这种差异吃平。

3. HAL库定时器PWM的配置逻辑:CubeMX与关键参数解析

3.1 为什么选定时器的PWM模式而不是软件延时模拟

实现PWM有很多种方式,最“笨”的是用软件延时控制GPIO翻转。对1个舵机来说,纯粹用延时或许能凑合,但系统的实时性会变得很差——CPU在主循环里被占用得无法抽身处理其他事情。更关键的是,延时方波的时序受中断影响会产生抖动,舵机对这种抖动非常敏感,表现就是角度不够准确、低频晃动。如果你要控制多个舵机,软件延时方案的代码会膨胀成一场灾难。

用定时器的PWM模式则完全不同。定时器硬件自动产生PWM波形,从产生到输出,全程不需要CPU参与。你只需要做一件事:设定好周期(重载值ARR)和比较值(CCR),然后启动PWM输出。之后想改角度,只需修改CCR寄存器,硬件会在下一个周期自动应用新值。CPU的负载几乎为零,波形精准到微秒级别,对舵机来说这是最理想的驱动方式。这也是这个项目用HAL库重做一遍的意义所在——不是为了贴新代码,而是为了建立一套干净、扩展性强的PWM控制框架。

3.2 CubeMX中各参数怎么填:以70MHz系统时钟为例

现在进入实操配置。假设你的芯片系统时钟我已配好为70MHz,且定时器时钟源为内部时钟(RCC的CK_INT),那么要产生50Hz的PWM信号,计算方法如下:

先确定自动重载值ARR。定时器时钟为70MHz,取预分频系数PSC=139,则定时器计数频率为:

计数频率=70MHz/(139+1)=500kHz

每个计数周期 = 1/500kHz = 2微秒。如果ARR=199,则PWM频率为:

PWM频率=500kHz/(199+1)=2500Hz

这对SG90来说太高了。要得到50Hz,需要:

ARR=500kHz/50Hz-1=9999

所以正确参数组合是PSC=139,ARR=9999。PWM频率=(70MHz / ((PSC+1)×(ARR+1))) = 70,000,000/(140×10000)=50Hz。

在CubeMX中操作时的步骤:设置定时器时钟源为Internal Clock,在Parameter Settings里选择Mode为PWM Generation CH1,Pulse设为50(这个值只是初始值,后面代码中修改)。预分频PSC设为139,自动重载值Counter Period设为9999。不过这里有个容易踩坑的陷阱:CubeMX中Counter Period的单位有微秒,但是如果你勾选了“使用内部时钟并设置激活计数模式”的话,这里直接填9999就行,不用考虑单位换算。最保险的做法是这个公式:Counter Period = (定时器时钟频率 / (PSC+1) / 目标频率) - 1。

3.3 HAL库PWM相关API与核心函数的实用注释

配置完成后,HAL库生成的代码会让你调用的API集中在几个固定的函数上。我在下面列出最常用的几个,附上个人翻译成“人话”的注释。

第一个是初始化定时器PWM的底层函数:HAL_TIM_PWM_Init()。它通常在MX_TIMx_Init()内部被调用。这个函数负责设置定时器的时基单元:PSC、ARR、计数模式等。正常情况下你不需要直接调用它,CubeMX生成的代码会帮你处理好。

第二个是启动PWM输出的关键函数:

HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);

这个函数让定时器真正开始产生PWM波形。如果你忘了调用它,GPIO上没有任何输出,舵机一动不动。很多人第一次调试舵机失败,原因就是这个函数没调用。

第三个是动态修改占空比的函数:

__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 75);

这个宏直接修改捕获/比较寄存器CCR的值。例如,PSC=139、ARR=9999时,CCR=50对应0.5ms脉宽(0°),CCR=75对应0.75ms脉宽(22.5°),CCR=150对应1.5ms脉宽(90°),CCR=250对应2.5ms脉宽(180°)。所以你会发现角度控制漂亮地转成了对“比较值”的线性控制。这个转换关系用公式表达就是:

CCR=脉宽(ms)/20ms×10000

脉宽=0.5+角度180×2.0

两者一合并,就是代码里实现角度控制的核心逻辑。你在驱动SG90时,只需要隔一段时间调用一次__HAL_TIM_SET_COMPARE()即可改变角度。

3.4 GPIO复用与内部上拉的心得

另一个值得注意的点是GPIO的复用配置。在CubeMX中,将定时器通道对应的引脚设置为复用功能(Alternate Function),工作模式一般选推挽输出(Push-Pull)。GPIO速度可以设为High,因为PWM信号的上升沿越陡峭,越有利于舵机控制电路识别。即使频率只有50Hz,上升沿如果太缓,会导致舵机内部处理的触发点不稳定,表现为角度抖动。

内部上拉/下拉在PWM输出模式下通常不需要,因为推挽输出本身有足够的驱动能力。但是曾经有人问过我说“不设置上拉舵机会不会乱转”——我的经验是不会。如果你发现舵机在待机状态会朝某个方向缓慢漂移,那多数原因是PWM输出未启动,此时引脚悬空,舵机内部寄存器受到噪声干扰。解决办法是把未使用的PWM通道设置为输出低电平的普通GPIO,而不是把问题归咎于缺少上拉。

4. 硬件接线与供电可靠性:最容易忽略的“幕后元凶”

4.1 接线表与颜色识别

SG90的舵机线通常有3根:棕色(或黑色)是GND,红色是VCC,橙色或黄色是信号线。常见的开发板上,GND接GND,VCC接5V,信号线接定时器通道对应的GPIO。比如你用TIM2_CH1,在部分开发板上对应的引脚是PA0,那信号线就接PA0。这里的“共地”问题要特别说明:如果你的STM32和舵机使用不同的电源供电,必须把两者的GND连在一起,否则PWM信号没有参考地,舵机无法识别电平逻辑。

这个接线问题听着简单,但我遇到过不下三次“舵机不转”但代码和配置都正确的案例,最后都是因为GND没连。每次排查到最后,心情都很复杂,所以我把这个坑放在前面重点强调。凡是涉及外部电源给舵机供电的电路,第一件事就是确认“共地”。

4.2 供电能力与纹波、稳压方案

SG90如果只是空载转一转,一个小型USB口的5V供电就够了。但一旦舵机带负载——比如推动机械臂的关节——启动瞬间的电流会增大,造成电压跌落。如果跌落幅度过大,STM32会直接复位。我曾经遇到的现象是:舵机一动作,整个板子的指示灯就闪一下,程序从头开始跑。后来用示波器测量,发现舵机启动瞬间电源轨跌到了3.3V以下。

这个问题的根源非常典型:**舵机由和单片机相同的USB口供电,且中间没有滤波电容。**这种场景下,舵机转动时产生的反电动势和启动大电流会在电源上产生剧烈波动,直接干扰单片机的稳定运行。

解决方案从简单到可靠排序:

  • 至少加一个470μF或更大的电解电容并联在舵机电源两端,引脚尽量短。
  • 采用独立的5V电源给舵机供电,单片机的3.3V或5V独立输出,两个电源共地。
  • 如果需要更高质量,用一颗线性稳压芯片给舵机单独供电。

你可能会问:“我不加电容行不行?”在短暂实验场景中也许行,但在需要反复动作、长时间运行的项目中,不加电容会让整个系统非常脆弱。我自己的习惯是:凡是项目里有舵机,接线上一定会预留一个电容位。前期调试可以不加,但一轮完整的负载测试之前一定补上。

4.3 信号线要不要串电阻?

关于串电阻,我的实际经验是:在信号线上串联一个100Ω到1kΩ的电阻是个好习惯。SG90的输入阻抗不高,如果GPIO与舵机之间的走线较长,信号边沿会产生过冲;串一个小电阻可以抑制振铃,避免舵机控制板接收到多余的脉冲信号。如果你用杜邦线连接,长度超过10cm,这个电阻的作用会更加明显。

另外,不要在信号线上并联大电容,否则会把PWM的上升沿彻底抹平,导致舵机识别不到脉宽。这是一种“好心办坏事”的操作,需要克制住。

5. 写代码让舵机转起来:从单个角度到连续运动

5.1 核心代码结构与角度映射

在CubeMX配置完成并生成基础代码后,接下来的任务是在业务代码中封装角度控制函数。下面这段代码是我个人项目中的基础模板,可直接参考和使用:

// 舵机角度设置函数 // 输入角度范围0~180 void Servo_SetAngle(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t angle) { if(angle > 180) { angle = 180; } // 计算比较值: 脉宽(ms) = 0.5 + (angle/180)*2.0 // 比较值占比: 比较值 = (脉宽/20ms) * (ARR+1) uint32_t compare = (uint32_t)((1000 + (angle * 1000) / 90) * 10 / 20); __HAL_TIM_SET_COMPARE(htim, channel, compare); }

我简化一下这个计算过程:把脉宽用微秒表示,0.5ms就是500微秒,2.0ms就是2000微秒。angle=0时compare=250;angle=90时是750;angle=180时是1250。但是回忆一下,ARR+1=10000,比较值在ARR范围(0~9999)内,所以这些值都是合法的。上面的公式中(angle * 1000) / 90实际算出的是“0.5ms之上的增量微秒”,乘以10再除以20换算成比较值。这段代码可以直接用,只要你确认自己的ARR+1确实是10000。

5.2 在主程序中控制舵机转动

启动PWM输出和调用角度函数的流程,完整的代码框架是这样的:

int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM2_Init(); // 启动PWM输出 HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); while(1) { // 转到0° Servo_SetAngle(&htim2, TIM_CHANNEL_1, 0); HAL_Delay(1000); // 转到90° Servo_SetAngle(&htim2, TIM_CHANNEL_1, 90); HAL_Delay(1000); // 转到180° Servo_SetAngle(&htim2, TIM_CHANNEL_1, 180); HAL_Delay(1000); // 回到90° Servo_SetAngle(&htim2, TIM_CHANNEL_1, 90); HAL_Delay(1000); } }

这段逻辑简单直观:舵机依次转到0°、90°、180°、90°,每个位置停留1秒。你烧录进开发板后,如果一切正常,看到的应该是一个稳定、无抖动、明确到位的转动过程。如果舵机发出连续的“滋滋”声,说明它在某个方向顶住了——最可能的原因是角度给到了机械限位之外。

5.3 连续平滑运动:用“步进定时”代替粗暴延时

如果你不想让舵机在0°和180°之间来回跳,而是像机械臂那样缓慢划出轨迹,可以改用步进方式。基本思路是:用主循环里的一个小延时,每10毫秒改变一小格角度,比如每次步进1°或2°。这样100°的行程大约需要50次步进,总耗时0.5秒到1秒,看起来就是平滑的转动。

另一种更好的方案是:把角度控制函数放到定时器中断中执行,或者使用操作系统的软件定时器。这样主循环可以处理按键、传感器数据,而舵机角度依然会根据预设的速度曲线更新。不过对初学者来说,先用主循环+延时把逻辑跑通,再逐步腾挪到中断里,是一个有序的进阶过程。

5.4 多个舵机的扩展思路

如果你打算同时控制2个或更多舵机——例如做一个二自由度云台——优先选择定时器的两个不同通道,或者两个不同的定时器。每个通道对应一条独立的PWM输出,但角度控制函数是通用的。要特别注意的是:多个舵机同时启动时,供电电流需求是叠加的。2个SG90同时转,峰值电流可能逼近1A,普通USB口会吃不消。这时候要么使用外部供电,要么限制多个舵机不要同一瞬间启动。

一种稳妥的做法是把多个舵机的启动时间错开:先让通道1转到目标角度,延时100毫秒,再让通道2启动。这个缓冲不仅保护电源,也让机构上的受力更均匀,减少机械振动。

6. 调试过程中的常见问题与排查思路

6.1 舵机完全不动:从电源到代码的排查顺序

舵机完全不动,是新手遇到最多的故障。排查也不是随便换线,而是按顺序来:

第一步,看电源。用万用表测量舵机VCC与GND之间是否有5V左右。如果你用的USB转串口供电,注意有些劣质线材电流上限极低,电压会被瞬间拉低,舵机自然不动。第二步,看信号。用示波器或逻辑分析仪测量GPIO输出。如果有50Hz、占空比可调的方波,说明定时器配置没问题;如果没有或者频率不对,回到CubeMX检查PSC和ARR。第三步,看接线。信号线是否插对了引脚,GND是否共地。这一步产生的故障概率比我预计的高得多。第四步,换舵机。如果你的舵机之前被暴力堵转烧过,控制板可能已经损坏——这种情况下输入引脚上即使有正确的PWM,舵机内部也不会有任何反应。

6.2 角度不准或抖动:用好“标定偏移”与滤波电容

角度不准的原因往往与信号质量或机械结构相关,大致有以下几类:一是舵机本身批次差异,0.5ms和2.5ms对应的不是严格的0°和180°。这时可以在代码中做线性修正,比如把角度映射关系改成:脉宽=0.4 + (角度/180)×2.1。标定的方法很简单:先用一个固定比较值,比如CCR=150,量出实际角度,再反推映射参数,两到三次就能收敛。二是供电电压不足:电压降到4.5V时,舵机的力矩明显下降,带负载后可能无法到达目标角度,表现为停在途中或来回摆动。三是信号抖动:脉冲沿不干净,导致舵机内部的脉宽解析结果不稳定。解决方法是前面提到的串100Ω电阻,并在VCC和GND之间加470μF电容。

6.3 上电瞬间舵机自己转了一下:一个常见但简单的现象

很多人在给板子上电的一瞬间,发现舵机会轻微地动一下。这个现象通常不是故障,而是PWM输出启动前的GPIO电平不确定导致舵机收到一个杂散的短脉冲。如果你不希望它有这一下动作,办法是在初始化阶段先把GPIO配置为低电平,等系统时钟和定时器准备好后,再启动PWM输出。CubeMX中,可以把GPIO初始输出电平设置成Low,或者在你的代码中先调用HAL_GPIO_WritePin()把引脚拉低,再启动PWM。

6.4 舵机突然反向打死:检查机械限位和用户代码

反向打死的情况大多不是代码造成的,而是你给的角度已经超出0°到180°的物理范围。比如你想让舵机从135°快速转到0°,有些SG90在快速运动下会因为惯性冲过头,碰到限位块后开始抖。解决方法是限制单次步进角度,每次不要超过5°,给舵机留出响应时间。同时,避免在舵机带负载时快速来回打满行程,齿轮磨损会急剧上升。

以下是我整理的一张常见问题速查表,方便你直接对照排查:

症状可能原因排查方法解决方案
舵机完全不动未启动PWM输出检查是否调用HAL_TIM_PWM_Start调用启动函数
舵机完全不动电源电压不足万用表测量VCC外接5V电源或加大电容
舵机完全不动接线错误/未共地对照接线表重新接线,确保GND连接
舵机抖动频率不是50Hz示波器测量PWM频率重新计算PSC和ARR
舵机角度不准舵机批次差异实测角度逆推代码中加入标定偏移
舵机堵转滋滋响角度超出机械范围观察转轴位置限制角度到0~180°内
上电瞬间乱动GPIO初始电平不确定观察上电动作初始化时先拉低GPIO
舵机带载无力供电内阻过大测量动态电压独立供电+大电容

7. 这个项目还能怎么扩展:从舵机到整套运动控制系统

当你把SG90控制通了之后,整个“定时器产生PWM + 动态修改比较值”的能力就完全打开了。我强烈建议你沿着这个方向继续做一轮扩展,因为同样的知识框架可以迁移到很多其他应用场景。

第一个方向是LED呼吸灯与亮度调节。你不需要改定时器频率,只需把频率提高到1kHz左右,占空比从0%到100%平滑变化,就能实现柔和的呼吸灯效果。这和舵机控制一样用的是PWM,但关注点从“脉宽时长”转变成了“平均电平比例”。

第二个方向是无源蜂鸣器播放音乐。无源蜂鸣器的音调由PWM频率决定,音长由持续时长决定。通过修改ARR来改变频率,就可以演奏简单的旋律。这一步会让你明白:定时器的ARR不仅可以控制“角度”,还能控制“音高”。

第三个方向是直流电机调速,配合H桥驱动芯片如某款经典双H桥模块,通过PWM占空比控制电机转速。这里面还会引入“死区”和“N-MOS驱动”的概念,属于电力电子和运动控制的交叉区域。

第四个方向是做一个3路舵机控制的简易机械臂,配合蓝牙模块,在手机端发送角度指令。这类项目对综合能力的要求会高一些,涉及通信协议解析、状态机设计、多通道PWM管理,但工程价值也更高。

如果你对这些方向感兴趣,建议按顺序动手做。每做一步,回过来看看PWM的核心公式,你会发现底层的思维方式是贯通的。一块SG90舵机只是一个窗口,窗口背后的硬件定时器才是真正值得掌握的东西。

再回到开头的那句话,这个项目只是“让舵机动起来”,做好之后你会获得两样东西:一个是硬件层面“电源共地、信号干净、时序准确”的工程直觉,另一个是软件层面“配置、启动、动态修改”这套HAL库驾驶肌肉记忆。这两样东西,在后面的每一个嵌入式项目里都会反复用到。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询