语音动作机器人:让会说话的小智动起来
- 做出一台会听会说、还能按语音指令做“点头/转身/挥手”动作的桌面机器人
- 把 P5 的小智语音链路和卷 R 的舵机运动能力,用 MCP 这一层接成一件完整成品
- 想清“语音识别出意图 → 意图映射成动作 → 舵机执行动作序列”这条贯穿链路,每一环归谁管
- 学会用 MCP/Function Calling 把硬件动作注册成大模型能自己调用的工具,而不是写死 if-else
- 认清动作与语音抢时间、舵机独立供电、动作序列被打断三大坑,提前避雷
| 器材 | 数量 | 参考 |
|---|---|---|
| 小智硬件(带 ESP32-S3、麦克风、喇叭的开发板/成品盒) | 1 | —约 60-200 元(视是否带屏/外壳,以商城实际为准) |
| 舵机(点头/转身用 SG90 小力矩即可,带负载换 MG996R 金属齿) | 1-3 | —SG90 约 5-10 元/个、MG996R 约 15-30 元/个(以商城实际为准) |
| PCA9685 16 路 PWM/舵机驱动模块(多舵机或想省主控引脚时用) | 1 | —约 8-18 元(以商城实际为准) |
| 独立 5V 大电流电源(适配器 5V/3A 以上或电池组,专供舵机) | 1 | —约 15-40 元(以商城实际为准) |
| 杜邦线、机械结构件(转头支架/小车底盘按形态选) | 1 套 | —约 10-60 元(以商城实际为准) |
价格随渠道波动,以购买页实时为准。
你已经有了一个会说话的小智——喊它一声就醒,问它话它能答。可它是个"缸中之脑":脑子转得挺好,身体一动不动,永远端坐在桌上。这个项目要做的,就是给它长出身体:你说"点个头",它真的低头点一下;你说"转过来看我",它把头转过来;你说"跟我打个招呼",它抬起小手挥两下。会听会说的音箱,就此变成一台会听会说会动的桌面机器人。
它对你的意义,和前面那些"把两个知识点拼起来"的项目不太一样。这是一个贯穿项目——它要串起来的不是两个零件,而是你之前做过的两整条线:一条是「会说话的小智」(小智旗舰项目),另一条是「会动的机器人」(卷 R 的舵机运动)。 所以这一篇的重点不是从零教你怎么配 I2S、怎么调舵机——那些前面都讲透了——而是教你怎么把这两条各自成型的线,在中间接上一层,让说出来的话真的变成动作。
这层"中间接口",就是 MCP。理解它在这里扮演什么角色,是这整个项目的题眼。
第一步:想清这台机器人的架构,再谈选型
动手前先把一件事钉死:你说一句"点个头",从张嘴到低头,中间到底发生了什么。 想清这条链路,架构就出来了,选型也就有了依据。
把它拆成三段,正好对应三个"归谁管":
语音 → 意图(这段归 P5 的小智语音链路)。麦克风收音、本地唤醒、语音流上传、服务端 ASR 转文字、大模型理解——最后得到的不是一段音频,而是一个结构化的意图:"用户想让我点头"。这一整条"听 → 醒 → 传 → 识 → 想",你在小智旗舰里已经搭过,这里直接复用,一个字不重讲。
意图 → 动作调用(这段归 MCP/Function Calling)。大模型想清"该点头了"之后,它不会自己去写控舵机的代码——它做的是调用一个你提前注册好的工具,比如
nod()。这一层就是 MCP 协议 和 Function Calling 的活:把"点头"这个硬件能力,描述成大模型能发现、能调用的一件工具。这是整台机器人的接缝,也是本项目真正的新东西。动作调用 → 舵机执行(这段归卷 R 的运动能力)。
nod()被调起来后,落到你真正控硬件的代码:驱动舵机按一条角度序列转动,完成"低头—抬头"这个动作。多舵机怎么协调、独立供电怎么接,你在机械臂项目和卷 R 里练过,这里同样复用。
三段一串,题眼就清楚了:P5 负责"听懂",卷 R 负责"动手",中间靠 MCP 这一层把"听懂的意图"翻译成"要调用哪个动作"。 这台机器人 90% 的零件你都做过,你要新建的,就是中间那一层薄薄的"翻译"。想更深理解为什么"大模型调硬件"是机器人的未来形态,可以先读 什么是具身智能——它讲清了"感知—理解—行动"这个闭环,本项目就是这个闭环最朴素的一台实物。
选型:在小智硬件上加动作
架构定了,选型就一句话:在你已有的小智硬件上,加上舵机和它的供电。
- 主控:直接用你做小智时那块 ESP32-S3 板子。它已经背着麦克风、喇叭、联网、语音链路,动作能力是加法,不用换主控。
- 动作机构:最简单的形态是一个舵机做点头(转头支架);想更丰富就两三个舵机做"转身 + 点头 + 挥手"。舵机选型和机械臂一样——练手用 SG90,要带得动结构换 MG996R。
- 舵机驱动:单个舵机可以直接挂主控一个 PWM 脚;到三个以上、或想省主控引脚、想统一供电,就上 PCA9685(16 路 PWM,走 I2C,只占两根线)。
- 独立电源:这是本项目唯一必须严肃对待的选型——舵机绝不能和主控共用一路供电。 详见下一步接线。
BOM 的报价区间见页首 parts 表,一律以商城实际为准。
第二步:接线——舵机独立供电是这里的头号红线
语音那半边(麦克风、喇叭、联网)你做小智时已经接好、跑通了,这一步不动它。要新接的只有动作这半边:舵机 + 驱动 + 独立电源。接线不复杂,但有一条红线,踩了整台机器人都不稳。
舵机必须用独立的 5V 大电流电源,不能从主控的 3V3 或 USB 取电。 舵机启动和堵转的瞬间电流可以冲到几百毫安甚至上安,而 ESP32-S3 板子那点供电根本喂不动。硬要共用一路电,最典型的翻车是:一说"点头"、舵机一动,主控电压被瞬间拉垮,板子直接重启——你会看到它答话答到一半突然断线、重连、忘了刚才在干嘛。记住一句:主控走主控的电,舵机走舵机的电,两者只共 GND(地)。 这也是机械臂项目里反复强调的同一条铁律。
接线骨架(以"主控 + PCA9685 + 独立 5V"为例):
主控 ESP32-S3 ──I2C(SDA/SCL 两根线)──▶ PCA9685
│
独立 5V/3A 电源 ──▶ PCA9685 的 V+/GND(专供舵机大电流)
│
├─ 通道0 ──▶ 舵机1(点头)
├─ 通道1 ──▶ 舵机2(转身)
└─ 通道2 ──▶ 舵机3(挥手)
共地:主控 GND ── PCA9685 GND ── 独立电源 GND 三者必须连到一起
- I2C 两根线:主控的 SDA/SCL 接 PCA9685 的 SDA/SCL,避开 S3 的选脚雷区(strapping 脚 GPIO0/3/45/46、flash 区 26-37、USB 脚 19/20、以及 S3 上根本不存在的 22-25),随手挑两根安全脚即可。
- 舵机供电:独立 5V 电源接 PCA9685 的 V+ / GND 端子,专门喂舵机。主控这边只通过 I2C 发指令,不给舵机供一分电。
- 共地是命门:主控、PCA9685、独立电源三者的 GND 必须连成一片。不共地,I2C 电平没有共同参考,信号会乱——这是新手最容易漏、又最难查的一根线。
只用单个舵机点头、不上 PCA9685 时,接法简化成:舵机信号线接主控一个安全 PWM 脚,舵机的红黑(正负)接独立 5V 电源,同样只和主控共 GND。
第三步:分步把"说话变动作"接起来
现在到本项目的核心。前面说过,语音链路(P5)和舵机执行(卷 R)你都有了,这一步只干一件事:把中间那层 MCP 接上,让大模型说出的意图,落到舵机的动作上。 我们分三步长出来,每步都能单独验证。
步 1:先把"点头"写成一个能被调用的动作函数
抛开语音,先让"点头"这个动作,作为一个普通函数能跑通。这一步纯是卷 R 的活——控舵机按一条角度序列转动,你练过。
// nod():一个纯粹的动作函数,先不管谁来调它
// 让点头舵机从中位低下去、再抬回来,完成一次点头
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define NOD_CH 0 // 点头舵机在 PCA9685 的通道号
#define ANGLE_MID 90 // 抬头(中位)
#define ANGLE_DOWN 120 // 低头
// servo_set_angle(通道, 角度):封装好的“把某通道舵机转到某角度”,
// 内部走 PCA9685 的 I2C 写占空比,实现见机械臂项目,这里当已有积木用
extern void servo_set_angle(int ch, int angle);
static void nod(void)
{
servo_set_angle(NOD_CH, ANGLE_DOWN); // 低头
vTaskDelay(pdMS_TO_TICKS(300));
servo_set_angle(NOD_CH, ANGLE_MID); // 抬回
vTaskDelay(pdMS_TO_TICKS(300));
}
你应该看到:在 app_main 里直接调一次 nod(),舵机低头再抬起,干净利落。看到这个,说明动作这一环独立可用了——这一步的意义就是把"舵机执行"隔离验证掉,后面接语音时出了问题,你能一口咬定不是这一层的锅。
turn()(转身)、wave()(挥手)都照nod()的模子写:无非是换个通道、换一串角度序列。挥手就是让手臂舵机在两个角度间来回摆两三下。把每个动作都封装成一个不带参数、调用即执行的小函数,是这一步的关键——因为下一步 MCP 要调的,正是这样一个个"招之即动"的动作。
步 2:把动作函数注册成 MCP 工具,让大模型能"看见"它
这是接缝所在。光有 nod() 函数还不够——大模型不知道它存在。你得按 MCP 的规矩,把这个动作描述给模型:它叫什么、干什么、什么时候该调。这样模型在"想"的那一步,就能自己决定"现在该调 nod 了"。
// 概念示意(非完整实现):把三个动作注册成 MCP 工具
// 真实的注册格式以小智 xiaozhi-esp32 上游和 MCP 官方为准,这里只为“看见”结构
//
// 每个工具就是四件套:名字 + 描述 + 参数 + 执行
// 大模型靠“描述”判断该不该调、靠“名字”点名、靠“执行”落到你的动作函数
registerMcpTool({
name: "nod",
description: "让机器人点一下头,表示同意、打招呼或回应",
params: {}, // 点头不需要参数
onCall: (args) => { nod(); return "已点头"; }
});
registerMcpTool({
name: "turn",
description: "让机器人转身/转头面向某个方向",
params: { dir: "字符串:left 或 right" }, // 有参数:往哪转
onCall: (args) => { turn(args.dir); return "已转向"; }
});
registerMcpTool({
name: "wave",
description: "让机器人挥手,用于打招呼或告别",
params: {},
onCall: (args) => { wave(); return "已挥手"; }
});
关键全在那层"注册",不在 nod() 那一行。 nod() 是你步 1 早就写好的、控舵机的代码;MCP 干的是外面这层——用统一格式把这个能力描述出来,于是大模型在语音链路的"想(LLM)"那一环,就能把你说的"点个头"匹配到 nod 这个工具,自己发起调用。你从"写死 if-else 判断用户说了啥",升级成了"把能力摆出来、让模型自己挑该调哪个"——这正是 Function Calling 和 Agent 思维的核心。
描述(description)写得好不好,直接决定模型调得准不准。描述是给模型看的"使用说明书"——nod 写成"表示同意、打招呼或回应",模型才知道你说"嗯知道了"时也可以点个头,而不只是听到"点头"两个字才动。这一层不是编程,是"教模型什么场景用什么动作",值得你多花心思打磨措辞。具体格式一律照小智上游的样例抄,别自己发明字段。
步 3:把动作嵌回语音链路,跑通"说 → 动"全程
到这一步,其实不需要你再写多少代码——因为语音链路(P5)已经会在"想"那一环调用注册好的 MCP 工具了。 你要做的是把三块拼到一起烧进去:小智固件(语音链路)+ 步 1 的动作函数 + 步 2 的工具注册。
数据流回顾一遍,你会发现每一环都是现成的,你只补了中间一小段:
你说“点个头”
→ 麦克风收音、本地唤醒 (P5 现成)
→ 语音流上传、ASR 转成文字 (P5 现成)
→ 大模型理解意图,决定调 nod 工具 (MCP 注册让它“看得见”nod ← 你补的)
→ onCall 落到 nod() 驱动舵机 (卷 R 现成)
→ 舵机低头抬头,机器人点了个头
→ TTS 回你一句“好的” (P5 现成)
你应该看到:喊醒小智,说"点个头",它一边(可能)回你一句"好的",一边真的低头点了一下。第一次看到它因为你一句话而动起来,就是这个项目的高光时刻——你亲手把"会说"和"会动"焊成了一台会听会说会动的机器人。
到这里回头看,你写的新代码少得惊人:几个动作函数 + 一层工具注册。贯穿项目的功夫,从来不在写多少新代码,而在看清两条成型的线该在哪一层接、用什么接。 这一层接口,就是 project 比单篇 guide 多出来的那层功夫。想看这套"语音+动作"更完整的参考实现和形态取舍,语音+动作:把小智变成会动的桌面机器人 那篇讲得更细,可以对照着看。
第四步:调试——对不上就查这张表
贯穿项目链路长,出问题时最怕"面对一条全黑的链路干瞪眼"。分段验证的好处就在这——按下表对号入座,能一眼缩小范围到底是哪一段坏了。
| 现象 | 最可能的原因 | 归哪一段 / 怎么办 |
|---|---|---|
| 一说动作、板子就重启/断线 | 舵机和主控共用一路电,动作瞬间把主控电压拉垮 | 供电:舵机换独立 5V 电源,只共 GND,见第二步红线 |
| 说话它答得好好的,就是不动 | 动作函数没注册成 MCP 工具,或描述太窄模型没匹配上 | MCP 层:确认工具已注册;把 description 写得更贴合口语(步 2) |
单独调 nod() 也不动 |
舵机接线错/供电没到/通道号写错 | 动作层:先把语音撇开,单测步 1;查独立电源、共地、通道号 |
| 动作乱抖、转不到位 | 舵机供电不足、角度序列太急、PWM 频率不对 | 动作层:确认独立电源电流够;放慢角度序列;PCA9685 频率设 50Hz |
| 模型总调错动作(说点头却挥手) | 各工具的 description 写得含糊、区分度低 | MCP 层:把每个动作的描述写得更专、更不重叠(步 2 tip) |
| I2C 通信失败 / 舵机全不响应 | 主控、PCA9685、独立电源三者没共地 | 接线:三方 GND 连成一片,这是最易漏的一根线 |
| 动作做一半被下一句话打断、卡住 | 动作在语音回调里同步阻塞,抢了链路的时间 | 架构:把动作放独立 FreeRTOS 任务里跑,别阻塞语音回调(见进阶) |
一次只验证一段。链路这么长,同时怀疑语音、MCP、舵机三段,你根本查不动。从中间切一刀:先单独调 nod() 确认动作层没问题,再看模型调没调到工具(串口日志会打),最后才怀疑语音。分段定位,是长链路调试唯一不抓瞎的办法。
第五步:从"能动的 demo"到"更像个机器人"
到这它已经会听会说会动了。但"能动"和"像个机器人"之间还差几步,正好通向更前沿的地方,给你指条路。
让动作和说话不打架
现在动作若写在语音回调里同步执行,做点头那 600 毫秒,链路是卡住的——下一句话可能被吞掉。更好的做法是把动作丢进一个独立的 FreeRTOS 任务去跑:MCP 的 onCall 只负责"往动作队列里塞一个'点头'任务"就立刻返回,真正的舵机转动在后台任务里慢慢做。这样它能一边点头一边继续听你说话,像个真的在多线并行的机器人。这套"背景行为独立成任务"的骨架,你在触摸台灯里就见过。
加更复杂的动作和视觉
三个单动作只是起点。你可以把动作组合成序列——"打招呼"= 转身面向你 + 挥手 + 点头,注册成一个 greet() 工具让模型一次调用。再往上,给它加个摄像头(串到 AI 摄像头项目),让它"看见"你在哪、转头看向你——这时它就从"听指令动"进化到"感知环境自己决定动",真正踏进具身智能的门槛。
串到卷 R,把"身体"做深
本项目的动作还很朴素。想把机器人的"身体"这条线做深——多自由度协调、运动学、平衡控制、乃至大模型直接规划连续动作——那是卷 R(R6 具身智能) 整条线的内容。这台会说会动的小智,正是你进入卷 R 的最佳起点:它已经把"大模型 → 动作"的接口打通了,往后你在这套骨架上加多复杂的身体都行。系统了解具身智能这个方向,回看 什么是具身智能 和机器人专题 /robot/。
小结 · 你做出了什么、下一步去哪
- 你做出了一台会听、会说、会动的桌面机器人——说"点个头"它真的点头,把"会说话的小智"(P5 小智旗舰)和"会动的机器人"(卷 R 舵机运动)两条成型的线,第一次串成了一件成品。
- 你看清了这台机器人的贯穿架构:P5 负责"听懂"、卷 R 负责"动手",中间靠 MCP / Function Calling 这一层,把"听懂的意图"翻译成"该调哪个动作工具"。你写的新代码极少——几个动作函数加一层工具注册,功夫全在"看清两条线在哪接、用什么接"。
- 你踩过并避开了贯穿项目的三大坑:舵机独立供电(不然主控被拉垮重启)、三方共地(不然 I2C 乱)、动作别阻塞语音回调(丢进独立任务并行跑)。
下一步:想让它从"听指令动"进化到"看见环境自己决定动",给它加摄像头、串 AI 摄像头项目;想把"身体"这条线做深——多关节协调、运动规划、平衡——顺着卷 R(R6 具身智能) 走,参考 语音+动作那篇 和机器人专题 /robot/。回看全部实战项目见项目总览。这台会说会动的小智,是你手里最值得继续喂养的一台——它已经把"大模型指挥硬件"这道最关键的接口焊通了,剩下的,是让它的身体越来越强。
本文为公开资料整理,非亲测。关键参数与代码请结合实物与下列官方来源验证。