“手机一点,舵机就转”——这个画面感很强,也是我推荐新手入门ESP32的原因。ESP32本身就带蓝牙,舵机又是学习PWM控制最典型的执行器,两者一组合,你不仅能理解蓝牙串口到底怎么工作,还能亲手完成第一个无线控制项目。准备材料很便宜,逻辑也不复杂,没有电子基础也能做。
说实话,我见过很多朋友买回ESP32和舵机之后,不是卡在接线,就是卡在蓝牙配对,最后只能看着示例代码发呆。其实这个项目拆开看只有三部分:ESP32负责接收蓝牙指令并输出PWM信号,舵机根据PWM信号转到指定角度,手机上的蓝牙串口助手负责发送角度值。整个过程中没有任何一步需要高深的编程基础,只要你会复制代码、会接三根线、会装Arduino IDE,就能跑通。
下面我从硬件选型讲到代码调试,把每个环节里最容易被新手忽略的坑都提出来。
1. 这个项目到底在做什么?零基础也能有个清晰的概念
1.1 最终效果和原理拆解
先别急着开干,花两分钟搞清楚这件事的本质。
做出来的效果是:手机打开蓝牙串口助手,搜到ESP32,点连接,然后发送一个数字,比如90,舵机立刻转到90度;发送180,舵机转到180度;发送0,舵机回到零位。整个过程响应很快,基本没有延迟感,所以看起来就像“手机一点,舵机就转”。
原理其实是一根“无线串口线”。ESP32的经典蓝牙里有个SPP协议,全称Serial Port Profile,翻译成人话就是把蓝牙通信虚拟成一条串口线。手机通过App连接之后,它发的字符就会出现在ESP32的蓝牙缓冲区里。你的代码做的事情很简单:读取缓冲区里的字符,拼出一个数字,解析成角度,然后调用舵机库输出对应的PWM信号。
很多人听到“蓝牙协议”就觉得难,但这里你完全不用关心协议栈的细节。ESP32官方库已经把底层封装好了,你只需要把它当成一个叫BluetoothSerial的串口对象来用,跟用Serial.print在电脑串口上打印日志一样简单。
1.2 零基础需要准备的硬件清单
这个项目的物料成本很低,我把清单列一下:
- ESP32开发板一块,建议用经典的DevKit V1或者NodeMCU-32S,38Pin版本最常见,资料也最多。
- SG90舵机一个,这是入门最常用的9g舵机,几块钱到十几块钱都有。
- 杜邦线3根(或者直接焊线也行),用来连接舵机和ESP32。
- 面包板一个,非必需,但我强烈建议准备,接线会舒服很多。
- 手机一台,Android优先,原因后面会讲。
- USB数据线一根,用来连接电脑烧录程序,注意选能传数据的线,很多买电子模块送的线只能充电。
这里再强调一下选型理由。SG90扭矩小、功耗低、价格便宜,对于入门项目完全够用。如果你手里已经有MG996R这种大舵机,接线方式一样,但供电要求更高,后面会专门说电源的坑。开发板选择DevKit V1是因为它引脚间距是标准的2.54mm,插面包板方便,而且很多开发板都带自动下载电路,新手烧录成功率更高。
1.3 为什么不用Arduino UNO加蓝牙模块
我知道很多新手会搜到Arduino UNO加HC-05蓝牙模块的方案,但我的建议是:既然现在买的是ESP32,就完全没必要再外挂蓝牙模块了。
原因有三点。第一,ESP32芯片本身同时支持经典蓝牙和BLE,也就是蓝牙低功耗,CPU性能也远超UNO,价格却没有贵多少。第二,Arduino UNO没有蓝牙,你需要额外接线去调HC-05模块。HC-05的AT指令配置对新手非常不友好,凡是搜过“hc05蓝牙模块连接不上”的人大概都有体会,波特率、主从模式、配对密码,任何一个设置不对都可能连不上。而ESP32的BluetoothSerial库开箱即用,不需要AT指令,不需要复杂的模块配置。第三,ESP32通过USB线就能下载程序,UNO加HC-05的话还需要操心模块和USB转TTL之间的交叉接线,多了一堆出错点。
所以这个项目用ESP32自带蓝牙,属于把硬件复杂度降到最低的做法。你省下的时间可以用来理解舵机控制和指令协议,这才是真正有用的东西。
2. 接线前要搞懂的舵机逻辑和电源问题
2.1 舵机是靠PWM脉宽“持续跟踪”角度的
接线之前,必须先知道舵机在等什么电信号。
SG90这种模拟舵机,内部结构是控制电路加直流电机加电位器。它工作的核心是PWM信号:一个周期为20毫秒的脉冲,其中高电平的持续时间叫做脉宽。0.5毫秒的脉宽对应0度,2.4毫秒的脉宽对应180度,中间按比例映射。也就是说,你想让舵机转到90度,就要持续输出一个周期20毫秒、脉宽1.45毫秒左右的方波。
这里有个很多人容易误解的地方:舵机不是收到一次脉冲就转到位然后停在那边了。它是“持续跟踪”的,每隔一段时间就要收到一个刷新脉冲,告诉它“你还应该保持在这个角度”。如果信号丢失,舵机可能失去舵力,用手可以轻轻掰动。所以你的代码在初始化之后,Servo库会负责不断地发送PWM信号,你不需要自己在loop里反复写。
这个概念和普通直流电机完全不同。普通电机是给高电平就转,不给就停。舵机是一个闭环系统,内部控制电路会不断读取电位器的角度,和当前脉宽对应的目标角度比较,然后驱动电机修正误差。你用生活经验来类比的话,舵机就像一个有刻度盘的方向盘,PWM脉冲是在告诉它“把这个刻度对准中线”,它自己负责执行。
2.2 哪些引脚能接舵机,哪些不能乱接
ESP32的引脚比较多,但不是每个引脚都能随便用来输出PWM。很多新手随便挑一个GPIO接舵机,结果发现编译失败、烧录失败或者舵机不动,最后排查一圈才发现是引脚选错了。
第一个禁忌是GPIO6到GPIO11这六个引脚,它们默认连接到板载flash芯片,是不能当普通IO使用的。你如果非要用它输出PWM,程序能编译,但实际不会有任何反应,甚至可能干扰flash通信。
第二个禁忌是GPIO34到GPIO39这六个引脚,它们只能作为输入,不能输出PWM。也就是说,接舵机信号线必然没反应。
第三个要注意的是GPIO1和GPIO3默认是串口引脚,虽然很多开发板能复用,但不建议新手在这里折腾。我的建议是直接用GPIO18或者GPIO19,这两个引脚既能安全输出PWM,又不涉及串口和flash,是舵机控制最稳的选择。如果你以后想控制多个舵机,GPIO21、GPIO22也都可以用。
我见过一个比较典型的故障:有人把舵机信号线接到GPIO16,程序烧了好几次都失败,最后发现那块板子的GPIO16接到了PSRAM,虽然理论上可以用,但会自动占用。所以新手阶段就按照推荐引脚来,不要为了省一根线去碰这些特殊引脚。
2.3 SG90接线方式与“共地”原则
接线非常简单,但越简单越容易在供电上翻车。
SG90舵机引线一般有三种颜色:棕色线是GND,红色线是电源正极,橙色线是信号线。接线方式如下:
- 棕色线接到GND,可以是ESP32的GND,也可以是外部电源的GND,最好两者都接。
- 红色线接到5V电源正极。
- 橙色线接到GPIO18。
这里最关键的是“共地”。如果你的舵机电源来自外部5V电源,比如充电宝、锂电池、稳压模块,那么外部电源的GND必须和ESP32的GND连在一起。因为舵机信号是一个相对于GND的电压差,如果两个设备的地电位不统一,信号线送到舵机控制板的电压就不准,舵机要么不转,要么乱抖。
那能不能直接从ESP32板子的5V引脚给舵机供电?可以,但前提是你只带一个SG90,而且USB供电的电流要足够。SG90正常转动时的电流大概在几十到一百多毫安,堵转瞬间可能冲到几百毫安。ESP32本身跑WiFi或蓝牙时的电流也不小,板载稳压芯片如果压不住,电压一跌,轻则舵机抽搐,重则ESP32直接重启。所以我的做法是:调试阶段直接用ESP32的5V引脚给SG90供电,跑通之后如果发现舵机抖动,就老老实实外接一个5V/2A的电源,把GND和ESP32连在一起。
如果你用的是MG996R这类大舵机,强烈建议直接外接电源,不能用开发板供电。大舵机堵转电流动不动就上安培,板载LDO根本扛不住。关于电源这个问题,我在后面避坑章节还会再详细说,因为它真的能制造出各种“像代码BUG但其实不是代码BUG”的现象。
3. Arduino IDE里的ESP32开发环境:第一次顺利烧录的关键
3.1 安装ESP32开发板包并配置镜像源
ESP32不是Arduino官方支持的主控芯片,所以需要在Arduino IDE里额外安装开发板包。
打开Arduino IDE后,先进入“文件”菜单里的“首选项”,找到“附加开发板管理器网址”这一栏,填入Espressif官方维护的JSON地址:
https://espressif.github.io/arduino-esp32/package_esp32_index.json如果你下载速度很慢,可以把这个JSON地址替换成国内镜像源。具体的镜像地址网络上很好找,我这里不单独推荐某一家。填好之后,点击“工具”菜单,选择“开发板”下的“开发板管理器”,搜索“esp32”,应该能看到“esp32 by Espressif Systems”。点击安装,推荐安装正式版而不是预览版。安装过程会下载几百兆文件,耐心等几分钟。
这一步看起来简单,但新手最容易卡在“JSON地址填了但下载失败”上。如果失败,先检查Arduino IDE是不是2.x版本,管理器和1.x版本有区别。另外安装过程可能需要访问外网,网络不稳定时就换镜像源,多试几次就好。
3.2 板卡选择与端口识别
安装完成之后,在“工具”菜单里找到“开发板”,选择“ESP32 Arduino”下的“ESP32 Dev Module”。注意不要选成“ESP32S2 Dev Module”或者“ESP32C3 Dev Module”,哪怕你的芯片型号看起来像,这里建议统一选ESP32 Dev Module,大部分通用开发板都能正常烧录。
然后选择端口。把ESP32开发板用USB线连上电脑,工具菜单的“端口”里会出现一个串口设备。如果是CP2102芯片的板子,端口名通常是COM3这样的数字;如果是CH340芯片,也会显示COM口。如果端口列表为空,多半是USB驱动没装好,需要手动安装CH340或CP2102驱动。Windows系统插上板子如果提示USB设备未识别,十有八九就是驱动问题。
选好板子和端口之后,烧录速度建议保持默认的921600。如果烧录老是失败,可以把波特率降到115200,成功率会高很多。
3.3 先烧一个舵机测试程序,确认硬件没问题
搞完环境,先不要急着上蓝牙。我强烈建议先烧一个最简单的舵机测试程序,把硬件链路确认好,这样后面做蓝牙对接时出问题更容易定位。
在Arduino IDE里新建一个项目,复制下面的代码:
#include <ESP32Servo.h> #define SERVO_PIN 18 Servo myservo; void setup() { myservo.attach(SERVO_PIN, 500, 2400); myservo.write(90); } void loop() { }这段代码的作用是让舵机转到90度并保持。需要先在“库管理器”里搜“ESP32Servo”并安装,这个库在ESP32上的兼容性比Arduino自带的Servo库更好。attach函数里的500和2400表示舵机在0度和180度时对应的脉宽微秒数,SG90默认在这个范围,直接照抄就行。
烧录好之后,如果舵机“咔”一声转到90度,说明供电、信号线、引脚全都没问题。如果舵机不动,先确认舵机信号线是不是插在GPIO18,再确认红色线是不是接了5V,最后检查共地。这个过程需要单独测试,别把蓝牙问题混在一起。
4. 蓝牙部分:为什么用经典蓝牙SPP而不是BLE
4.1 ESP32双模蓝牙和SPP方案的选择
ESP32的蓝牙是双模的,既支持经典蓝牙,也支持BLE低功耗蓝牙。很多评测文章都喜欢强调BLE,但我要说的是,零基础做“手机控制舵机”这个项目,经典蓝牙SPP比BLE友好太多。
原因很直白:BLE不是像串口那样直接发字节,你需要理解GATT服务、服务UUID、特征值UUID,还要在手机端写一个配套App或者用支持BLE调试的工具去读写特征值。这些概念对新手来说太抽象了。而经典蓝牙SPP把通信通道仿真成一个串口,手机端随便下载一个蓝牙串口助手App,连接之后就能直接收发字符。ESP32这边用BluetoothSerial库,基本就是把Serial换成SerialBT,API几乎一样。
所以这个项目选SPP,本质是把精力集中在舵机控制和字符解析上,而不是去啃蓝牙协议栈。等你把这个项目跑通,理解了“接受字符、解析指令、执行动作”这套思路之后,再去玩BLE也不迟。
不过这里有一个手机端的限制需要提前告诉你们:SPP在Android平台的支持很成熟,App也很多,但在iPhone上比较麻烦。iOS不开放SPP通道给第三方App访问,除非用特定的BLE方案,否则非常折腾。所以如果你手里是iPhone,建议换个Android手机来玩这个项目,或者直接跳过SPP,转向BLE方案。
4.2 手机配对与蓝牙串口助手的真实连接流程
蓝牙配对这里藏着一个很多人都会踩的坑:在手机系统蓝牙设置里搜索到“ESP32_Servo”并配对成功,但打开蓝牙串口助手后却发现连不上。
原因是这样的:经典蓝牙SPP需要在App端建立一个实际的数据连接,而不只是系统层面的配对。手机系统的配对只是把两个设备“认识”了,但具体使用数据通道还需要App去主动connect。所以正确流程是:先打开手机系统蓝牙,搜索设备,找到“ESP32_Servo”后点击配对,配对码一般是0000或1234。配对成功之后,再打开蓝牙串口助手,在软件里的设备列表中选择“ESP32_Servo”,这时App才真正建立SPP连接。
如果App里显示连接失败,不要反复去系统设置里找,先把之前配对的设备“取消配对”,然后重新搜索和配对,再回App连接。我遇到过很多次,就是因为系统记住了旧的配对状态,导致后面App连接不上。这个习惯很重要,遇到蓝牙连接异常,第一反应不是研究App,而是先把配对记录清掉重来。
4.3 BluetoothSerial库的核心用法
先介绍一下库的基本函数,心里有数之后再看代码会更顺畅。使用BluetoothSerial库时,你需要先定义一个全局对象:
BluetoothSerial SerialBT;然后在setup里启动:
SerialBT.begin("ESP32_Servo");括号里的字符串就是蓝牙搜索时看到的设备名称,你可以随便改。之后的操作跟串口完全一样,用SerialBT.available()判断有没有收到字符,用SerialBT.read()读一个字符,用SerialBT.print()和SerialBT.println()回消息给手机。
这段代码之所以简单,就是因为ESP32官方已经把所有SPP底层细节封装好了。你在手机上发送的字符流会一个个出现在SerialBT这个对象里,就像你在电脑上往ESP32的Serial串口发数据一样。理解了这一点,后面写指令解析就水到渠成。
5. 完整示例代码:手机指令如何变成舵机角度
5.1 需要提前安装的库
正式写代码之前,确认两个库已经就位:一个是前面说过的ESP32Servo,需要在库管理器里手动搜索安装;另一个是BluetoothSerial,这个库包含在ESP32开发板包里,不需要单独安装。如果你在编译时提示找不到BluetoothSerial.h,说明ESP32开发板包没有装好,先回到第3节检查。
5.2 指令解析为什么不能只用一个toInt
再说一个新手特别容易踩的坑:用SerialBT.readString()配合toInt()去解析角度,看起来简单,实际用起来很痛苦。因为手机端蓝牙串口助手发送数据并不是一次一个字符,它可能一次性发来一串字符,也可能只发一半,而toInt()在缓冲区里挑数字跳着读,容易把奇怪的内容当成0,舵机毫无征兆归零。
更通用的做法是自己维护一个字符串缓冲区,把收到的字符一个一个拼起来,直到收到换行符,再判断这串字符是不是合法角度。这样做的逻辑更清晰,也方便调试。手机App端发送数字时如果勾选“发送新行”,程序就能按行切分;即使没勾选,我还会加一个超时机制,字符停顿一段时间后也视为一条命令结束,容错性更好。
5.3 完整示例程序与逐段解释
直接给出完整代码:
#include <BluetoothSerial.h> #include <ESP32Servo.h> #define SERVO_PIN 18 BluetoothSerial SerialBT; Servo myservo; String buffer = ""; unsigned long lastReceiveTime = 0; int parseAngle(String s) { // 只允许纯数字命令,避免非数字内容误转成0 if (s.length() == 0) return -1; int sum = 0; for (unsigned int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (c < '0' || c > '9') return -1; sum = sum * 10 + (c - '0'); if (sum > 180) return -1; } return sum; } void processCommand(String cmd) { cmd.trim(); if (cmd.length() == 0) return; // 兼容 "<90>" 这种带尖括号的写法,方便某些串口助手发送 if (cmd.startsWith("<") && cmd.endsWith(">") && cmd.length() >= 3) { cmd = cmd.substring(1, cmd.length() - 1); } int angle = parseAngle(cmd); if (angle >= 0 && angle <= 180) { myservo.write(angle); Serial.println("Angle: " + String(angle)); // 电脑端调试观察 SerialBT.println("OK:" + String(angle)); // 手机端回显 } else { SerialBT.println("ERR:0-180"); } } void setup() { Serial.begin(115200); // 电脑串口调试 myservo.attach(SERVO_PIN, 500, 2400); SerialBT.begin("ESP32_Servo"); // 蓝牙设备名 Serial.println("Bluetooth Servo ready"); } void loop() { while (SerialBT.available()) { char c = SerialBT.read(); if (c == '\n' || c == '\r') { processCommand(buffer); buffer = ""; } else { buffer += c; lastReceiveTime = millis(); } } // 部分手机App不发送换行符,字符停顿超过50ms也视作命令结束 if (buffer.length() > 0 && millis() - lastReceiveTime > 50) { processCommand(buffer); buffer = ""; } }代码看起来不短,但逻辑很清楚。setup里做了三件事:打开电脑串口,把舵机信号绑定到GPIO18,启动蓝牙服务并命名为ESP32_Servo。loop里每一轮都检查蓝牙缓冲区,有字符就读进来,遇到换行符就调用processCommand处理。parseAngle函数的作用是校验收到的内容是不是0到180之间的数字,避免“abc”这种乱码被当成0而把舵机打到零位。
processCommand先处理带尖括号的格式,再解析角度,合法就转动并回显,不合法就回复错误信息。这里的回显很重要,你可以在手机App上看到一个“OK:90”,如果发送的指令有误,也能立刻看到“ERR:0-180”,方便判断究竟是手机App的问题还是代码的问题。
5.4 编译和烧录注意事项
复制这段代码到Arduino IDE时,记得先确认开发板是“ESP32 Dev Module”,端口选对。编译过程中如果报错说找不到ESP32Servo.h,回到库管理器搜索安装ESP32Servo。有些版本还需要在工具菜单里把“Flash Size”设置成4MB,如果默认已经识别就不用动。
烧录过程中如果出现“Failed to connect to ESP32: Timed out”,不用慌,按住开发板上的BOOT按钮不放,点击烧录,看到“Connecting... ...”提示时再松开BOOT按钮,绝大多数情况下都能成功。这个问题在USB转串口芯片不稳定的板子上尤其常见,多试几次完全正常。
6. 手机侧实操与调试:一整套完整的验证方法
6.1 蓝牙串口助手App怎么选
打开应用商店搜“蓝牙串口助手”或者“Serial Bluetooth Terminal”,能找到一大堆,原理基本一样。我的建议是找一个界面里有输入框、发送按钮、和“发送新行”复选框的App,因为这三个功能对我们调试最有用。大多数蓝牙串口助手在连接成功后会有一个提示,你可以先在手机系统蓝牙里完成配对,再在App里选择设备连接。
如果你手头的App连接不上,不妨换个App试试。不同App对不同品牌手机蓝牙栈的兼容性有差异,多换几个不丢人,毕竟目标是让项目跑通。
6.2 发送指令的准确姿势和调试顺序
连接成功后,先在手机App的输入框里输入“90”,点击发送。如果舵机没反应,但App界面里收到了“OK:90”的回复,说明蓝牙通信是通的,问题在舵机供电或接线。如果连回复都没有,说明蓝牙数据通道没建立起来,回去检查配对状态。
我建议第一次测试按这个顺序来:
- 慢慢输入“90”,发送,看舵机是否转。
- 发送“0”,看舵机是否回零位。
- 发送“180”,看是否能转到另一侧。
- 发送“abc”,看是否收到“ERR:0-180”。
最后一步非常重要,能验证你的指令解析是否可靠。如果你发现发送“abc”舵机反而转到0度,说明代码里用的是简单toInt(),需要换成我前面提供的parseAngle版本。
另外要记得,手机App发送时最好勾选“发送新行”,这样发送的字符串后面会自动带上换行符,代码一收到换行符就会立刻处理。如果你用的App没有这个选项,代码里的50毫秒超时机制也能兜底,但响应速度会稍慢一点点,不影响使用。
6.3 跑完之后我发现的那些奇葩现象
调试过程中会遇到一些看起来特别像模块坏了的情况,这里列一个对照表方便排查:
- 现象:发送“90”后舵机完全不动,但App显示连接正常。对策:首先确认GPIO18是否正确,然后检查红色线是否接了5V,最后看信号线是不是插在面包板上接触不良。
- 现象:舵机抖动,像抽搐一样。对策:八成是电源问题,换外部5V电源,共地,不要只靠USB供电。
- 现象:发送“180”能转到大概位置,但角度误差很大。对策:检查
attach里的微秒参数,不同批次SG90的脉宽范围有差异,可以手动微调500和2400这两个数字。 - 现象:发送几次之后蓝牙自动断开。对策:某些手机蓝牙省电策略会在设备空闲时关闭SPP连接,去系统设置里关闭该蓝牙设备的省电优化,或者换一台手机测试。
- 现象:发送“10”之后舵机有轻微转动,但是找不到90度的位置。对策:这种一般是脉宽范围不对,SG90的0度和180度不一定完全对应0.5ms和2.4ms,可以根据实测把attach的第二、第三个参数改成550和2450这类接近值。
这些都是实际项目里很常见的问题,我以前调试时被供电不足折磨过整整一下午。现在回想起来,如果一开始就遵循“先测舵机测试程序,再上蓝牙”的流程,至少能省出一个小时。
7. 蓝牙控舵机跑通之后:扩展思路与我的调试习惯
当你做完这一步,其实已经掌握了两类重要技能:一类是ESP32的PWM控制能力,另一类是蓝牙串口的数据交互能力。这两个能力组合起来可以玩出很多花样。
先说说多路舵机。ESP32Servo库支持同时控制多个舵机,你只需要再定义几个Servo对象,分别绑定到不同引脚,然后手机端发送类似“1:90,2:45”这样的复合指令,解析时按第一个数字选舵机通道,后面跟角度值。这也是未来做机械臂、四足机器人、云台的基础。要注意的是,多路舵机并联时供电压力更大,每一路舵机都需要充足的电流,否则会互相干扰。
再说说总线舵机。普通模拟舵机只能接收角度指令,无法把当前角度回传给主控。如果你想做机械臂闭环控制,比如让机械臂知道自己现在在什么位置,那就要用带反馈的总线舵机。这类舵机走UART或者RS485协议,ESP32有多个串口,配合总线舵机的库也能控制,但指令协议就比PWM复杂得多。我建议先把今天的普通舵机玩熟,再决定要不要跳进这个坑。
还有一个方向是BLE。如果你需要iPhone控制,或者未来想接小程序、App,BLE是主流方案。ESP32的BLE开发可以用BLE UART这样的现成库,其实也是封装好的,只是手机端需要一个真正的BLE调试工具,比如LightBlue这类,而且是读写特征值而不是直接收发字符。等你在SPP模式下理解了什么叫做“收到一个字节”,再去看BLE的service和characteristic,就没有那么懵了。
最后分享一个我个人的调试习惯。每次拿到新板子或者新模块,我一定会先写一个最小测试程序,把硬件部分单独验证一遍,就像前面说的舵机归零测试。然后再叠加功能。如果一次项目里所有功能都混在一起,出了问题就很难排查,你会分不清到底是蓝牙断了、还是代码崩了、还是舵机电源不够。把问题拆小,一步步验证,看起来慢,实际是最快的。
这个项目的价值在于,每一步你都能看到实实在在的东西:信号线接对了,舵机会动;蓝牙连上了,手机能回显;代码写对了,指令能控制角度。这三样东西组合在一起,就是未来无数智能硬件的雏形。后面如果你想给ESP32加个网页控制界面,或者接上温度传感器做自动调节,无非是在这个框架里多加一条输入或输出的链路而已。