☰
ESP32舵机控制全指南:从PWM原理到多舵机平滑运动
2026/10/23 1:21:17 网站建设 项目流程

如果你也是那种“看到舵机就不会走路”的人,或者你手头正躺着一堆 SG90、MG995 想做点什么东西,那么用 ESP32 来驱动舵机,基本是这几年我最推荐的玩法。网上讲舵机控制的教程很多,但大多数停留在“接一根线、写一行代码、动一下角度”的层面,真到想做一个机械臂、一个多自由度机器人、或者一套带遥控的上位机系统时,就会发现自己根本使不上劲。这篇文章我打算从最底层的 PWM 原理开始,把 ESP32 在舵机控制上的特有优势、三种代表性的控制方式、平滑运动处理、多舵机协调、以及供电和调试这些坑,一次性讲透。不管是刚入门的玩家,还是已经做了一两个项目的朋友,都能找到可以直接抄作业的部分。

先说明一点:这篇文章默认你用的是 ESP32 开发板,舵机以最常见的 SG90、MG995 或者兼容 9g 舵机为主。我不会去写那些只适合特定硬件的代码,所有代码基于 Arduino 框架和 ESP-IDF 通用思路,你拿到任何一款 ESP32 开发板上都能跑。

1. 为什么说 ESP32 是舵机控制的“优等生”

1.1 舵机控制到底控的是什么:PWM 脉冲宽度

先把基础夯实。舵机看起来是一个“旋转马达”,但本质上它是一个位置伺服系统。标准舵机有三根线:电源、地、信号。信号线上传输的是 PWM(脉冲宽度调制)波形,也就是一个固定频率下、高电平时间不断变化的方法。舵机内部的电路会去读取这个高电平的持续时间,然后和舵机内部的位置电位器做比较,驱动电机转到对应的角度位置。

这里有一个非常关键的数字:舵机的标准刷新频率是 50Hz,也就是周期固定为 20ms。在这个周期内,高电平的宽度通常在 0.5ms 到 2.5ms 之间。0.5ms 对应 0 度,2.5ms 对应 180 度(或者根据标称不同,也有 0.5ms 对应 -90 度、2.5ms 对应 +90 度的舵机)。中间按线性关系映射。例如 1.5ms 的高电平就是 90 度。

用生活中的事情来类比,PWM 就像是你给一个快递员发“位置坐标”。0.5ms 是地址 A,2.5ms 是地址 B,舵机就是那个收到坐标后自己走过去停下来的快递员。你只需要告诉他“目标地址”,不需要管它怎么走。这也是为什么舵机控制的核心门槛不在硬件驱动,而在“如何稳定、精确地生成这个脉冲序列”。

很多新手误以为 PWM 是“占空比控制速度”的东西,那是用在直流电机调速上的。舵机不一样,它要的不是平均电压,而是每个周期的脉宽信息。因此频率必须稳定,脉宽必须精确,否则舵机会抖动、会偏位。

1.2 ESP32 在 PWM 资源上的独特优势

如果你用过 Arduino Uno 驱动多个舵机,一定经历过这样的窘境:Servo 库默认使用了定时器,一个定时器往往只能管 12 路舵机,而且 8 位单片机的定时器精度有限,当你要同时做其他事、比如读取传感器或者驱动电机时,很容易出现定时器冲突。这还只是资源紧张的问题,更大的问题是 CPU 主频和架构在复杂任务下的力不从心。

ESP32 的情况完全不同。它内部集成了 LEDC(LED Control)外设,这是一种专门用来输出 PWM 波形的硬件控制器。它有 16 个通道,可以自由分配到任意引脚输出 PWM,而且频率可调范围极宽,分辨率从 1 位到 14 位可配置。这意味着你可以同时控制 16 路舵机,只要板子的引脚资源足够。更舒服的是,这些 PWM 波形由硬件自动产生,一旦配置好,CPU 完全不用操心,可以腾出精力去跑 WiFi、做传感器采集、执行控制算法。

我在实际项目中最喜欢的一点是它的分辨率。Arduino Uno 上写 analogWrite 通常只有 8 位(0-255),而 ESP32 的 LEDC 分辨率可以通过参数设置到 10 位、12 位甚至 14 位。控制舵机角度时,你可以很精细地调整脉宽,实现接近“无级”的角度变化。这在做机械臂或云台时,观感差距非常明显。

另外,ESP32 原生支持双核和 WiFi/蓝牙,这就打开了很多可能性:手机网页控制舵机、云端下发动作指令、多舵机协调运动、甚至接一个 IMU 做姿态反馈。这些都是 8 位单片机很难舒服地完成的。这就是为什么在我的项目里,只要涉及舵机控制,第一选择基本都是 ESP32。

2. 动手前的准备:硬件选型与接线避坑

2.1 舵机怎么选:SG90、MG995 还是其他

舵机这东西,便宜和贵之间差距比想象中大得多。常见的 SG90 是 9g 塑料齿轮微型舵机,工作电压 4.8V 左右,扭矩大概 1.8kg·cm,适合做小云台、小爪子、简单的玩具机械臂。它的优点是用 5V 供电基本就能跑,电流需求比较低,直接接开发板的 5V 引脚也能应付(但要注意别同时带好几个大电流设备)。缺点是齿轮偏脆,一旦受外力撞击容易扫齿,而且抖动稍微明显。

如果你要做机械臂主力关节,或者需要带动有一定重量的结构件,建议直接上 MG995、MG996R 这类金属齿轮舵机。它们的扭矩能做到 10kg·cm 以上,但代价是电流需求大得多。起步电流几百毫安,堵转电流甚至可能超过 2A。这种舵机绝对不能直接从 ESP32 的 5V 引脚取电,否则一块好好的开发板,几下就给你搞到电压跌落、反复重启。

在选择舵机时,还需要关注你需要的转角范围。标准舵机是 180 度范围,也有一些 270 度、360 度连续旋转舵机。360 度舵机内部结构不同,它接收到的脉宽控制的是转速和方向,而不是角度,这一点千万别搞混。如果你要做小车轮驱动,建议用专门的小车底盘电机(如 TT 马达)而不是连续舵机,这是另一个话题了。

2.2 接线铁律:信号、电源、共地

接线本身不复杂,但细节决定成败。SG90 的棕线是地,红线是电源,橙线是信号线。MG995 的线序通常是棕、红、橙,分别是地、电源、信号。把所有舵机的地线、电源线并联,信号线分别接到 ESP32 的不同引脚。

电源是整个舵机控制里最大的坑。一个 5V 供电的舵机,在空转时电流可能只有几十毫安,但当它带负载时电流会猛增。我实测过 MG995 在带机械臂时,瞬时电流能冲到 1.5A 以上。如果你用的是电脑 USB 口供电,几个舵机同时一动,USB 口直接输出电压塌陷,主机那边甚至弹“无法识别的设备”。

所以我强烈建议:舵机电源和逻辑电路电源分开。用一节 7.4V 锂电池加一个 5V/3A 的降压模块,或者一个 6V/4A 的电源适配器,给舵机单独供电。ESP32 开发板通过 USB 或者另一个 5V 电源供电。然后,舵机电源的地线和开发板的地线一定要连在一起,这就是共地。如果不共地,信号线上的 PWM 脉冲就失去了参考基准,舵机会乱抖,甚至完全不动。

另外,建议在每个舵机电源线上并联一个 470uF 到 1000uF 的电解电容,容量视舵机数量而定。这个电容的作用是缓冲启动瞬间的电流浪涌,能明显减少电源电压的瞬间跌落。不要把它当作省掉独立电源的借口,但加了电容之后,系统稳定性确实会好很多。

2.3 开发环境的两个选择:Arduino 框架还是 ESP-IDF

大部分人入门舵机控制会选 Arduino 框架,我也一样。在 Arduino 环境里,安装 ESP32 开发板支持包之后,可以直接用 Servo 库或者 ESP32Servo 库。这种方式的优点是代码极简,连 PWM 频率都不用自己算,几行代码就能让舵机动起来。如果你重点在“快速做原型”或“先验证机械结构”,Arduino 框架是最省时间的。

但如果项目需要追求性能和稳定性,比如同时控制十几个舵机、需要低延迟响应、或者要在脱机状态下稳定运行,那我推荐直接基于 ESP-IDF(Espressif IoT Development Framework)开发。它更贴近硬件底层,配置 LEDC 通道时自由度更大,也方便使用 FreeRTOS 的任务调度机制来管理多个并发控制任务。缺点是需要学习的东西多,编译环境配置也比 Arduino 麻烦不少。

考虑到这篇文章的适用面,主体示例我都用 Arduino 框架来写,但在进阶部分会点出如何迁移到 ESP-IDF 的思路。我个人的习惯是:原型阶段 Arduino,量化产品阶段再搬到 ESP-IDF,两者并不冲突。

3. 基础篇:3 种让舵机动起来的方式

3.1 用 ESP32Servo 库:和 Arduino 一样简单

如果你只想尽快看到舵机转起来,就在 Arduino IDE 里安装 ESP32Servo 库(在库管理器搜索 Servo 时,认准适配 ESP32 的版本)。这个库对 ESP32 的 LEDC 外设做了封装,用法和经典 Servo 库几乎一样。

下面是一个最简单的示例,让舵机在 0°、90°、180° 之间来回摆动。

#include <ESP32Servo.h> Servo myServo; void setup() { myServo.setPeriodHertz(50); // 标准舵机刷新频率 50Hz myServo.attach(13, 500, 2500); // 信号引脚 13,脉宽范围 500us~2500us } void loop() { myServo.write(0); delay(1000); myServo.write(90); delay(1000); myServo.write(180); delay(1000); }

看起来很简单,但对 ESP32 有几个细节值得注意。第一,attach 函数的第二和第三个参数分别是最小、最大脉宽(微秒)。不同舵机的脉宽范围不完全一样,很多国产舵机是 500us 到 2400us,标称 0° 到 180°。如果舵机到不了两端,或者到了两端发烫,通常是脉宽范围没校准好。第二,如果你把脉宽范围调成 0 和 180,但舵机本身是 270 度的,就需要根据实际脉宽计算映射关系,不能无脑套。

这个库的底层自动分配 LEDC 通道,最多支持 16 路。它内部使用了一个排序算法来管理通道,在频繁 detach 和 attach 时可能会有点开销,但对我们大多数应用场景来说完全够用。有一点需要注意:ESP32Servo 库不能和普通 Servo 库同时使用,因为两者会争抢底层资源,编译能过,运行时大概率出错。

3.2 用 LEDC 原生 PWM:理解背后的原理

用库固然方便,但如果你想真正理解舵机为什么转、转到哪、有没有精度限制,我建议至少亲手用 LEDC 原生接口写一次。

LEDC 是 ESP32 内部专门输出 PWM 的硬件外设。你需要做三件事:配置通道频率和分辨率、绑定引脚、输出脉宽。在 Arduino 框架里,对应的函数是 ledcSetup、ledcAttachPin、ledcWrite。

先看代码:

#define SERVO_PIN 13 #define PWM_CH 0 void setup() { ledcSetup(PWM_CH, 50, 16); // 50Hz 频率,16 位分辨率 ledcAttachPin(SERVO_PIN, PWM_CH); // 绑定引脚到通道 } void setServoAngle(int angle) { // 脉宽 500us ~ 2500us,对应角度 0 ~ 180 // 周期 20ms = 20000us // 16 位分辨率下,整个周期对应的计数值是 2^16 = 65536 // 500us 对应的计数 = 500 / 20000 * 65536 = 1638 // 2500us 对应的计数 = 2500 / 20000 * 65536 = 8192 uint32_t pulseWidth = 500 + (uint32_t)((float)angle / 180.0f * 2000.0f); uint32_t duty = pulseWidth * 65536 / 20000; ledcWrite(PWM_CH, duty); } void loop() { setServoAngle(0); delay(1000); setServoAngle(90); delay(1000); setServoAngle(180); delay(1000); }

这段代码的核心是把“角度”转换成“计数值”。为什么要 16 位分辨率?因为 50Hz 下周期是 20ms,如果你用 8 位分辨率,整个周期只有 256 个刻度,每个刻度对应的脉宽大约是 78us。舵机对脉宽的分辨率要求很高,78us 的步进会让角度调节很粗糙,所以推荐至少 14 位以上。16 位分辨率下,每个计数单位对应约 0.3us 脉宽,足够细腻。

用 LEDC 原生接口的好处是你可以完全控制精度和频率。比如你想用 100Hz 刷新率控制某些高速数字舵机,或者用 280Hz 驱动特定的微型舵机,只需要改动 ledcSetup 的第一个参数即可。有一点别忘了:频率越高,周期越短,最大脉宽受到周期限制。如果周期小于最大脉宽,舵机无法到满角度。

还有一个我在调试中经常踩的坑:ESP32 的 LEDC 通道虽然多,但每个引脚能复用的通道是有限制的,官方文档里有详细的映射表。如果你用 detach 后重新 attach 到新引脚,务必确认通道没有被其他代码占用,否则会出现“写通道但引脚无输出”的诡异问题。

3.3 用 MicroPython:快速验证逻辑的轻量方案

如果你平时用 MicroPython 写 ESP32,驱动舵机也很方便。MicroPython 的 machine.PWM 模块可以设置频率和占空比,配合 esp32.RMT 甚至可以精确控制脉冲宽度。

from machine import Pin, PWM import time servo = PWM(Pin(13), freq=50) def set_angle(angle): # 微秒转占空比,MicroPython 的 duty_us 直接接收微秒数 us = int(500 + angle / 180 * 2000) servo.duty_us(us) set_angle(90) time.sleep(1) set_angle(180)

MicroPython 的 duty_us 是一个很贴心的接口,直接传微秒,不用自己去算占空比计数值。这种方式适合快速验证舵机角度和机械结构,但如果你要做多舵机高性能运动控制,解释型语言的实时性会比编译型语言差一些,尤其是多任务并发时会比较吃力。

4. 进阶篇:让舵机动作“有质感”

4.1 从写一个角度到平滑运转:梯形加减速

很多人控制舵机是直接用 write 跳到目标角度。这样做的结果是舵机“啪”一下猛甩过去,不仅观感生硬,还会给机械结构和齿轮带来冲击。做机器人或者展示型项目时,这种动作一看就很廉价。要解决这个问题,最简单有效的方法就是让舵机按一定速度曲线运动,而不是瞬间到达。

最常见的速度曲线是梯形加减速:从当前角度出发,先加速到一个最大速度,然后匀速,最后减速接近目标。但要注意,舵机本身内部自带位置闭环,你没有直接控制它的“速度”的能力,能做的其实是控制角度随时间的变化率。我常用的做法是,在每一帧里让目标角度只移动一小步,步长由当前速度决定,而速度由“和终点的距离”决定。

一个相对简单但足够平滑的思路是“分段直线插值加终点减速”。

float currentAngle = 0; float targetAngle = 180; float speedStep = 0.5; // 每帧最大步长,单位:度 float stopRange = 20; // 接近终点开始降速的范围 void updateServo() { float diff = targetAngle - currentAngle; if (abs(diff) < 0.1) { currentAngle = targetAngle; } else { float step = speedStep; if (abs(diff) < stopRange) { step = speedStep * abs(diff) / stopRange; // 减速 } currentAngle += (diff > 0 ? step : -step); if ((diff > 0 && currentAngle > targetAngle) || (diff < 0 && currentAngle < targetAngle)) { currentAngle = targetAngle; } } myServo.write((int)currentAngle); }

在 loop 里每 20ms 调用一次 updateServo,而不是用 delay 阻塞。这样舵机就有一个“起步加速、接近目标减速”的过程,机械动作明显更柔和。更精细的做法是用梯形速度曲线或者 S 曲线,在路径规划阶段生成一段角度序列,然后用定时器逐点发送。后面做机械臂的多段轨迹时非常有用。

我实际试下来的心得是:不要让舵机的目标角度变化速率超过它自身的能力范围。有些金属舵机速度很快,你可以给较大的 speedStep;有些微型舵机反应慢,步长太大反而显得突兀。多试几次就找到适合自己项目的值了。

4.2 多舵机同步与协调运动

做双自由度云台或者机械臂时,需要多个舵机同时运动。最直接的做法是写多个舵机对象,然后按顺序写角度。但因为每个舵机的启动时间、执行速度不同,顺序写会导致动作“歪歪扭扭”。比如你想让机械臂从 A 点直线移动到 B 点,两个关节必须同时、按一定的插值比例运动,否则末端轨迹就会走出一条奇怪的弧线。

一个不错的组织方式是把舵机状态放在一个结构体或数组里,在同一个时间基准下更新每个舵机的插值进度。

struct ServoAxis { Servo servo; float current; float target; float step; bool busy; }; ServoAxis axes[4]; void moveAllTo(float angles[], int n, float durationMs) { // 按 durationMs 计算每个轴每帧的步长 for (int i = 0; i < n; i++) { axes[i].target = angles[i]; axes[i].step = abs(axes[i].target - axes[i].current) / (durationMs / 10.0f); axes[i].busy = true; } } void updateAllServos() { bool finished = true; for (int i = 0; i < n_axes; i++) { if (axes[i].busy) { float diff = axes[i].target - axes[i].current; if (abs(diff) < axes[i].step) { axes[i].current = axes[i].target; axes[i].busy = false; } else { axes[i].current += (diff > 0) ? axes[i].step : -axes[i].step; } axes[i].servo.write((int)axes[i].current); finished = finished && !axes[i].busy; } } }

这样,所有舵机在同一个循环周期里被更新,即便它们的角度变化量不同,也能在同一时间窗内到达目标,整体动作协调性会好很多。

对于“同一时间窗到达”这件事,我再举个例子:假如舵机 A 要转 90 度,舵机 B 只要转 30 度。如果两者都用同样的角速度跑,B 会比 A 先到,那么机械臂末端会先在一端停下等 A,动作就有了停顿。所以上面的代码用“总时长”均匀分配步长,而不是统一固定角速度。这种方式在轨迹规划里叫“定时协调”,做机械臂动作时十有八九是必要的。

4.3 外部控制:按钮、电位器和 WiFi 遥控

舵机控制如果只写在代码里,可玩性会受限。我一般会加几种输入方式。

第一,电位器直接控制角度。用 ESP32 的 ADC 读取电位器的模拟值,映射到 0 到 180 度,然后输出给舵机。这个方式适合做调试工具,比如手动校准机械臂的关节位置。

int potValue = analogRead(34); // ESP32 ADC 引脚 float angle = (potValue / 4095.0f) * 180.0f; myServo.write((int)angle);

第二,按钮控制预设的角度序列。按下一次切换一个动作,适合演示机械臂的分步流程。注意按钮要从引脚上拉到 GND 或者接入上拉电阻,并做简单的防抖处理。

第三,WiFi 网页遥控。这是 ESP32 的强项。在 Arduino 框架中,你可以创建一个 WebSocket 服务器,在浏览器里拖动滑块,通过 WebSocket 把角度数据实时传到 ESP32,然后驱动舵机。实现上比想象中简单,核心代码是 WebServer 的回调函数中解析参数,然后调用 write 接口。如果你做得更完整一些,还可以在页面上绘制角度曲线、实时显示舵机状态。

但要注意:WiFi 遥控带来的最大问题不是实现,而是安全。你在浏览器里拖动滑块,一旦误操作,舵机可能瞬间转到一个机械上不允许的角度,导致卡死或损坏。所以我强烈建议在程序里做软限位,只允许输出在某个安全角度范围内;同时加“超时自动回到安全位”的逻辑,比如 5 秒没有收到新的控制指令,就慢慢回中或者回到某个默认位置。这不是复杂活,但对设备的寿命影响很大。

5. 高级应用:让它更聪明、更可靠

5.1 从开环到闭环:加位置反馈与 PID

前面说到的舵机控制,本质上是一种开环控制:你给一个角度指令,舵机内部自己去完成角度闭环。但如果你的项目对外部负载、突然的阻力、或者精度要求很高,你可能需要知道舵机实际转动到了哪个角度。这时候就需要外部反馈。

一般有两种方案。第一种是换用带反馈信号的扩展舵机,比如舵机自带角度反馈引脚,输出的电压对应当前角度。第二种是在旋转轴上安装一个电位器或者磁编码器,通过 ADC 读取实际角度值。有了实际角度之后,就可以做 PID 闭环了。

PID 在这种场景下的意义不是让舵机转得更准——舵机内部本来就有位置闭环——而是在于当外部负载导致舵机被拖离目标角度时,系统能够及时修正。当然,更常见的应用场景是连续旋转舵机或电机需要精确定位时,PID 几乎必不可少。

PID 的离散实现本身不复杂,无非是处理 Kp、Ki、Kd 三个参数。真正困难的是调试参数。我的经验是,先把 Ki 和 Kd 设置为 0,只用 P 项找到一个能稳定工作的区间,再一点点加入 D 项抑制超调,最后用 I 项消除稳态误差。不要一上来三个参数一起调,那样基本调不出来。

5.2 用状态机组织复杂动作流程

当你需要让机械臂执行一个相对完整的动作序列时,比如“拿起物体、移动到左边、放下、回到原位”,代码里如果写一长串 delay 和 write,会非常难维护,而且无法处理“中途按钮取消”这类事件。更好的方式是用有限状态机来管理。

我常写的结构是:

  • 定义动作枚举,比如 IDLE、PICK_UP、MOVE_LEFT、PUT_DOWN、RETURN。
  • 每个状态里有进入时执行的动作(要写的目标角度、等待条件)和状态转换条件(比如某舵机已经到达目标角度,或者延时计数器超过了设定值)。
  • 主循环里只负责推进状态机,不做具体延时。

下面是一个简化版示例:

enum State { IDLE, PICK_UP, MOVE_LEFT, PUT_DOWN, FINISH }; State state = IDLE; unsigned long stateStartTime = 0; void stateMachine() { switch (state) { case IDLE: moveServoTo(0, 90); // 关节回到初始 state = PICK_UP; stateStartTime = millis(); break; case PICK_UP: moveServoTo(1, 160); // 上臂抬起 if (millis() - stateStartTime > 1000) { state = MOVE_LEFT; stateStartTime = millis(); } break; case MOVE_LEFT: moveServoTo(2, 60); if (servoReached(2)) { state = PUT_DOWN; stateStartTime = millis(); } break; // ... } }

这种写法的好处是,任何时刻你都可以在 loop 里检查外部输入(按钮、传感器、上位机指令),然后强行更改 state,实现中断和紧急停止。调试起来也很有条理,每个状态对应一个输入条件,出问题时候知道是卡在哪一个环节了。

5.3 时间管理与系统性能:别让看门狗把你的板子重启

用 ESP32 做多路舵机控制时,一个容易被忽视的问题是:代码执行时间过长,导致看门狗溢出重启。Arduino 框架下,loop 函数里如果写了大量 delay,或者某个 while 循环一直等待某个条件,一旦超过 watchdog 的时间限制,系统就会自动重启。

我在做多舵机协调运动时,一开始犯了这样的错误:在等待舵机到达目标角度时,用了 while (!servoReached()) {} 这种阻塞式等待。结果舵机还没转到,系统先重启了。后来我改成非阻塞设计,把“检查是否到达”和“继续插值”都放在一个每 10ms 执行一次的定时任务中。

具体做法有两种思路。一种是保留在 loop 里,每次都运行 updateAllServos,并用 millis() 来判断是否到了该执行下一帧的时间。另一种是创建 FreeRTOS 任务,在任务里用 vTaskDelay(10 / portTICK_PERIOD_MS) 作为时间基准,然后把舵机控制代码放在一个独立的核心上运行。

如果你用 Arduino 框架,最简单的还是第一种,伪代码如下:

unsigned long lastUpdate = 0; void loop() { if (millis() - lastUpdate >= 20) { lastUpdate = millis(); updateAllServos(); checkInputs(); } }

这样 loop 的整个周期非常快,看门狗完全不会被打扰。注意不要在循环里做太长时间的串口打印,尤其是 WiFi 连接时,处理网络请求占用的时间也可能让舵机更新的节拍变得不均匀。如果发现舵机运动有微妙的不稳定,优先检查你的主循环里有没有“耗时大户”。

6. 常见问题与排查技巧实录

6.1 舵机抖动、发烫、转不到位

这几个问题基本都是“供电”和“PWM 配置”两大类原因。

抖动最常见的原因是供电不足。你用 USB 口给 ESP32 和舵机同时供电,舵机一转,电压就掉下来,舵机内部的控制电路检测到电压不稳,就会反复尝试、抖动。排查方法很简单:给舵机单独供电,电源地线和开发板地线共地,看抖动是否消失。

发烫则可能是脉宽范围设置过大,导致舵机在机械限位处“顶死”,电机一直堵转发热。这时候检查一下代码里 write 的极值是不是已经超过了舵机物理行程,或者 attach 时给出的脉宽范围是不是太宽。我曾经拿一个标称 0°~180° 的舵机,用 500~2500us 的默认参数,结果它在 180° 附近明显过流发烫,把范围改成 500~2400us 后就正常了。

转不到指定角度可能是舵机扭矩不足,或者齿轮被机械结构卡住。你可以先用手轻轻拨动一下连杆,看看是否顺畅。如果机械阻力大,即使舵机没有坏,也到不了目标角度。

6.2 代码写了但不转、死机重启

首先检查信号线接对没有。不少绿色 PCB 的舵机线序和标准不同,用万用表量一下电源和地,确认红线确实是电源正极。

其次,ESP32 的某些引脚在启动时是特殊用途,比如 GPIO 0、GPIO 2、GPIO 12、GPIO 15 等,这些引脚上电时的状态可能影响启动模式。如果你发现舵机信号线接到某些引脚后,开发板无法正常启动或者反复重启,就换到 GPIO 13、GPIO 26、GPIO 33 这些相对安全的引脚。

还有一个经常被忽略的问题:多个舵机共用一个 LEDC 通道。如果你调用了 ledcAttachPin 把同一个通道绑到两个引脚上,后一个会覆盖前一个,表现就是“其中一个舵机不动”。检查代码里有没有重复的通道号。

6.3 精度和角度偏移校正

不同厂家、不同批次舵机的脉宽-角度映射并不完全一致。同一句 write(90),A 舵机可能真的在 90°,B 舵机可能偏了 5°。做一个稍微严谨一点的项目,都需要对每一个舵机做校准。

校准方法是:先让舵机转到理论上的 0°,用角度尺量实际角度;再转 180°,量实际角度。根据这两个测量值,重新计算脉宽映射的起点和终点。如果你用 ESP32Servo 库,可以直接调整 attach 时的最小、最大脉宽参数;如果用原生 LEDC,可以修改角度到脉冲宽度的线性换算公式。

我的经验是,对不影响功能的小偏差可以忽略,但对机械臂这类对末端位置敏感的机构,每个关节的校准数据都应该记录到配置文件里,形成“软件校准表”。这个表不需要多复杂,就是每个舵机两个参数:脉宽偏移量和比例系数。后续换舵机或者调结构时,只需要重新标定一次。

6.4 调试技巧:善用日志和软限位

最后说说我个人的调试习惯。做完一个舵机控制项目,我总会预留几个调试口:

  • 启动时打印每个舵机的目标角度、实际控制角度和脉宽值,方便确认写进去的数字是对的。
  • 在代码里做一个“安全角度范围”常量,所有外部控制指令都先 clamp 到范围内,再输出给舵机。
  • 如果舵机长时间连续工作,加一个温度检测或者超时保护,避免长时间堵转烧坏电机。

这套习惯帮我省了很多排查时间。尤其是软限位,看起来是小事情,但对机械结构的保护效果是决定性的。我在一个云台项目里,忘了加软限位,上位机一误操作,云台直接撞到支架上,齿轮当场崩了一个齿。从那以后,每一版舵机程序我都会写防呆逻辑。

这个项目看起来基础,但做深了发现,从 PWM 原理到多轴联动,再到闭环和状态机,每一步都有值得深挖的地方。ESP32 提供了非常充裕的硬件资源和生态支持,只要把原理吃透,后面做各种舵机应用都会变得顺手很多。最后再分享一个小技巧:如果你发现自己的舵机动作总是干涩生硬,先别急着换舵机或者调参数,试着在 20ms 更新周期里用更细的步进加上终点减速,多半会有立竿见影的改善。

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

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

立即咨询