资讯详情

STM32F103驱动SG90舵机:PWM原理、CubeMX配置与HAL库实战

📅 2026/10/22 11:22:15 | 华诺云谱 👁 阅读
STM32F103驱动SG90舵机:PWM原理、CubeMX配置与HAL库实战
挺久没发纯新手向的教程了。后台一直有人问我手里有块STM32F103C8T6最小系统板想驱动SG90这种180°舵机但网上教程要么只给代码不讲原理要么讲的都是Arduino套到STM32上根本用不了。这篇文章就把这件事彻底讲透从舵机内部原理、PWM信号格式到CubeMX配置、HAL库代码再到供电和抖动这些常见坑全部过一遍。零基础能跟着做出来有基础也能查漏补缺。1. 舵机的控制原理为什么必须是20ms周期的PWM1.1 先从SG90内部说起SG90是模拟舵机内部就是一个微型直流电机加一组减速齿轮再加一个电位器做位置反馈。你能通过PWM控制它转到指定角度本质是利用了舵机内部的控制电路加上反馈闭环——控制电路持续比较输入信号和电位器反馈的位置有偏差就驱动电机往相应方向转直到偏差归零舵机就停在这个角度。这就是为什么舵机不像普通电机那样转个不停你给它一个特定宽度的脉冲它就老老实实待在那个角度还要保持一定的力矩顶住外部负载。这个特性在机械臂、云台、小车转向结构里特别好用。1.2 PWM信号的时间和脉宽SG90需要的是周期20ms对应频率50Hz的PWM信号脉宽在0.5ms到2.5ms之间。脉宽大致角度0.5ms0°1.0ms45°1.5ms90°2.0ms135°2.5ms180°脉宽和角度基本是线性关系。1.5ms正好是中间位置也就是90°。你可能会想为什么不是一个固定周期也能让它转比如给一个高电平持续1ms、低电平持续10ms的波周期是11ms舵机真的也能动很多舵机对周期其实没那么挑15ms到25ms都能工作。但正规用法还是20ms周期频率稳力矩表现和线性度都最好尤其是需要多路舵机协同的时候。注意SG90常见的规格书会写“0°对应0.5ms180°对应2.5ms”但实际不同批次的舵机硬件会有个体差异。有的舵机脉宽到2.3ms就已经是物理极限再大就会堵转、嗡嗡响。后面代码里我会写一个限幅函数专门防这个问题。1.3 定时器怎么产生这个信号STM32F103C8T6主频72MHz产生50Hz的PWM有几种办法。最简单的思路是定时器计数到一定值就翻转电平但这种方式占CPU资源、抖动大毕竟中断里还得做软件计数精度的确不好保证。正确做法是使用定时器的PWM输出模式——硬件自动比较、自动翻转不占用CPU时间。我给这套方案定的参数定时器时钟72MHz预分频PSC71得到1MHz的计数频率也就是每个计数单位1微秒自动重载值ARR199得到200微秒的计数周期再乘以100就是20ms等等这里算出来是20ms吗其实要配合PWM模式里的计数周期。我把定时器配置成PWM模式1输出极性搞对计数到ARR值就溢出重来。脉冲宽度通过CCR寄存器控制写1就是1微秒写500对应的就是0.5ms写2500对应的就是2.5ms。之所以选预分频71而不是72是因为定时器时钟从APB1总线过来是72MHz预分频器是从0开始计数的所以71分频才是72分频效果。这个细节初学容易错我在第三部分详细写。2. 硬件接线与供电这步错了后面全白搭2.1 引脚选择和接线表STM32F103C8T6的定时器资源有TIM1、TIM2、TIM3、TIM4其中TIM1是高级定时器TIM2/3/4是通用定时器。驱动SG90不需要高级定时器的互补输出功能所以用通用定时器就够了。我用的是TIM2的CH1通道对应引脚PA0。如果你想用别的引脚可以查数据手册里的复用功能映射表比如TIM3的CH1是PA6CH2是PA7都是常用的PWM输出脚。接线非常简单三根线SG90引脚颜色接到哪电源正极红色外部5V正极地线棕色外部电源地和STM32共地信号线橙色/黄色STM32的PA0注意看我没有让你把舵机电源正极接到STM32板的3.3V也没有让你接5V引脚直接带。SG90空载电流大约100-200mA堵转时能飙到500-800mA而STM32最小系统板的稳压芯片通常是给MCU供电用的电流余量很小。你要是直接把舵机接到板子的5V引脚上一堵转就可能把板子拉复位严重时稳压芯片烧掉。正确做法是用外部5V电源单独给舵机供电比如手机充电器、5V电源模块或者锂电池组但必须和STM32共地——把外部电源负极接到STM32的GND。如果你的实验台上没有独立5V电源临时用板载5V也不是完全不行但只能空载转一转千万别带负载而且要并一个大电容缓冲稳住电压。2.2 为什么要在电源端并联电容SG90电机堵转瞬间电流变化非常剧烈会让电源电压产生毛刺。即使是独立电源导线本身也有电感长线供电时电压波动会传导到信号电路。我习惯在舵机电源两端并联一个100uF电解电容再并联一个0.1uF陶瓷电容电解电容扛大电流波动陶瓷电容滤高频噪声配合起来效果最好。这是很便宜的器件但能解决很多莫名其妙的抖动和复位问题。别省这个钱也别觉得麻烦焊一个电容也就是两分钟的事。2.3 杜邦线的坑我调试的时候遇到过一种情况舵机信号线用了一根很长的母对母杜邦线结果PWM波形到舵机那边已经衰减变形舵机动一下停一下。问题不是杜邦线质量而是线太长、接触电阻太大而且中间还串了个面包板面包板内部金属弹片的接触电阻本来就不稳定。经验是信号线越短越好实验台上控制在10-20cm以内面包板接触不良是常见的间歇故障源舵机不动的时候先用手按一按接线端子看看是不是接触问题。要是做正式项目直接把线焊到舵机引脚上或者用带锁扣的端子别再用面包板了。3. CubeMX配置五个步骤生成PWM工程3.1 定时器时钟树怎么选STM32F103C8T6的时钟树里通用定时器TIM2/3/4挂在APB1总线上当APB1预分频系数不是1的时候定时器时钟会被强制翻倍成72MHz。CubeMX默认配置下SYSCLK是72MHzAPB1分频系数是2所以TIM2的时钟源就是72MHz。如果你配置时钟到别的频率比如用外部晶振跑64MHz那定时器时钟也要跟着变PSC和ARR的数值就要重新算一遍。所以教程里所有参数都是以72MHz主频为前提。你如果改了主频后面所有定时器参数都要重新计算。3.2 创建工程打开STM32CubeMX选择芯片型号STM32F103C8Tx然后在Pinout Configuration界面里做以下配置在左侧Categories里找到Timers展开TIM2。勾选Channel1作为PWM Generation CH1。在Parameter Settings里把Prescaler设为71Counter Period设为199。在PWM Generation Channel 1里把Pulse设为0意思是初始输出0%占空比等会代码里动态改。如果下面有Polarity选项保持High即可。这里我说一下为什么把Pulse初始设0。因为舵机上电瞬间如果收到一个未知脉宽会猛地甩到一个奇怪角度你如果刚好手拿着舵机会被吓一跳机械限位也可能受损。初始脉宽设0等代码跑起来再给信号安全得多。时钟配置不用改默认就是72MHz我确认一下这点。然后Project Manager里填好工程名Toolchain选MDK-ARM或者你的IDE生成代码。3.3 生成后的工程结构CubeMX生成的代码里定时器初始化逻辑在MX_TIM2_Init函数里会自动配置时基和PWM通道。时钟配置在SystemClock_Config里不要去手动改。用户代码加在main函数的/* USER CODE BEGIN */注释区间里这样下次从CubeMX重新生成代码时不会被覆盖掉。3.4 使能PWM通道这一步很容易漏。CubeMX生成的初始化函数把定时器时基配置好了但PWM输出通道默认是关闭的必须在主函数里调用HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1);很多人生成完工程下载进去舵机没反应就是忘了这一步。只有调用这个函数CCR比较值才会真正映射到引脚上输出PWM波形。4. 代码实现从脉宽到角度的完整控制逻辑4.1 角度到脉宽的换算前面讲过脉宽和角度是线性关系所以换算公式非常简单脉宽值 500 (角度 / 180.0) * 2000解释一下0°对应脉宽0.5ms按1MHz计数频率就是500个计数单位180°对应2.5ms就是2500个计数单位。中间差2000个计数单位均分到180°上每一度对应约11.11个计数单位。在代码里我封装成这样一个函数参数是浮点角度输出是定时器比较值uint32_t angle_to_ccr(float angle) { if(angle 0) angle 0; if(angle 180) angle 180; return (uint32_t)(500 (angle / 180.0f) * 2000); }限幅这一步是硬性要求。前面提到的个体差异问题如果不做限幅舵机转到物理极限还继续被命令往极限外走电机堵转发热长期下来齿轮磨损、控制板烧掉。提示如果你手里的舵机实测角度范围不是标准的0到180比如你发现180°对应实际只有170°可以把公式里的180和2000改成你实测的值。校准方法先写两个脉宽观察实际角度再线性插值算补偿。4.2 主函数实测代码初始化顺序很重要MX_GPIO_Init、MX_TIM2_Init这些由CubeMX生成初始化调用然后启动PWM最后在while循环里控制角度。int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM2_Init(); // 启动PWM输出 HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1); // 初始停在0度 __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, angle_to_ccr(0)); HAL_Delay(500); while (1) { // 从0度匀速转到180度 for (float angle 0; angle 180; angle 1.0f) { __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, angle_to_ccr(angle)); HAL_Delay(10); // 每1度停顿10ms大约1.8秒走完全程 } HAL_Delay(1000); // 到180度后停留1秒 // 从180度转回0度 for (float angle 180; angle 0; angle - 1.0f) { __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, angle_to_ccr(angle)); HAL_Delay(10); } HAL_Delay(1000); // 停留1秒后再次循环 } }这里用的是__HAL_TIM_SET_COMPARE宏直接修改CCR寄存器的值。也有人在main里直接写htim2.Instance-CCR1 xxx效果一样但用标准宏更规范、可读性更好。4.3 关于延时和运动平滑度初学者容易犯一个错用HAL_Delay做循环延时来控制转速。说实话在单线程环境下这样写是能工作的效果也凑合。但HAL_Delay是阻塞式的延时期间你什么都干不了如果需要同时处理按键、传感器数据这段代码就是灾难。进阶方案是状态机加非阻塞延时用定时器或者HAL_GetTick()记录时间戳在中断或主循环里分步执行角度更新。我不会在这篇文章里展开写状态机完整代码但提示一下方向把目标角度、当前角度、加减速时间记录下来每次循环判断时间差是否达到步进间隔再决定是否更新CCR。这种结构在真正的项目里才实用。5. 舵机控制的进阶扩展多路舵机和动态调速5.1 多路舵机怎么接一个舵机玩明白之后自然会想控制两个、三个甚至更多。STM32F103C8T6的TIM2有4个通道TIM3也有4个通道理论上一个定时器就能接4路舵机。原理很简单同一个定时器的所有通道共用同一个时基也就是共用PSC和ARR所以频率肯定一样完全满足50Hz要求。各通道的CCR值独立互不干扰。你只需要在CubeMX里把TIM2的Channel1、2、3、4都勾选成PWM Generation然后分别把它们映射到不同引脚PA0、PA1、PA2、PA3就可以分别控制4路舵机。__HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, angle_to_ccr(angle1)); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_2, angle_to_ccr(angle2)); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_3, angle_to_ccr(angle3)); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_4, angle_to_ccr(angle4));一个我踩过的坑4路舵机同时动作瞬时电流加起来可能超过1A电源问题立即变得非常突出。我建议每路舵机的电源线尽量单独从电源输出端引不要在面包板上串成一串因为面包板的电源轨电阻本来就大4路同时大电流时舵机之间的电压会互相干扰表现出来就是某个舵机抖、某个舵机没力。5.2 动态调速的实现思路前面那个for循环里HAL_Delay(10)是匀速控制延时越小转动越快。有些场景需要加减速曲线比如机械臂启动时慢慢加速、快到目标位置时慢慢减速防止惯性冲击。实现也不复杂不是固定延时而是根据当前角度和目标角度的差值动态调整步进大小。最笨但有效的做法是差值大时步进2度差值小时步进0.5度。这样的效果比匀速控制柔和很多但还不够“丝滑”。更好的方案是用梯形加减速曲线先线性加速到最大速度中间匀速最后减速停住。这个逻辑放到状态机里实现角度步进由速度决定速度由加减速表决定。我不展开写完整代码因为不同项目的负载、舵机力矩都不一样参数要自己调写了反而误导。5.3 舵机角度和PWM的标定技巧同一型号的SG90不同的批次同一个脉宽对应的角度也可能差好几度。比如我手里两个SG90同样给1.5ms脉宽一个在90°另一个在93°。这在单舵机应用里无所谓但在多舵机机械结构比如双足机器人的两条腿里会导致两腿不齐。解决方法是上电后用角度尺实测拿代码给定角度记录实际角度算出一组补偿参数。测量的时候别急着删数据实测下来每个舵机在0°、90°、180°三个点校准就够了线性插值出全范围补偿表。这是做实际项目才用得上的细节但确实值得养成习惯。6. 常见问题排查舵机不动、抖动、乱转怎么办这部分内容是我觉得最有价值的。很多新手写完了代码舵机表现不正常第一反应是代码问题其实硬件层面的坑更多。我按经验频率排序来讲。6.1 舵机完全没反应先看电源灯亮不亮用手转一下舵机输出轴如果转起来很费劲或者完全卡死先怀疑舵机堵转断电检查输出齿轮是不是卡住。如果不卡用手轻轻带动输出轴能感到阻力说明舵机内部控制电路活着。然后把示波器或者逻辑分析仪接到信号线上看有没有波形频率是否50Hz脉宽是否在0.5-2.5ms范围内。没有示波器的话用万用表测信号线对地的电压平均电压应该在0.3V到2.5V之间晃动如果稳定在0V或者3.3V大概率是PWM没输出。代码层面检查三件事HAL_TIM_PWM_Start有没有调用TIM2的引脚映射对不对引脚有没有被复用成其他外设功能。6.2 舵机抖动、嗡嗡响抖动的常见原因有三个电源纹波过大、PWM脉宽不稳定、舵机转到物理极限被堵转。电源纹波引起抖动是因为SG90内部的控制电路对供电电压比较敏感电压下降会导致控制芯片误判信号。解决方法是把供电改成独立电源并联电容。PWM脉宽不稳定通常是代码问题比如中断里频繁修改CCR值导致波形占空比跳变或者你用软件延时模拟PWM导致脉冲宽度时大时小。把产生PWM的任务改成硬件定时器输出中断里只改CCR不改通道开关。堵转引起的嗡嗡声是因为舵机一直想转但转不动电流大、发热快。处理方式是检查角度限幅看看是不是程序里给的脉宽超出了舵机实际物理范围。6.3 舵机乱转、不受控这种情况多半是信号线接触不良或者信号线被电源线干扰了。舵机的三根线如果不做整理信号线和电源线平行走线很长距离电机转动时的电流变化会通过电磁感应耦合到信号线上产生毛刺舵机控制电路就会误判脉宽。我的习惯是信号线用双绞线或者屏蔽线电源线和信号线拉开距离实在不能拉开就让信号线垂直跨过电源线而不是平行长距离走在一起。实验台上这个影响不明显但用杜邦线搭的比较乱的时候也确实遇到过。6.4 代码烧录后没现象但示波器波形正常这种情况有一个容易被忽略的原因复位后CubeMX生成代码默认关闭PWM输出你需要确认HAL_TIM_PWM_Start是否在while之前调用。如果你不小心把它写在while里面程序每次循环都会重新启动一次PWM会有微秒级的打嗝舵机表现为轻微抖动。启动函数放在主函数初始化阶段只执行一次这个细节不仔细看真发现不了。6.5 舵机一上电就转、不受程序控制这个是电源时序问题。STM32先上电、舵机后上电时PA0引脚在MCU初始化期间是高阻态外部噪声可能被舵机当作无效信号舵机会随机转到某个位置。然后MCU初始化完成后才输出PWM舵机跳变到程序设定的初始角度。解决办法要么让舵机电源和STM32同时上电但接一个电源开关等程序初始化完成后再打开舵机电源要么在程序的最早期、甚至在main入口处就先把PA0拉低或输出固定电平避免高阻期。我习惯的做法是初始化阶段先HAL_GPIO_WritePin把PA0拉低然后配置成PWM模式再启动通道这样舵机上电时收到的信号始终是稳定的低电平。7. 写在最后几个我踩过坑之后的习惯驱动SG90这个事难度其实不高但你真的上手时会发现写代码只占20%的时间剩下的时间全在查供电、查接线、查地线。我刚开始做的时候第一次点亮舵机花了整整一个晚上最后发现是面包板上一根杜邦线接触不良。从那之后我养成了几个习惯独立电源、舵机电源端并电解电容、信号线尽量短、所有模块共地、上电前先用万用表量一下电源电压。这几个检查项能帮你避开绝大多数新手期的坑。如果你照着这篇文章做出来之后还想往深了走建议尝试做一个小云台两个舵机配合用ADC采样摇杆电位器的电压作为角度输入让舵机跟随摇杆运动。这个项目能把PWM控制、ADC采样、状态机逻辑全部串起来做完之后你对STM32的定时器使用基本就上手了。到时候再回头看今天这篇文章里提到的各种小细节会有完全不同的感受。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑