STM32 PWM控制舵机:从原理到实战的完整指南
2026/7/30 4:31:53 网站建设 项目流程

1. 项目概述:从信号到动作的精准控制

玩过机器人或者航模的朋友,对舵机一定不陌生。那个能精确转动到指定角度的小家伙,是让模型“活”起来的关键。但你是否想过,我们发送给舵机的,究竟是一个什么样的指令?为什么一个简单的脉冲宽度变化,就能让它乖乖地停在0度、90度或者180度?今天,我们就来彻底拆解这个看似简单却内涵丰富的主题:如何使用STM32的PWM功能来控制舵机。

这不仅仅是写几行代码、配置一下定时器那么简单。背后涉及到对PWM信号本质的理解、对舵机内部工作原理的洞察,以及对STM32定时器外设的精准操控。无论是做机械臂、云台,还是智能小车的转向控制,这都是一个必须掌握的基础技能。我见过不少初学者,代码照着抄能转,但一旦要求换个角度、换个速度,或者遇到舵机抖动、不归零的问题就束手无策了。究其原因,还是对“脉宽调制”这四个字和舵机接收电路之间的“约定”没吃透。

接下来,我会以一个最常见的SG90舵机为例,带你从硬件原理到软件配置,从基础功能到进阶调试,完整走一遍STM32控制舵机的全流程。目标是让你不仅能实现功能,更能理解每一个参数设置的意义,从而具备独立设计和排查问题的能力。无论你是刚接触STM32的新手,还是想巩固底层知识的开发者,这篇内容都会有所收获。

2. 核心原理:PWM与舵机的工作“契约”

在动手写代码之前,我们必须搞清楚两件事:舵机到底认什么样的信号,以及STM32如何产生这样的信号。这就像两个人沟通,必须先统一语言和协议。

2.1 舵机如何解读PWM信号

舵机,本质上是一个带有反馈控制系统的直流电机套件。它内部集成了电机、减速齿轮组、控制电路和一个位置传感器(通常是电位器)。我们用户不需要关心它内部的PID调节过程,只需要给它一个标准的控制信号。

这个标准信号,就是一种特殊的PWM信号。请注意,这里的关键不是频率,也不是占空比的百分比,而是高电平脉冲的绝对宽度。对于最常见的模拟舵机(如SG90),它遵循这样一个“契约”:

  • 控制周期(Period):通常为20ms(即频率为50Hz)。这是一个标准值,舵机期望每隔20ms收到一个控制脉冲。周期允许有一定的误差范围(如18-22ms),但为了稳定,我们尽量贴近20ms。
  • 脉冲宽度(Pulse Width):这决定了舵机的输出角度。在一个周期内,高电平持续的时间长度,对应着特定的角度。
    • 0.5ms脉宽:通常对应舵机的最小角度(如0度)。
    • 1.5ms脉宽:通常对应舵机的中间角度(如90度)。
    • 2.5ms脉宽:通常对应舵机的最大角度(如180度)。

这是一个线性关系。对于180度舵机,脉宽从0.5ms变化到2.5ms,角度从0度线性变化到180度。计算公式可以简化为:目标脉宽(ms) = 0.5 + (目标角度 / 180) * 2

注意:这个“契约”并非绝对。不同品牌、型号的舵机,其中立位置(1.5ms)对应的脉宽可能略有差异,行程范围也可能不是严格的180度。因此,在实际应用中,往往需要通过实测来校准。例如,有些舵机的实际可用脉宽范围是0.5ms-2.4ms,对应0-180度。

2.2 STM32定时器如何生成所需PWM

STM32的通用定时器(TIM2, TIM3, TIM4等)和高级定时器(TIM1, TIM8)都具备强大的PWM输出功能,非常适合用来驱动舵机。

其核心原理是利用定时器的自动重装载寄存器(ARR)和捕获/比较寄存器(CCR)。我们可以这样理解:

  • ARR决定了PWM波的周期。定时器从0开始计数,计到ARR值后归零,重新开始,形成一个周期。
  • CCR决定了PWM波中高电平的宽度(即脉宽)。当计数器值小于CCR时,输出高电平;大于等于CCR时,输出低电平(或根据极性配置相反)。

因此,要产生一个20ms周期、1.5ms高电平的PWM波,我们需要:

  1. 根据系统时钟和预分频器,计算出计数器每增加1所代表的时间。
  2. 设置ARR值,使得ARR * 计数时间 = 20ms
  3. 设置CCR值,使得CCR * 计数时间 = 1.5ms

STM32的硬件PWM模块会自动、精确地维持这个波形,无需CPU频繁干预,极大地解放了主控资源。

3. 硬件连接与工程配置

理论清晰后,我们开始搭建实战环境。这里我以STM32F103C8T6(蓝色药丸核心板)和SG90舵机为例,使用STM32CubeMX进行初始化配置,Keil MDK进行开发。

3.1 必不可少的硬件连接

连接非常简单,但有两个细节至关重要:

  1. 信号线(通常为橙色或白色):连接到STM32的任意一个定时器通道引脚,例如PA8(TIM1_CH1)。
  2. 电源线(红色):连接到5V电源。切记,绝大多数标准舵机的工作电压是5V,不是3.3V!直接接到STM32的3.3V引脚可能导致舵机无法工作或力矩不足。
  3. 地线(棕色或黑色):与STM32共地。

实操心得:电源隔离与滤波舵机在启动和堵转时,电流可以瞬间达到数百mA甚至上安培,这会在电源线上产生较大的电压波动和噪声,严重时可能导致STM32复位或程序跑飞。强烈建议:使用独立的5V电源(如UBEC模块)为舵机供电,或者至少在舵机电源入口处并联一个470μF或更大的电解电容进行滤波。STM32的3.3V则由另一路LDO从该5V降压得到,或使用其他电源,确保数字部分供电干净。

3.2 使用CubeMX配置定时器

STM32CubeMX极大地简化了初始化过程。我们目标是配置TIM3的通道1(PA6引脚)产生50Hz的PWM。

  1. 选择定时器:在Pinout视图中,找到TIM3,将Channel1设置为PWM Generation CH1。这会自动将PA6引脚功能映射为TIM3_CH1。
  2. 配置定时器参数:切换到Configuration标签页,点击TIM3进行配置。
    • Prescaler (PSC - 预分频器):假设系统主频是72MHz(STM32F103常见值)。我们希望计数器时钟慢一些,以便用ARR得到一个较大的周期值。设置PSC = 72 - 1。此时,计数器时钟 = 72MHz / 72 =1MHz,即每微秒计数1次。
    • Counter Period (ARR - 自动重装载值):我们需要20ms的周期。计数器时钟周期是1μs,所以ARR = 20000 - 1。(因为计数器从0开始计数到ARR,总共ARR+1个周期)。
    • Pulse (CCR1 - 捕获比较寄存器初始值):先设为中间值,CCR1 = 1500 - 1(对应1.5ms脉宽)。
    • Counter Mode:Up(向上计数)。
    • AutoReload Preload:Enable(使能ARR预装载,避免更新时产生毛刺)。
  3. 生成代码:配置好工程名、路径和IDE后,生成代码。

这样,CubeMX就帮我们生成了完整的定时器和GPIO初始化代码,PWM波形已经准备好,但尚未启动。

4. 软件实现:从基础控制到平滑运动

打开生成的工程,我们在main.c的用户代码区开始编写控制逻辑。

4.1 基础角度控制函数

首先,我们需要一个函数,输入角度值,自动计算并设置对应的CCR值。

// 宏定义,方便修改和阅读 #define PWM_PERIOD (20000-1) // ARR值,对应20ms周期 #define PWM_MIN_PULSE 500 // 0.5ms 对应的计数值 #define PWM_MAX_PULSE 2500 // 2.5ms 对应的计数值 #define ANGLE_MIN 0.0f #define ANGLE_MAX 180.0f /** * @brief 设置舵机角度 * @param angle: 目标角度,范围0-180度 * @retval None */ void Servo_SetAngle(float angle) { uint16_t pulse_width; // 角度限幅 if(angle < ANGLE_MIN) angle = ANGLE_MIN; if(angle > ANGLE_MAX) angle = ANGLE_MAX; // 线性映射:角度 -> 脉宽计数值 // 公式:pulse = MIN_PULSE + (angle / 180) * (MAX_PULSE - MIN_PULSE) pulse_width = PWM_MIN_PULSE + (angle / ANGLE_MAX) * (PWM_MAX_PULSE - PWM_MIN_PULSE); // 设置TIM3通道1的捕获比较寄存器 __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pulse_width); }

main函数中,初始化后启动PWM输出,然后就可以调用这个函数了:

int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM3_Init(); // 启动TIM3的PWM输出 HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); // 设置舵机到90度位置 Servo_SetAngle(90.0f); HAL_Delay(1000); // 设置舵机到0度位置 Servo_SetAngle(0.0f); HAL_Delay(1000); // 设置舵机到180度位置 Servo_SetAngle(180.0f); HAL_Delay(1000); while (1) { // 主循环 } }

下载程序,舵机应该会依次转动到90度、0度、180度。如果不动,请立即进入下一章的排查环节。

4.2 实现舵机的平滑运动

让舵机瞬间从一个角度跳到另一个角度,会产生很大的机械冲击和噪音,对齿轮寿命也不好。更优雅的方式是让角度平滑过渡。

这里介绍两种方法:

方法一:延时步进法Servo_SetAngle函数内部实现渐变。这种方法会阻塞CPU。

void Servo_SetAngle_Smooth(float target_angle, uint16_t duration_ms) { float current_angle, step; uint16_t steps = 50; // 分50步完成 uint16_t step_delay = duration_ms / steps; // 获取当前角度(需要根据当前CCR值反算,这里简化处理,假设已知) // 实际项目中可能需要一个全局变量来记录当前角度 static float s_current_angle = 90.0f; // 假设初始90度 step = (target_angle - s_current_angle) / steps; for(uint16_t i = 0; i < steps; i++) { s_current_angle += step; Servo_SetAngle(s_current_angle); // 调用基础设置函数 HAL_Delay(step_delay); } // 确保最终位置精确 Servo_SetAngle(target_angle); s_current_angle = target_angle; }

方法二:非阻塞式状态机法(推荐)利用定时器中断,在主循环中更新角度,不阻塞其他任务。

// servo_control.c typedef struct { float current_angle; float target_angle; float step_angle; uint8_t is_moving; uint16_t update_interval_ms; uint32_t last_tick; } Servo_Ctrl_t; static Servo_Ctrl_t g_servo; void Servo_Init(void) { g_servo.current_angle = 90.0f; g_servo.target_angle = 90.0f; g_servo.is_moving = 0; g_servo.update_interval_ms = 20; // 每20ms更新一次,即50Hz Servo_SetAngle(g_servo.current_angle); } void Servo_SetTargetAngle(float angle, uint16_t move_time_ms) { if(move_time_ms == 0) { // 立即到位 g_servo.current_angle = angle; g_servo.target_angle = angle; g_servo.is_moving = 0; Servo_SetAngle(angle); } else { g_servo.target_angle = angle; uint16_t steps = move_time_ms / g_servo.update_interval_ms; if(steps == 0) steps = 1; g_servo.step_angle = (angle - g_servo.current_angle) / steps; g_servo.is_moving = 1; g_servo.last_tick = HAL_GetTick(); } } void Servo_Update(void) { // 此函数需要被主循环或定时器中断定期调用 if(!g_servo.is_moving) return; uint32_t current_tick = HAL_GetTick(); if(current_tick - g_servo.last_tick >= g_servo.update_interval_ms) { g_servo.last_tick = current_tick; g_servo.current_angle += g_servo.step_angle; // 判断是否到达目标 if( (g_servo.step_angle > 0 && g_servo.current_angle >= g_servo.target_angle) || (g_servo.step_angle < 0 && g_servo.current_angle <= g_servo.target_angle) ) { g_servo.current_angle = g_servo.target_angle; g_servo.is_moving = 0; } Servo_SetAngle(g_servo.current_angle); } } // main.c 中 int main(void) { // ... 初始化 Servo_Init(); // 命令舵机在1000ms内平滑转动到135度 Servo_SetTargetAngle(135.0f, 1000); while(1) { Servo_Update(); // 非阻塞更新舵机位置 // 这里可以执行其他任务,如按键扫描、传感器读取等 } }

第二种方法虽然代码量稍多,但它是实际项目中更实用的架构,实现了控制与执行的解耦。

5. 进阶话题与性能优化

基础功能实现后,我们可以探讨一些更深入的问题,以优化性能和可靠性。

5.1 多路舵机控制与定时器资源分配

一个项目往往需要控制多个舵机,例如六足机器人需要18个。如何高效管理?

  1. 单定时器多通道:一个通用定时器(如TIM3)通常有4个独立的捕获/比较通道,可以产生4路独立的PWM,只需配置不同的CCR值。这是最经济的方式,所有通道共享同一个ARR(周期),确保所有舵机信号同步。

    // 同时设置TIM3的四个通道 __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pulse1); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, pulse2); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, pulse3); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_4, pulse4);
  2. 多定时器:当需要的舵机数量超过一个定时器的通道数,或者需要不同的PWM频率时(虽然舵机都是50Hz,但其他应用可能需要),就需要启用多个定时器。

  3. PWM输出比较模式与DMA(高级技巧):对于需要同时更新大量舵机(如人形机器人)的场景,频繁调用__HAL_TIM_SET_COMPARE会产生CPU开销。可以利用定时器的输出比较模式和DMA,预先将一组CCR值存入数组,然后由DMA在定时器更新事件时自动、静默地将整个数组搬运到各个通道的CCR寄存器中,实现所有舵机位置的同步、无延迟更新。这对于需要高同步精度的复杂动作序列至关重要。

5.2 精度提升与抖动消除

有时你会发现舵机有细微的抖动或无法精确停在某个角度,可以从以下方面排查和优化:

  • 电源噪声:如前所述,加装大容量滤波电容是最有效的措施之一。
  • 信号噪声:PWM信号线应尽量短,远离电机、电源等强干扰源。对于长距离传输,可以考虑使用屏蔽线或转换为差分信号。
  • STM32时钟精度:确保系统时钟(HSE或HSI)稳定。使用外部晶振(HSE)通常比内部RC(HSI)精度更高,产生的PWM周期更准确。
  • CCR值计算与浮点数:我们的Servo_SetAngle函数使用了浮点数运算。在资源紧张的MCU上,频繁的浮点运算可能带来性能和精度问题。可以改用定点数运算来优化。
    // 使用定点数(Q格式)计算脉宽,假设用整数表示0.1度 #define ANGLE_SCALE 10 // 1度 = 10个单位 #define PULSE_PER_ANGLE_UNIT ((PWM_MAX_PULSE - PWM_MIN_PULSE) * ANGLE_SCALE / (180 * ANGLE_SCALE)) // 需根据实际情况调整计算 uint16_t Servo_AngleToPulse_FixedPoint(uint16_t angle_fixed) // angle_fixed = 实际角度 * 10 { // 使用整数乘法和移位代替浮点除法 uint32_t pulse_range = PWM_MAX_PULSE - PWM_MIN_PULSE; uint16_t pulse = PWM_MIN_PULSE + (angle_fixed * pulse_range) / (180 * ANGLE_SCALE); return pulse; }
  • 机械负载与死区:舵机带动的负载过重,可能使其无法到达指令位置而在目标点附近振荡。确保负载在舵机额定扭矩内。此外,有些廉价舵机内部电位器有死区,在中间位置附近一小段范围不敏感,这是硬件问题,软件无法根治。

6. 常见问题排查与实战调试记录

即使按照教程操作,你也可能会遇到一些问题。下面是我在项目和教学中遇到的一些典型情况及其解决方法。

问题现象可能原因排查步骤与解决方案
舵机完全不动,无反应1. 电源未接通或电压不对。
2. 信号线连接错误或接触不良。
3. PWM未启动。
4. 脉冲宽度超出舵机识别范围。
1. 用万用表测量舵机红黑线间电压,确保为5V左右。
2. 检查信号线是否接在了正确的TIMx_CHy引脚,并用示波器或逻辑分析仪查看该引脚是否有波形输出。没有示波器?可以用另一个方法:将信号线接到STM32的普通GPIO,写程序手动模拟一个1.5ms高电平、18.5ms低电平的脉冲,如果舵机转动到中间,说明舵机是好的,问题在定时器配置。
3. 检查代码中是否调用了HAL_TIM_PWM_Start()
4. 尝试将CCR值设置为一个非常明确的值,如1500(1.5ms)。
舵机只向一个方向转动到底,并发出“吱吱”声这是典型的“堵转”现象。脉冲宽度超出了舵机的机械限位,但控制电路仍在驱动电机。1.立即断电!长时间堵转会烧毁舵机内部电机。
2. 检查计算的CCR值是否在合理范围(500-2500)。
3. 用示波器测量实际脉冲宽度,确认是否与代码设定值相符。可能是ARR或PSC计算有误,导致周期不是20ms,舵机无法正确解析脉宽。
舵机抖动,无法稳定1. 电源功率不足或干扰大。
2. PWM信号不稳定或有毛刺。
3. 机械结构卡顿或负载过重。
4. 代码中频繁、微小地更新CCR值。
1. 为舵机供电单独走线,并并联大电容(470μF以上)。
2. 检查STM32的时钟配置是否稳定,降低系统时钟或定时器预分频试试。
3. 卸下负载,空载测试是否还抖动。
4. 确保角度更新函数不是在一个非常快的循环中被频繁调用。加入最小更新间隔判断。
角度不准确,有偏差1. 舵机个体差异,中位脉宽不是精确的1.5ms。
2. 机械安装存在偏差。
3. 线性映射公式误差。
1.校准:编写一个测试程序,让舵机以50为步进,从500到2500变化CCR值,记录下实际到达0度和180度时的CCR值,替换代码中的PWM_MIN_PULSEPWM_MAX_PULSE
2. 在软件中设置一个“偏移量”进行补偿。pulse_calibrated = pulse_calculated + offset
控制多个舵机时,有的动有的不动1. 电源总功率不足,导致部分舵机欠压。
2. 定时器通道未全部使能。
3. 引脚复用功能未正确配置。
1. 计算所有舵机同时工作的总电流,确保电源(如5V稳压模块)能提供足够电流(通常单个SG90空载约100-200mA,带负载可能翻倍)。
2. 检查代码,是否为每个使用的PWM通道都调用了HAL_TIM_PWM_Start()
3. 在CubeMX中检查,每个使用的引脚是否都正确配置为对应的定时器通道。

调试利器:没有示波器怎么办?对于没有示波器的爱好者,可以巧妙利用STM32本身和一个LED来粗略判断PWM。

  1. 将控制舵机的信号线,同时接到一个LED灯(串联限流电阻)的正极。
  2. LED负极接地。
  3. 当PWM输出1.5ms脉宽时,由于高电平时间很短,LED会非常暗(几乎不亮)。
  4. 当PWM输出2.5ms脉宽时,高电平时间变长,LED亮度会明显增加。
  5. 如果LED常亮或常灭,说明PWM周期或占空比完全不对。虽然无法测量精确时间,但可以快速定性判断PWM是否在工作以及脉宽的大致变化趋势。

7. 从模拟舵机到数字舵机与总线舵机

我们上面讨论的都是最基础的模拟舵机。它的控制电路简单,每隔20ms才检测一次脉冲宽度。市场还有数字舵机总线舵机,它们拥有更强的性能。

  • 数字舵机:内部由微处理器取代了模拟电路。虽然仍然接受50Hz的PWM信号,但MCU可以以更高的频率(例如300Hz)读取脉冲宽度,并进行更快速、更精准的PID运算,因此具有更快的响应速度、更高的定位精度和更大的保持扭矩。使用方法与模拟舵机完全兼容。
  • 总线舵机(如舵机):这是革命性的变化。它采用异步串行通信(如TTL电平),所有舵机并联在同一根总线上,每个舵机有唯一ID。主控发送数据包(包含ID、目标位置、速度、扭矩等指令)来控制舵机。优势极其明显:
    • 一根线控制多个舵机,极大简化布线。
    • 可反馈:可以读取舵机的实时位置、温度、负载、电压等信息。
    • 高级控制:可以设置运动速度、扭矩限制、PID参数等。
    • 位置精度高:通常能达到0.3度或更高。

使用总线舵机,STM32端就不再需要产生PWM了,而是使用一个UART串口。你需要根据舵机的通信协议(常见的有半双工和全双工两种)来编写数据包的发送和接收函数。协议通常是公开的,核心就是构造一个包含包头、ID、指令、参数、校验和的字节数组,然后通过串口发送出去。

// 伪代码示例:发送位置指令给ID为1的舵机 void BusServo_SetPosition(uint8_t id, uint16_t position, uint16_t time) { uint8_t cmd_packet[10]; // 假设数据包长度为10字节 cmd_packet[0] = 0xFF; // 包头 cmd_packet[1] = 0xFF; cmd_packet[2] = id; // 舵机ID cmd_packet[3] = 0x07; // 指令长度 cmd_packet[4] = 0x03; // 写位置指令 cmd_packet[5] = position & 0xFF; // 位置低字节 cmd_packet[6] = (position >> 8) & 0xFF; // 位置高字节 cmd_packet[7] = time & 0xFF; // 时间低字节 cmd_packet[8] = (time >> 8) & 0xFF; // 时间高字节 cmd_packet[9] = Calculate_Checksum(cmd_packet, 9); // 校验和 HAL_UART_Transmit(&huart1, cmd_packet, 10, 100); }

从PWM控制升级到总线控制,是从事更复杂机器人项目(如人形机器人、机械臂)的必经之路。它虽然初期学习成本稍高,但带来的系统整洁性、控制精度和功能丰富性是PWM方式无法比拟的。

回过头看,STM32的PWM控制舵机,就像学习驾驶手动挡汽车:你需要了解离合器、油门和挡位的配合。而总线舵机则像是自动挡,甚至电动车,你把意图告诉它,它就能高效、平稳地完成。但无论技术如何演进,理解最基础的PWM控制原理,永远是构建稳定可靠机电系统的基石。当你下次看到舵机精准转动时,希望你能清晰地知道,是那一个个精准的脉冲宽度,在背后默默指挥着这一切。

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

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

立即咨询