
简介本资源是一套面向嵌入式初学者的STM32控制SG90舵机实战代码包聚焦于ARM Cortex-M平台下伺服电机的精确角度驱动适用于电子设计、机器人入门及课程实践等场景。资源共205个文件涵盖34个C源文件含stm32f10x_tim.c、usart.c等标准外设驱动、35个头文件.h、36个编译中间文件.o/.d以及Keil5工程核心文件uvprojx、uvoptx、sct链接脚本等完整呈现基于HAL库或标准外设库的PWM定时器配置、GPIO初始化、角度映射函数及调试配置压缩包大小为4.8MB。已有1497人学习下载内容结构清晰从底层寄存器配置到可直接烧录运行的工程模板一应俱全配套注释详尽便于理解50Hz PWM生成逻辑、脉宽-角度换算关系及常见硬件调试要点。 说实话我刚开始接触嵌入式的时候第一个让我觉得“有点成就感”的项目不是点亮LED也不是跑个串口打印而是让一个SG90舵机转起来。原因很简单LED亮了只能说明程序没写错但舵机转动是“物理世界”的反馈——你能看到机械结构在响应你的代码那种感觉完全不一样。STM32控制SG90舵机这段代码可以说是每个嵌入式初学者绕不过去的一课。网上相关的教程和例程多如牛毛但大多是直接贴一段代码让你复制粘贴很少有人把背后的原理、参数的计算逻辑、硬件连接的注意事项讲清楚。结果就是很多人代码抄过去了舵机却不转或者疯狂抖动甚至直接把舵机烧了然后一脸懵。这篇文章我尽量按照实际做项目的思路来讲从PWM原理到CubeMX配置再到代码实现和踩坑排查争取让你看完之后不只是会复制而是真的理解每一行代码为什么这么写。如果你想玩明白STM32和舵机这篇文章应该能帮你省下不少瞎折腾的时间。1. 舵机控制原理SG90为什么听PWM的话很多初学者第一次接触舵机的时候都会有一个疑问我给它接上电源它为什么不转为什么非要那个黄色的信号线接什么PWM才动这里先花点篇幅把底层原理讲透后面你看代码就会豁然开朗。SG90本质上是一个“位置伺服系统”它内部集成了直流电机、减速齿轮组、电位器角度传感器和控制电路。控制电路通过电位器实时检测当前输出轴的转角然后和你的输入信号做比较哪个方向有误差就往哪个方向转直到消除误差。那“输入信号”是什么就是PWM波的占空比即高电平持续时间。SG90的典型控制要求是PWM周期为20ms即50Hz高电平脉宽在0.5ms到2.5ms之间对应舵机转轴从0°到180°。不同品牌的舵机参数略有差异比如有些是0.5ms~2.5ms有些是1ms~2ms但SG90基本都遵循前者。高电平1.5ms是中间位置90°2.5ms是180°。这里有一个很关键的认知舵机控制系统真正关心的不是“频率”而是“高电平脉宽”。周期20ms只是SG90等模拟舵机的“标准帧率”只要高电平时间是对的哪怕你把周期改成10ms甚至50ms舵机也大概率能转但会有抖动或响应异常不建议。所以后面配置定时器的时候你的核心任务是精确控制比较寄存器里的值也就是CCR而不是纠结那个20ms精确到小数点后两位。需要注意的是角度和脉宽之间的关系是线性的。这个线性关系用公式表达就是angle (pulsewidth - 0.5) / 2.0 * 180反过来给一个角度求脉宽pulsewidth 0.5 angle / 180 * 2.0后面代码里我用的就是这个公式的变体。理解了这个原理你再回头看网上那些舵机控制代码就会发现它们基本都是同一套逻辑先用定时器产生一个50Hz的PWM然后通过修改占空比来控制角度仅此而已。2. 硬件连接没那么简单供电、抖动与复位都是坑如果你直接把SG90的黄线插在STM32的GPIO上红线接3.3V棕线接GND然后下载代码大概率会出现两种情况一舵机完全不动二舵机刚转一下单片机就重启了。这不是代码的问题而是硬件连接踩了坑。2.1 电源必须独立供电SG90堵转电流可以达到650mA左右正常工作电流也在100mA~250mA之间。而STM32开发板的3.3V输出通常只有几十到几百毫安的驱动能力根本喂不饱舵机。更致命的是舵机一转电压会被拉低STM32看门狗或者电压监测电路就会触发复位表现就是“代码好像跑了一会儿然后闪断”。所以正确的接线方式是舵机的电源线红线接外部5V电源信号线黄线接STM32的GPIOPWM输出引脚地线棕线必须和STM32共地。所谓共地就是把舵机电源的负极和STM32的GND连在一起。否则信号线输出的0~3.3V参考的是单片机的GND舵机控制电路参考的是自己电源的GND两者不一致信号就会乱套。2.2 信号电平可以直连很多人会担心SG90的信号线标称5V电平STM32的GPIO输出只有3.3V这样能驱动吗实际测试下来3.3V的高电平对SG90来说是完全可以识别的。绝大多数STM32的GPIO输出高电平也在3.0V以上超过了SG90控制电路的逻辑高电平门槛所以直接连没问题。当然如果你是“严谨派”可以加一个电平转换电路但对于这种轻量级的教学项目完全没有必要。2.3 抖动、啸叫的排查方向如果你发现舵机在某个位置附近高频抖动或者发出“滋滋”的啸叫声常见的原因有三个供电不足或者电源纹波太大舵机的控制电路在反复修正位置形成震荡。这时候可以加一个大电容比如470uF电解电容并在舵机电源引脚上或者换一个输出能力更强的电源适配器。信号线上有干扰特别是PWM线很长又没有屏蔽的时候。把信号线缩短或者远离电机线、电源线这类强干扰源。占空比刚好设置在两个角度之间的临界值舵机在两边犹豫。这种情况通常出现在你用了浮点计算角度映射而精度又不够的时候后面代码部分我会再解释。我在第一次做舵机实验的时候就是因为用了开发板上的3.3V供电舵机一转LED就闪当时我还以为是程序跑飞了检查了半天代码后来才想到去量电压一量直接掉到2.5V人瞬间就清醒了。所以硬件上的坑往往是先于代码出现的。3. 定时器PWM这么配从CubeMX到HAL库代码解读STM32产生PWM有很多种方式——可以用高级定时器TIM1/TIM8可以用通用定时器TIM2~TIM5也可以用基本定时器配合DMA。对于控制舵机来说最常用的是通用定时器的PWM输出模式。我这里用的是STM32F103C8T6蓝板板加HAL库CubeMX配置起来非常直观下面把关键步骤和计算逻辑掰开讲讲。3.1 为什么用定时器PWM而不是软件延时模拟有些新手图省事会想直接用GPIO翻转加delay去模拟PWM。比如输出高电平0.5ms再拉低19.5ms循环往复。这种方式在控制单个舵机、而且对精度要求不高的时候确实能凑合但有几个致命问题第一CPU被占满了期间干不了任何其他事情第二延迟函数的精度受中断影响很大稍微有个定时器中断插进来脉宽就飘了第三如果你要控制多个舵机或者同时处理传感器、显示屏软件模拟PWM根本忙不过来。所以正规做法永远是“硬件PWM输出CPU定时更新占空比”这样CPU大部分时间都在干别的活PWM波由定时器硬件自己产生精确又省心。3.2 CubeMX配置参数计算打开STM32CubeMX选择你的芯片型号进入Pinout界面找到你要用的定时器和通道。比如我用的是TIM3的通道2引脚是PA1。在Timers - TIM3里Clock Source选Internal ClockChannel2选PWM Generation CH2下面设参数Prescaler预分频系数PSC 71Counter Period自动重载值ARR 19999Pulse初始占空比先随便填个500后面代码里会改这里的计算逻辑要搞清楚。STM32F103的系统主频是72MHz也就是1秒钟72,000,000个时钟周期。定时器时钟经过预分频器之后除以PSC1。所以定时器计数频率 72MHz / (71 1) 1MHz也就是说定时器每计数一次耗时1微秒自动重载值ARR 19999意味着计数从0加到19999一个完整周期是20000次计数 20000微秒 20ms20ms正好对应50Hz也就是SG90要求的PWM频率这个组合的好处是因为定时器计数频率是1MHz所以CCR寄存器里的值直接就等于脉宽的微秒数。你要0.5ms脉宽CCR就写500要2.5ms就写2500。后面写代码的时候脑子完全不需要做二次换算非常方便。这个技巧值得记下来以后你控制别的舵机或者做PWM调光都会用到。3.3 初始化代码到底干了什么CubeMX生成的初始化代码看起来有点多核心部分其实就几步。生成工程后默认的MX_TIM3_Init函数会配置好时基和通道然后你要在主函数中手动开启PWM输出HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_2);这句话的意思是启动TIM3的通道2让它开始产生PWM波。如果忘了调用这个函数初始化做得再漂亮引脚上也不会有波形输出。之后控制舵机角度就靠修改比较寄存器__HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, pulse);把pulse改成你想要的高电平微秒数PWM占空比就变了舵机角度也跟着变。HAL库这个宏本质上是往TIM3-CCR2寄存器里写值这个寄存器决定了每次周期内高电平的长度。到这里PWM产生的硬件链路已经打通了。4. 实用的舵机控制代码角度映射与初始化细节硬件的路通了接下来就是写控制代码。这一步其实有点“业务逻辑”的味道我给你一个角度值你怎么把它翻译成上面说的那个pulse值。很多新手看到网上代码里突然冒出来的0.5、2.0、180这些魔法数字一头雾水实际上就是角度和脉宽之间的线性映射。下面用一段完整的HAL库代码来讲。4.1 角度映射实现别滥用浮点先看我实际项目里使用的代码片段// 定义舵机的脉宽边界单位us #define SG90_MIN_PULSE 500 // 0.5ms对应0度 #define SG90_MAX_PULSE 2500 // 2.5ms对应180度 #define SG90_MAX_ANGLE 180 void Servo_SetAngle(TIM_HandleTypeDef *htim, uint32_t channel, float angle) { // 限幅防止超出范围导致舵机堵转 if(angle 0) angle 0; if(angle SG90_MAX_ANGLE) angle SG90_MAX_ANGLE; // 线性映射角度 - 脉宽 uint32_t pulse SG90_MIN_PULSE (uint32_t)((angle / SG90_MAX_ANGLE) * (SG90_MAX_PULSE - SG90_MIN_PULSE)); __HAL_TIM_SET_COMPARE(htim, channel, pulse); }用法很简单主函数里先初始化定时器并启动PWM然后调用Servo_SetAngle(htim3, TIM_CHANNEL_2, 90); // 转到90度 HAL_Delay(1000); Servo_SetAngle(htim3, TIM_CHANNEL_2, 0); // 转到0度 HAL_Delay(1000);这个函数逻辑很直白但有几个细节值得展开说。第一为什么要限幅因为SG90的物理结构决定了它只能转0°到180°超出这个范围齿轮组会撞到机械限位电机继续堵转电流飙升轻则发烫重则烧毁控制板。软件上做限幅是成本最低、最有效的保护手段。第二为什么用uint32_t强转而不是float直接写进去因为__HAL_TIM_SET_COMPARE最终写的是一个整数寄存器。如果只用浮点运算赋值时编译器也会做隐式转换但在某些编译器优化级别下浮点运算可能会引入舍入误差。虽然这个误差对舵机角度来说微乎其微但养成使用整数运算的习惯在嵌入式这种资源受限的环境里总是好的。第三SG90_MIN_PULSE和SG90_MAX_PULSE这两个值不同厂家出厂的舵机可能会有10~20微秒的差异。如果你发现舵机在两端角度不准确可以微调这两个边界值顺便校准一下中点。比如我的舵机在0°时实测脉宽是550us而不是500us那我就把MIN改成550。这种“软校准”在舵机机械臂项目中非常常用比手动去拧舵机输出轴上的螺丝要省事得多。4.2 初始化和主循环的注意事项初始化部分除了刚才说的MX_TIM3_Init()和HAL_TIM_PWM_Start()我还建议在启动后先把舵机放到一个中间位置比如90°HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_2); Servo_SetAngle(htim3, TIM_CHANNEL_2, 90); HAL_Delay(500);为什么不直接转到0°因为很多机械结构比如云台、机械臂在启动瞬间如果舵机猛转会产生很大的惯性冲击容易损伤齿轮组或结构件。从中间位置开始给舵机一个比较温和的起始状态是个好习惯。主循环里还有一个容易犯的错连续多次调用Servo_SetAngle后不延时舵机就会像抽风一样来回甩。因为舵机的物理转动需要时间你给它下了一个目标角度它还没有转到你又下了新指令它就一直追着目标跑表现出来就是抖动和啸叫。所以每次改变角度后至少要留出几百毫秒的稳定时间具体多少取决于舵机的速度和负载SG90空载大约0.1s可以转60°你可以据此估算。5. 踩坑实录舵机不动、抖动、角度跑偏的排查链路这部分是这篇文章的重头戏。我把自己在开发和指导别人过程中遇到的高频问题整理成了一套排查链路按照这个顺序去查绝大多数舵机问题都能定位。直接给出答案没意思我带你们走一遍排查思路。5.1 现象A舵机完全不动第一步先确认代码有没有跑起来。很多新手喜欢把HAL_TIM_PWM_Start写在某个条件分支里结果条件永远不成立PWM根本没启动。建议在Servo_SetAngle前后加上串口打印或者翻转一个LED确定程序执行到了这一行。第二步用万用表量舵机电源引脚电压。正常应该在4.8V~5.5V之间。如果电压低于4.5V检查供电是否独立、电源线是否太细、接线端子是否松动。我之前就遇到过一个案例舵机电源是独立5V但是用了杜邦线串联杜邦线细且接触电阻大舵机一转电压掉到4V直接不动。换了一根粗短的线就好了。第三步用示波器看PA1引脚波形。观察有没有50Hz的方波以及高电平脉宽是否在0.5ms~2.5ms之间。如果你没有示波器也可以用逻辑分析仪再不行就用万用表直流档测平均电压。理论上占空比50%即1ms脉宽时平均电压约1.65V。如果测到0V或者3.3V说明PWM输出没有生效。第四步检查信号线是否接对引脚。CubeMX里配置的是TIM3_CH2对应PA1但有些人实际杜邦线插的是PA0或者PA2那当然不会输出。这个错误低级但真的常发生我见过不下五次。5.2 现象B舵机抖动或啸叫抖动的问题在前面硬件部分提过这里再补充两个软件层面的原因。第一个是PWM周期不对。有些人改ARR的值比如为了省事把ARR改成999导致周期变成1ms频率1kHz。虽然舵机也能转但明显会更迟钝、更容易抖。解决方法就是严格把周期配到20ms。第二个是占空比落在临界点。比如你设置高电平脉宽2501us而舵机在2500us和2510us之间反复修正。这在用浮点计算角度时可能出现。排查方法是把角度值固定在一个整数比如直接用__HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, 1500)看舵机还抖不抖。如果不抖了说明是映射计算的精度问题那就要检查浮点转整数的舍入逻辑。5.3 现象C角度不准每个角度都偏这种情况通常是脉宽边界值不合适。每个SG90的出厂参数都存在个体差异标称0.5ms对应0°实际可能是0.48ms甚至0.55ms。解决方案就是手动校准先用__HAL_TIM_SET_COMPARE直接写最小脉宽比如500us观察舵机是否在0°如果不是慢慢加减脉宽直到它正好停在0°对最大角度也做同样的操作。然后把你测到的真实边界值更新到宏定义里。这个校准过程会让你的代码在换了一个新舵机后依然准确。如果你的舵机接在带负载的机械臂上偏角还可能因为负载而下垂。那就不是代码能解决的了要考虑力矩更大的舵机或者增加PWM控制频率试试有些数字舵机支持更高刷新率。SG90这种模拟舵机就老老实实做好软校准吧。6. 从单个到多个舵机控制的资源规划与扩展思路跑通了单个舵机控制下一步自然是控制多个舵机做云台、简易机械臂这类项目。这里提几个方向性的问题和思路。6.1 多个舵机的定时器资源分配一个定时器通常有多个独立通道比如TIM3有CH1~CH4理论上可以产生4路PWM也就是说一个定时器最多可以同时控制4个舵机。如果你的项目需要8路舵机比如六足机器人或机械臂那你需要使用两个甚至三个定时器。CubeMX配置的时候记得给每个通道设置不同的Pulse初始值否则启动后所有舵机都会“跳”到同一个角度。这里有个坑要注意不是所有定时器的所有通道都映射到了你想用的引脚上。比如TIM2的CH1是PA0CH2是PA1但PA0、PA1可能已经被别的外设占用了。最好在CubeMX的Pinout视图中直接点引脚看它有哪些复用功能以免后面焊好了板子发现引脚冲突。6.2 让舵机运动得优雅插值与平滑控制如果你让舵机直接从0°跳到180°会看到它“咔”的一声猛甩过去这种突变在机械臂里会造成很大的顿挫甚至影响结构寿命。解决思路是让角度做插值比如从当前角度到目标角度分成100个小步每步间隔20ms执行for(float a current_angle; a target_angle; a 1.0f) { Servo_SetAngle(htim3, TIM_CHANNEL_2, a); HAL_Delay(20); }这种线性插值实现简单但运行起来其实还是有速度突变。更平滑的做法是用S形曲线或者梯形速度规划。这个属于运动控制的范畴新手可以先不必深入了解但至少要意识到“舵机控制不只是发一个角度值这么简单”。6.3 加入角度回读实现闭环控制前面说过SG90内部有电位器检测角度但这个信号没有引出来你就只能“开环”控制发出指令默认舵机转到那个角度但到底转没转到代码是不知道的。如果项目对精度要求高就要换带角度反馈的舵机比如串行总线舵机或者外部加一个角度传感器比如AS5600磁编码器来实时读取舵机输出轴的角度然后做PID闭环。这就是另一个层面的知识了但底子还是“定时器PWM产生”。我个人的建议是先用SG90把一个完整的PWM控制链路彻底打通然后你再去玩AS5600、玩PID、玩机械臂就会顺利很多。毕竟舵机看着简单但它把定时器、PWM、电源设计、机械结构这些嵌入式基本功全串起来了值得认真做好笔记。最后再分享一个小细节控制舵机的时候别忘了给舵机装一个舵盘就是配套的塑料十字臂然后做一个简单的角度指示贴纸。这样你调试的时候眼睛能直观看到角度位置比对着串口数字猜方便得多。用SG90做实验的时候别空转太久没有负载的情况下连续正反转动舵机齿轮磨损比想象中快偶尔也要让它歇一歇。希望我踩过的这些坑可以帮你少走一点弯路。本文还有配套的精品资源点击获取