教程二 · 小智机器狗(ESP32-C6 + 4 个 SG90 舵机)

给已经能说话的小智装上四条腿,让它听得懂「坐下」「趴下」并真的动起来。

难度 ★★★★☆ 硬件成本 约 ¥120(4 个舵机 + 独立电源) 动手时间 约 4 小时 成品 说"趴下"它就趴下,说"摇摇身子"它会摇

前置条件:教程一 · 小智 AI 语音助手(ESP32-C6 + OLED + 麦克风 + 功放) 已经做完,固件能正常对话。

这一篇是在同一块板子、同一份固件上加外设,不是从零开始。

动手顺序:

① 看成品 → ② 备材料 → ③ ★接供电 → ④ 接舵机 → ⑤ 改代码

→ ⑥ 烧录 → ⑦ 验收 → ⑧ 常见问题

⚠️ 接舵机前请先读完第 3 节。 舵机是全教程唯一会烧硬件的地方,

供电接错一次可能烧掉板子。后面那几节写代码的部分都可以慢慢来,这节不行。

1. 做完是什么样

• 语音指令 → AI 判断语义 → 调用 MCP 工具 → 舵机执行动作

• 14 个语义动作:站立 / 坐下 / 趴下 / 睡觉 / 鞠躬 / 警觉 / 摇身 / 举左手 / 举右手 / 前进 / 后退 / 左转 / 右转 / 唤醒

• 对话时可以用软件偏置微调四条腿的机械对齐,不用拆机械

🔊 小智:能说话

│

┌────┴────┐

│ ESP32-C6│

└────┬────┘

GPIO3/18/22/23

│ │ │ │

╭╯ ╰╮ ╭╯ ╰╮

RF RB LF LB ← 4 × SG90,机械臂当腿

这个形态是四足、每条腿一个自由度 —— 只做髋部摆动,不做真步行。

这是有意的取舍:4 自由度 × 4 腿的逆运动学对舵机精度和机械结构要求很高, 而这个形态已经足够好玩,而且不会因为相位没对齐就摔。

2. 材料清单

2.1 必备件

物料

规格

数量

说明

主控板

ESP32-C6 面包板开发板

1

复用教程一,不用新买

OLED + 麦克风 + 功放 + 喇叭

——

——

同样复用

舵机

SG90,180° 限位版

4

外接电源

12V 可调电源(数显)

1

★ 输出调到 6.5V 左右,详见第 3 节

降压模块

LM2596 类,输出 5V

1

★ 插面包板,详见第 3 节

面包板

复用

1

2.2 工具

• 万用表 ×1(这一篇比上一篇更需要它,量电压要用)

• 小螺丝刀 ×1(拧降压模块的旋钮和端子)

• 扎带或双面胶(固定线束,可选)

2.3 ★ 买之前确认

舵机买 180° 限位版。 连续旋转版(有的叫 360°)没有角度概念, 你给多少脉宽它都只会朝一个方向转 —— 接上去表现是"一直嗡嗡转不停"。

3. ★ 接供电(动手前必读)

3.1 电源怎么搭

外接一个 12V 可调电源(输出能力 ≥3A),输出调到 6.5V 左右, 接一个 LM2596 降压模块降到 5V。模块插在面包板上,给舵机和 ESP32 供电。

可调电源不直接给 12V,是因为降压模块输入和输出差得越多发热越严重; 6.5V 只比 5V 高一点,模块只是微温。

项目

数值

SG90 工作电压

4.8 ~ 6 V

静止电流

约 20~30 mA/个

堵转电流

约 300~600 mA/个

4 个同时动作最坏

~2.6 A

所以电源和降压模块的输出能力都要 ≥3A。

⚠️ 上电前先调电压:万用表搭在降压模块输出端,拧模块上的蓝色旋钮调到 5V;

再量舵机两端,不要超过 6V(SG90 硬上限),也不要低于 4.8V

—— 欠压时舵机只会「嗡嗡」响,那是它在挣扎,不是接错了。

3.2 供电拓扑

这一节照着接,一根都不能少:

12V 可调电源(调到 6.5V 左右)

└──→ 降压模块(LM2596,输出 5V,插面包板)

├──→ 4 个舵机红线(并联)

└──→ ESP32 的 5V/VIN ← ★ 别忘了这个

降压模块 GND

├──→ 4 个舵机棕/黑线

└──→ ESP32 任意 GND ← ★ 共地线,不能少

ESP32 GPIO3/18/22/23

└──→ 4 个舵机的信号线(橙/白)

3.3 ★ 共地线是必须的

PWM 脉冲的有效电平 = V(信号) − V(GND)。

不共地时,两边各自浮动的参考电平会让舵机完全收不到有效脉冲 —— 表现是「有电、有锁死阻力、但纹丝不动」。

⚠️ 这个症状和「信号线没插好」几乎无法从外部区分,是这一篇最容易误判的地方。

3.4 关于 USB 线

如果你打算拔掉 USB 独立运行,注意 USB 的三个作用:

USB 的作用

拔掉会怎样

供电(5V → LDO → 3.3V)

可以替代:降压模块输出接板子 5V/VIN

串口日志

丢失,看不到开机日志

烧录固件

无法烧录 —— 得先插 USB 烧完再拔

⚠️ 常见症状:「必须插着 USB 屏才亮」。

这通常不是屏的问题 —— 是降压模块只接了舵机,ESP32 还在靠 USB 供电。

排查这个问题画供电拓扑图比读代码快得多:谁给谁供电,一目了然。

3.5 上电前检查清单

□ ESP32 的 5V/VIN 接了降压模块输出(不只是舵机那路)

□ 所有 GND 汇到一处,有一根线接到 ESP32

□ 万用表量过:降压模块输出已调到 5V,舵机两端不超过 6V

□ 舵机臂不会撞到机械限位

□ 舵机信号线远离按键信号线,不交叉

五条全过了再上电。

4. 接舵机

4.1 四个舵机的位置

舵机

部位

GPIO

颜色约定

舵机 1

右前腿 RF

GPIO3

舵机 2

右后腿 RB

GPIO18

舵机 3

左前腿 LF

GPIO22

舵机 4

左后腿 LB

GPIO23

4.2 每个舵机三根线

SG90 一般是三根线,按颜色:

颜色

含义

接哪

棕 / 黑

GND

降压模块输出负极

红

正极

降压模块输出正极(5V)

橙 / 白 / 黄

信号(PWM)

ESP32 对应 GPIO

4.3 选脚依据

为什么是这四个脚:这块板子实际空闲的脚是 GPIO3 / 14 / 15 / 18 / 22 / 23, 四个舵机占了 3/18/22/23,把 14/15 留着给后续功能。

避开了所有禁区:

禁区

原因

GPIO24-30

内部 SPI flash,占用会启动失败

GPIO16-17

UART0,调试输出

GPIO8 / GPIO0

strapping 脚,决定启动模式

GPIO1/2/4-7/9/10-11/19-21

教程一已经占用(音频、OLED、按键)

4.4 ★ 左右镜像安装

左侧腿(3/4)与右侧腿(1/2)的摆臂朝向要关于身体中轴线对称。

代码里用 right = 180 - left 处理这个镜像关系。装的时候如果两条同侧的腿 朝同一方向转了,说明装反了,把其中一条的摆臂拆下来转 180° 再装回去。

4.5 看一眼实物

机器狗整机接线实拍:可调电源、黄色降压模块、ESP32-C6 开发板、OLED、功放、喇叭与舵机外壳

图:机器狗整机接线实拍(俯视)。

• 右上是可调电源(数显 + 旋钮)和黄色降压模块:电源调到 6.5V 左右,模块输出 5V 插面包板

• 中间面包板上插着 ESP32-C6 开发板(带金属外壳)、OLED 屏和功放模块

• 上面板并排插着 4 个舵机接头:红/黑取电,黄线是信号

• 左上那只黑色喇叭是独立接线的,不在面包板上

• 右下白色外壳内是 3D 打印的舵机支架

⚠️ 看这张图时留意:降压模块的 GND 到 ESP32 GND 有一根共地线,

板子上容易看漏 —— 少了它就会表现为「有电但不动」。

5. 改代码

到这一步已经过了最难的部分。下面的改动都是纯软件,改坏了能回退。

5.1 先建分支

cd xiaozhi-esp32

git checkout -b feature-dog

bash

动手前确认能回退:

git checkout baseline-ai-working && idf.py build # 回退测试

git checkout feature-dog # 回来继续

bash

config.h 是所有外设引脚的单一来源,动它等于动整个板子。

5.2 加引脚宏

打开 main/boards/bread-compact-c6/config.h,加四行:

#define SERVO_GPIO_1 3 // 右前腿

#define SERVO_GPIO_2 18 // 右后腿

#define SERVO_GPIO_3 22 // 左前腿

#define SERVO_GPIO_4 23 // 左后腿

c

5.3 新建控制器

新建文件 main/boards/common/dog_leg_controller.h(只有头文件,没有 .cc)。

为什么用 header-only:main/CMakeLists.txt 里 boards/common/*.cc 必须逐个显式列出(只有 boards/${BOARD_DIR}/*.cc 才用 file(GLOB))。 加 .cc 就得改 CMakeLists;改 .h 就不会动构建系统,也不影响其他板型。 同款做法参考仓库里已有的 lamp_controller.h。

关键是这个换算函数:

static constexpr uint32_t kDutyResBits = 20;

static constexpr uint32_t kServoPeriodUs = 20000;

uint32_t AngleToDuty(int deg) const {

const int pulse_us = kPulseMinUs + (kPulseMaxUs - kPulseMinUs) * deg / 180;

return (uint32_t)((uint64_t)pulse_us * (1UL << kDutyResBits) / kServoPeriodUs);

}

cpp

⚠️ `duty` 不是微秒。 这是本教程最贵的坑。

先读完#附录-aduty-不是微秒 再往下写。

再加一个调试开关(dog_leg_controller.h 顶部):

#ifndef DOG_AUTO_CYCLE

#define DOG_AUTO_CYCLE 1 // 1 = 开机自动循环摆腿,0 = 只听指令

#endif

c

调舵机时置 1,上电会自动循环走 16 步姿态, 这样不用依赖网络和语音就能看到硬件是否工作。 调完记得改回 0。

5.4 注册

打开 main/boards/bread-compact-c6/compact_c6_board.cc:

#include "dog_leg_controller.h"

cpp

在 InitializeTools() 里注册三个工具:

工具

参数

说明

self.dog.do_action

action(必填)+ repeat(1-5)

执行语义动作

self.dog.get_state

无

读当前姿态

self.dog.calibrate

op + 腿 + 角度

在线标定

⚠️ 只暴露语义动作,不给 AI 裸角度。

AI 可能输出任何角度值,而 SG90 顶死机械限位会烧齿轮。

语义动作天然限制了取值范围 —— AI 只能说"趴下",不能"转到 179 度"。

5.5 PWM 参数

参数

值

频率

50 Hz(周期 20ms)

脉宽映射

500 µs = 0°,2500 µs = 180°

硬限位

10° / 170°(防堵转发热)

实现

LEDC,1 个 timer + 4 个 channel 共享时基

分辨率

20 位 → 一个计数 0.019µs,远超 SG90 需要的 5~10µs

上电先回 90° 中位,延时 ≥300ms 后再使能输出 —— 防止四个舵机同时猛冲到限位堵转发热。

5.6 姿态库

7 个姿态 + 14 个动作:

动作

语义

stand

站立 / 复位到 90° 中位

sitdown

坐下(前腿收起)

lie

趴下

dosleep

睡觉

wake

唤醒

bow

鞠躬(AI 打招呼时)

alert

警觉(被唤醒时)

wag

摇身(兴奋时)

raise_left / raise_right

举左手 / 右手

front / back

前进 / 后退

turn_left / turn_right

左转 / 右转

动作之间用插值过渡(每步 5°、步间 20ms),不是「写角度 + delay」的硬跳。 既保护齿轮,也更像真狗。

6. 烧录

先看这一条,然后照着教程一烧就行。

⚠️ ★ 舵机通电状态下不要烧录。

烧录时板子会复位,舵机的信号线在这瞬间会失去确定电平。

运气好只是抖一下,运气不好会顶到限位堵转,电机和舵机都可能坏。

烧录前先把外接电源关掉,烧完再开。

只插 USB 给板子供电是可以的。

6.1 两条路选一条

路线 A:用命令烧(最省心)

# 烧录前先关外接电源

idf.py build

idf.py -p <COM11> flash

bash

手动进下载模式的按法、怎么找 COM 口,见 01-xiaozhi-ai.md#46-找到板子的串口。

路线 B:用工具软件烧(推荐给你反复改固件的情况)

这一篇要反复编译、反复烧。用图形工具比敲命令快:

idf.py build

python scripts/release.py # 产出 build/merged-binary.bin

bash

然后打开乐鑫Flash 下载工具,按 01-xiaozhi-ai.md#49-以后都用工具软件烧更省事 的六步走:芯片型号选 `ESP32-C6`、加载模式选 `UART`、 加载 merged-binary.bin、勾上左边的勾选框、地址填 `0x0`、选 COM 口,点 START。

📌 两种烧法随时可以混用。 命令烧进去的固件,和工具烧进去的完全一样,

随便挑顺手的。工具的好处是不用记偏移地址,也不用手动进下载模式那么麻烦。

编译产物参考大小:xiaozhi.bin 约 2.3 MB(分区剩余约 44%)。

7. 验收

7.1 先验硬件,别急着接语音

确保 DOG_AUTO_CYCLE 是 1,编译烧录。日志应该出现:

DogLegCtrl: ready: 4x SG90 @50Hz 500-2500us, pins=3,18,22,23

text

⚠️ `ready` 不代表引脚真的有输出。

ledc_channel_config() 返回 ESP_OK 只保证寄存器写进去了,

不保证引脚没被占用、不保证焊接正常、不保证 duty 算得对。

现在看它有没有自己动。动了就说明硬件和代码都通了,继续 7.2。 没动就用这个判据分清是软件还是硬件:

设备有电 + 有机械阻力 + 完全不动

│

├─ 万用表量 / 看有没有明显电流

│ 有电流但无位移 → 脉冲无效 → 优先怀疑软件(量纲、频率、占空比)

│ 无电流 → 优先怀疑硬件(供电、共地、接线)

│

└─ 纯软件 bug 会完美伪装成硬件故障

有电流但不动,八成是 duty 算错了。 去读 #附录-aduty-不是微秒。

7.2 语音联调

把 DOG_AUTO_CYCLE 改回 0,重新编译烧录(烧录前关外部电源)。

然后按这个顺序试:

1. 「坐下」→ 右前 140° / 左前 40°

2. 「趴下」→ 170/10/10/170

3. 「睡觉」

4. 「摇摇身子」/「鞠个躬」

5. 「举左手」/「举右手」

6. 「往前走」/「后退」/「左转」/「右转」

💡 别用「站立」验第一条。

stand 是 90°,和上电中位一样,肉眼看不出变化 —— 用它验证等于什么都没验。

7.3 机械对不齐怎么办

装舵机臂时不要反复拆机械 —— 用软件偏置对齐比重装效率高得多,而且不用重烧固件。

int offset_[kServoCount] = {0, 0, 0, 0}; // 每条腿的安装偏置(度)

int ApplyOffset(int i, int deg) const { return ClampAngle(deg + offset_[i]); }

c

对着小智说「把右前腿调高 3 度」就能在线调。偏置自身夹在 ±40°,防误操作把腿打歪。

支持的说法:英文全称、缩写(fr/br/fl/bl)、中文(右前/右后/左前/左后)。

8. 常见问题

8.1 舵机不动

按这个顺序查,不要跳步:

#

检查

怎么判断

1

有没有电流

万用表量舵机红线对 GND。有电流说明供电到位

2

有没有共地

外部电源负极到 ESP32 GND 有一根线吗

3

duty 打对了吗

看开机日志那行 0deg=26214 90deg=78643 170deg=125199

4

信号线插紧了吗

换一个面包板插孔试(接触不良很常见)

5

外部电源电压够吗

万用表量,要 ≥4.8V

6

DOG_AUTO_CYCLE 是不是 1

置 0 的时候它不会自己动

第 3 条是最容易被跳过的。舵机有电、有阻力、但纹丝不动,

十有八九是 duty 量纲错了 —— 详见#附录-aduty-不是微秒。

8.2 嗡嗡响

类型

判据

怎么办

正常保持音

轻微、均匀、低音量;外壳仅微温

不用管

堵转报警

音量大 / 高频尖叫;外壳明显发烫;掰不动

立刻断电,否则烧齿轮

如果是很响的嗡嗡声伴随不动,先量电压 —— 欠压是头号原因。

8.3 板子重启 / 屏闪一下就黑

舵机同时动作把电压拽下去了。万用表量降压模块的输出, 掉得厉害就检查降压模块和可调电源。

8.4 舵机一直转不停

买成 360° 连续旋转版了。退掉换 180° 限位版。

8.5 编译或烧录

见01-xiaozhi-ai.md#71-烧录阶段,这几篇是通的。

9. 做完之后

1. 换更强舵机 —— MG90S 比 SG90 扭矩大得多,代价是更耗电,电源要跟着升

2. 加腿部传感器 —— 每条腿一个角度传感器,做真正的闭环

3. 接电池 —— 独立供电 + 充电管理,做成可以拿着走的

4. 加新动作 —— 现在是 7 个姿态,加动作只要往姿态库里加一行

5. 真实步行 —— 需要改成多自由度并做逆运动学,工程量大得多

附录:原理与排查

前面九节是动手主线。下面是主线里压缩掉的判断依据,卡住了再回来读。

附录 A:duty 不是微秒

这是这一篇最贵的坑,值得单独讲。

duty 是 2^resolution 量纲的计数值,不是微秒:

duty = pulse_us × 2^20 / 20000 = pulse_us × 52.4288

角度

脉冲

duty

0°

500 µs

26214

90°

1500 µs

78643

180°

2500 µs

131072

把微秒数直接当 duty 传进去,驱动理解为「1500 个计数周期」:

实际脉冲 = 1500 / 1048576 × 20000µs = 28.6µs

SG90 的识别阈值是 ≥500µs。也就是说舵机收到的是一堆它完全看不懂的窄脉冲, 相当于只给了 1/18 的脉宽。

A.1 为什么这个坑这么难查

观察到的

通常被解读为

实际情况

舵机通电有阻力

"已经通电了"

齿轮摩擦阻力,什么都证明不了

滋滋响

"供电不够"

电机空转挣扎 —— 收到了脉冲但宽度非法

日志打印 ready

"初始化成功了"

只说明 API 调用返回了 ESP_OK

完全不动

"信号线/共地/接线问题"

脉冲宽度不合法

这四个现象加起来,几乎不可能让人想到「占空比算错了」。

这是「软件 bug 完美伪装成硬件故障」的典型案例。

为此白拆了两轮硬件 —— 每次都在还没拿到物理事实之前就改接线。

A.2 防御措施

在构造函数里把换算结果打出来:

ESP_LOGI(TAG, "duty(2^%d scale): 0deg=%u 90deg=%u 170deg=%u",

kDutyResBits, AngleToDuty(0), AngleToDuty(90), AngleToDuty(170));

// 应输出:duty(2^20 scale): 0deg=26214 90deg=78643 170deg=125199

cpp

上电就能核对,不用等舵机动起来才发现不对。 如果这行日志打的是 500 1500 2500,那就是错的 —— duty 不可能是这么小的数。

用 uint64_t 做中间运算,避免 32 位乘法溢出。

A.3 通用规律:写任何 PWM 之前先确认量纲

不同库的约定完全不同:

API

duty 的单位

Arduino Servo.write(µs)

微秒 ✅ 直接给

Arduino analogWrite(pin, 0-255)

8 位占空比

ESP-IDF `ledc_set_duty`

计数值(2^resolution 量纲) ❌

Linux sysfs PWM

通常是 period 百分比或 ns,视驱动而定

唯一可靠的办法:读头文件注释。不要凭直觉。

ESP-IDF 的 driver/ledc.h 里写得很清楚:

@param duty Set the LEDC duty, the range of duty setting is [0, (2**duty_resolution)]

位宽决定有效精度。20 位分辨率 + 50Hz,一个计数 = 20000µs / 1048576 = 0.019µs, 远小于舵机需要的 5~10µs,直接线性映射即可。但如果选了 8 位, 一个计数 = 20000 / 256 = 78µs —— 太粗,舵机会明显抖动。

定分辨率要看负载的实际需求,不是越高越好(越高也越慢)。

写完做一次反向验算,成本十分钟能省几小时:

for deg, us in [(0, 500), (90, 1500), (180, 2500)]:

duty = int(us * 1048576 / 20000)

back = duty / 1048576 * 20000

print(f"{deg:3d}deg -> duty={duty:6d} -> {back:.2f}us (err {abs(back-us):.3f})")

python

其他 PWM 载体的量纲陷阱:

外设

常见量纲陷阱

MCPWM(ESP32-C6 也支持)

duty 也是计数值;另有独立的频率和死区配置

LEDC 定时器

duty > 2^resolution 会被静默截断或断言失败

PCNT(脉冲计数)

计的是边沿数不是时间

RMT

长度单位是 tick,与 rmt_tick 频率强相关

附录 B:为什么用 ESP32-C6

ESP32-C6

STM32F103C8T6

SRAM

512 KB

20 KB

Flash

4 MB

64 KB

主频

160 MHz

72 MHz

无线

Wi-Fi 6 / BLE 5.3 / Zigbee / Thread 内置

无,需外挂

实时确定性

一般

优

4 个 SG90 不需要强实时,而小智固件现成,舵机只是加一组 PWM 外设 —— C6 优势明显。

动作序列参考了开源的嘉立创 EDA-Robot(ESP8266 版,4 舵机 + Servo 库)。 嘉立创用 ESP8266 而不是 STM32,正是因为同属乐鑫系,PWM 约定一致、动作代码可平移。

附录 C:软件偏置的细节

int offset_[kServoCount] = {0, 0, 0, 0};

int ApplyOffset(int i, int deg) const { return ClampAngle(deg + offset_[i]); }

c

一个关键细节:current_[] 仍记录逻辑角度(姿态库原值), 只有下发给硬件的角度加偏置 —— 这样插值计算不受偏置干扰。

self.dog.calibrate 的 5 个操作:

op

作用

probe(leg, angle)

单腿试角度

trim(leg, delta)

增量调偏置

set(leg, offset)

绝对设偏置

read

读四条腿偏置

reset

全部归零

附录 D:五次误判

五次误判,四次指向「硬件坏了」,全部被推翻。

#

曾经的假设

实际情况

靠什么推翻的

1

GPIO18/19 被 USB Serial/JTAG 占用

面包板插孔接触不良,换孔就好

换插孔

2

舵机不动 = 共地没接 / 信号线接错

duty 量纲错,脉冲只有 28.6µs

加自动循环固件

3

"有阻力"说明舵机已通电

齿轮摩擦阻力,什么都证明不了

看是否真有位移

4

按键恒低 = 线插进了 GND 导轨

三脚微动开关按二脚接法,同端短接自己

数开关脚数

5

GPIO19/20 被 USB JTAG 抢走

驱动源码里一个 gpio claim 都没有

grep 源码

共同点:每次都在还没拿到物理事实之前就下结论,然后引导去改硬件。为此拆装硬件两次。

工具和耐心应该花在获取事实上,而不是说服自己重接线。

下次你遇到类似情况,请先读完

教程三 · 外设不工作怎么查(通用排查手册) 的「五条铁律」再动手。

附录 E:烧录速查

# ★ 烧录前:先关掉外接电源(舵机不能带电复位)

# 路线 A:命令烧

idf.py -p <COM11> flash

# 路线 B:工具烧(先打包)

idf.py build && python scripts/release.py

# 然后 Flash 下载工具:ESP32-C6 / UART / merged-binary.bin / 勾选 / 0x0 / COM / START

# 回退到语音助手那版固件

git checkout baseline-ai-working && idf.py build && idf.py flash

bash

相关文档

• 舵机不动 → 教程三 · 外设不工作怎么查(通用排查手册),先看「有电流还是没电流」那条判据

• 屏又黑了 → 01-xiaozhi-ai.md#73-屏幕黑屏

• 烧录连不上 → 01-xiaozhi-ai.md#71-烧录阶段

• 找不到 COM 口 → 01-xiaozhi-ai.md#46-找到板子的串口

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐