简介这份源码面向嵌入式初学者与需要快速验证舵机控制的开发者围绕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-171。自动重装载值 ARR 决定 PWM 周期周期 20ms 对应 20000 个计数所以 ARR 设为 20000-119999。这样定时器每 20ms 溢出一次CCR 值直接写 500 到 2500 就是 0.5ms 到 2.5ms 的高电平。公式很简单PSC 72 - 1ARR 20000 - 1CCR 500 ~ 2500。注意如果你用的是 TIM1 或 TIM8 这类高级定时器还需要额外使能 MOE主输出使能否则 PWM 出不来。TIM2 到 TIM5 不需要这一步。2.3 为什么不用软件延时模拟 PWM有人图省事用 GPIO 加 delay 函数手动翻转电平来模拟 PWM。这种做法在驱动 SG90 时几乎必翻车。因为软件延时会被中断打断一旦有串口接收、定时器中断或者其他任务插入脉宽就会抖动舵机跟着抖。而且软件延时占用 CPU你没法同时做别的事。用硬件定时器输出 PWMCPU 只需要在改变角度时写一次 CCR剩下的事定时器自己完成稳定且不占资源。这是驱动 SG90 最稳妥的方案没有之一。3. 从零写一份可复用的 SG90 驱动源码3.1 工程结构把舵机驱动拆成独立模块我一般会把 SG90 驱动做成两个文件sg90.c和sg90.h放在Hardware文件夹下。这样主函数里只需要调用SG90_Init()和SG90_SetAngle()两个接口换引脚或者换定时器时只改驱动文件不用动业务代码。下面是一个基于 TIM3 通道 1PA6的完整实现。// 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 通道1PA6 输出 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 1MHzARR 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.5ms90 度 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 是 71ARR 是 19999定时器的计数频率公式是计数频率 定时器时钟 / (PSC 1)。STM32F103C8T6 在标准库默认配置下TIM3 的时钟是 72MHz所以72MHz / (71 1) 1MHz每个计数 1us。PWM 周期公式是周期 (ARR 1) / 计数频率代入(19999 1) / 1MHz 20ms正好是 SG90 要求的周期。CCR 值直接对应高电平时间因为计数频率是 1MHzCCR500 就是 500 个计数即 0.5ms。如果你用的不是 72MHz 时钟比如某些低功耗场景把系统时钟降到 48MHz那 PSC 就要改成 47ARR 改成 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 度对应 CCR600180 度对应 CCR2400那就把映射公式改成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 用定时器中断做多路舵机软 PWMSTM32F103C8T6 的硬件 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 次摸舵机外壳温度如果明显发烫说明脉宽范围超了或者电源有问题。这三个检查点能覆盖九成以上的驱动问题。我自己的习惯是每次新画一块板子先把舵机驱动单独跑通用示波器确认波形无误再接其他外设。这个顺序能省下大量排查时间因为舵机对电源和信号质量太敏感一旦和其他外设混在一起调试很容易把问题归错方向。希望帮到你。本文还有配套的精品资源点击获取