先把结论放在最前面:做 ESP32 舵机控制,十个项目里有八个不是死在代码上,而是死在供电和占空比上。我最早用 SG90 舵机接 ESP32,直接把信号线怼到 GPIO,然后把舵机电源线接到开发板的 3V3 引脚,结果板子疯狂重启,舵机抖得跟筛子一样。查了一晚上才搞明白,是舵机瞬间电流把稳压器拖垮了。这篇文就从这种最真实的坑写起,从单舵机的基础 PWM 控制一步步走到多舵机、网络远程控制、角度闭环,适合刚接触 ESP32 的新手,也适合已经会一点 Arduino、想往高级应用走的同学。
1. 整体设计思路:先把 ESP32 舵机控制的框架搭起来
1.1 为什么选 ESP32 而不是常见的 8 位开发板
先聊一个很多人纠结的问题:控制舵机而已,随便找块板子不行吗?还真不是。常见的 8 位入门开发板在舵机控制上能用,但有几个硬伤:PWM 输出通道少、主频低,一旦同时带四五个舵机,定时器分配就很紧张。ESP32 的优势首先是通道多,内置 LEDC PWM 控制器支持最多 16 个通道,这意味着你不需要额外扩展板就能同时控制十几个舵机,当然电源压力另说。
其次是 ESP32 自带 Wi-Fi 和蓝牙,这是很多人选择它的直接原因。舵机控制从来不只是"转个角度"这么简单,到了高级应用阶段,你需要手机控制、网页控制,甚至通过 MQTT 接入智能家居系统。如果先用了普通 8 位板子,后面再想加网络功能,要么换板子,要么外挂一个 Wi-Fi 模块,复杂度直接翻倍。
另外,ESP32 的 LEDC 频率和分辨率可以独立配置,不像某些平台只有一个固定的 PWM 频率。舵机对频率很敏感,标准是 50Hz,但有些特殊舵机需要更高的刷新频率,ESP32 可以通过参数调节,这一点在后面的代码里会具体解释。
1.2 舵机到底是怎么转起来的
舵机的控制原理并不复杂,可以理解为:每 20ms 发送一个脉冲信号,这个脉冲里高电平持续的时间长短,决定了舵机转到哪个角度。
常见的模拟舵机,比如 SG90,角度范围大约是 0° 到 180°。一般情况下,0.5ms 的高电平对应 0°,1.5ms 对应 90°,2.5ms 对应 180°。你可能会看到不同舵机的参数有点差异,比如有的舵机是 1ms 到 2ms,这取决于遥控模型那种老规格,但实际上原理都是同一套:脉冲宽度映射角度。
用一个生活化的类比来说,这就像你在对讲机里喊"向左转 30 度",舵机收到的是电信号,它内部的 PWM 解码芯片会把脉宽解析成具体的角度指令,然后驱动电机转到对应位置。所以,只要脉冲宽度稳定,舵机角度就稳定;脉冲宽度抖动,舵机就会跟着抖。这也是为什么后面要花大篇幅讲供电和信号线问题。
1.3 控制方式选型:库、原生 PWM、还是舵机驱动板
ESP32 控制舵机,主流有三种做法,我建议根据项目规模来选。
第一种是用现成的 ESP32Servo 库。这是最省事的方案,把底层的定时器、PWM 通道分配全部封装好了,你只需要创建一个 Servo 对象,调用 attach 和 write 就能让舵机动起来。适合做原型验证、课程设计、单舵机或少量舵机的场景。
第二种是直接用 ESP32 的 LEDC 硬件 PWM。不需要外部库,通过 ledcSetup 和 ledcWrite 两个函数配置频率和占空比,控制精度更高,也方便你理解底层原理。适合需要精确控制脉宽的场合。
第三种是外接舵机驱动板,最常见的是 PCA9685。ESP32 虽然有 16 路 LEDC,但如果你要做 16 轴以上的机械臂,或者不想让主控承担太多 PWM 计算压力,用驱动板是更稳的选择。它通过 I2C 通信,ESP32 只需要两根线就能控制 16 路舵机,还能外接单独的大功率电源。
我个人的建议是:不管最后用库还是用驱动板,都至少用原生 LEDC 手写一次 PWM。因为只有你自己亲手算过那几百个占空比数值,后面遇到舵机不转、角度偏了、乱抖这些问题时,才不用靠瞎猜。
2. 基础实操准备:接线、供电与引脚规划
2.1 认识舵机的三根线
模拟舵机通常引出三根线,颜色虽然不同厂商有差异,但功能基本一样。以最常见的 SG90 为例:红色线是电源正极,接 5V 左右;棕色线是电源负极,也就是 GND;橙色线是信号线,接 ESP32 的 GPIO 引脚。买 MG996R 这类更大扭矩的舵机,线材颜色可能稍有变化,比如有的信号线是黄色或白色,但红色和黑色的正负极基本不变。
如果你手里的舵机没有标注颜色,最简单的方法就是看插头规格和厂家说明。千万别靠猜,接反正负极,轻则舵机烧掉,重则把开发板也带走。我见过不止一次有人把信号线接到 5V 上,结果舵机内部芯片直接冒烟。
2.2 供电避坑:别用开发板的 3V3 引脚喂舵机
这是新手最容易踩的坑,值得单独拿出来说。
舵机在转动的时候,瞬间电流可以到几百毫安,堵转时电流更大,比如 SG90 堵转电流能到 700mA 左右,MG996R 甚至能到 2A 以上。ESP32 开发板上的 3V3 引脚是从 USB 的 5V 经过板载稳压器降压出来的,稳压器能提供的电流普遍有限,你让舵机直接挂在 3V3 上,电压一被拉低,ESP32 就会复位,舵机则因为供电不足而抖动或者干脆不动。
正确做法是:舵机单独用外部 5V 电源供电,比如 USB 电源、移动电源、或者 6V 的电池组。电源的正极接舵机红线,负极和 ESP32 的 GND 共地,信号线接 GPIO。注意,共地这个动作非常重要,如果不共地,信号线没有一个公共参考点,舵机可能收到乱七八糟的脉冲,直接乱转。
2.3 引脚选择和通道分配
ESP32 的 LEDC 通道并不是固定绑定某个引脚的,你可以把任意 PWM 通道映射到大部分 GPIO 上,但有几个限制:GPIO 34 到 39 是输入专用引脚,不能输出 PWM;另外,如果你同时用到 Wi-Fi 和蓝牙,某些 GPIO 在启动时会有特定状态,容易引起舵机乱转,需要避开。
我自己常用的舵机引脚是 GPIO 13、14、15、25、26 和 27,这些引脚输出能力稳定,也没和 Flash 等功能冲突。如果你用的是开发板,还要注意板上丝印的标注,有些引脚被板载 LED、按钮或者 Flash 占用,接上后可能影响启动。
举一个典型分配:控制 3 个舵机,就分配 GPIO 13、14、15,对应 LEDC 通道 0、1、2。这里通道号和引脚号不用一致,但建议固定一张映射表,后期写代码不容易乱。
2.4 动手接线:一套可以直接复制的方案
假设你只需要控制 1 个 SG90 舵机,接线可以这样做:
- 舵机红线和棕线,分别接到外部 5V 电源的正极和负极
- 舵机信号线,接到 ESP32 的 GPIO 13
- 外部 5V 电源的负极,额外接一根线到 ESP32 的 GND
这套接线看起来简单,但很多人会漏掉共地这一步。只要电源负极和 ESP32 GND 连上了,信号线才有一个稳定的电压参考基准。如果你用同一个 USB 口既给 ESP32 供电又给舵机供电,务必要确认 USB 输出电流足够。我记得有一次用电脑 USB 口带 SG90,舵机一转弯,电脑直接提示设备复位,后来换了带独立供电的集线器才好。
3. 快速上手:最基础的单舵机控制代码
3.1 使用 ESP32Servo 库让舵机动起来
在 Arduino IDE 或者 PlatformIO 里安装 ESP32Servo 库之后,写单舵机控制非常简洁。下面是一段经典的扫描程序,舵机会从 0° 转到 180°,再从 180° 转回 0°,循环往复。
#include <ESP32Servo.h> Servo myServo; const int servoPin = 13; void setup() { myServo.attach(servoPin); } void loop() { for (int angle = 0; angle <= 180; angle++) { myServo.write(angle); delay(15); } for (int angle = 180; angle >= 0; angle--) { myServo.write(angle); delay(15); } }这里的 attach 函数默认会使用 50Hz 频率,脉冲范围也按照常见舵机自动做了映射。所以这段代码上传后,舵机应该立刻开始来回扫动。如果你发现舵机方向反了,不要急着改代码,先检查信号线是不是接到了相邻的 PWM 引脚,这种低级错误其实占比很高。
3.2 不依赖库,手写原生 PWM 控制
用库很方便,但为了搞清楚占空比到底是怎么回事,我建议手动写一次 PWM。ESP32 的核心 API 是 ledcSetup、ledcAttachPin 和 ledcWrite。
const int servoPin = 13; const int pwmChannel = 0; const int frequency = 50; const int resolution = 14; const int dutyFor0Deg = 410; const int dutyFor90Deg = 1229; const int dutyFor180Deg = 2048; void setup() { ledcSetup(pwmChannel, frequency, resolution); ledcAttachPin(servoPin, pwmChannel); } void loop() { ledcWrite(pwmChannel, dutyFor0Deg); delay(1000); ledcWrite(pwmChannel, dutyFor90Deg); delay(1000); ledcWrite(pwmChannel, dutyFor180Deg); delay(1000); }这段代码会让舵机依次停在 0°、90°、180°,每个角度停留 1 秒。如果你用这段代码发现角度不准,并不是代码有问题,而是你的舵机脉宽映射和默认 0.5~2.5ms 之间有偏差,这一点在后面会详细说。
3.3 核心参数计算:50Hz、脉宽、角度映射
先解释为什么频率是 50Hz。标准模拟舵机的工作周期是 20ms,换算成频率就是 1/0.02=50Hz。所以 ledcSetup 里的 frequency 参数填 50,逻辑上就是让 PWM 每 20ms 重复一次。
分辨率我选了 14 位,也就是 2 的 14 次方,16384 个电平等级。在 50Hz 周期下,每个占空比步进对应的时间约为 20ms / 16384 ≈ 0.0012ms,完全够用。
计算方式如下:占空比数值 = 16384 × 高电平时间 / 20ms。
- 0°:高电平 0.5ms,占空比 = 16384 × 0.5 / 20 = 409.6,取 410
- 90°:高电平 1.5ms,占空比 = 16384 × 1.5 / 20 = 1228.8,取 1229
- 180°:高电平 2.5ms,占空比 = 16384 × 2.5 / 20 = 2047.5,取 2048
如果你的舵机实测是用 1ms 到 2ms 对应 0° 到 180°,那 0° 的占空比就是 819,90° 是 1229,180° 是 1638。这里的关键是,不要照搬网上别人写好的数值,最好自己用示波器或者靠舵机实际反应来校准。
3.4 跑起来之前先检查的三件事
给舵机供电并上传代码之前,一定要花 30 秒做三查:查电源供电是否独立、查信号线是否插错、查 ESP32 和舵机是否共地。如果代码上传后舵机不动,先别怀疑代码逻辑,大概率是供电不足或者接线问题。还有一个很容易忽略的点:ESP32 上电和刷写程序的瞬间,GPIO 状态可能不确定,如果你的舵机接在这类引脚上,会出现上电乱转一下的情况。解决方法是把舵机电源和 ESP32 的供电独立,先给 ESP32 上电,等程序跑起来之后再给舵机通电,或者使用外部舵机电源再接一个物理开关控制。
4. 进阶控制:多舵机与平滑运动
4.1 多舵机控制:通道与引脚映射
当项目从单个舵机扩展到多个舵机时,第一个问题是通道分配。在 ESP32Servo 库里,你可以创建多个 Servo 对象,每个对象对应一个引脚。在原生 LEDC 环境里,则需要给每个舵机分配不同的 channel。
我做过一个三舵机云台,当时的映射是这样的:通道 0 接 GPIO 13 控制水平舵机,通道 1 接 GPIO 14 控制垂直舵机,通道 2 接 GPIO 15 控制负载旋转舵机。这种映射方式清晰,写代码时一眼就能知道当前在控制哪个舵机。
多舵机还有一个隐藏坑:舵机并联在同一电源上,启动瞬间的冲击电流会被放大。如果电源余量不够,三个舵机同时启动,电压可能被拉低到 4V 以下,结果就是一个舵机都动不了。我的建议是,每增加一个舵机,就要重新估算电源电流,最好预留 20% 以上的余量。
4.2 在单线程里实现多舵机"同时"动作
ESP32 是单线程跑 Arduino 的 loop,直接写延时会让所有舵机顺序执行,造成动作不同步。比如你想要机械臂的肩部和肘部同时运动,如果写一个舵机转完再写另一个舵机,效率很低。
常见做法是用非阻塞的时间调度,把目标角度和当前角度拆开,循环里每次只更新一点。下面是一段示意性的非阻塞多舵机运动代码:
Servo servoH; Servo servoV; int hAngle = 90; int vAngle = 90; int hTarget = 90; int vTarget = 90; unsigned long lastUpdate = 0; void updateServos() { if (millis() - lastUpdate < 10) return; lastUpdate = millis(); if (hAngle < hTarget) hAngle++; if (hAngle > hTarget) hAngle--; if (vAngle < vTarget) vAngle++; if (vAngle > vTarget) vAngle--; servoH.write(hAngle); servoV.write(vAngle); }这段代码的核心思路是:每个舵机都向目标角度逼近,一次只变化 1°,并且用一个 10ms 的计时器控制更新频率。这样多个舵机看起来是同步运动的,代码本身也不会因为 delay 阻塞。
4.3 平滑运动:从跳变到插值
直接 write 一个目标角度,舵机会用最快的速度冲过去,这在机械臂场景里不仅看起来生硬,还容易因为惯性造成结构晃动。平滑运动的本质是插值:把一段大角度移动拆成很多小步进,每步之间留一点时间。
最基本的线性插值可以这样做:设定一个运动总时长,比如 1000ms,每隔 20ms 更新一次,总共更新 50 次。每次的角度变化量等于目标角度和起始角度的差值除以 50。如果你希望动作更自然,还能用缓入缓出函数,开头慢、中间快、结尾再慢,这对机械结构更友好。
实际项目中,我习惯把运动时长做成可配置参数。比如云台转向用 500ms 会比较利落,机械臂抓取用 1500ms 会更稳重。这个参数需要根据舵机扭矩和机械结构实际调试,没有一个万能值。
4.4 抖动问题:别把所有舵机写到同一个上限
多舵机还有一个常见现象:舵机在目标位置附近不停抖动,尤其是负载比较大的情况下。这里要分清两种原因:一种确实是脉冲不稳定,另一种是舵机本身在高速修正位置。
我在调试中转台时遇到过,代码逻辑完全没变,但舵机就是嗡嗡响。后来发现,我用了外部 5V 电源,但电源线太细太长,舵机一全力保持位置,电流就把电压拉出波纹,导致舵机内部不断修正。换粗线、加一个 470µF 到 1000µF 的电解电容后,问题立刻消失。所以不要一看到抖动就怀疑代码,先排查供电回路。
5. 高级应用:远程控制、反馈与闭环
5.1 用 HTTP 做远程控制端
ESP32 自带 Wi-Fi,最简单的远程控制方案是搭建一个 HTTP 服务器。你可以在浏览器里输入类似 192.168.1.100/control?pos=90 的地址,ESP32 解析 URL 里的 pos 参数,然后控制舵机转向。
代码思路如下:创建 WebServer 对象,在响应函数里读取 arg("pos"),转成整数,再写给我舵机。这个方案的好处是没有任何外部依赖,手机浏览器也能直接操作。缺点是 HTTP 是无状态协议,每发一次控制命令,都要重新请求一次,频繁操作时延迟会比较明显。
如果你只做简单的实验,比如控制一个云台上下左右,HTTP 完全够用。但如果要做多轴实时控制,或者需要把舵机状态反馈到页面,HTTP 就有点吃力了。
5.2 WebSocket 实时控制,真正的低延迟方案
WebSocket 可以在 ESP32 和浏览器之间建立一条长连接,一旦建立,双向传输非常快。控制舵机时,你在网页上拖动滑块,浏览器通过 WebSocket 把角度值发给 ESP32,ESP32 收到后立即更新舵机角度,几乎感觉不到网络延迟。
ESP32 端使用 WebSocketsServer 库,回调函数里判断消息类型,如果是文本消息就直接转成 int 角度。前端则可以放一个 input 滑块,监听 input 事件,每次变化都 send 一次角度。需要注意的是,频率不能太快,否则 WebSocket 消息堆积反而会让舵机卡顿。我一般把前端滑块的消息发送频率控制在 30ms 左右,后端再配合 20ms 定时刷新,实测非常顺滑。
5.3 读取舵机反馈并做角度闭环
很多普通舵机没有反馈引脚,你写 90°,它到底有没有转到 90°,控制器是不知道的。高级一点的玩法,是给舵机轴安装外部角度传感器,比如电位器、磁编码器,然后把反馈信号读回到 ESP32 的 ADC 引脚,形成一个简单的闭环控制。
闭环控制的思路是:目标角度和当前实际角度之间存在误差,PID 控制器根据误差输出 PWM 指令,直到误差趋近于零。这样即使舵机负载变化、机械结构摩擦,系统也能自动纠正位置。
在 ESP32 上实现 PID 并不复杂,核心就是算比例项、积分项、微分项。但要注意,读数本身会引入噪声,ADC 值不稳定会让 PID 输出抖动。我在做自动跟随时,给角度传感器加了一个简单的低通滤波,效果立马稳了很多。
5.4 结合传感器做一个自动跟随的小项目
如果你想把舵机控制玩得更有意思,可以试着做一个低成本自动跟随台:用两个光敏电阻或者距离传感器检测目标位置,根据差值控制云台舵机转向,让传感器始终对准目标。
这个项目看起来不起眼,但覆盖了传感器采集、ADC 读取、舵机控制、闭环修正这些关键知识点。我在测试时发现,传感器数据刷新率不要设太高,配合舵机 50Hz 的刷新频率,每 20ms 更新一次控制量是比较合理的。更新太快,舵机还没转到位,新的指令又来了,反而会加剧抖动。
6. 常见问题排查与避坑手册
6.1 舵机完全不动,先按这个顺序查
遇到舵机不动,我有一套固定的排查流程:先量电源电压,确认舵机正负极之间有 5V 左右的电压;再确认信号线接的是 PWM 输出的 GPIO,而不是只支持输入的引脚;最后再看程序是否正常运行,可以往串口打印调试信息。
有一个典型案例:某同学反映舵机不动,代码烧录成功但没有任何反应。远程指导后才发现,他把信号线接到了 GPIO 34。这块板子上的 34 号引脚是输入专用,根本没有 PWM 输出能力。所以,每次动手前,先拿一张引脚图确认,能省去大量排查时间。
6.2 舵机抖动、啸叫、发烫
这类问题在模拟舵机上特别常见。抖动和啸叫的根源,往往是电源不够稳定或者信号占空比不断变化。排查时,先用手轻轻按住舵机输出轴,如果按住之后啸叫更明显,说明舵机在持续用力,位置误差已经很大了。
一个被很多人忽略的因素是舵机信号线的干扰。我在地下室测试时,房间里有一台大功率设备,舵机每隔几秒就会抽动一下,后来把舵机信号线换成双绞线并远离电源线,问题就消失了。如果只是做实验,至少要让舵机信号线和电源线保持一定距离。
6.3 角度不准,写 90° 实际只有 80°
这种情况通常是脉宽范围和舵机实际规格不匹配。你手里那个舵机可能是 1ms 到 2ms 的映射,而代码默认用 0.5ms 到 2.5ms,中间角度自然对不上。
解决方法有两个:一个是在 ESP32Servo 库的 attach 函数里指定最小和最大脉宽,另一个是在应用层做角度校准。我比较推荐后者,因为不用记住每个舵机的硬件参数。你只需要测出 0°和 180°对应的实际角度,然后建立线性映射关系,再写入控制代码。
6.4 多舵机上电乱转、一闪而过
上电瞬间乱转,是因为 ESP32 复位时 GPIO 状态不确定,LEDC 通道尚未初始化,舵机信号线上出现了不该有的脉冲。这个问题在很多开发板上都存在,程度不同而已。
可靠的解决方案是使用外部舵机电源并加一个电源开关。先把 ESP32 通电并完成初始化,再打开舵机电源。如果项目要求自动上电,可以在舵机电源线上加一个 MOS 管开关,由 ESP32 的一个 GPIO 在 setup 完成后主动打开舵机电源。
下面整理了一个速查表:
| 现象 | 可能原因 | 快速处理 |
|---|---|---|
| 舵机不动 | 供电不足、信号线接错 | 先量电压,再查引脚 |
| 抖动、啸叫 | 电压波纹、信号干扰 | 加电容、换粗线 |
| 角度不准 | 脉宽映射不匹配 | 手动校准 0° 和 180° |
| 上电乱转 | GPIO 状态不确定 | 舵机电源独立开关 |
| 多舵机卡顿 | 电源电流余量不够 | 加大电源容量 |
7. 实操心得与扩展建议
7.1 供电是灵魂:电容、稳压模块、电池的选择
做了这么多舵机项目,我可以很肯定地说,供电环节决定了一个舵机项目的成败。单个 SG90 用 USB 5V 供电勉强能跑,但一旦挂了两三个大扭矩舵机,最好老老实实配一个 5V 3A 以上的稳压模块,或者使用 6V 的镍氢电池组。舵机输入电压范围通常是 4.8V 到 6V,电压高一点扭矩会大一点,但也不要盲目超过规格。
在舵机电源输入端并联一个 470µF 以上的电解电容,能有效吸收瞬时尖峰。如果你发现舵机在快速来回扫动时电压被拉低,电容和粗线基本能解决一大半问题。
7.2 信号线和接插件细节
舵机线本身就比较细,一旦延长线过长,信号衰减就可能让舵机接收不到正确脉冲。我的经验是,信号线延长越短越好,尽量不要超过 30cm。如果必须延长,使用双绞线或者屏蔽线,并且把信号地和电源地分开布线。
接插件也值得注意。很多人用杜邦线做舵机连接,杜邦线容易接触不良,一碰就脱线,舵机自然乱动。稍微稳定一点的做法是使用白色 XH2.54 插头或者直接焊接。做机械臂原型时,我会把所有舵机线汇总到一个接线排上,方便排查故障,也方便日后维护。
7.3 从控制舵机到控制机器人:还可以继续扩展的方向
基础舵机控制熟练之后,可以做很多有意思的分支。第一个方向是机械臂逆运动学,你给机械臂一个目标坐标,它自己算出每个关节应该转多少度,这就需要用到多个舵机联动和角度插值。第二个方向是接入语音识别或视觉识别,让舵机根据摄像头图像信息自动跟踪目标,这会让项目变得非常有趣。
我在做小型桌面机械臂时,最深的体会是:舵机控制本身不复杂,真正复杂的是如何把一个明确的想法转换成稳定可靠的机械动作。反过来讲,你只要把本文提到的供电、接线、PWM 参数、平滑运动、问题排查这些东西吃透,后面玩任何机器人项目,都会比其他新手省下大量试错时间。最后想说的还是那句老话:先让舵机动起来,再让舵机稳定地动起来,最后让舵机按你想要的方式动起来,整个过程没有捷径,但每一步踩坑都有价值。