PWM 舵机通信协议与堵转保护功能说明
适用范围
本文适用于所有支持标准 PWM 控制接口的数字舵机,具体电气参数、机械规格与接线定义请参见各型号的规格书。
1. PWM 控制协议
1.1 信号定义
PWM 舵机使用单线脉宽调制信号进行控制。主机只需向舵机输入一路 PWM 脉冲,舵机根据脉冲的高电平持续时间(脉宽)将输出轴转动到对应的角度位置。
| 术语 | 含义 | 是否承载角度信息 |
|---|---|---|
| 周期 (Period) | PWM 脉冲的重复周期,决定刷新频率 | 否 |
| 脉宽 (Pulse Width) | 单个脉冲的高电平持续时间 | 是 |
| 占空比 (Duty Cycle) | 脉宽 / 周期 | 间接 |
单片机 PWM 外设实际配置的参数是占空比,需要按"目标脉宽 → 占空比"换算后写入。
1.2 PWM 周期范围
PWM 信号的周期 (Period) 决定了舵机接收命令的刷新频率,虽然不直接承载角度信息,但对控制效果有重要影响。规格书一般会给出舵机可接受的周期范围(如 3 ~ 20 ms,即约 50 ~ 333 Hz),在此范围内,周期可以由用户根据应用场景选择。
周期与刷新率
周期 T 与刷新率 f 互为倒数:
刷新率 f (Hz) = 1 / 周期 T (s)
- T = 20 ms → f = 50 Hz (每秒更新 50 次目标位置)
- T = 10 ms → f = 100 Hz
- T = 3 ms → f ≈ 333 Hz
刷新率越高,主机对舵机的命令更新越频繁,响应越平滑、越跟手;但相应的,主机侧 PWM 外设的开销也越大。
50 Hz 的历史由来
20 ms 周期 (50 Hz) 是 PWM 舵机的传统标准,源自航模遥控(R/C)行业,沿用至今。绝大多数通用 PWM 舵机的规格书会以 50 Hz 为"推荐/默认"周期。该标准的好处是:
- 周期足够长,电机在每个周期内能完成一次完整的"测速-驱动"循环
- 占空比变化对脉宽精度影响小
- 与航模接收机直接兼容,生态最成熟
1.3 角度-脉宽映射
舵机目标角度与输入脉宽呈线性映射关系,通用公式如下:
角度 = (脉宽 - 中位脉宽) / 满量程脉宽 × 转角范围
其中:
- 中位脉宽 — 对应输出轴机械中位的脉宽
- 满量程脉宽 — 中位到一端极限位置的脉宽差
- 转角范围 — 舵机总转角(度)
具体的中位脉宽、极限脉宽与转角范围因型号而异,请查阅对应舵机规格书。
方向约定
舵机旋转方向遵循统一约定:脉宽增大,角度沿约定的方向单调增加(具体转向因型号而异,见规格书)。控制系统设计时,建议按"目标脉宽 → 目标角度"的顺序进行计算,避免在算法中混淆方向。
1.4 与总线舵机的差异
PWM 舵机与 UART / CAN 等总线舵机在协议层有本质区别:
| 维度 | PWM 舵机 | 总线舵机 (UART / CAN) |
|---|---|---|
| 信号方向 | 单向(主机 → 舵机) | 双向(命令 + 应答) |
| 参数回读 | 不支持 | 支持(位置 / 电流 / 温度 / 等) |
| 舵机识别 | 无 ID 概念 | 舵机 ID |
| 多机控制 | 需独立 PWM 通道 | 总线级联 |
| 堵转状态获取 | 需主机侧额外检测(见第 3 节) | 可通过指令回读 |
2. 基本控制方法
2.1 兼容性概述
PWM 舵机协议因其简洁性,广泛兼容各类主机平台与控制器,常见支持方式包括:
- Arduino — 通过
Servo.h库或软 PWM 输出 - ESP32 — 使用 LEDC 外设产生高频 PWM
- 树莓派 — 通过
pigpio/RPi.GPIO库产生硬件定时 PWM - STM32 — 使用 HAL 库的 TIM 定时器 PWM 模式
- 通用舵机控制器 — 通过 UART / USB 配置目标角度,内部生成 PWM 信号
上述所有平台最终都通过占空比控制 PWM 信号的脉宽,再由脉宽控制舵机角度。换算关系:
占空比 (%) = 目标脉宽 (μs) / 周期 (μs) × 100
2.2 Arduino
使用 Arduino 内置的 Servo.h 库,直接以微秒为单位写入脉宽,不受默认 0-180° 角度映射的限制。
#include <Servo.h>
Servo myservo;
void setup() {
// attach(引脚, 最小脉宽μs, 最大脉宽μs)
myservo.attach(9, 500, 2500);
myservo.writeMicroseconds(1500); // 中位
}
void loop() {
// 写目标脉宽 (μs),范围参见舵机规格书
int pulse_us = 1500;
myservo.writeMicroseconds(pulse_us);
delay(15);
}
要点:
Servo.h在 Uno / Nano 等 8 位板上使用 Timer1,占用定时器资源- 若使用 Mega / Due 等多定时器板,可在多路舵机间分散定时器
writeMicroseconds()直接以微秒写入,适合需要精细脉宽控制的场景
完整示例:带错误处理的舵机扫描
以下示例展示如何在 Arduino 上实现舵机往返扫描,并包含边界检查与错误处理:
#include <Servo.h>
// 舵机参数配置(根据实际舵机规格书调整)
const int SERVO_PIN = 9; // 信号线接 D9
const int PULSE_MIN = 500; // 最小脉宽 (μs)
const int PULSE_MAX = 2500; // 最大脉宽 (μs)
const int PULSE_CENTER = 1500; // 中位脉宽 (μs)
const int STEP_DELAY = 15; // 每步延时 (ms)
const int SWEEP_STEP = 10; // 每次增量 (μs)
Servo myServo;
int currentPulse = PULSE_CENTER;
int direction = 1; // 1:正向, -1:反向
void setup() {
Serial.begin(115200);
// 初始化舵机
if (!myServo.attach(SERVO_PIN, PULSE_MIN, PULSE_MAX)) {
Serial.println("[ERROR] 舵机初始化失败,检查引脚连接!");
while(1); // 停止运行
}
Serial.println("[INFO] 舵机已初始化");
Serial.print("[INFO] 脉宽范围: ");
Serial.print(PULSE_MIN);
Serial.print(" - ");
Serial.print(PULSE_MAX);
Serial.println(" μs");
// 归中位
myServo.writeMicroseconds(PULSE_CENTER);
delay(500);
}
void loop() {
// 更新目标脉宽
currentPulse += direction * SWEEP_STEP;
// 边界检查与反向
if (currentPulse >= PULSE_MAX) {
currentPulse = PULSE_MAX;
direction = -1;
Serial.println("[INFO] 到达最大脉宽,开始回程");
} else if (currentPulse <= PULSE_MIN) {
currentPulse = PULSE_MIN;
direction = 1;
Serial.println("[INFO] 到达最小脉宽,开始正向");
}
// 写入舵机
myServo.writeMicroseconds(currentPulse);
// 周期性输出当前状态(每500ms)
static unsigned long lastPrint = 0;
if (millis() - lastPrint > 500) {
Serial.print("[STATUS] 当前脉宽: ");
Serial.print(currentPulse);
Serial.println(" μs");
lastPrint = millis();
}
delay(STEP_DELAY);
}
运行效果:
- 舵机从中位开始,缓慢向一侧运动至极限,然后反向扫描至另一侧,往复循环
- 串口监视器每 500ms 输出当前脉宽值,便于调试
- 若舵机未连接或引脚错误,程序会在 setup() 中报错并停止
2.3 ESP32
ESP32 Arduino 核心提供 LEDC (LED Control) 外设,可产生高频 PWM 并精确控制占空比。16 位分辨率足够覆盖 0.1 μs 级脉宽精度。
const int servoPin = 18; // 信号线接 GPIO 18
const int channel = 0; // LEDC 通道号
const int freq = 50; // 50 Hz PWM 周期
const int resBits = 16; // 16 位分辨率
const int periodUs = 20000; // 50 Hz 对应 20000 μs
void setup() {
ledcSetup(channel, freq, resBits);
ledcAttachPin(servoPin, channel);
}
void loop() {
// 目标脉宽 (μs),例:中位 1500
int pulse_us = 1500;
int max_duty = (1 << resBits) - 1; // 65535
int duty = (long)max_duty * pulse_us / periodUs;
ledcWrite(channel, duty);
delay(15);
}
要点:
- ESP32 的 LEDC 支持 16 个独立通道,可同时驱动多路舵机
- 频率与分辨率是矛盾的,需在初始化时选定(本例 50 Hz / 16 bit 是常用组合)
- 改占空比时按 32 位整数重新计算,避免浮点误差累积
2.4 树莓派 + pigpio
树莓派通过 pigpio 守护进程使用硬件定时器产生稳定 PWM,精度可达 1 μs,且不占用 CPU。
import pigpio
import time
SERVO_PIN = 18 # 信号线接 GPIO 18 (Pin 12)
pi = pigpio.pi()
if not pi.connected:
raise RuntimeError("pigpio 守护进程未启动 (sudo pigpiod)")
def set_pulse(pulse_us: int) -> None:
"""写入目标脉宽 (μs),0 表示停止 PWM 输出"""
pi.set_servo_pulsewidth(SERVO_PIN, pulse_us)
try:
set_pulse(1500) # 中位
while True:
# 在此更新 set_pulse(<新脉宽>) 即可控制角度
time.sleep(0.02)
except KeyboardInterrupt:
set_pulse(0) # 停止输出
pi.stop()
要点:
- 需先启动守护进程:
sudo pigpiod - 支持任意 GPIO 引脚,但同一时间硬件 PWM 通道有限(树莓派为 2 路)
set_servo_pulsewidth()内部将脉宽限制在 500-2500 μs,超出范围舵机不响应
2.5 STM32
STM32 通过 TIM 定时器的 PWM 模式产生 50 Hz 脉冲,精度由定时器时钟决定。以下示例使用 HAL 库、72 MHz 系统时钟、TIM2_CH1 输出。
CubeMX 配置要点
| 参数 | 值 | 说明 |
|---|---|---|
| Prescaler | 71 | 72 MHz / 72 = 1 MHz 计数频率 |
| Counter Mode | Up | 向上计数 |
| Counter Period | 19999 | 1 MHz / 20000 = 50 Hz |
| Pulse (初始) | 1500 | 中位脉宽 1500 μs |
| CH Polarity | High | 高电平有效 |
示例代码
/* main.c */
#include "main.h"
#include "tim.h"
static void set_servo_pulse(uint16_t pulse_us) {
/* 改变 CCR1 寄存器即可更新脉宽 */
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, pulse_us);
}
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_TIM2_Init();
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); // 启动 PWM
while (1) {
set_servo_pulse(1500); // 中位
HAL_Delay(15);
}
}
要点:
- 通用 / F1 / F4 系列均适用,只需调整预分频与重载值匹配系统时钟
- 多个舵机可分配到不同 TIM 通道,实现多路独立控制
- 修改
__HAL_TIM_SET_COMPARE即可实时调整脉宽,无延迟
2.6 通用舵机控制器
通用舵机控制器(俗称"舵机驱动板")通常自带 PWM 生成电路,主机通过 UART / USB / I2C 下发目标角度,控制器内部转换为 PWM 脉宽。
┌────────┐ UART/USB ┌──────────────┐ PWM ┌──────┐
│ 主机 │ ──────────> │ 通用舵机控制器 │ ─────>│ 舵机 │
└────────┘ └──────────────┘ └──────┘
│
├── 电源输入(单独供电)
└── GND
使用提示:
- 接线时舵机电源独立供电,控制器只提供信号线,避免控制器过流
- 主机侧协议多为私有(各厂商不同),需对照控制器手册配置波特率 / 数据格式
- 适合"主机资源紧张"或"需要并联多路舵机"的场景
3. 堵转保护功能 (Stall Protection)
说明:堵转保护是部分 PWM 数字舵机提供的特别功能,本文统一描述其行为。具体型号是否支持,请参见对应舵机规格书。
3.1 功能目的
当舵机输出轴被外力阻挡而无法转动时,电机将持续输出满功率以维持位置,电流急剧上升。长时间堵转会导致:
- 电机线圈过热,绝缘老化甚至烧毁
- 齿轮组承受超额扭矩,加速磨损
- 驱动电路过流,缩短寿命
堵转保护 (Stall Protection) 用于在检测到持续堵转后,主动降低输出功率,直至堵转解除后自动归位、恢复正常工作。
3.2 触发条件
堵转持续时间 ≥ 1 秒。
短时抖动或瞬态阻挡(小于 1 秒)不会触发保护,确保舵机在常规抗扰场景下保持满功率输出。
3.3 保护状态参数
保护激活后,舵机进入受限工作模式:
| 参数 | 保护前(正常) | 保护激活后 |
|---|---|---|
| 输出功率 | 100% | 30% |
| 工作电流 | 视负载 | 降低约 70% |
| 锁力 | 满扭矩 | 约 30% 扭矩 |
| 控制信号接收 | 正常 | 正常 |
3.4 状态机时序
stateDiagram-v2
[*] --> 正常
正常 --> 保护激活 : 堵转持续 ≥ 1s
保护激活 --> 回位中 : 堵转解除 / 以 30% 功率到达目标位置
回位中 --> 正常 : 抵达目标位置 / 恢复全功率
三个状态的行为:
- 正常 — 满功率运行,响应主机控制信号,电流视负载
- 保护激活 — 堵转 ≥ 1s 后进入,功率降至 30%
- 回位中 — 堵转解除后,以 30% 功率驱动舵机到达目标位置,抵达后自动恢复全功率正常状态
4. 常见问题与故障排查
4.1 舵机无响应
| 可能原因 | 排查方法 | 解决方案 |
|---|---|---|
| 接线错误 | 检查信号线、VCC、GND 顺序 | 参照规格书重新接线 |
| 电源未供电 | 测量舵机 VCC 引脚电压 | 确保供电(电压范围参考对应规格书) |
| 脉宽超出范围 | 确认发送的脉宽值 | 调整至规格书规定范围(通常 500~2500μs) |
| PWM 周期过短/过长 | 检查 PWM 频率设置 | 使用推荐的 50Hz(20ms 周期) |
4.2 舵机抖动或异响
| 可能原因 | 排查方法 | 解决方案 |
|---|---|---|
| 电源纹波过大 | 示波器测量电源波形 | 电源端并联 100μF~470μF 电解电容 |
| 信号线受干扰 | 检查信号线是否与电源线并行 | 信号线与电源线分离走线,或使用屏蔽线 |
| 脉宽抖动 | 检查 MCU 定时器中断是否被打断 | 提高 PWM 生成优先级或使用硬件 PWM |
| 负载过重 | 手动转动输出轴检查阻力 | 减小负载或更换大扭矩舵机 |
4.3 转角不准确
| 可能原因 | 排查方法 | 解决方案 |
|---|---|---|
| 脉宽映射错误 | 测量实际输出的 PWM 波形 | 重新标定脉宽-角度映射关系 |
| 舵机机械磨损 | 手动转动检查有无卡顿 | 更换舵机或检修齿轮组 |
| 占空比计算精度不足 | 检查浮点运算是否溢出 | 使用整数运算或增加定时器分辨率 |
| 周期设置错误 | 确认 PWM 周期配置 | 确保周期在规格书规定范围内(通常 3~20ms) |
4.4 舵机发热严重
| 可能原因 | 排查方法 | 解决方案 |
|---|---|---|
| 长时间堵转 | 检查机械结构是否有干涉 | 排除机械干涉,启用堵转保护 |
| 供电电压过高 | 测量实际供电电压 | 调整至规格书推荐电压 |
| 频繁大幅度运动 | 观察工作模式 | 降低运动频率或增加冷却时间 |
| 舵机过载 | 检查负载扭矩 | 减小负载或更换更大扭矩型号 |
附录
A. 术语表
| 术语 | 英文 | 含义 |
|---|---|---|
| 脉宽 | Pulse Width | PWM 信号的高电平持续时间,决定舵机目标角度 |
| 周期 | Period | PWM 信号的重复周期,决定刷新频率 |
| 占空比 | Duty Cycle | 脉宽与周期的比值,单位为百分比 |
B. 参考资源
- Arduino Servo 库文档
- ESP32 LEDC 外设文档
- pigpio 库文档
- STM32 HAL TIM 参考: STM32CubeMX 生成代码中的
stm32xxxx_hal_tim.h头文件