轮式小车太简单了,四条腿才是真正的入门功课。一只四足机器人得同时管好三件事:三条腿撑住地面、一条腿抬起来迈步、身体重心在支撑腿围成的三角形里来回挪。任何一件没算对,它就在你面前趴下。
这篇教程用一块 ESP32、八个 MG90S 舵机、一块 3D 打印的蜘蛛架子,搭一台手机蓝牙遥控的四足机器人。物料成本大概 43 美元,不含打印件和电池,动手三小时能走起来。核心思路很简单:每条腿一个髋关节加一个膝关节,所有动作都走同一套平滑插值,再加一个校准模式让你在软件里修直一条歪腿,不用拧螺丝。代码是完整可烧的,不是示意片段。

成品俯拍:四条腿是 3D 打印件,红色六边形主控板压在机身中间,蓝色微型舵机分别装在髋和膝两个关节上,橙红两色是走线的杜邦线。
材料清单
先把手上的东西点一遍,缺一样都装不完整。
ESP32 开发板一块,38 针、CP2102 串口芯片、USB-C 口。这里有个硬要求:必须是原版 ESP32。S3 和 C3 没有经典蓝牙 SPP,代码里那句 BluetoothSerial 直接编译不过。
MG90S 金属齿轮微型舵机八个,髋四个、膝四个。金属齿轮是必须的,塑料齿轮的 SG90 撑不住四足落地那一下冲击,走两圈就扫齿。
XL4015 降压模块一个,5A 输出,专门给舵机供电。这是整套里最容易被省掉、也最不能省的一件。
两节 18650 电池盒一个,串起来 7.4 到 8.4 伏。
电解电容一颗,470 到 1000 微法,跨接在舵机电源轨上。
杜邦线若干、M2 螺丝一包、3D 打印的蜘蛛框架一套(身体一块、髋四块、腿四条),外加一台安卓手机,装一个蓝牙串口终端 App。
有一条必须记住:八个舵机同时动的瞬间电流有好几安培,绝对不能从 ESP32 的 5V 或者 3V3 引脚取电。板上那颗稳压芯片额定几百毫安,一冲就掉压,表现就是走到一半突然重启。遇到这种症状,九成九是电源问题,不是代码问题。
第一步:先把电搞定
装腿之前先单独把电源调好,这一步花五分钟,能省掉后面两小时的莫名其妙。
电池盒的输出接 XL4015 输入端,输出端先不接任何负载,万用表搭在输出上,拧那颗电位器,把电压调到 5.5 伏。为什么不是 5 伏:MG90S 标称工作电压 4.8 到 6 伏,取中间偏上,扭矩够、发热也不至于太夸张。而且降压模块带载之后输出会往下掉一点,空载调 5.5,接上八个舵机大概落在 5.3 左右,还在安全区。

XL4015 降压模块实物,蓝色板子上一颗大电感和一颗可调电位器。输入端接两节 18650 的 7.4 到 8.4 伏,输出调到 5.5 伏再喂给八个舵机。
电压确认之后接线:XL4015 输出正极接八个舵机的红线,全部并联;输出负极接八个舵机的棕线,同时接 ESP32 的 GND。电容正负极跨在舵机电源轨上,尽量靠近舵机那一端。
电容这颗东西很多人省,但它干的活很实在:舵机启动那一瞬间要从电源抽一大口电流,线材有电阻,抽的瞬间整条轨的电压会被拉低。电容像个就近的小水库,先顶一下,让电压别塌。470 微法够用,1000 微法更稳,注意耐压选 16 伏以上。
还有一条:舵机的地和 ESP32 的地必须接在一起。信号是靠电压差传的,两边地不共,信号电平就没有共同参考点,舵机会乱抖。这是新手最常见的坑之一。
第二步:引脚怎么分
舵机信号线是橙色的,八根分别接到 ESP32 的八个 GPIO。腿的编号定成这样:0 号左前、1 号右前、2 号左后、3 号右后。
髋关节四个接 GPIO 13、16、18、25。膝关节四个接 GPIO 27、17、19、26。
这组引脚不是随手挑的,它避开了 ESP32 的 strapping pins,也就是 GPIO 0、2、5、12、15 这五个。这几个脚在上电那一刻会被芯片采样,用来决定从哪个分区启动、跑什么时钟。如果你把舵机信号挂在这上面,上电瞬间舵机内部把线拉高或者拉低,芯片就采样到错误电平,直接进不了 boot,板子看起来像坏了。

舵机控制板的布线与元件位图,标了 3V3、GND、IO 的走线走向,还有 220、100uF、470uF、SB560 这些元件位号。自己打板的话照着这张对位。
另外 34 到 39 这几个脚是只输不能输出的,也别拿来驱动舵机。剩下的脚里,避开 strapping、避开只输入、避开串口占用的 1 和 3,可用范围就很清楚了。
第三步:烧固件
先装 Arduino IDE,从库管理器装上 ESP32Servo 库,板子选 ESP32 Dev Module。顺序很重要:在装腿之前就把程序烧进去。因为烧完程序舵机会立刻归中到 90 度,如果腿已经装上了,它会突然一拧,可能把打印件别断。
#include "BluetoothSerial.h" // 经典蓝牙SPP,只有原版ESP32支持
#include "ESP32Servo.h"
BluetoothSerial bt;
// 腿编号:0左前 1右前 2左后 3右后
const int HIP_PIN[4] = {13, 16, 18, 25};
const int KNEE_PIN[4] = {27, 17, 19, 26};
int hipDir[4] = {1, -1, 1, -1};
int kneeDir[4] = {1, -1, 1, -1};
int hipTrim[4] = {0, 0, 0, 0};
int kneeTrim[4] = {0, 0, 0, 0};
const int SWING = 25, LIFT = 35, STAND = 0;
Servo hip[4], knee[4];
int hipPos[4], kneePos[4];
void writeLeg(int i) {
hip[i].write(90 + hipDir[i] * hipPos[i] + hipTrim[i]);
knee[i].write(90 + kneeDir[i] * kneePos[i] + kneeTrim[i]);
}
void glide(const int h[4], const int k[4], int ms) {
int h0[4], k0[4];
memcpy(h0, hipPos, sizeof h0);
memcpy(k0, kneePos, sizeof k0);
for (int s = 1; s <= 20; s++) {
for (int i = 0; i < 4; i++) {
hipPos[i] = h0[i] + (h[i] - h0[i]) * s / 20;
kneePos[i] = k0[i] + (k[i] - k0[i]) * s / 20;
writeLeg(i);
}
delay(ms / 20);
}
}
void stepLeg(int i, int hipTarget) {
int h[4], k[4];
memcpy(h, hipPos, sizeof h);
memcpy(k, kneePos, sizeof k);
k[i] = LIFT; glide(h, k, 120);
h[i] = hipTarget; glide(h, k, 120);
k[i] = STAND; glide(h, k, 120);
}
void stand() {
int h[4] = {0, 0, 0, 0};
int k[4] = {STAND, STAND, STAND, STAND};
glide(h, k, 300);
}
void walk(int dir) {
const int order[4] = {0, 3, 1, 2};
for (int n = 0; n < 4; n++) stepLeg(order[n], dir * SWING);
int h[4] = {0, 0, 0, 0}, k[4];
memcpy(k, kneePos, sizeof k);
glide(h, k, 400);
}
void turn(int dir) {
for (int i = 0; i < 4; i++) stepLeg(i, dir * hipDir[i] * SWING);
int h[4] = {0, 0, 0, 0}, k[4];
memcpy(k, kneePos, sizeof k);
glide(h, k, 400);
}
void wave() {
int h[4], k[4];
memcpy(h, hipPos, sizeof h);
memcpy(k, kneePos, sizeof k);
k[0] = 70; glide(h, k, 300);
for (int n = 0; n < 3; n++) {
h[0] = 30; glide(h, k, 250);
h[0] = -30; glide(h, k, 250);
}
stand();
}
void setup() {
Serial.begin(115200);
for (int i = 0; i < 4; i++) {
hip[i].setPeriodHertz(50);
hip[i].attach(HIP_PIN[i], 500, 2400);
knee[i].setPeriodHertz(50);
knee[i].attach(KNEE_PIN[i], 500, 2400);
hipPos[i] = 0; kneePos[i] = STAND; writeLeg(i);
}
bt.begin("ShillehTek-Spider");
}
void loop() {
static char mode = 'S';
char c = 0;
if (bt.available()) c = toupper(bt.read());
else if (Serial.available()) c = toupper(Serial.read());
if (c && strchr("FBLRWCS", c)) mode = c;
switch (mode) {
case 'F': walk(1); break;
case 'B': walk(-1); break;
case 'L': turn(1); break;
case 'R': turn(-1); break;
case 'W': wave(); mode = 0; break;
case 'S': stand(); mode = 0; break;
case 'C': for (int i = 0; i < 4; i++) { hip[i].write(90); knee[i].write(90); } mode = 0; break;
default: delay(20);
}
}烧完八个舵机会归中到 90 度并保持住。代码里两个头文件用的是引号不是尖括号,这一点别改。
代码结构值得说一下,因为它决定了后面怎么改。核心是三个函数:writeLeg 负责把内部角度换算成舵机实际角度并写出去;glide 负责在两个姿态之间插值,它把一次移动切成 20 小步,每步之间 delay(ms/20),所以任何动作都是滑过去的,不是瞬移;stepLeg 是抬腿、摆髋、落脚三个动作的组合,每段 120 毫秒。
walk 函数里的 order 数组是 0、3、1、2,这是爬行步态(crawl gait)的抬腿顺序:左前、右后、右前、左后。这个顺序保证任何时刻都只有一条腿离地,另外三条构成一个稳定的三角形,身体重心始终落在这个三角形里面。所以它慢,但它几乎不会摔。
第四步:校准中位
这一步决定它走不走得直,是整个教程里最需要耐心的地方。
先往串口发一个 C 命令,八个舵机全部归到精确 90 度。然后装舵机摇臂和腿,目标是让每条髋关节朝对角线方向水平伸出、每条腿水平。
麻烦在于舵机花键只能每 15 度一档,摇臂几乎不可能刚好落在完美的中位上,总会偏个三五度。硬拧会把齿咬坏。正确做法是用代码里的 hipTrim 和 kneeTrim 数组补偿:改几度、重新烧录、发一个 S 看站姿,反复调到四脚均匀着地、身体平行桌面为止。
八个 trim 值调完,这套参数就是你这台机器人的身份证,记下来存好。以后换电池、换腿都不用重调。
第五步:手机连上
手机蓝牙搜索 ShillehTek-Spider,配对之后打开串口终端 App,安卓上推荐 Serial Bluetooth Terminal。连上就能发单字符命令:F 前进、B 后退、L 左转、R 右转、S 站定、W 挥手、C 全部归中。
App 里可以把这几个字符设成快捷按钮,做成一个遥控面板,按下去就走。
按 F 的时候你会看到它一步一步挪:抬左前腿、摆出去、落下,然后右后腿,然后右前,然后左后。四步一轮,身体跟着微微前后晃。这就是最朴素也最可靠的四足行走,不需要任何动态平衡算法,纯靠静态稳定。
第六步:调参
第一次跑基本不会完美,几个常见症状对应几个参数。
某条腿迈步时往后踢而不是往前,说明那个髋舵机装反了,把 hipDir 数组里对应位置的正负号翻一下就行。脚往桌面下扎而不是抬起来,同理翻 kneeDir 的对应项。
走起来脚拖地,把 LIFT 从 35 调到 40 或者 45,抬得高一点。但别调太高,抬得越高落地冲击越大,舵机越吃力。
步伐太大导致身体左右晃,把 SWING 从 25 降到 20。反过来想走快点就往上加,但加到 35 以上支撑三角形会变小,稳定性明显下降。
这些参数可以一边跑串口一边改,改完重烧,不用拆机。这也是为什么要把校准和调参全放在软件里,机械上不留调节余地。
它为什么能站住
很多人第一次看四足行走会觉得玄,其实原理不复杂。四足在任意时刻只要有三条腿着地,这三个着地点就构成一个三角形。只要身体重心在水平面上的投影落在这个三角形内部,它就是静态稳定的,不动也不会倒。
爬行步态就是把这件事做到极致:永远只抬一条腿,支撑三角形永远存在,只是形状在变。代价是慢,四个舵机轮流工作,速度上不去。
真正难的是再往上走一步。小跑步态要同时抬对角两条腿,那时候支撑只剩一条对角线,重心必须靠身体的动量和加速度维持,这就进入动态平衡的范畴,得算零力矩点、得做姿态反馈、得上 IMU。那已经是另一个量级的项目了。
所以这个教程的价值不在于那几十行步态代码,而在于让你真正理解抬腿、落地、身体位移这三件事怎么咬合。glide 和 stepLeg 这两个函数吃透了,后面想做小跑、对角步态、加视觉跟踪,都是在同一个框架上叠东西。
还能往哪加
基础版能走能转能挥手,剩下的空间很大。
想避障,前面装一个 HC-SR04 超声波,在 walk 里加一句距离判断,15 厘米内自动转弯。
想解放 GPIO 同时拿到更稳的 PWM,把舵机迁到 PCA9685 驱动板上,I2C 两根线控十六路,ESP32 的脚就空出来接传感器了。
想摆脱安卓手机,把蓝牙串口换掉,让 ESP32 自己开一个网页,手机浏览器直接连,iPhone 也能用。
想加摄像头做视觉跟随,那就要换主控了,ESP32 的算力跑不动实时目标检测,得往上走到能跑轻量模型的板子。
从一堆零件到一台会走路的四足,中间其实就隔着电源、引脚、插值这三件事。把这三件做对,剩下的都是加法。












这个蠕步步态的思路很清晰——每次抬一条腿另外三条撑地。我之前做的四足用Trot对角步态,调试起来麻烦得多。入门还是蠕步靠谱。
XL4015调5.5V给舵机供电这个建议太实用了。我之前用USB直接带6个舵机,动不动就重启,折腾了两天才找到原因。
请问这个3D打印的蜘蛛框架STL文件在哪里?ShillehTek的页面好像只提到了零件链接,没看到框架文件。
代码里的glide()函数写得挺讲究的,20步插值比直接跳变顺滑多了。不过我试了一下用S3板子,蓝牙SPP确实不行,只能换回原版ESP32。