news 2026/10/22 11:46:44

STM32F103C8T6驱动SG90舵机:PWM脉宽控制与无阻塞实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103C8T6驱动SG90舵机:PWM脉宽控制与无阻塞实现

1. 先搞清楚SG90到底在等什么信号:脉冲宽度才是控制语言

STM32F103C8T6驱动SG90舵机这件事,很多新手第一次碰就是一脸懵:明明照着网上的代码抄,舵机要么不动,要么抖得像筛子,要么只往一个方向转。问题往往不在代码本身,而是没弄明白SG90舵机的工作原理就急着接线。

SG90是PWM控制的舵机,这里的PWM不是用来调速或者调亮度的那种“大致给个占空比就行”的PWM,而是有严格周期和脉宽要求的信号。它只认一个东西:信号线上高电平持续的时间,也就是脉宽。对于SG90这个180°舵机来说,标准的控制信号是周期20ms的PWM波,也就是频率50Hz,而高电平脉宽从0.5ms到2.5ms变化,对应舵机输出轴从0°转到180°。脉宽越宽,角度越大。

脉宽理论转角占空比(20ms周期)
0.5ms0°2.5%
1.0ms45°5.0%
1.5ms90°7.5%
2.0ms135°10.0%
2.5ms180°12.5%

需要提前说明一个很多教程都不强调的细节:SG90有180°版本和360°连续旋转版本,外观几乎一样,但前者才是根据脉宽转到指定角度,后者是根据脉宽控制旋转速度和方向。本文讲的整套代码和思路只适用于180°版本,如果你手里的是360°版本,接上去转起来别奇怪。

还有一个容易忽略的坑:不同批次、不同厂家的SG90,对脉宽和角度的映射关系会有细微差异。有的舵机在0.5ms脉宽时不一定正好停在0°,可能偏了5到10°。这不是你代码写错了,是舵机本身的机械限位和电位器的线性度决定的。所以写代码的时候不要死磕理论值,先用示波器或者逻辑分析仪确认脉宽,再用实际角度去校准。

我认为理解SG90的原理是整篇教程的基础,因为后面所有的定时器配置、代码计算、故障排查,全都是围绕“20ms周期+脉宽决定角度”这两句话展开的。只要这个逻辑通了,后续的一切都顺理成章。

2. 器材选择和硬件接线:STM32F103C8T6的引脚选哪个,供电怎么解决

先列一下我这次用的整套器材清单,都是最常见的配置,照着买就行:

  • STM32F103C8T6最小系统板(就是我们常说的蓝色药丸板)
  • SG90 180°舵机一个
  • 5V外部电源(我用的是一个面包板电源模块,可以USB供电,能稳定输出5V和3.3V)
  • 杜邦线若干(公母、母母各准备几根)
  • ST-Link V2下载器(也可以用USB转TTL通过串口下载,但ST-Link调试更方便)
  • 面包板一块(方便接线和测量)

硬件设计的部分,这里要重点说供电的问题。SG90舵机工作电压是4.8V到6V,标称5V,但它的工作电流有个特性:启动瞬间电流可能冲到500mA甚至更高,堵转的时候电流能到1A左右。而STM32F103C8T6最小系统板上的板载稳压芯片,一般只适合给单片机和小功率外设供电,如果你直接用STM32板子的5V引脚去给舵机供电,舵机一启动,瞬时大电流会直接拉低整个系统的电压,轻则单片机复位,重则舵机压根转不动,只能在原地嗡嗡叫。

我实测过直接用板子5V给舵机供电的情况:刚上电舵机自己抖了一下,然后单片机就开始无限复位,串口打印乱得一塌糊涂。这就是典型的大电流负载拉垮了电源。所以正确的做法是:舵机用独立的5V电源供电,并且把电源的负极和STM32板子的GND接到一起,也就是共地。信号线上的电平是STM32输出的3.3V,只要和舵机的GND保持同一个参考地,这个3.3V对舵机来说就是有效的逻辑高电平。

关于信号线的选择,SG90的三根线颜色要记清楚:棕色线是GND,红色线是5V电源,橙色线是信号线。不同厂家的线色可能略有差异,我就见过黄色信号线的版本,但红色电源、深色地线这个规律基本不变,实在不确定就看舵机自带的那个三针接口座,上面一般会印引脚名称。

信号线接STM32的哪个引脚,这部分其实很自由。我用的是TIM2_CH1对应的PA0引脚,因为这个引脚引出了最小系统板,接线方便,而且TIM2是通用定时器,PWM资源完全够用。你需要做的就是把这个引脚在代码里配置为复用功能输出,让它输出PWM波。

引脚功能说明
PA0TIM2_CH1输出PWM信号到舵机信号线
外接5V电源正极舵机红线独立电源供电
外接5V电源负极舵机棕线同时接STM32板子的GND
STM32的3.3V或GND—不要用板载电源给舵机供电

接线顺序上有个安全原则:先接GND和电源,最后接信号线。这样舵机先上电完成初始化,再接信号线能避免热插拔信号线时的尖峰干扰。我之前图省事,带电插拔过信号线,结果舵机猛抽了一下,还丢了一个从机的状态,虽然没烧,但确实吓一跳。

3. 为什么用定时器的PWM功能而不是普通GPIO翻转:无阻塞控制才是正解

很多新手最开始驱动舵机时,第一反应是用GPIO输出高电平,然后延时,再拉低,再延时,靠自己模拟一个PWM波形。这个思路在原理上没错,但实际工程里有几个解决不了的问题:

  • 占用了CPU的全部执行时间。舵机需要在20ms周期内持续输出脉宽,而整个脉宽输出期间,CPU都被delay卡死了,这段时间做不了任何其他事。如果项目里还要用串口打印、读传感器、处理按键,整个系统会变成一卡一卡的。
  • 延时精度不够。HAL库的HAL_Delay底层是依靠SysTick,精度在毫秒级别,但SG90的脉宽是0.5ms到2.5ms,中间很多位置需要微秒级的调整。靠延时函数去凑微秒级脉宽,误差会很大,舵机的角度就是飘的。
  • 代码的可维护性差。一旦想实现类似“舵机先转到90°,然后等待传感器触发,再转到180°”这种逻辑,用延时法写出来的代码会非常别扭。

定时器PWM输出的思路完全不一样:配置好定时器的周期和比较值之后,PWM波形由定时器硬件自动生成,CPU彻底被解放出来。你只需要在某个时刻改一下比较寄存器的值,舵机的角度就变了,期间CPU可以干别的事情。这在工程上叫无阻塞控制,是嵌入式系统设计里的基础素养。

具体到STM32F103C8T6上,标准库或者HAL库配置PWM的原理是一样的:

  1. 时钟源选择,STM32F103C8T6的系统时钟最高72MHz,TIM2挂载在APB1总线上,通过时钟树计算后,TIM2的时钟源是72MHz;
  2. 预分频器PSC负责把72MHz分频到我们想要的计数频率;
  3. 自动重装载寄存器ARR决定计数周期,也就是PWM的周期;
  4. 比较寄存器CCR决定高电平持续的时间。

关键计算逻辑是这样的。舵机要20ms的周期,也就是0.02s,那PWM的频率就是50Hz。72MHz的时钟,经过预分频PSC=71(也就是实际分频72倍)后,定时器计数频率变成1MHz,每计数一次是1微秒。然后设置ARR=19999,也就是从0数到19999,一共20000次,20000微秒等于20ms,正好一个周期。

在这个配置下,比较值CCR的单位直接就是微秒。想让舵机转到90°(脉宽1.5ms),CCR就是1500,想让舵机转到0°(脉宽0.5ms),CCR就是500,想让舵机到180°(脉宽2.5ms),CCR就是2500。一切清清楚楚。

这里有一个算的时候容易糊涂的地方:预分频器PSC的寄存器和实际分频值差1。PSC=71意味着72分频,PSC=239意味着240分频。记这个规律就行:PSC寄存器的值加1,才是实际分频数。

4. 基础PWM输出代码:从CubeMX配置到让舵机转到任意角度

我现在以HAL库为例,用STM32CubeMX生成初始化代码,然后再手工补上我们的控制逻辑。这个流程最接近现在的主流工程习惯,也比手写寄存器易懂。

4.1 CubeMX里的关键配置步骤

新建一个STM32F103C8T6的工程,然后进入Pinout配置界面:

  • 找到PA0引脚,把它设置为TIM2_CH1的复用功能;
  • 左侧Categories里选择Timers,然后选中TIM2,在Parameter Settings里,把Clock Source选为Internal Clock;
  • PRC(Prescaler)填71,这是72MHz时钟的预分频值;
  • Counter Period填19999,周期20ms;
  • Auto Reload Preload选Enable;
  • PWM Generation Channel 1设为Enable。

然后在Project Manager里设置好工具链,我一般用MDK-ARM,生成工程代码。

需要提醒的是,STM32CubeMX生成的代码里,定时器PWM是默认不启用的,你需要在main函数里手动调用HAL_TIM_PWM_Start,否则PA0引脚不会有输出。

4.2 让舵机转起来的最小代码

下面是完整的main.c主体代码,我加了详细注释,重点看定时器初始化和角度设置那两段:

#include "main.h" TIM_HandleTypeDef htim2; void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_TIM2_Init(void); // 核心小工具:把舵机转到指定角度(0~180) void Servo_SetAngle(TIM_HandleTypeDef *htim, uint32_t CompareChannel, uint8_t angle) { // 脉宽映射:angle=0时脉宽500us,angle=180时脉宽2500us // 计算式:pulse = 500 + (angle/180) * 2000 uint32_t pulse = 500 + ((uint32_t)angle * 2000 / 180); __HAL_TIM_SET_COMPARE(htim, CompareChannel, pulse); }

这里的换算逻辑看代码就能很直观:LOWA。当angle等于0的时候,pulse等于500;当angle等于180的时候,pulse等于500加上2000,也就是2500。由于CCR的计数频率是1MHz(也就是1计数等于1微秒),这里得到的pulse直接就是微秒数。如果你的角度范围超过0到180,记住把它限定在合法区间内,后面我会讲为什么不能过度超出。

然后看main函数:

int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM2_Init(); // 启动TIM2通道1的PWM输出 HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); while (1) { // 从0°转到180°,每步加10°,停200ms for (uint8_t angle = 0; angle <= 180; angle += 10) { Servo_SetAngle(&htim2, TIM_CHANNEL_1, angle); HAL_Delay(200); } // 从180°转回0°,每步减10°,停200ms for (uint8_t angle = 180; angle >= 10; angle -= 10) { Servo_SetAngle(&htim2, TIM_CHANNEL_1, angle); HAL_Delay(200); } } }

烧录之后,舵机应该会来回摆动,每次跨10°,我一帧一帧观察下来非常稳定。如果舵机没动,优先检查的就是上一节说的供电问题,而不是代码。

4.3 让舵机尽快到达指定位置:阶梯逼近和转动速度控制

上面的代码是每一帧直接给目标脉宽,舵机会以它自己能达到的最大速度冲过去,不是匀速的。如果你需要舵机以肉眼可见的匀速转到指定位置,就需要把目标角度切分成很多小阶梯一点点逼近。

这个逻辑其实不复杂:如果当前角度是0°,目标角度是120°,我们可以每50ms把角度加1°,经过大约6秒慢慢转到120°。代码示例如下:

void Servo_FastTo(Servo_Type *servo, uint8_t target_angle) { while (servo->current_angle != target_angle) { if (servo->current_angle < target_angle) servo->current_angle++; else servo->current_angle--; Servo_SetAngle(servo->htim, servo->channel, servo->current_angle); HAL_Delay(servo->speed_ms); } }

这个函数用了增量逼近的思路,每次只改1°,速度就由speed_ms这个延时间隔决定。不过用阻塞延时的话,在转舵机过程中CPU依然被占用了,如果项目里需要同时处理别的事情,可以把这个逻辑改成用定时器中断驱动,这就是后话了。

5. 我实测踩过的坑:供电不足、抖动、超量程,以及如何用示波器确认信号

前几节都是能跑通的路,这一节专门记录我在实践过程中踩过的坑,也是这类教程很少写但几乎人人都会遇到的问题。

5.1 坑一:舵机原地嗡嗡叫、不动或者乱抖,90%是供电问题

这是最经典的问题。表现形式是:程序烧好了,PWM输出也启动了,但舵机就是不动,或者轻微抖动,或者像抽搐一样来回闯。排查思路如下:

  1. 先确认信号线上确实有PWM波形,用示波器或者逻辑分析仪看PA0引脚的高电平脉宽;
  2. 确认示波器接地夹子和舵机GND之间的参考关系;
  3. 如果信号正常,那基本锁定是供电。把舵机电源换成独立的5V电源,一定要和单片机共地,问题多半能解决。

我还遇过一个比较隐蔽的情况:舵机接独立5V电源后,刚开始能转,但转了几轮之后越来越慢,最后干脆不动了。查来查去发现是那个面包板电源模块的5V输出在负载大了之后电压跌落到4.2V,而SG90的堵转保护机制让它在电压不足时拒绝工作。换了一条更粗的杜邦线从电源模块直接给舵机供电,问题就消失了。导线压降在小电流场景下没人会在意,但在舵机这种瞬时电流大的负载上,差的可能就是那0.5V的电压。

5.2 坑二:PWM输出高电平是3.3V,SG90能不能识别

STM32F103C8T6的GPIO输出电压是3.3V,而SG90的标称控制信号是按5V逻辑设计的。那3.3V够不够?我的实测结论是:绝大多数SG90都能在3.3V高电平下正常工作,但我确实也遇到过一部分批次对3.3V高电平兼容性一般,表现为舵机反应迟钝或者角度不准。

如果你遇到舵机对信号不敏感的情况,可以考虑加一个简单的电平转换电路。最省事的方案是用一个NPN三极管搭一个电平上拉线路:信号线经过三极管控制,集电极接5V上拉,发射极接GND,这样STM32输出3.3V时,三极管导通,舵机收到的是5V,波形反向,反过来配置即可。不过说实话,如果不是严谨的工业项目,我建议先直接3.3V驱动试试,成功率很高,不必一上来就做电平转换。

5.3 坑三:别把舵机掰到极限之外,脉宽超范围会堵转

前面我反复强调脉冲宽度对应0.5ms到2.5ms,这是理论值,其实SG90的机械转角限位通常比电信号的0°到180°略小。如果你在代码里把比较值设置到小于500或者大于2500,舵机内部的电位器已经转到物理尽头,但电机还在使劲往那个方向顶,就会出现堵转。堵转的电流很大,长时间堵转会把舵机内部减速齿轮磨出痕迹,甚至烧毁驱动芯片。

我在实际项目中曾经想做一个稍微超过180°的转向动作,把脉宽设成2600,结果舵机到极限之后不是停下来,而是发出尖锐的蜂鸣声,摸外壳已经明显发烫。所以代码里一定要加角度限幅,哪怕是为了迎合那些转180°刚好的位置需求,也不要去挑战极限:

#define SERVO_MIN_PULSE 500 #define SERVO_MAX_PULSE 2500 uint32_t Servo_ClampPulse(uint32_t pulse) { if (pulse < SERVO_MIN_PULSE) return SERVO_MIN_PULSE; if (pulse > SERVO_MAX_PULSE) return SERVO_MAX_PULSE; return pulse; }

5.4 坑四:用示波器确认波形比自己瞎猜靠谱

我调试舵机的时候,只要遇到异常,第一步永远是拿起示波器看PA0引脚的波形。看两个关键信息:一是PWM的频率是不是50Hz左右,二是高电平脉宽是不是在我设置的区间内。如果频率不靠谱,大概率是预分频或者自动重装载算错了;如果频率正确但是脉宽和代码里设置的CCR不一致,那可能是初始化PWM之后又有人改了定时器的其他寄存器。

没有示波器的话,逻辑分析仪也可以,更便宜的方案是用一个LED串联电阻接到PA0引脚,看LED亮度变化粗判占空比变没变,但精度只够判断“大概有没有PWM”,不够精确测量脉宽。有条件还是建议备一台示波器,或者借实验室的用一用,排查这类问题能快很多。

6. 进一步的功能扩展思路:多个舵机的控制、角度回读和定时器中断

单舵机跑通之后,很多人的下一个需求是驱动多个舵机,或者要求舵机的位置可被系统实时反馈。这里说几个可以直接往上叠加的思路。

6.1 用同一个定时器的多个通道驱动多个舵机

STM32F103C8T6的TIM2有4个通道,如果四个舵机都用同一个周期,那完全可以共用定时器的PSC和ARR,只用四个不同通道输出各自的CCR比较值就行。这样一条定时器资源就能同时控制四个舵机,CPU开销也小。

对应CubeMX里把TIM2的Channel 1到Channel 4都设为PWM Generation,然后代码里分别启动四个通道的PWM,之后分别设置每个通道的CCR,互不干扰。唯一要注意的是,同一个定时器的所有通道PWM频率必须一样,都要50Hz,舵机的周期一致,这个方案才成立。

6.2 舵机转动的加速度控制

之前提到的阶梯逼近是匀速控制,如果要做更细腻的运动,比如机器人关节的平滑起步和收尾,可以引入加速度曲线:角度差的绝对值大时,每帧跨的步长也大,越接近目标角度,每帧跨的步长越小。

简单实现就是在原有阶梯逼近函数里加一个动态步长:

uint8_t step = abs(target_angle - current_angle) / 10 + 1; if (current_angle < target_angle) current_angle += step; else if (current_angle > target_angle) current_angle -= step;

这样舵机开始转动时快,快到位时慢下来,动作看起来就是从急到缓,很有机械感。很多仿生项目的舵机控制都是用这种思路调整运动曲线的。

6.3 角度回读和位置校准

SG90内部有电位器,但市面上买到的SG90没有输出反馈引脚的版本(部分数字舵机才有)。如果你的项目确实需要知道舵机当前的实际角度,要么换带反馈引脚的舵机,要么在舵机输出轴上自己加一个角度传感器,比如磁编码器、电位器或者IMU。STM32F103C8T6的ADC可以用来读取电位器输出,然后换算成角度。这个扩展就不在这里展开讲了,但思路是现成的,遇到需求可以直接套。

6.4 把PWM生成放到中断里实现多任务控制

前面多次提到阻塞延时的问题,如果你要同时控制好几个舵机分组运动,又不想卡CPU,可以把舵机的角度更新逻辑放进一个固定周期的定时器中断里。例如用TIM3产生1ms的更新中断,每次中断里把当前角度向目标角度逼近一格,然后更新PWM比较值。这样主循环就完全不会被舵机控制占用了,传感器读取、串口交互、逻辑判断都可以照常跑。

这种解耦设计,本质是把舵机控制变成后台服务,主程序只需在某个瞬间设置“目标角度”,剩下的交给中断慢慢处理。工程上这个思路可以继续延展到多路舵机、动作队列、状态机,是最适合进阶的方向。

7. 复盘总结:从SG90到其他舵机的迁移方法

SG90只是舵机家族里最入门的一个。掌握了SG90,其他舵机基本就是调整PWM参数的事。常见的MG996R、MG995是金属齿轮舵机,扭矩大很多,控制信号同样是50Hz,脉宽范围也差不多,但工作电流会到几百毫安甚至更高,供电要求更严苛。而无刷数字舵机虽然信号频率可能要求更高,比如300Hz甚至更高,但只要查清手册里的信号规格,用STM32的定时器去配置对应的ARR和PSC,套路完全一样。

从我自己调试舵机的体会来说,最重要的三件事分别是:供电一定要稳,信号频率和脉宽一定要准,角度限幅一定要做。这三条在SG90上适用,在大多数舵机上同样适用。

以后不管是做机器人云台、遥控小车转向、机械臂关节还是自动门模型,这套“20ms周期+脉宽映射角度+定时器无阻塞输出”的组合,都会一直陪着你。把这套东西吃透之后,再看那些复杂的舵机控制板或者舵机驱动库,你就会发现底层用的还是同一套逻辑,区别只是外面包了一层协议和处理逻辑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 11:46:42

三相整流器FCS-MPC模型预测控制Simulink仿真全解析

我在一个整流器预研项目上&#xff0c;最初还是老老实实用电压外环PI加电流内环PI、再配SVPWM调制的老路子。电流内环要解耦&#xff0c;电压外环带宽要和内环拉开&#xff0c;再加上电网电感L估计不准&#xff0c;整定参数花了两天&#xff0c;结果负载突变时电流还是超出了预…

作者头像 李华
网站建设 2026/10/22 11:46:39

Flink本地模式开启WebUI完整指南:调试流处理的核心入口

1. 项目概述&#xff1a;为什么本地模式WebUI是Flink开发绕不开的第一课 刚接触Flink的人&#xff0c;十有八九会在“跑通第一个Job”上卡住半天——不是环境配不起来&#xff0c;就是任务提交后两眼一抹黑&#xff0c;根本不知道它到底跑没跑、卡在哪、数据流到哪一步了。我带…

作者头像 李华
网站建设 2026/10/22 11:44:36

AI Agent开发框架对比:LangGraph、AutoGen、CrewAI与微软Agent选型指南

最近总有人问我同一个问题&#xff1a;LangGraph、AutoGen、CrewAI 和 Microsoft Agent Framework 四个框架到底怎么选。说实话&#xff0c;这个问题很难一两句回答&#xff0c;因为它们只是名字里都带“Agent”&#xff0c;设计哲学和适用场景其实差得很远。我在过去一段时间里…

作者头像 李华
网站建设 2026/10/22 11:44:33

51单片机出租车计价器完整设计:从霍尔传感器到计费算法

“基于51单片机的出租车计价器”——这绝对是我见过最经典的嵌入式入门题目之一。每年都有一大批电子相关专业的学生做这个选题&#xff0c;网上资料却是两极分化&#xff0c;不是流水账式地贴个大段代码&#xff0c;就是甩一张看不清的原理图。很多人做完能点亮数码管就以为大…

作者头像 李华
网站建设 2026/10/22 11:43:34

技能构建方法论:从知识堆积到可迁移技能系统的实操框架

1. 从“skills”这个标题说起&#xff1a;一个被低估的硬核话题 “skills”这个词&#xff0c;乍一看平平无奇&#xff0c;甚至有点太宽泛了。但如果你在技术社区、职场论坛或者个人成长类内容里泡久了&#xff0c;就会发现一个很有意思的现象&#xff1a;越是简单的词&#xf…

作者头像 李华