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

发布时间:2026/10/22 11:46:51
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板子的GNDSTM32的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的原理是一样的时钟源选择STM32F103C8T6的系统时钟最高72MHzTIM2挂载在APB1总线上通过时钟树计算后TIM2的时钟源是72MHz预分频器PSC负责把72MHz分频到我们想要的计数频率自动重装载寄存器ARR决定计数周期也就是PWM的周期比较寄存器CCR决定高电平持续的时间。关键计算逻辑是这样的。舵机要20ms的周期也就是0.02s那PWM的频率就是50Hz。72MHz的时钟经过预分频PSC71也就是实际分频72倍后定时器计数频率变成1MHz每计数一次是1微秒。然后设置ARR19999也就是从0数到19999一共20000次20000微秒等于20ms正好一个周期。在这个配置下比较值CCR的单位直接就是微秒。想让舵机转到90°脉宽1.5msCCR就是1500想让舵机转到0°脉宽0.5msCCR就是500想让舵机到180°脉宽2.5msCCR就是2500。一切清清楚楚。这里有一个算的时候容易糊涂的地方预分频器PSC的寄存器和实际分频值差1。PSC71意味着72分频PSC239意味着240分频。记这个规律就行PSC寄存器的值加1才是实际分频数。4. 基础PWM输出代码从CubeMX配置到让舵机转到任意角度我现在以HAL库为例用STM32CubeMX生成初始化代码然后再手工补上我们的控制逻辑。这个流程最接近现在的主流工程习惯也比手写寄存器易懂。4.1 CubeMX里的关键配置步骤新建一个STM32F103C8T6的工程然后进入Pinout配置界面找到PA0引脚把它设置为TIM2_CH1的复用功能左侧Categories里选择Timers然后选中TIM2在Parameter Settings里把Clock Source选为Internal ClockPRCPrescaler填71这是72MHz时钟的预分频值Counter Period填19999周期20msAuto Reload Preload选EnablePWM 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) { // 脉宽映射angle0时脉宽500usangle180时脉宽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输出也启动了但舵机就是不动或者轻微抖动或者像抽搐一样来回闯。排查思路如下先确认信号线上确实有PWM波形用示波器或者逻辑分析仪看PA0引脚的高电平脉宽确认示波器接地夹子和舵机GND之间的参考关系如果信号正常那基本锁定是供电。把舵机电源换成独立的5V电源一定要和单片机共地问题多半能解决。我还遇过一个比较隐蔽的情况舵机接独立5V电源后刚开始能转但转了几轮之后越来越慢最后干脆不动了。查来查去发现是那个面包板电源模块的5V输出在负载大了之后电压跌落到4.2V而SG90的堵转保护机制让它在电压不足时拒绝工作。换了一条更粗的杜邦线从电源模块直接给舵机供电问题就消失了。导线压降在小电流场景下没人会在意但在舵机这种瞬时电流大的负载上差的可能就是那0.5V的电压。5.2 坑二PWM输出高电平是3.3VSG90能不能识别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周期脉宽映射角度定时器无阻塞输出”的组合都会一直陪着你。把这套东西吃透之后再看那些复杂的舵机控制板或者舵机驱动库你就会发现底层用的还是同一套逻辑区别只是外面包了一层协议和处理逻辑。