最近我把开源的小智AI从“能说话的音箱”升级成了“能听懂话、能看见东西、能上手干活”的桌面机器人。说白了,就是用一块ESP32-S3开发板当大脑,接上麦克风、喇叭、摄像头和一台总线舵机机械臂,跑通了一条完整的语音→理解→视觉→抓取链路。这篇文章把整个项目的选型思路、接线细节、代码链路和踩过的坑全部记录下来,想做一个能动的具身智能小机器人的朋友,可以直接照着抄。
这个项目适合谁?如果你玩过ESP32、看过小智AI的语音交互,又想让机器人真正“动手”而不是只动嘴,那这篇就很对路。如果你完全没接触过嵌入式,也没关系,我会尽量把原理讲清楚,元器件怎么选、线怎么接、程序怎么分模块拆开写,都按实操顺序展开。
1. 项目定位与整体架构:先想清楚“手臂”和“眼睛”怎么协同
1.1 小智AI与ESP32-S3为什么能搭在一起
小智AI(xiaozhi-esp32)是个非常成熟的开源语音助手方案,底层跑在乐鑫的ESP-IDF框架上,专门针对ESP32-S3做了大量优化。它内置了唤醒词检测、语音识别、大模型对话、语音合成这些能力,开箱就能当成一个智能音箱来用。但大部分人拿到手只停留在“聊聊天”的阶段,因为官方默认固件里没有执行机构,更没有视觉输入。
ESP32-S3这颗芯片之所以适合做机器人的主控,主要有三件事:
- 自带向量指令和较高的主频,轻量的音频预处理、图像缩放、颜色检测能在本地跑得动;
- 支持PSRAM(外部扩展内存),跑语音缓冲和JPEG图像解码时内存不至于一下子爆掉;
- 外设丰富,I2S接口接麦克风和功放,DVP接口接摄像头,UART接总线舵机,一路下来基本不用外挂MCU。
所以这个项目本质上不是“拼硬件”,而是把一颗芯片上已经能跑通的三条通路(语音输入输出、视觉采集、舵机控制)用逻辑层串起来。小智AI固件负责听和说,视觉模块负责看,机械臂负责动,三者通过一个简单的状态机协同工作。
1.2 端到端架构长什么样:语音、视觉、机械臂的分工
我先画一个文字版的职责图,方便后面展开:
- 输入层:INMP441数字麦克风采集语音,OV2640摄像头周期性抓拍桌面画面;
- 理解层:小智AI固件完成唤醒和ASR(语音转文字),把文本发给大模型接口,大模型返回结构化指令(比如“抓取红色杯子”转为JSON);
- 执行层:ESP32-S3解析JSON,调用视觉模块识别目标位置,换算成机械臂坐标,再通过UART把角度指令发到总线舵机;
- 反馈层:机械臂的舵机返回实际角度,摄像头拍照确认抓没抓到,播报结果语音,完成闭环。
一开始我犯过一个大错误,就是把每一步都当成独立功能来做:先花两周调语音,再花一周让机械臂动,最后接摄像头时才发现整条链路根本串不起来。原因是每一层之间缺一个统一的“数据契约”。后来我先把接口定义清楚,语音层只输出文本、大模型只输出JSON、视觉只输出坐标、机械臂只消费角度指令,每一层独立测试,端到端联调才顺利起来。
1.3 为什么很多人做不成端到端:问题出在链路,不在单点
拆开看每个模块,网上教程都很多:让小智说话、让总线舵机转起来、用ESP32-CAM拍照,单独做都不难。难的是联调,尤其是时间同步和异常兜底。举个最常见的场景:用户说“把积木推到左边”,大模型识别出意图了,但视觉在拍照时图像还没稳定,机械臂就收到了一个错误坐标,结果抓了个寂寞。
所以我在这篇文章里反复强调一个词:端到端。不是把三个demo拼在一起,而是让每一步的输出格式、时序、超时机制都对齐。比如视觉识别结果统一返回“目标是否存在、目标中心坐标、置信度”,机械臂层只认这个格式,不关心你是用什么模型识别的。这样以后再换更好的视觉方案,机械臂和语音层都不用动。
2. 硬件选型与连线实操:0.1mm细节决定成败
2.1 完整硬件清单与为什么这样选
先列清单,都是我这套方案里实际用过的,不是纸上谈兵。
| 硬件 | 型号/规格 | 备注 |
|---|---|---|
| 主控 | ESP32-S3-DevKitC-1(带PSRAM版本) | 必须带PSRAM,摄像头和JSON缓存才够用 |
| 数字麦克风 | INMP441 | I2S接口,抗干扰比模拟咪头强很多 |
| 音频功放 | MAX98357A + 3W小喇叭 | I2S接口,3.3V或5V供电均可 |
| 摄像头 | OV2640 模组(DVP接口) | 200万像素足够,宽角镜头最好 |
| 机械臂 | 4自由度总线舵机机械臂(例如LX-16A) | 串行总线控制,带角度反馈 |
| 电源 | 5V/5A适配器 + DC-DC降压模块 | 舵机和主控分开供电 |
| 其他 | 杜邦线、洞洞板、铜柱、扎带 | 用于固定摄像头和整理线缆 |
关于“ESP32-S3 USB摄像头”这个说法得澄清一下:很多新手以为S3有USB口就能插普通USB摄像头,其实S3的USB口主要是烧录和串口调试用的,并不提供USB Host功能。要跑视觉,老老实实选DVP接口的OV2640这类摄像头模组,省事且稳定。
2.2 GPIO分配与接线表:照着抄就能少走弯路
我用的是板载默认引脚的常用方案,把I2S、摄像头、舵机串口分到不同引脚上,避免互相打架。摄像头的DVP接口比较占引脚,接线时一定要对着原理图逐根核对,我就是因为XCLK和PCLK接反,整整查了一晚上才排掉。
| 功能 | 引脚 | 连接对象 |
|---|---|---|
| I2S 麦克风 BCLK | GPIO41 | INMP441 SCK |
| I2S 麦克风 LRCLK | GPIO42 | INMP441 WS |
| I2S 麦克风 DATA | GPIO2 | INMP441 SD |
| I2S 功放 BCLK | GPIO5 | MAX98357A BCLK |
| I2S 功放 LRCLK | GPIO25 | MAX98357A LRC |
| I2S 功放 DATA | GPIO26 | MAX98357A DIN |
| 舵机总线 TX | GPIO17 | 机械臂串口 RX |
| 舵机总线 RX | GPIO18 | 机械臂串口 TX |
| 摄像头 | GPIO10-16等 | OV2640 DVP |
要注意:舵机串口的电平标准如果是5V,需要用逻辑电平转换芯片或者确认舵机串口兼容3.3V。有些总线舵机直接3.3V也能通信,但某些型号的TTL电平要求高,直接接会把ESP32-S3的引脚烧掉。我这套用的LX-16A是兼容3.3V的,接线前一定先查手册。
2.3 总线舵机机械臂的选择与控制协议
机械臂的选择上,我强烈建议用总线舵机而不是传统的PWM舵机。PWM舵机方案看着便宜,实际坑很多:
- 每个舵机至少一根信号线,4个舵机就要占4路PWM,而且S3的LEDC通道有限;
- PWM舵机没有角度反馈,机械臂撞到障碍物、丢步了你完全不知道;
- 多个PWM舵机同时动作时,很容易因为频率和占空比抖动导致末端晃动。
总线舵机(serial bus servo)就不一样。它内部有MCU和角度传感器,通过一根串口线把所有舵机级联起来,芯片发一个带ID的帧,就能控制任意一个舵机转到指定角度,还能读取当前角度。电路上相当于UART + RS485/单线半双工,最多一根数据线串几十个舵机。
协议方面,LX-16A这类常见的总线舵机帧格式大致是:
- 每个指令帧以
0x55 0x55开头; - 然后是指令长度、舵机ID、指令码、参数、校验和;
- 角度控制指令一般是
SERVO_MOVE_TIME_WRITE,参数包含目标角度和运动时间。
我建议不是自己裸写协议,而是先找现成的Arduino/ESP-IDF库把舵机调通,再根据项目需要改协议层。裸写协议不是不行,但校验容易出错,尤其是多舵机同时动作时,一帧错位后面全乱。
2.4 供电方案与电流估算:先算账再上电
这个项目里最容易被忽略的就是电源。很多人拿着USB线给ESP32-S3供电,结果舵机一转就重启,摄像头画面开始水波纹,麦克风里全是杂音。原因很简单:电流不够,或者噪声窜进了模拟电路。
我实测的电流数据大概这样:
- ESP32-S3(带摄像头和功放)全负载约500mA;
- 单个LX-16A总线舵机正常动作0.3A~0.5A,堵转能到1.2A左右;
- 4个舵机短时间同时动作,峰值按3A算比较稳妥。
所以我最后用的是5V/5A适配器,经DC-DC降压模块给摄像头和功放单独供电,舵机直接从电源取电,主控用另一路3.3V的稳压。关键是一定要把所有GND接到同一个参考地,否则舵机大电流回流会造成地电位漂移,轻则串口丢帧,重则芯片复位。
3. 端到端链路的核心实现:从语音指令到抓取动作
3.1 软件框架选择与工程组织
固件层我直接基于小智AI开源项目(xiaozhi-esp32)来改,没有从零开始写语音链路。原因很直白:唤醒词、ASR、大模型API调用、TTS这些轮子,社区已经打磨了很久,重复造一遍没有意义。我需要做的,是在它的基础上增加两个自定义模块,一个是vision_handle,一个是arm_handle。
工程组织上,我按照功能拆成几个层次:
- 应用层:语音对话主逻辑、状态机、意图分发;
- 服务层:视觉服务(拍照、上传识别、坐标映射)、机械臂服务(坐标转角度、舵机指令封装);
- 驱动层:摄像头驱动、总线舵机串口驱动、I2S音频驱动。
这样做的好处是每一层都能独立编译和测试。比如我先在电脑上串口模拟“抓取红色杯子”的JSON,验证机械臂控制逻辑没问题,再接入语音层,最后才接视觉。不然所有模块一起上电,出了问题根本不知道是哪里崩的。
小智AI的固件里原本就有一个“动作”的扩展点,通过自定义SDK可以往对话响应里插入指令。我在IDE里注册了一个自定义动作,大模型返回的文本里若带特定标记,固件就把它解析成机器指令而不是直接播报出来。这一步是整个改造的核心入口。
3.2 语音唤醒与意图解析:让大模型输出结构化指令
语音部分主要是调参:唤醒词用默认的“你好小智”,唤醒灵敏度调到区间中间,太高容易被电视声误唤醒,太低则离远一点就喊不醒。ASR把语音转成文字后,固件会把文字拼到系统提示词里,请求大模型接口。
关键点在于,不能直接让大模型返回一段自然语言让机器人“理解”,而要约定返回JSON。我用的系统提示词大致是这样规定的:
你是桌面机器人的控制大脑。用户指令会被送入,请判断用户想做什么。 如果用户想抓取或移动某个物体,返回JSON: {"intent":"pick","object_name":"<物体名称>","confirmation_required":false} 如果只是闲聊,返回JSON: {"intent":"chat","reply":"<回答文本>"} 不要返回JSON以外的任何内容。固件收到响应后,用JSON解析库提取intent字段。是闲聊就走普通的TTS播报流程,是抓取就进入视觉识别子流程。这里有一个稳定性要点:大模型偶尔会返回“```json”这种markdown包裹,所以解析前需要先做一次字符串清洗,只截取第一个大括号到最后一个大括号之间的内容。
3.3 视觉识别与坐标映射:摄像头看到的怎么变成机械臂能用的坐标
视觉部分是这个项目里最需要讲清楚的一环。ESP32-S3这颗芯片算力有限,跑不了大型目标检测模型,所以我的方案是“本地轻量预处理 + 远端视觉模型识别”。摄像头先拍一张JPEG,通过WiFi把图片发到服务器,服务器调用视觉大模型分析图片,返回目标物体在图像中的位置坐标。
传输图片我走的是HTTP multipart上传,代码大致思路是这样:
// 拍摄并获取JPEG缓冲 camera_fb_t *fb = esp_camera_fb_get(); // 通过HTTP POST multipart/form-data上传到本地服务端 // 服务端请求视觉大模型,得到目标物体的rect坐标 // 返回结果如:{"found":true,"x":520,"y":340,"width":80,"height":90} esp_camera_fb_return(fb);服务端我这里用了一个简单的Python脚本接收图片,再转发给视觉大模型接口,最后把坐标返回给ESP32-S3。为什么要绕一道?因为目前很多视觉大模型接口要求图片走HTTPS,而ESP32端直接拉大模型太占内存,UDP又传不了大图,一个轻量中转服务更稳定。
坐标映射是一个很容易被忽略但极其关键的问题。图像坐标系和机械臂的工作台坐标系不是一回事,必须做标定。我的标定方法是:
- 把机械臂的末端分别移到桌面的四个角,记录每个位置的机械臂工作台坐标;
- 同时用摄像头拍下末端位置在图像里的像素坐标;
- 用四点仿射变换(就是一个简单的线性映射方程)算出图像坐标到机械臂坐标的换算系数。
只要机械臂固定、摄像头固定,这个映射关系就能保持很久。每次开机时做个简单校准:先让机械臂末端移到一个已知点,摄像头确认一下,微调映射参数。这一步解决了“视觉检测选不准、机械臂抓偏”的大多数问题。
3.4 机械臂运动控制:从目标坐标到舵机角度
拿到目标在机械臂工作台坐标系里的位置之后,下一步就是怎么把末端移过去。如果机械臂只有2个旋转自由度(肩和肘),工作范围限制在一个平面内,可以直接用几何法做逆运动学,完全不需要复杂矩阵运算。
假设大臂长度是L1,小臂长度是L2,末端目标位置相对肩关节是(x, y),那先用余弦定理算出肘关节角度:
- 末端到肩关节的距离 d = sqrt(x^2 + y^2);
- 肘角 cos_elbow = (L1^2 + L2^2 - d^2) / (2 * L1 * L2),再用acos求出角度;
- 肩角则是目标点方位角与大臂相对夹角之和。
我用C++写了一个简单的逆解函数,输入工作台坐标,输出两个舵机的目标角度,再叠加一个底部旋转舵机的角度,就形成了三自由度平面机械臂的完整控制。轨迹规划上我没有上特别复杂的算法,只做了一步“直线插补”:把末端要从A点走到B点的过程拆成N个小段,每段之间让舵机慢速运动,避免末端画弧线或者抖动。这个方法对桌面抓取来说完全够用,而且计算量很小。
控制指令最终会封装成总线舵机的串口帧发送。我写了一个SendServoAngle(servo_id, angle, move_time_ms)函数,内部组帧、校验、发送,并且等待舵机返回角度值。通过读取实际角度,就能判断机械臂是否被卡住,如果偏差超过设定阈值,就让机械臂退回原位并播报“抓取失败”,而不是傻傻地继续执行。
3.5 端到端主循环:一个状态机把整条链路串起来
端到端链路不是顺序执行一次就完了,而是一个循环。我实现了一个简单的状态机,每个状态有明确的超时时间,超时或者失败就跳回安全状态。
IDLE(待机,等待唤醒) -> LISTENING(听到请求,ASR识别) -> UNDERSTANDING(请求大模型,解析JSON) -> 如果是闲聊:TTS播报,返回IDLE -> 如果是抓取:进入VISION_CHECK -> VISION_CHECK(拍照,上传识别,坐标映射) -> 如果找不到目标:TTS播报“没看到”,返回IDLE -> 如果找到:进入ARM_MOVE -> ARM_MOVE(逆运动学解算,直线插补,舵机执行) -> 读取舵机反馈角度校验,成功则进入VERIFY -> 失败则TTS播报错误,返回IDLE -> VERIFY(再次拍照确认目标是否被夹住/推动) -> 播报结果,返回IDLE这个状态机的价值在于永远不会死循环。哪怕大模型返回了乱码、摄像头超时没拍到图、舵机串口断线,最终都会在几秒内回到IDLE状态,恢复待机。我在实际运行时最怕的就是机器人“卡在一个动作里不动”,状态机加上超时机制之后,这个问题彻底解决了。
4. 常见问题与排查技巧实录:三次返工换来的经验
4.1 供电问题:开机重启、舵机一动就重启基本都是电源的锅
这个问题排在第一位,是因为我在调试过程中被它折磨了最久。现象是插上USB一切正常,但只要机械臂一动,开发板立刻重启,串口日志里全是“Brownout detector was triggered”。排查思路很固定:先量电源电压,再看电流,最后查地线。
我最后定位出两个原因。一是USB口供电上限只有500mA左右,峰值电流一上来就触发欠压复位;二是舵机电源和主控共用了同一个5V线路,舵机堵转瞬间的压降传导到了逻辑电路。解决办法就是把电源彻底分开,舵机单独从适配器取电,主控用独立稳压,主控地、舵机地、摄像头地统一接入同一个接地点。现在整机连续工作一整天,再也没有无端重启。
4.2 音频问题:麦克风听不到、喇叭爆音的排查顺序
小智AI固件本来就有音频配置,但换上INMP441之后一度完全没声音。排查的时候先别急着改代码,按顺序做这几件事:
- 用I2S扫描工具确认BCLK和LRCLK信号是否产生,示波器或逻辑分析仪一看便知;
- 检查麦克风的LRCLK引脚是否和功放的LRCLK共用了一个GPIO,我一开始就是两路I2S配反了,导致采集异常;
- 确认麦克风方向,INMP441是MEMS麦克风,底部有一个声孔,如果贴反了灵敏度会急剧下降;
- 功放爆音通常是I2S数据位宽配置和DAC不匹配,或者供电纹波太大,给功放单独加一个100uF电解电容就能缓解。
音频是感知层的核心,这一块最容易让人失去耐心,因为问题往往不是软件逻辑错,而是硬件接触不良。建议把I2S的接线全部用杜邦线换成品线,甚至焊接到洞洞板上,能减少大量接触电阻引入的噪声。
4.3 视觉问题:摄像头花屏、识别不准的排查顺序
OV2640刚点亮时,画面上经常出现花屏或者纯色条纹。我总结出三个高频原因:电源纹波、时钟线干扰、PSRAM配置不对。ESP32-S3跑摄像头必须开PSRAM否则内存不够,而且menuconfig里要把摄像头帧缓冲大小和像素格式选对,我选的是JPEG格式,分辨率设为800x600,太大了上传和解析都慢,太小了机械臂坐标映射误差又会变大。
识别不准的问题有另一个坑:机械臂动作时会把摄像头震到,哪怕只有一两毫米的位移,映射到工作台坐标就是十几毫米的误差。所以我把摄像头固定在了桌面的独立支架上,没有和机械臂底座共用一个固定板,震动问题基本解决。如果识别目标经常被机械臂自己挡住,那就别把摄像头架在正前方,稍微抬高一点俯拍视角,效果会好很多。
4.4 机械臂偏差与抖动:总线舵机也救不了所有问题
总线舵机有角度反馈,但并不意味着机械臂精度就一定高。实际跑起来我发现三个偏差来源:减速齿轮回差、连杆结构形变、控制周期不同步。回差和形变只能靠机械结构补强,比如给关节轴承上润滑、用更结实的3D打印件、螺丝全部紧固到标准扭矩。控制周期不同步则是软件问题,多舵机同时运动时,如果每路指令间隔不一致,末端轨迹就会歪。
我的调试建议是:先把速度设慢,例如一次抓取用3秒完成而不是1秒。慢速下能明显看出末端路径是否平直,也能逐个关节排查回差。等每个关节都正常了,再逐步把速度提上来。如果你想让机械臂变得更好用,可以再往下扩展的东西不少,比如在上位机加一个手势识别来远程指定抓取目标,或者给机械臂末端加一个微型吸盘来抓取非金属小物件。
这个项目我一共改了三个大版本才稳定下来,最大的体会是:单点功能做好只是第一步,端到端链路里所有的坑,都藏在模块之间的“接口”上——时序、格式、供电、容错。如果你也在做类似的桌面机器人,建议先跑通一个最简单的闭环,哪怕只是“说一句话→机械臂动一下”,然后把视觉一个一个加上去,会比一上来就追求完整效果省下至少一半时间。最后留一个小技巧:所有超时时间都设计成可配置参数,放到一个配置文件里,调参的时候你会感谢这个决定。