三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

STM32 HAL库驱动SG90舵机:从PWM原理到多路控制与调试

STM32 HAL库驱动SG90舵机:从PWM原理到多路控制与调试

1. 项目概述:从零开始驱动SG90舵机

最近在做一个需要精确角度控制的小玩意儿,核心就是用一块STM32F103C8T6(也就是大家常说的“蓝桥杯”或“最小系统板”那块核心芯片)来控制一个SG90舵机。网上资料虽然多,但很多是基于标准库的,或者讲得比较零散。我这次全程用的是STM32CubeMX配合HAL库来开发,感觉这条路对新手和快速原型开发特别友好。这篇文章,我就把从原理到代码、从配置到调试的完整过程,以及我踩过的几个坑,详细记录下来。无论你是刚接触STM32和舵机的学生,还是想快速验证某个机械结构可行性的工程师,这篇实操笔记应该都能给你一个清晰的参考。

SG90舵机是一种位置(角度)伺服驱动器,在机器人关节、摄像头云台、模型控制等领域应用极广。它的控制信号是周期为20ms(50Hz)、脉宽在0.5ms到2.5ms之间的PWM波。STM32F103C8T6作为一款经典的ARM Cortex-M3内核MCU,其丰富的定时器资源天生就是为产生精准PWM信号而准备的。而HAL库,作为ST官方主推的硬件抽象层库,通过STM32CubeMX进行图形化配置,能极大简化初始化流程,让我们更专注于应用逻辑。接下来,我会拆解整个项目的设计思路、HAL库的PWM配置细节、角度到脉宽的换算、以及如何让舵机动作更平滑。

2. 核心硬件与原理深度解析

2.1 主角介绍:STM32F103C8T6与SG90舵机

STM32F103C8T6属于STM32F1系列的“中等容量”产品,拥有64KB Flash和20KB SRAM,主频最高72MHz。它内置了多达3个通用定时器(TIM2, TIM3, TIM4)和1个高级定时器(TIM1),每个定时器都有多个通道可以独立输出PWM,驱动几个舵机绰绰有余。我选择它,一是因为性价比极高,二是因为其生态成熟,资料和社区支持都非常好。

SG90舵机是一个微型舵机,工作电压通常在4.8V~6V,但很多情况下5V供电也能正常工作。它有三根线:红线(VCC,电源正极)、棕线或黑线(GND,电源负极)、橙线或黄线(信号线)。其核心控制原理是脉宽调制(PWM)。舵机内部有一个控制电路,它会测量输入信号线的脉冲高电平持续时间(即脉宽),并将这个脉宽映射到一个特定的输出轴角度。

注意:务必确保给舵机供电的电源有足够的电流输出能力。单个SG90在空载时工作电流约100-200mA,但在堵转(轴被卡住无法转动)时,电流可能瞬间飙升到500mA以上。如果直接用开发板的5V引脚给多个舵机供电,很可能导致开发板复位或烧毁USB口。建议使用外接的5V稳压电源(如手机充电器加降压模块)或专用的舵机驱动板来供电。

2.2 PWM控制舵机的原理与关键参数

SG90舵机期望的控制信号是一个周期固定为20ms(即频率为50Hz)的方波。在这个周期内,高电平的持续时间(脉宽)决定了舵机的角度。这是一个线性映射关系:

  • 0.5ms的脉宽对应舵机0度位置(通常是逆时针极限)。
  • 1.5ms的脉宽对应舵机90度位置(中间位置)。
  • 2.5ms的脉宽对应舵机180度位置(顺时针极限)。

这个关系是线性的,因此我们可以推导出公式:目标脉宽(ms) = 0.5ms + (目标角度 / 180) * 2.0ms。在代码中,我们最终需要操作的是定时器的“比较寄存器”值,所以必须将这个时间值转换为定时器计数器的计数值。

这里就引出了定时器配置的核心参数:ARR(自动重装载寄存器)PSC(预分频器)。定时器的计数频率F_cnt=MCU主频/(PSC + 1)。计数器从0计数到ARR,然后溢出归零,形成一个计数周期。一个PWM周期对应的计数器总值就是ARR + 1。我们需要让(ARR + 1) / F_cnt = 20ms,同时,控制角度的比较值CCRx满足CCRx / F_cnt = 目标脉宽

举个例子,如果MCU主频是72MHz,我们设置PSC=71,则分频后计数频率F_cnt = 72MHz / (71+1) = 1MHz,即计数器每1us加1。为了得到20ms的周期,我们需要ARR满足(ARR+1) * 1us = 20000us,所以ARR = 19999。此时,0.5ms脉宽对应的比较值CCRx = 500,2.5ms脉宽对应的CCRx = 2500。这样,我们只需要在代码中设置CCRx在500~2500之间变化,就能控制舵机在0~180度之间转动。

3. 开发环境搭建与工程配置

3.1 工具链准备:STM32CubeMX与Keil MDK

我使用的开发环境是STM32CubeMX v6.11搭配Keil MDK-ARM v5.37。STM32CubeMX是ST官方出品的图形化配置工具,可以直观地配置引脚、时钟、外设等,并生成HAL库初始化代码,能避免大量底层寄存器操作,提升开发效率。Keil则是经典的编译和调试环境。

首先,在CubeMX中新建一个工程,选择MCU型号为STM32F103C8Tx。接下来进行关键配置:

  1. 系统核心(SYS):在“Debug”下拉菜单中,选择“Serial Wire”。这是因为F103C8T6支持SWD调试(仅需SWDIO和SWCLK两根线),这比传统的JTAG占用引脚更少。如果你使用了ST-Link V2调试器,这个配置是必须的。

  2. 时钟配置(RCC):在“High Speed Clock (HSE)”选择“Crystal/Ceramic Resonator”。我们的最小系统板外部通常有一个8MHz的晶振,需要在这里启用它。然后转到“Clock Configuration”标签页,将系统时钟源切换到PLL,并调整倍频系数,使系统时钟(SYSCLK)达到72MHz。这是F103系列在标准供电下的最高主频,能提供最佳性能。

  3. 定时器配置(TIM):这是本项目最核心的一步。假设我们使用TIM3的通道1(对应引脚PA6)来输出PWM。在左侧引脚图找到PA6,将其功能设置为“TIM3_CH1”。然后在左侧外设列表找到TIM3,进行模式配置。

    • Clock Source:选择“Internal Clock”(内部时钟)。
    • Channel1:选择“PWM Generation CH1”。
    • Parameter Settings标签页:
      • Prescaler (PSC - 16 bits value):设置为71。计算逻辑:72,000,000 Hz / (71+1) = 1,000,000 Hz (1MHz)。
      • Counter Mode:Up(向上计数模式)。
      • Counter Period (ARR - 16 bits value):设置为19999。计算逻辑:PWM周期 = (19999+1) / 1,000,000 = 0.02秒 = 20ms。
      • Internal Clock Division (CKD):No Division。
      • auto-reload preload:Enable。这个选项建议启用,它允许在下一个更新事件时才将新的ARR值载入,可以防止在修改周期时产生毛刺。
    • 下方Pulse的值可以先设为1500(对应1.5ms,90度位置)。这个值后续在代码中动态修改。
    • GPIO Settings标签页可以检查PA6的配置,默认应为推挽输出(Push-Pull),输出速度可以设为“High”。
  4. 工程管理(Project Manager)

    • 设置项目名称和存储路径。
    • 在“Toolchain / IDE”中选择“MDK-ARM V5”。
    • 在“Code Generator”中,我习惯勾选“Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”,这样每个外设的代码会单独成文件,结构更清晰。同时务必勾选“Copy all used libraries into the project folder”,这样工程可以脱离CubeMX环境独立编译。

配置完成后,点击“GENERATE CODE”生成工程代码,然后用Keil打开生成的工程文件。

3.2 HAL库PWM驱动代码剖析

CubeMX生成的代码已经完成了定时器和GPIO的初始化。我们需要在用户代码区域添加控制逻辑。主要用到两个HAL库函数:

  • HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);// 启动TIM3通道1的PWM输出
  • __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pulse);// 动态设置比较值(脉宽)

第一个函数通常在main函数初始化部分,在MX_TIM3_Init()调用之后执行。第二个函数则在我们需要改变舵机角度时调用。

一个常见的需求是让舵机在0度和180度之间往复运动。我们可以编写一个简单的函数来实现:

// 设置舵机角度函数 // htim: 定时器句柄,如 &htim3 // Channel: 定时器通道,如 TIM_CHANNEL_1 // angle: 目标角度,范围0~180 void Servo_Set_Angle(TIM_HandleTypeDef *htim, uint32_t Channel, uint8_t angle) { // 将角度线性映射到500~2500的脉冲计数值 // 公式:pulse = 500 + (angle / 180.0) * 2000 // 为避免浮点运算,使用整数乘除:pulse = 500 + angle * 2000 / 180 uint16_t pulse = 500 + (angle * 2000) / 180; __HAL_TIM_SET_COMPARE(htim, Channel, pulse); }

然后在主循环中调用它,并加上延时,就能看到舵机运动了。

while (1) { Servo_Set_Angle(&htim3, TIM_CHANNEL_1, 0); // 转到0度 HAL_Delay(1000); // 等待1秒 Servo_Set_Angle(&htim3, TIM_CHANNEL_1, 180); // 转到180度 HAL_Delay(1000); // 等待1秒 }

实操心得:直接使用HAL_Delay()进行长时间延时会导致CPU空转,效率低下。在实际项目中,如果系统还有其他任务(如读取传感器、通信),应该使用定时器中断或者RTOS的延时函数来替代HAL_Delay(),实现非阻塞控制。例如,可以设置一个1ms的定时器中断,在中断服务函数里维护一个全局计时变量,主循环中判断时间是否到达,从而控制舵机按特定轨迹运动。

4. 进阶控制与性能优化实践

4.1 多路舵机控制与资源分配

一个定时器通常有4个通道,这意味着一个TIMx理论上可以独立控制4个舵机。例如,我们可以用TIM3的CH1, CH2, CH3, CH4分别控制四个舵机。在CubeMX中,只需将对应引脚(如PA6, PA7, PB0, PB1)都配置为TIM3的通道输出即可。它们的PWM周期(由ARR决定)是共享的,但比较值(CCRx)是独立的,这正是我们需要的。

注意:当需要控制多于4个舵机时,就需要使用多个定时器。F103C8T6的TIM1、TIM2、TIM3、TIM4都可以用来输出PWM。要特别注意引脚的重映射功能,有些通道的默认引脚可能与其他功能冲突,需要查看数据手册的“Alternate function mapping”表格。使用CubeMX可以直观地避免引脚冲突。

4.2 运动平滑处理与速度控制

直接让舵机从一个角度瞬间跳到另一个角度,会产生很大的瞬时电流和机械冲击,可能损坏齿轮或负载。更优雅的方式是进行平滑插值。最简单的实现是线性插值:在起点角度和终点角度之间,以固定的时间间隔(例如每20ms)逐步改变目标角度,直到到达终点。

// 简单的线性平滑移动函数(阻塞式,仅作示例) void Servo_Smooth_Move(TIM_HandleTypeDef *htim, uint32_t Channel, uint8_t start_angle, uint8_t end_angle, uint16_t duration_ms) { int16_t step = (end_angle > start_angle) ? 1 : -1; // 步进方向 uint8_t current_angle = start_angle; uint16_t step_delay = duration_ms / abs(end_angle - start_angle); // 每度间隔时间 if(step_delay == 0) step_delay = 1; // 防止除零 while(current_angle != end_angle) { current_angle += step; Servo_Set_Angle(htim, Channel, current_angle); HAL_Delay(step_delay); } }

这个函数让舵机以大致恒定的速度从start_angle运动到end_angle,总时间为duration_ms。当然,这是一个阻塞式函数。在真实应用中,你应该基于状态机和非阻塞定时器来重构它,避免占用主循环。

4.3 供电与噪声处理实战经验

舵机,尤其是多个舵机同时运动时,对电源系统是一个严峻考验。以下是我总结的几点硬件层面的经验:

  1. 电源隔离:强烈建议将MCU的逻辑电源(3.3V)和舵机的动力电源(5V/6V)分开。可以使用两个独立的稳压模块,或者一个电源输入经过两个不同的LDO/DCDC分别供电。地线(GND)最后在一点共地。
  2. 大电容缓冲:在舵机供电电源的正负极之间,并联一个大容量电解电容(如470uF~1000uF,耐压10V以上)和一个小容量陶瓷电容(如0.1uF)。大电容用于应对舵机启动和堵转时的大电流需求,小电容用于滤除高频噪声。这个电容应尽可能靠近舵机的电源引脚焊接。
  3. 信号线保护:虽然舵机信号线电流很小,但长导线可能引入干扰。可以在MCU的PWM输出引脚和舵机信号线之间串联一个100~220欧姆的电阻,用于限流和阻尼。如果环境干扰严重,可以考虑使用光耦隔离信号。
  4. 软件消抖:在给舵机发送新的角度指令后,可以适当延时10-20ms,等待舵机运动到位、电流稳定后,再进行其他精密操作(如ADC采样),避免电源波动影响其他电路。

5. 调试技巧与常见问题排查

5.1 基础调试:确认PWM信号是否正常

当舵机不转动或转动异常时,第一步永远是确认MCU是否输出了正确的PWM信号。

  • 方法一:万用表测量电压:将万用表打到直流电压档,测量信号线(相对于GND)的电压。当PWM输出时,你应该能测到一个介于0V和3.3V之间的平均电压。舵机在0度(脉宽0.5ms)时,平均电压约为 (0.5/20)*3.3V ≈ 0.08V;在180度(脉宽2.5ms)时,平均电压约为 (2.5/20)*3.3V ≈ 0.41V。改变角度代码,观察平均电压是否相应变化,可以快速判断PWM是否受控。
  • 方法二:示波器观察波形:这是最准确的方法。用示波器探头连接信号线,可以看到清晰的方波。检查关键参数:周期是否为20ms(50Hz)?高电平脉宽是否在0.5ms~2.5ms之间随代码变化?波形是否干净,有没有毛刺?如果周期不对,检查定时器ARR和PSC配置;如果脉宽不对,检查比较值计算和设置函数。

5.2 典型问题与解决方案速查表

下表整理了我调试过程中遇到的一些典型问题及解决方法:

问题现象可能原因排查步骤与解决方案
舵机完全不动,无反应1. 供电问题(电压不足、电流不够、电源接反)
2. 信号线未连接或接触不良
3. PWM未启动输出
1. 用万用表测量舵机VCC和GND之间电压,确保在4.8V-6V之间。带载测试。
2. 检查杜邦线连接,确保信号线连接到正确的MCU引脚。
3. 检查代码是否调用了HAL_TIM_PWM_Start()。用示波器或万用表法确认信号。
舵机抖动或吱吱叫,不固定位置1. PWM周期不准(不是20ms)
2. 电源功率不足,导致舵机无法到达指定位置
3. 机械负载过重,舵机扭矩不足
1. 用示波器检查PWM周期,严格调整为20ms(50Hz)。
2. 检查电源,尝试单独给舵机供电,或加大电源滤波电容。
3. 卸下负载测试,如果正常,说明需要换更大扭矩的舵机(如MG995)。
舵机角度范围不对(如只能转90度)脉宽范围设置错误检查代码中角度到脉宽计数值的映射公式。确认0度和180度对应的计数值是否为500和2500(在1MHz计数频率下)。
舵机响应迟钝,运动速度慢1. 控制信号更新率太低
2. 平滑移动函数的延时过长
1. 确保主循环运行速度足够快,没有其他耗时任务阻塞。
2. 调整平滑移动函数的步进延时,减少总运动时间。
多个舵机同时运动时系统复位电源瞬间跌落触发MCU的欠压复位(BOR)这是典型的电源问题。必须将舵机电源与MCU电源隔离,并在舵机电源端加大容量储能电容。检查MCU的VDD电压在舵机动作时的波动情况。
代码修改后,舵机行为异常CubeMX重新生成代码覆盖了用户代码在CubeMX生成的用户代码区(/* USER CODE BEGIN xxx *//* USER CODE END xxx */之间)编写代码,这部分不会被覆盖。

5.3 利用调试器实时监控与修改参数

Keil的调试功能非常强大。你可以在调试模式下,将pulse变量或angle变量添加到“Watch”窗口进行实时监控。更实用的是,你可以在程序运行时,直接修改这些变量的值,舵机会立即响应。这比反复修改代码、编译、下载要快得多,非常适合寻找最优的控制参数(比如某个特定动作的角度序列)。

具体操作:进入调试模式,暂停程序,在“Watch”窗口右键添加要监控的变量。然后可以在“Memory”窗口或直接双击“Watch”窗口中的值进行修改,修改后继续运行程序,就能观察效果。

6. 项目扩展与高级应用思路

掌握了基础的单舵机控制后,这个项目可以朝很多有趣的方向扩展:

  1. 闭环位置控制:SG90是开环控制,它只管“转到指令位置”,但不知道自己是否真的转到了。可以尝试在舵机输出轴上安装一个电位器或绝对值编码器,将实际角度反馈给STM32的ADC,实现真正的闭环PID控制,提高精度和抗干扰能力。
  2. 上位机控制与协议:通过STM32的串口(UART)接收来自电脑(如串口助手、Python脚本)或手机(蓝牙)的指令。可以定义简单的文本协议,例如“#ANG 90\n”表示转到90度,实现远程交互控制。
  3. 构建多自由度机械臂:组合3-6个舵机,通过精心设计的结构件,可以搭建一个多自由度机械臂。核心挑战在于逆运动学求解(给定末端位置,计算每个关节的角度)和轨迹规划。STM32F103C8T6的性能足以进行简单的实时计算。
  4. 融入传感器反馈:结合超声波传感器(HC-SR04)或红外测距,让舵机驱动的云台可以自动跟踪物体或保持与障碍物的距离。结合陀螺仪(MPU6050),可以实现自平衡或姿态控制。
  5. 使用DMA释放CPU:如果需要控制非常多路(如16路)舵机,且要求精确同步,可以考虑使用定时器的DMA功能。将每个通道的比较值(CCRx)存放在一个数组中,通过DMA自动定时搬运到定时器寄存器中,无需CPU干预,可以产生极其稳定且同步的多路PWM信号。

我个人在做一个六足机器人项目时,就用了TIM2和TIM3的四个通道,结合一个PCA9685(16路PWM扩展芯片,通过I2C控制)来驱动总共18个舵机。STM32F103C8T6作为主控,负责运动学解算和协调,PCA9685负责提供稳定的PWM信号,两者分工合作,效果非常稳定。这其中的关键,就是把本文介绍的单路PWM控制原理吃透,然后灵活地应用到更复杂的系统中去。

← 返回列表