
做嵌入式这些年我玩过的控制组合里ESP32 和舵机算是最能出活的一对了。ESP32 舵机控制这个方向从最简单的云台摆动、小车转向到机械臂多关节联动、摄像头自动追踪目标底层都是同一套逻辑用 ESP32 输出特定频率和脉宽的脉冲信号让舵机转到指定角度。这篇文章我按自己实际做项目的路径来讲从原理、接线、基础代码到多舵机同步、平滑运动、Web 远程控制最后把踩过的坑整理成排查清单。适合三类人看刚接触 ESP32 想用舵机做点小东西的新手已经能让舵机转起来但没搞明白多通道和平滑控制的老手以及想直接把舵机接到物联网和自动化场景里的开发者。1. 整体设计思路ESP32 控制舵机的方案选型做舵机控制前先得理解舵机本身是怎么被控制的。舵机内部有一个直流电机、一组减速齿轮和一个位置反馈电位器控制信号是一路 PWM脉宽调制波形舵机内部的闭环电路根据脉宽判断目标角度驱动电机转过去并持续保持。这个信号的标准频率是 50Hz也就是周期 20ms脉宽范围通常是 0.5ms 到 2.5ms对应 0 到 180 度。1.1 舵机控制的核心原理脉宽和角度的换算关系是舵机控制的基石。假设一个标准舵机脉宽 0.5ms 对应 0 度1.5ms 对应 90 度中位2.5ms 对应 180 度。这相当于舵机每隔 20ms 就收到一次位置指令内部电路把脉宽换算成电压值和电位器反馈的当前位置做比较差值不为零就驱动电机转动直到差值归零。所以舵机其实是一个自带闭环的位置伺服系统我们的任务只是把脉宽说清楚。这里要提醒一句不同品牌、不同型号的舵机对脉宽范围的标定不一样。有些舵机 1ms 到 2ms 就能覆盖 0 到 180 度有些甚至能超程到 270 度。你在代码里写死 500us 到 2500us 之前最好先看舵机标称参数或做一次实际校准否则很容易出现转不到底或者还没到指定角度就卡住的情况。1.2 为什么 ESP32 比传统单片机更适合舵机控制如果只控制一两个舵机Arduino UNO 也能干但一旦场景复杂ESP32 的优势就非常明显。它内置的 LEDC 外设可以输出最多 16 路独立 PWM每一路的频率和分辨率都能单独配置这意味着你可以稳定驱动十几个舵机而且互不干扰。更重要的是ESP32 自带 Wi-Fi 和蓝牙舵机控制可以完全脱离 USB 线变成网页滑块直接拖动的远程控制或者手机 APP 里的一个按钮。从系统架构的角度看ESP32 把舵机控制器和通信节点合并成了一个芯片省掉了一大堆外围模块。选型上我个人的倾向是做固定姿态的展示项目优先用 ESP32 ESP32Servo 库开发效率高做对脉宽精度有要求的场景比如机械臂、双足机器人用 LEDC 原生驱动来写虽然代码量多一点但你能完全掌控时序。MicroPython 也是个选项但考虑到舵机实时性要求较高我主推 Arduino 框架下面所有代码都以 Arduino 为例。2. 硬件准备与供电设计这部分我特意放在代码前面因为新手翻车十有八九是供电问题。舵机看着小启动瞬间电流非常猛。一个小 SG90 堵转时电流能冲到 500mA 以上三五个舵机同时动作瞬间电流很容易超过 2A。而大多数 ESP32 开发板靠 USB 供电USB 口输出也就 500mA 左右电压一旦被拉低ESP32 直接重启。2.1 舵机选型从 SG90 到 MG996R先根据项目需求选舵机不要只盯着价格。我按扭矩和使用场景列了一个对照表型号扭矩参考重量适用场景注意点SG901.8 kg·cm9g小型云台、小机械臂关节、玩具车转向塑料齿轮磕碰容易扫齿MG996R9.4 kg·cm55g中型机械臂、机器人腿部金属齿轮但电流需求明显增大DS321820 kg·cm 级别60g竞赛机器人、重负载关节数字舵机反应快价格偏高连续旋转舵机看具体型号变化大小车驱动、传送带控制的是转速和方向不是角度SG90 是新手入门的首选价格低、重量轻、容易调通。等你的项目开始承担负载比如机械臂要抓取东西SG90 就明显力不从心了轻则抖动重则扫齿这时候换 MG996R 或更高扭矩的数字舵机。连续旋转舵机和标准舵机控制逻辑完全不同买之前务必确认自己要的是角度型还是转速型。2.2 供电方案三个典型层次供电这块我总结成三个层次从简单到复杂看你的舵机数量和功耗决定。第一个层次只驱动一两个舵机并且舵机不是长时间堵转。直接用 5V/2A 以上的 USB 充电器或手机充电头供电注意不是插 ESP32 的 USB 口而是把 5V 接在舵机电源线上同时把电源负极和 ESP32 的 GND 接在一起。共地非常关键信号线是相对的电压差不共地的话ESP32 输出的高电平在舵机看来可能是完全错误的电压。第二个层次用电池或外接电源模块供电。我比较推荐的方案是 18650 电池组或 2S 锂电经过 DC-DC 降压模块稳定输出 5V 或 6V 给舵机。降压实测下来很稳MP1584 这类模块十几块钱一个效率比 LDO 线性稳压高多了也基本不掉压。第三个层次多舵机、高扭矩、高频率动作。这时候要给舵机配独立的 BEC电池消除电路或者专门的舵机供电板并且可以考虑在信号线上加一级光耦隔离。光耦隔离的成本不高但能彻底阻断舵机大电流对 ESP32 数字电路的冲击。我做六足机器人时就用过这种方案效果非常明显ESP32 再也没被舵机电流拉重启过。2.3 接线顺序与常见硬件坑接线顺序我建议固定为先接舵机电源线和地线再接信号线。这样做的好处是避免信号线已经通电时舵机没有稳定供电导致舵机乱转或抖动。信号线插到 ESP32 支持 PWM 输出的 GPIO 上注意避开一些只支持输入或烧录时影响下载的引脚具体引脚分配放到第 4 部分统一讲。再强调一下舵机三根线的颜色SG90 通常是橙色信号、红色电源、棕色地线。MG996R 信号线常为黄色或橙色红线电源棕线地线。插反信号线和地线的情况下轻则舵机不动重则直接烧毁舵机控制板甚至把 GPIO 拉坏。如果你用的是自己没有排针的舵机线最好先拿万用表量一下三根线之间的导通关系尤其是电源和地别凭经验猜。3. 基础 PWM 驱动让舵机转起来前面铺垫了这么多现在到实操环节。用 Arduino 框架控制舵机有两种主流方式一种是直接用 ESP32Servo 库简单高效适合绝大多数应用另一种是用 LEDC 原生驱动适合需要精细控制脉宽的场合。我两种都讲你先用第一种上手再理解第二种的原理。3.1 LEDC 外设配置详解LEDC 是 ESP32 的硬件 PWM 控制器它有几个关键参数通道号0 到 15、频率、分辨率。分辨率决定了脉宽的精细程度比如 16 位分辨率下占空比的取值是 0 到 65535对应一个 20ms 周期内的高电平时长可以精确到约 0.3 微秒。这个精度对于舵机来说绰绰有余。先看 ESP32Servo 库的写法它把底层 LEDC 配置都封装好了#include ESP32Servo.h Servo myServo; void setup() { // 引脚 18最小脉宽 500us最大脉宽 2500us对应 0-180 度 myServo.attach(18, 500, 2500); myServo.write(90); // 转到 90 度 } void loop() { myServo.write(0); delay(1000); myServo.write(180); delay(1000); }这段代码最简单直接控制舵机在 0 度和 180 度之间来回扫动。attach 函数里后两个参数值得特别关注它们定义的是舵机实际的最小和最大脉宽。如果你发现舵机偏转不到位靠调这两个值就能校准不需要改任何其他逻辑。3.2 角度到脉冲宽度的换算方法再看原生 LEDC 写法。假设我们选择通道 0、频率 50Hz、分辨率 16 位数值范围 0 到 65535。20ms 周期在 16 位分辨率下对应的占空比数值是 65535那么换算公式就是0.5ms 脉宽对应0.5 / 20 × 65535 ≈ 16381.5ms 脉宽对应1.5 / 20 × 65535 ≈ 49152.5ms 脉宽对应2.5 / 20 × 65535 ≈ 8192你可以用 arduino 的 map 函数直接映射角度到占空比int duty map(angle, 0, 180, 1638, 8192); ledcWrite(0, duty);如果不用 map也可以用浮点计算double pulseWidth 500 (angle / 180.0) * 2000; // 单位微秒 int duty (pulseWidth / 20000.0) * 65535; ledcWrite(0, duty);这里的关键理解是LEDC 的 ledcWrite 写入的不是脉宽时间而是占空比数值。占空比数值 脉宽时长 / 周期 × 分辨率满值。所以一旦你改动了频率占空比数值对应的实际脉宽也会变化。比如频率改成 100Hz周期 10ms同样的 1638 占空比就不是 0.5ms 了。这就是为什么舵机控制里频率必须保持 50Hz 附近不要随意改。3.3 完整代码示例从单舵机到方向控制给你一个可以直接用的单舵机扫动示例我在注释里写清楚了每步含义#include ESP32Servo.h Servo panServo; // 云台水平舵机 #define SERVO_PIN 18 void setup() { panServo.attach(SERVO_PIN, 500, 2500); } void loop() { for (int angle 0; angle 180; angle 5) { panServo.write(angle); delay(50); } for (int angle 180; angle 0; angle - 5) { panServo.write(angle); delay(50); } }跑起来之后舵机会在 0 到 180 度之间往返扫动。如果你想做方向控制比如遥控小车转向可以直接把角度映射到方向盘逻辑通过串口接收指令#include ESP32Servo.h Servo steeringServo; #define SERVO_PIN 18 void setup() { Serial.begin(115200); steeringServo.attach(SERVO_PIN, 500, 2500); steeringServo.write(90); // 默认中位直行 } void loop() { if (Serial.available()) { int angle Serial.parseInt(); if (angle 0 angle 180) { steeringServo.write(angle); Serial.print(Set angle: ); Serial.println(angle); } } }用串口调试助手发一个 0 到 180 之间的数字加回车舵机会立刻转到对应角度这个基础框架后面接蓝牙、Wi-Fi 时只需要把串口数据源替换掉。4. 进阶控制多舵机同步与平滑运动单个舵机能动了之后你很快会发现直接 write 目标角度舵机会以最大速度啪一下甩过去。在云台上看还好在机械臂或机器人身上就非常生硬甚至会因为瞬时冲击力把结构件打歪。所以进阶阶段要解决两件事多舵机通道的规范管理以及舵机运动的平滑过渡。4.1 多舵机的通道与引脚规划ESP32 的 LEDC 支持 16 路通道但要注意不是每个 GPIO 都能绑定到任意通道芯片手册里有矩阵映射关系好在 Arduino 框架的 ledcAttachPin 会自动处理大部分情况。我的建议是把舵机信号线尽量集中安排在一组相邻的 GPIO 上方便布线并且在代码里用结构体统一管理。这里是一个管理 4 个舵机的代码结构#include ESP32Servo.h #define SERVO_NUM 4 struct ServoChannel { Servo servo; uint8_t pin; int currentAngle; }; ServoChannel channels[SERVO_NUM]; void setup() { int pins[SERVO_NUM] {18, 19, 21, 22}; for (int i 0; i SERVO_NUM; i) { channels[i].pin pins[i]; channels[i].servo.attach(pins[i], 500, 2500); channels[i].currentAngle 90; channels[i].servo.write(90); } }这样做的好处是后面你要遍历所有舵机做初始化或状态读取只需一个 for 循环就能解决。我也见过很多人偷懒把舵机数组写散结果调试时找一个舵机接口要翻半天代码所以强烈建议起步就规范化。另外ESP32 的某些 GPIO 在大电流下抗干扰能力偏弱信号线太长的话容易被电源噪声干扰建议尽量把线缩短或者用杜邦线固定好避免虚接。4.2 平滑缓动算法让动作不再抽搐平滑运动的核心思路是不要直接 write 目标角度而是把整个运动拆成很多小步每 20ms 移动一次。为什么要 20ms因为舵机信号本来就是 20ms 一帧你更新频率高于这个没有意义低了又会出现肉眼可见的卡顿。最基础的线性插值实现如下void moveServoSmooth(Servo servo, int startAngle, int targetAngle, int durationMs) { int steps durationMs / 20; for (int i 1; i steps; i) { int current startAngle (targetAngle - startAngle) * i / steps; servo.write(current); delay(20); } }线性插值的运动速度恒定但视觉上还是略显机械。更自然的做法是引入缓动曲线让舵机先慢-中间快-末尾慢模拟真实关节的运动状态。我常用的是正弦缓动int easeInOut(int i, int steps) { // 返回 0 到 1 之间的缓动系数正弦曲线 double t (double)i / steps; double value (1.0 - cos(t * PI)) / 2.0; return (int)(value * 100); } void moveServoSmoothEased(Servo servo, int startAngle, int targetAngle, int durationMs) { int steps durationMs / 20; for (int i 1; i steps; i) { int eased easeInOut(i, steps); int current startAngle (targetAngle - startAngle) * eased / 100; servo.write(current); delay(20); } }实际跑起来你会发现同样是从 0 度转到 180 度缓动版的动作比突变版舒服太多而且对舵机齿轮和固定支架的冲击也小很多。这个缓动函数在机械臂抓取、机器人步态里都是核心基础。4.3 利用 PCA9685 扩展通道数如果你要做的项目超过 16 路舵机或者你想把舵机控制和 ESP32 的计算任务分开PCA9685 是一个很成熟的选择。这是一颗 I2C 接口的 16 路 PWM 驱动芯片专门为舵机控制设计输出频率可以稳定在 50Hz通过板载跳线设置不同地址可以级联 60 多路舵机。接线非常简单VCC 接 5VGND 共地SCL/SDA 分别接 ESP32 的 I2C 引脚。库文件用 Adafruit PWM Servo Driver Library代码里指定舵机号就能控制#include Wire.h #include Adafruit_PWMServoDriver.h Adafruit_PWMServoDriver pwm Adafruit_PWMServoDriver(0x40); #define MIN_PULSE 150 // 0 度对应 PCA9685 的计数 #define MAX_PULSE 600 // 180 度对应计数具体值需校准 void setup() { pwm.begin(); pwm.setPWMFreq(50); // 50Hz舵机标准频率 } void setServoAngle(uint8_t num, double angle) { double pulseCount MIN_PULSE (angle / 180.0) * (MAX_PULSE - MIN_PULSE); pwm.setPWM(num, 0, (uint16_t)pulseCount); }PCA9685 的 12 位分辨率虽然比 ESP32 原生 16 位低但对舵机应用来说完全足够。我一般只在舵机数量特别多或者需要和主控做电气隔离时才用这个扩展方案普通项目直接用 ESP32 原生 LEDC 就够了。5. 高级应用Web 远程控制与自动化场景ESP32 最大的优势是自带联网能力所以舵机控制完全可以做成 Web 端实时操作或者和传感器联动实现全自动。这一节介绍两个我实际做过的高级方向WebSocket 网页实时控制以及传感器触发的自动化闭环。5.1 WebSocket 实时网页控制先讲网页控制。传统 HTTP 请求的缺点是每次控制都要重新建立连接延迟高拖动滑块时体验很差。WebSocket 是长连接服务器主动推数据实时性要好得多。控制逻辑很简单网页上放一个滑条拖到某个数值前端通过 WebSocket 把这个数值发给 ESP32后端收到后调用舵机控制函数。你需要两个库ESPAsyncWebServer 提供 Web 服务和 WebSocket 支持ArduinoJson 负责解析 JSON 格式的控制指令。#include WiFi.h #include ESPAsyncWebServer.h #include WebSocketsServer.h #include ArduinoJson.h #include ESP32Servo.h const char* ssid 你的WiFi名称; const char* password 你的WiFi密码; AsyncWebServer server(80); WebSocketsServer webSocket WebSocketsServer(81); Servo myServo; #define SERVO_PIN 18 void onWebSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) { if (type WStype_TEXT) { StaticJsonDocument128 doc; DeserializationError error deserializeJson(doc, payload); if (!error doc.containsKey(angle)) { int angle doc[angle]; myServo.write(angle); } } } void setup() { Serial.begin(115200); myServo.attach(SERVO_PIN, 500, 2500); WiFi.begin(ssid, password); while (WiFi.status() ! WL_CONNECTED) { delay(500); } webSocket.begin(); webSocket.onEvent(onWebSocketEvent); server.begin(); } void loop() { webSocket.loop(); }前端页面可以在 ESP32 的 SPIFFS 文件系统里放一个 HTML 文件通过 server.serveStatic 发布出来。滑条范围设置为 0 到 180每次变化就发送 JSON 数据。实际体验是拖滑条的时候舵机几乎同步跟着动延迟基本感觉不到比用 HTTP 轮询体验好一个数量级。5.2 传感器触发的自动化闭环Web 控制解决的是人来操作的问题但很多场景需要舵机自己根据环境响应。比如做一个智能追踪云台超声波传感器检测到障碍物距离舵机带动摄像头左右扫描寻找目标或者做一个自动开窗器温湿度传感器超过阈值舵机驱动窗户开合一个角度。这类系统的控制逻辑其实是一个状态机采集数据、做判断、更新舵机角度。需要注意两个细节一是传感器数据和舵机动作的同步通常在 loop 里以一个固定周期比如 100ms采集一次传感器然后根据状态决定舵机是否动作二是舵机的动作本身可能影响传感器读数比如舵机带动云台转动时超声波传感器的检测方向也变了这时要做合理的逻辑流程避免死循环。我做过一个具体案例用舵机带超声波传感器做扇形扫描每 30 度停一次采集障碍物距离最后把距离数组发到 Web 页面画出一张简易地图。核心代码如下int scanStep 30; for (int a 0; a 180; a scanStep) { moveServoSmooth(myServo, currentAngle, a, 200); delay(100); int dist readUltraSonic(); Serial.printf(Angle: %d, Distance: %d cm\n, a, dist); currentAngle a; }这种模式把舵机和传感器组合起来已经能完成很多初级巡检、测绘任务。5.3 与上位机或手机 APP 通信的思路如果你不想写网页也可以用手机 APP 直接控制。ESP32 的经典蓝牙串口模块可以将蓝牙数据透传到串口任何支持蓝牙串口的 APP 发送数据ESP32 就能收到。代码上只需要在原有串口解析逻辑上增加一个蓝牙初始化#include BluetoothSerial.h BluetoothSerial SerialBT; void setup() { Serial.begin(115200); SerialBT.begin(ESP32_Servo); myServo.attach(SERVO_PIN, 500, 2500); } void loop() { if (SerialBT.available()) { int angle SerialBT.parseInt(); if (angle 0 angle 180) { myServo.write(angle); } } }手机 APP 和上位机的核心原理一致把控制指令编码成数据然后传输区别只在于传输通道。理解了这一点你可以把通道换成 MQTT、TCP、HTTP外部系统接一个物联网平台舵机就变成了可以被云上逻辑控制的执行器。这也是智能家居、远程设备控制里最常见的架构。6. 常见问题排查与调试心得舵机控制调试起来说简单很简单说烦也很烦。我把过去项目里遇到频率最高的问题整理成一张速查表再逐个展开说明排查思路。现象最可能原因快速验证方法解决方案舵机抖动、乱转供电不足或电压不稳用万用表测舵机电源电压是否低于 4.8V换独立 5V 电源或加电容检查共地上电后舵机猛转到极限初始脉宽为极限值或信号线接触不良串口打印当前角度观察启动时序初始化时先写中位 90 度检查接线角度不准偏大或偏小脉宽范围与舵机标定不符用 writeMicroseconds 实测极限脉宽在 attach 函数里自定义最小最大脉宽舵机没反应信号线插错引脚或没共地检查 GPIO 是否支持 PWM 输出换用支持 PWM 的引脚舵机运行中发热严重长时间堵转或负载过大手摸舵机外壳温度增加限位逻辑降低负载加大散热6.1 舵机抖动、乱转先查电源舵机抖动时百分之七十的原因跟电源有关。舵机属于感性负载启停瞬间会产生较大的电流冲击如果供电内阻大或电流上限不够电压就会被拉低舵机内部逻辑电路检测到电压异常会不断复位表现出来就是抖个不停。排查时用万用表笔直接点在舵机电源引脚上观察电压波动如果看到数值在 5V 到 3V 之间跳基本可以确定是供电不足。解决方案有几种换一个 2A 以上的稳压电源、在舵机电源输入端并联一个 470uF 以上的电解电容、给舵机独立供电并和 ESP32 共地。电容的作用是瞬态储能能有效缓冲启动瞬间的电流峰值。6.2 角度不准确与零点校准角度偏差往往不是 ESP32 的问题而是舵机个体差异。同一个型号的两只舵机在相同脉宽下实际角度可能差 5 度。如果你的项目要求精准比如机械臂要按坐标定位就必须做零点校准。方法不复杂把舵机装到结构件上用 writeMicroseconds 写入一个很小的脉宽比如 500us然后看舵机实际指向的位置如果偏了就调整脉宽值直到位置到达真实的 0 度。记录这个校准值再对 180 度做同样的操作最后把两个值填到 attach 函数的第二个和第三个参数里。这里的 map 函数也会跟着变成映射到这两个校准值而不是通用的 500、2500。每次换舵机或者换传动结构后都要重新校准因为传动件的安装误差也会叠加到角度偏差里。实测下来校准一次能解决很大一部分看着转向却不到预定位置的问题。6.3 堵转发热与保护策略最后说一个容易忽略的隐患堵转。舵机转到物理极限位置后如果你还让它继续输出力电机就会堵转电流飙升控制板和齿轮组都在承受巨大压力。长期堵转轻则舵机发热寿命缩短重则烧毁驱动芯片。我之前在做机械臂时为了图简单直接把舵机角度限制设在 180 度结果末端关节经常顶到夹具上十几分钟就烫得厉害。后来加了一个软限位逻辑在代码里把角度范围限制在 10 到 170 度之间并且当电流或角度接近极限时自动降低保持力度。如果你用的舵机支持电流反馈还可以通过读取电流值来做更精确的保护否则保守处理就是留足冗余角度别把舵机逼到墙角。调试舵机系统的效率很大程度上取决于你是否把问题分层先查供电再查接线然后查信号占空比最后查机械负载。按照这个顺序排查大部分问题都能在几分钟内定位。我个人在实际项目里还有一个习惯在测试阶段永远给舵机控制程序加一个限位模式把最大角度限制在物理安全范围内。这样即使代码逻辑写错了也不会把舵机转到卡死位置配件损坏的概率会小很多。最后分享一个小技巧如果你的舵机在静止状态有轻微嗡嗡声那是电机的保持力矩在起作用正常现象但如果声音偏大且伴随温度上升那就不是正常表现要按排查清单去查供电和堵转问题了。ESP32 舵机控制这条路从基础走到高级其实就是不断把信号对不对、供电稳不稳、动作顺不顺这三件事打磨到位的过程。