☰
STM32F103C8T6驱动SG90舵机:PWM源码与调试避坑指南
2026/10/15 17:32:46 网站建设 项目流程

简介:这份源码面向嵌入式初学者与需要快速验证舵机控制的开发者,围绕STM32F103C8T6如何驱动SG90舵机展开,解决从PWM信号生成到角度精确控制的实际问题。资源包共4个文件,包含2个C源文件、1个头文件与1张舵机接口示意图,整体约24KB,其中C文件承担定时器与GPIO初始化、PWM占空比设置及角度换算等核心逻辑,头文件提供对外调用接口,图片则辅助理解接线方式。内容覆盖TIM模块PWM模式配置、预分频与ARR/CCR参数设定、复用推挽输出、中断或轮询更新占空比,以及0°至180°对应1ms至2ms脉宽的换算思路,并附有初始化与设定角度的函数示例。目前已有5073人学习下载,适合作为机器人、遥控模型等场景下舵机驱动的入门参考与调试起点。

1. 从一颗舵机抖动说起:STM32F103C8T6 驱动 SG90 到底难在哪

很多人第一次用 STM32F103C8T6 驱动 SG90 舵机,都会遇到同一个场景:代码烧进去,舵机要么纹丝不动,要么像抽风一样来回抖,要么转到一个角度就卡死。你以为是舵机坏了,换一个还是这样。问题往往不在舵机,而在于你给它的 PWM 信号不对。SG90 这类微型舵机靠的是周期 20ms、高电平 0.5ms 到 2.5ms 的脉冲来定位,对应 0 到 180 度。STM32F103C8T6 的定时器完全能产生这个信号,但时钟树配置、预分频、自动重装载值这三个参数只要有一个算错,输出频率就偏了,舵机自然不听话。这篇笔记就围绕「STM32F103C8T6 驱动 SG90 舵机源码」这件事,把定时器 PWM 的配置逻辑、源码结构、参数计算和实际调试中会踩的坑一次讲透,适合刚上手 STM32 的嵌入式新手,也适合想把这套驱动整理成可复用模块的熟手。

2. 先搞懂 SG90 的时序要求,再谈 STM32 定时器怎么配

2.1 SG90 的控制信号不是普通 PWM,是带死区的位置脉冲

SG90 内部有一块小控制板,它只认一种信号:周期固定 20ms,高电平宽度在 0.5ms 到 2.5ms 之间变化。0.5ms 对应 0 度,1.5ms 对应 90 度,2.5ms 对应 180 度。这个高电平宽度就是舵机角度的唯一依据,周期偏差一点没关系,但脉宽偏差超过 0.1ms,角度就会明显跑偏。很多新手用普通 PWM 输出 50Hz、占空比 50% 的信号,舵机确实会动,但动到中间就停,因为 50% 占空比对应 1.5ms 脉宽,也就是 90 度,它当然只停在中间。所以驱动 SG90 的核心不是「输出 PWM」,而是「输出脉宽可精确控制的 50Hz PWM」。

STM32F103C8T6 有 4 个通用定时器,TIM1 是高级定时器,TIM2、TIM3、TIM4 是通用定时器。驱动 SG90 用任意一个都行,我一般选 TIM3 或 TIM4,因为它们的通道多,配置起来也简单。关键是要让定时器工作在 PWM 模式 1,并且把计数频率设到 1MHz,这样每个计数就是 1us,写 CCR 寄存器时直接填 500 到 2500 就对应 0.5ms 到 2.5ms,非常直观。

2.2 时钟树、预分频、重装载值:三个参数决定脉宽精度

STM32F103C8T6 的系统时钟一般配到 72MHz,定时器挂在 APB1 上,APB1 的时钟是 36MHz,但定时器有个倍频器,实际给定时器的时钟是 72MHz。这一点很多人会忽略,以为 APB1 是 36MHz 就给定时器算 36MHz,结果频率差一倍。

要让定时器计数频率为 1MHz,预分频器 PSC 应该设为 72-1=71。自动重装载值 ARR 决定 PWM 周期,周期 20ms 对应 20000 个计数,所以 ARR 设为 20000-1=19999。这样定时器每 20ms 溢出一次,CCR 值直接写 500 到 2500 就是 0.5ms 到 2.5ms 的高电平。公式很简单:PSC = 72 - 1,ARR = 20000 - 1,CCR = 500 ~ 2500。

注意:如果你用的是 TIM1 或 TIM8 这类高级定时器,还需要额外使能 MOE(主输出使能),否则 PWM 出不来。TIM2 到 TIM5 不需要这一步。

2.3 为什么不用软件延时模拟 PWM

有人图省事,用 GPIO 加 delay 函数手动翻转电平来模拟 PWM。这种做法在驱动 SG90 时几乎必翻车。因为软件延时会被中断打断,一旦有串口接收、定时器中断或者其他任务插入,脉宽就会抖动,舵机跟着抖。而且软件延时占用 CPU,你没法同时做别的事。用硬件定时器输出 PWM,CPU 只需要在改变角度时写一次 CCR,剩下的事定时器自己完成,稳定且不占资源。这是驱动 SG90 最稳妥的方案,没有之一。

3. 从零写一份可复用的 SG90 驱动源码

3.1 工程结构:把舵机驱动拆成独立模块

我一般会把 SG90 驱动做成两个文件:sg90.c和sg90.h,放在Hardware文件夹下。这样主函数里只需要调用SG90_Init()和SG90_SetAngle()两个接口,换引脚或者换定时器时只改驱动文件,不用动业务代码。下面是一个基于 TIM3 通道 1(PA6)的完整实现。

// sg90.h #ifndef __SG90_H #define __SG90_H #include "stm32f10x.h" void SG90_Init(void); void SG90_SetAngle(float angle); #endif
// sg90.c #include "sg90.h" // 舵机初始化:TIM3 通道1,PA6 输出 void SG90_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 1. 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 2. 配置 PA6 为复用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 3. 定时器基础配置:72MHz / 72 = 1MHz,ARR = 19999 -> 20ms 周期 TIM_TimeBaseStructure.TIM_Period = 19999; TIM_TimeBaseStructure.TIM_Prescaler = 71; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); // 4. PWM 模式 1 配置 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 1500; // 默认 1.5ms,90 度 TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM3, &TIM_OCInitStructure); // 5. 使能预装载,避免写 CCR 时出现毛刺 TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM3, ENABLE); // 6. 启动定时器 TIM_Cmd(TIM3, ENABLE); } // 设置角度:0 ~ 180 度,对应 CCR 500 ~ 2500 void SG90_SetAngle(float angle) { uint16_t ccr; if (angle < 0) angle = 0; if (angle > 180) angle = 180; // 线性映射:angle / 180 * 2000 + 500 ccr = (uint16_t)(500 + angle * 2000.0f / 180.0f); TIM_SetCompare1(TIM3, ccr); }

这段代码的逻辑很直接:先开时钟,再把 PA6 配成复用推挽,接着算定时器参数,最后配 PWM 通道并启动。SG90_SetAngle里做了角度限幅,防止传入超范围值导致 CCR 超出 500 到 2500,舵机堵转发热。映射公式用的是浮点运算,虽然 STM32F103 没有硬件浮点,但一次角度设置只算一次,开销可以忽略。

3.2 参数计算:为什么 PSC 是 71,ARR 是 19999

定时器的计数频率公式是:计数频率 = 定时器时钟 / (PSC + 1)。STM32F103C8T6 在标准库默认配置下,TIM3 的时钟是 72MHz,所以72MHz / (71 + 1) = 1MHz,每个计数 1us。PWM 周期公式是:周期 = (ARR + 1) / 计数频率,代入(19999 + 1) / 1MHz = 20ms,正好是 SG90 要求的周期。CCR 值直接对应高电平时间,因为计数频率是 1MHz,CCR=500 就是 500 个计数,即 0.5ms。

如果你用的不是 72MHz 时钟,比如某些低功耗场景把系统时钟降到 48MHz,那 PSC 就要改成 47,ARR 改成 19999 仍然成立,因为 48MHz / 48 = 1MHz。关键是保证计数频率为 1MHz,这样 CCR 和脉宽的对应关系才不变。

3.3 主函数调用与角度扫描测试

写一个简单的测试程序,让舵机从 0 度转到 180 度再转回来,用来验证驱动是否正常。

#include "stm32f10x.h" #include "sg90.h" #include "delay.h" int main(void) { float angle = 0; uint8_t dir = 1; Delay_Init(); SG90_Init(); while (1) { SG90_SetAngle(angle); if (dir) { angle += 1; if (angle >= 180) dir = 0; } else { angle -= 1; if (angle <= 0) dir = 1; } Delay_ms(20); // 每 20ms 更新一次,给舵机留出响应时间 } }

这里每 20ms 更新一次角度,和 PWM 周期同步。如果更新太快,舵机内部的控制电路来不及响应,会出现嗡嗡声或者抖动。实际项目中,舵机角度更新频率建议在 20ms 到 50ms 之间,具体看舵机负载。空载时可以快一点,带载时最好慢一点,避免电流冲击。

4. 调试 SG90 时最容易翻车的几个地方

4.1 舵机不动,但示波器看 PA6 有波形

现象:用示波器测 PA6 有 50Hz 方波,但舵机就是不转,或者只轻微动一下。 原因:波形幅度不够或者驱动能力不足。STM32 的 GPIO 输出高电平是 3.3V,而 SG90 的信号线通常期望 5V 逻辑。虽然很多 SG90 在 3.3V 下也能识别,但个别批次就是不行。 解决:用逻辑电平转换电路,或者直接给舵机信号线加一个上拉电阻到 5V。更稳妥的做法是用一个 NPN 三极管做电平转换,基极接 STM32 引脚,集电极接 5V,发射极接舵机信号线。

4.2 舵机抖动严重,角度定不住

现象:舵机到达目标角度后来回小幅抖动,用手轻推能感觉到齿轮在反复调整。 原因:PWM 信号受到干扰,或者电源功率不足。SG90 堵转电流能到 700mA 以上,如果和 STM32 共用一路 LDO,电压会被拉低,导致舵机内部电路复位重启。 解决:舵机电源单独供电,或者至少加一个大电容(470uF 以上)在舵机电源引脚附近。信号线尽量短,远离电机电源线。另外检查TIM_OC1PreloadConfig是否使能,没使能的话写 CCR 时可能产生窄脉冲毛刺。

4.3 角度线性度差,0 度和 180 度到不了

现象:写 0 度时舵机停在 10 度左右,写 180 度时停在 170 度左右。 原因:SG90 的机械限位和个体差异。便宜舵机的实际脉宽范围可能不是标准的 0.5ms 到 2.5ms,而是 0.6ms 到 2.4ms。 解决:在代码里做校准。先写 500 和 2500,观察舵机实际能到的极限位置,然后记录对应的 CCR 值,用这两个值做线性映射。比如实测 0 度对应 CCR=600,180 度对应 CCR=2400,那就把映射公式改成ccr = 600 + angle * 1800 / 180。

4.4 多个舵机同时驱动时互相干扰

现象:单独驱动一个舵机正常,同时驱动两个以上时,舵机乱转或者复位。 原因:电源电流不够,或者定时器通道配置冲突。 解决:每个舵机单独供电,共地即可。如果用同一个定时器的不同通道,确认每个通道的 CCR 寄存器独立设置,不要共用同一个变量。另外注意 GPIO 引脚是否被其他外设占用,比如 PA6 同时是 SPI1_MISO,如果开了 SPI1 就会冲突。

4.5 烧录后第一次上电舵机猛转一下

现象:每次复位或上电,舵机都会先转到某个极限位置再回到设定角度。 原因:定时器初始化完成到第一次写入 CCR 之间,输出比较寄存器默认值是 0,导致输出一个极窄脉冲或者一直低电平,舵机把它当成 0 度信号。 解决:在TIM_Cmd之前先调用一次TIM_SetCompare1写入目标角度对应的 CCR 值,再启动定时器。这样第一个 PWM 周期就是正确脉宽,舵机不会乱动。

5. 把驱动做成可复用模块的几个进阶技巧

5.1 用宏定义切换定时器和引脚

如果你手头有多块板子,舵机接的引脚不一样,每次都改驱动文件很麻烦。我习惯在sg90.h里用宏定义把定时器、通道、引脚、时钟都抽出来。

// sg90.h 可配置部分 #define SG90_TIM TIM3 #define SG90_TIM_CLK RCC_APB1Periph_TIM3 #define SG90_GPIO GPIOA #define SG90_GPIO_CLK RCC_APB2Periph_GPIOA #define SG90_PIN GPIO_Pin_6 #define SG90_OC_CHANNEL 1

然后在sg90.c里用这些宏替换硬编码。换引脚时只改头文件,驱动逻辑不动。这个做法在多个项目之间移植时特别省事,血泪经验是千万别在.c文件里到处写TIM3和GPIO_Pin_6,否则改一次要全局搜索替换,容易漏。

5.2 用定时器中断做多路舵机软 PWM

STM32F103C8T6 的硬件 PWM 通道有限,如果你要驱动 8 个以上的舵机,硬件通道不够用。这时候可以用一个定时器做 1MHz 计数,在中断里手动翻转多个 GPIO 来模拟 PWM。具体做法是:定时器每 1us 中断一次,维护一个 20000 步的计数器,每个舵机有一个目标 CCR 值,计数器小于 CCR 时对应 GPIO 置高,否则置低。这样理论上可以驱动几十路舵机,代价是 CPU 中断开销大。实际测试中,72MHz 主频下驱动 8 路舵机,中断里只做比较和置位,CPU 占用率大约 15%,还能接受。

5.3 角度平滑过渡:别让舵机一步到位

直接写SG90_SetAngle(180)会让舵机全速冲向目标角度,齿轮冲击大,噪音也大。更好的做法是做一个缓动函数,每隔 20ms 把当前角度向目标角度靠近一小步。

void SG90_SmoothMove(float target, uint16_t step_delay_ms) { static float current = 90.0f; float step = 1.0f; while (current < target - step || current > target + step) { if (current < target) current += step; else current -= step; SG90_SetAngle(current); Delay_ms(step_delay_ms); } SG90_SetAngle(target); }

这个函数每次只移动 1 度,配合 20ms 延时,舵机转动非常顺滑。步长和延时可以根据舵机负载调整,轻载时步长可以到 2 到 3 度,重载时降到 0.5 度。注意这个函数是阻塞的,实际项目中最好放到定时器中断里做状态机,避免占用主循环。

5.4 验证驱动是否正常的三个检查点

第一,用示波器或者逻辑分析仪抓 PA6 波形,确认周期 20ms、高电平 0.5ms 到 2.5ms 随角度变化。第二,写一个固定角度,用手机慢动作视频拍舵机臂,看它是否稳定停在目标位置,没有来回抖动。第三,连续快速改变角度 100 次,摸舵机外壳温度,如果明显发烫,说明脉宽范围超了或者电源有问题。这三个检查点能覆盖九成以上的驱动问题。

我自己的习惯是,每次新画一块板子,先把舵机驱动单独跑通,用示波器确认波形无误,再接其他外设。这个顺序能省下大量排查时间,因为舵机对电源和信号质量太敏感,一旦和其他外设混在一起调试,很容易把问题归错方向。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询