本帖最后由 云天 于 2026-8-4 20:34 编辑
让小智AI不仅能听会说,还能驾驶麦克纳姆轮小车实现全向移动——前进、后退、横移、斜移、原地旋转,一切尽在语音指挥之中。 【项目缘起】
小智AI是一款开源的端侧语音助手项目,基于ESP32实现,支持唤醒词、语音识别、大模型对话等功能。而掌控板作为国内创客教育常用的主控板,集成了OLED屏幕、按键、I2C接口等资源,性价比极高。之前我在DF创客社区看到了不少关于麦克纳姆轮小车的项目[color=var(--dsw-alias-label-secondary)]-,也有将小智AI移植到掌控板的尝试。但它们大多采用遥控器或手机APP控制,还没有人把小智AI的语音能力与麦克纳姆轮的全向移动特性结合起来。
于是,我萌生了一个想法:用两块掌控板——一块跑小智AI固件作为“大脑”,一块跑MCP服务作为“手脚”——打造一辆可以通过自然语言语音控制的麦克纳姆轮全向移动小车。 经过一段时间的折腾,我成功实现了这个想法。本文将分享完整的制作过程,希望能给同样想折腾的朋友一些启发。 【准备工作】
硬件清单
硬件 | 数量 | 说明 | | 掌控板 2.0 | 2块 | 一块跑小智AI,一块跑MCP服务 | | 行空板K10 IO扩展板 | 1块 | 连接掌控板与电机驱动模块 | | L298N直流电机驱动模块 DF-MD V1.3 | 2块 | 驱动四个直流电机 | | 麦克纳姆轮(含直流电机) | 4个 | 左前、左后、右前、右后 | | I2S数字麦克风 | 1个 | 语音输入 | | I2S数字扬声器 | 1个 | 语音输出 | | 7.4V锂电池 | 1块 | 为电机和驱动模块供电 | | 杜邦线、螺丝等 | 若干 | 连接与固定 |
软件环境 系统架构
整个系统采用双掌控板分工协作的架构: 工作流程: 【一、小智AI掌控板:让掌控板“能听会说”】
1.1 硬件连接
小智AI掌控板负责语音交互,需要连接I2S麦克风、I2S扬声器、OLED屏幕和按键。
外设 | 掌控板丝印 | ESP32 GPIO | 说明 | | 麦克风 WS | P1 | GPIO32 | I2S字选 | | 麦克风 SCK | P8 | GPIO26 | I2S时钟 | | 麦克风 DIN | P13 | GPIO18 | I2S数据 | | 扬声器 DOUT | P14 | GPIO19 | I2S数据输出 | | 扬声器 BCLK | P15 | GPIO21 | I2S位时钟 | | 扬声器 LRCK | P16 | GPIO5 | I2S左右时钟 | | OLED SDA | - | GPIO23 | I2C数据 | | OLED SCL | - | GPIO22 | I2C时钟 | | 按键A(唤醒) | P5 | GPIO0 | BOOT按键 | | 按键B(ASR) | P11 | GPIO2 | 语音识别触发 |
1.2 引脚配置修改
小智AI的板级文件位于 main/boards/ 目录下。以 bread-compact-esp32 为模板,创建掌控板配置文件 main/boards/handpy-esp32/config.h:
- // main/boards/handpy-esp32/config.h
-
- #ifndef _BOARD_CONFIG_H_
- #define _BOARD_CONFIG_H_
-
- #include <driver/gpio.h>
-
- #define AUDIO_INPUT_SAMPLE_RATE 16000
- #define AUDIO_OUTPUT_SAMPLE_RATE 24000
-
- // ----- I2S 麦克风引脚 -----
- #define AUDIO_I2S_MIC_GPIO_WS GPIO_NUM_32 // P1
- #define AUDIO_I2S_MIC_GPIO_SCK GPIO_NUM_26 // P8
- #define AUDIO_I2S_MIC_GPIO_DIN GPIO_NUM_18 // P13
-
- // ----- I2S 扬声器引脚 -----
- #define AUDIO_I2S_SPK_GPIO_DOUT GPIO_NUM_19 // P14
- #define AUDIO_I2S_SPK_GPIO_BCLK GPIO_NUM_21 // P15
- #define AUDIO_I2S_SPK_GPIO_LRCK GPIO_NUM_5 // P16
-
- // ----- 按键引脚 -----
- #define BOOT_BUTTON_GPIO GPIO_NUM_0 // P5, A键(唤醒)
- #define TOUCH_BUTTON_GPIO GPIO_NUM_NC
- #define ASR_BUTTON_GPIO GPIO_NUM_2 // P11, B键(语音识别触发)
-
- // ----- OLED 显示屏(SH1106, 128x64, I2C)-----
- #define DISPLAY_SDA_PIN GPIO_NUM_23
- #define DISPLAY_SCL_PIN GPIO_NUM_22
- #define DISPLAY_WIDTH 128
- #define DISPLAY_HEIGHT 64
- #define DISPLAY_MIRROR_X false
- #define DISPLAY_MIRROR_Y false
-
- // ----- LED -----
- #define BUILTIN_LED_GPIO GPIO_NUM_NC
-
- #endif
复制代码 1.3 OLED屏幕配置(idf.py menuconfig)
小智AI默认使用SSD1306驱动,掌控板使用的是SH1106(128x64),需要在配置中修改: - cd xiaozhi-esp32
- idf.py menuconfig
复制代码
进入路径:Component config → Xiaozhi Assistant → Display Configuration: Display Type:选择 SH1106 Display Width:128 Display Height:64 I2C SDA Pin:23 I2C SCL Pin:22
1.4 编译与烧录
- idf.py set-target esp32
- idf.py build
- idf.py flash monitor
复制代码 1.5配置设备
第一步:Wi-Fi 配网(为设备配置网络)
烧录成功后,第一次启动设备会自动进入配网模式。 连接设备热点:使用手机或电脑,在Wi-Fi列表中找到并连接以 Xiaozhi-XXXX 命名的热点,这个热点由掌控板发出,没有密码。
访问配置页面:
连接成功后,部分设备会自动弹出配置页面。
如果没有自动弹出,请打开浏览器,在地址栏输入 http://192.168.4.1 并访问。
配置Wi-Fi:
在打开的页面中,选择你家里或办公室的2.4G Wi-Fi网络。
输入该Wi-Fi的密码,点击“连接”按钮。
等待重启:配置成功后,设备会显示“登录成功”并自动重启。重启后,设备将自动连接你刚才配置的Wi-Fi网络。
第二步:添加设备到管理后台
设备联网后,需要在小智AI平台完成绑定才能正常使用。 获取设备验证码:
设备成功联网后,屏幕会显示一个6位数字验证码。 如果设备没有屏幕,也可以通过语音唤醒(说“你好,小智”)来获取语音播报的验证码。
登录小智AI平台:在电脑或手机浏览器上访问 https://xiaozhi.me,点击“控制台”,并使用手机号注册或登录。
创建智能体(如无可跳过):如果还没有智能体,需要在控制台创建一个。进入“智能体”页面,点击“新建智能体”并命名即可。
添加设备:
在控制台页面,点击“添加设备”。 输入设备上显示的6位验证码,点击确定。
在协议选择中,选择“开源版”。
完成绑定:点击“开始使用”,设备即绑定成功。之后便可以正常与小智AI对话了。
2.1 硬件连接
MCP服务掌控板通过行空板K10 IO扩展板连接两块L298N电机驱动模块。 电机分配:
电机 | 掌控板丝印 | ESP32 GPIO | L298N模块 | L298N端子 | | 左后方向 | P8 | GPIO26 | 模块1 | M1 | | 左后PWM | P9 | GPIO25 | 模块1 | E1 | | 左前方向 | P6 | GPIO16 | 模块1 | M2 | | 左前PWM | P7 | GPIO17 | 模块1 | E2 | | 右前方向 | P13 | GPIO18 | 模块2 | M1 | | 右前PWM | P14 | GPIO19 | 模块2 | E1 | | 右后方向 | P15 | GPIO21 | 模块2 | M2 | | 右后PWM | P16 | GPIO5 | 模块2 | E2 |
2.2 Arduino IDE程序
MCP服务掌控板使用Arduino IDE编程,通过xiaozhi-mcp库注册move_robot工具。
完整代码: - #include <Arduino.h>
- #include <WiFi.h>
- #include <WebSocketMCP.h>
- #include <ArduinoJson.h>
-
- /********** WiFi 配置 ***********/
- const char* WIFI_SSID = "你的WiFi名称";
- const char* WIFI_PASS = "你的WiFi密码";
-
- /********** MCP 服务器地址 ***********/
- const char* MCP_ENDPOINT = "wss://api.xiaozhi.me/mcp/?token=你的token";
-
- /********** 电机引脚定义(掌控板丝印→GPIO) ***********/
- // 左后轮 (电机1, 模块1)
- #define DIR_LB 26 // P8
- #define PWM_LB 25 // P9
- // 左前轮 (电机2, 模块1)
- #define DIR_LF 16 // P6
- #define PWM_LF 17 // P7
- // 右前轮 (电机1, 模块2)
- #define DIR_RF 18 // P13
- #define PWM_RF 19 // P14
- // 右后轮 (电机2, 模块2)
- #define DIR_RB 21 // P15
- #define PWM_RB 5 // P16
-
- /********** 全局变量 ***********/
- WebSocketMCP mcpClient;
- bool wifiConnected = false;
- bool mcpConnected = false;
-
- bool isMoving = false;
- unsigned long moveStartTime = 0;
- float moveDuration = 0;
- String currentDirection = "stop";
-
- /********** setup ***********/
- void setup() {
- Serial.begin(115200);
- Serial.println("\n[MCP 麦克纳姆轮小车] 初始化...");
-
- // 初始化电机引脚
- pinMode(DIR_LB, OUTPUT); pinMode(PWM_LB, OUTPUT);
- pinMode(DIR_LF, OUTPUT); pinMode(PWM_LF, OUTPUT);
- pinMode(DIR_RF, OUTPUT); pinMode(PWM_RF, OUTPUT);
- pinMode(DIR_RB, OUTPUT); pinMode(PWM_RB, OUTPUT);
- stopAllMotors();
-
- // 连接WiFi
- WiFi.begin(WIFI_SSID, WIFI_PASS);
- while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
- wifiConnected = true;
- Serial.println("\nWiFi连接成功! IP: " + WiFi.localIP().toString());
-
- // 初始化MCP
- if (mcpClient.begin(MCP_ENDPOINT, onMcpConnectionChange)) {
- Serial.println("[MCP] 初始化成功");
- }
- }
-
- /********** loop ***********/
- void loop() {
- mcpClient.loop();
- // 定时停止检查
- if (isMoving && millis() - moveStartTime >= (unsigned long)(moveDuration * 1000)) {
- stopAllMotors();
- isMoving = false;
- Serial.println("[运动] 时间到,自动停止");
- }
- }
-
- /********** MCP连接回调 ***********/
- void onMcpConnectionChange(bool connected) {
- mcpConnected = connected;
- if (connected) {
- Serial.println("[MCP] 已连接,注册工具...");
- registerMcpTools();
- }
- }
-
- /********** 注册MCP工具 ***********/
- void registerMcpTools() {
- mcpClient.registerTool(
- "move_robot",
- "控制麦克纳姆轮小车运动",
- R"({
- "type": "object",
- "properties": {
- "direction": {
- "type": "string",
- "enum": ["forward","backward","left","right",
- "left_forward","right_forward",
- "left_backward","right_backward",
- "left_shift","right_shift","stop"],
- "description": "运动方向"
- },
- "duration": {"type": "number", "description": "持续时间(秒)"},
- "speed": {"type": "integer", "description": "速度 0~255", "default": 200}
- },
- "required": ["direction","duration"]
- })",
- [](const String& args) -> WebSocketMCP::ToolResponse {
- DynamicJsonDocument doc(256);
- deserializeJson(doc, args);
- String direction = doc["direction"].as<String>();
- float duration = doc["duration"].as<float>();
- int speed = doc.containsKey("speed") ? doc["speed"].as<int>() : 200;
-
- if (direction == "stop" || duration <= 0) {
- stopAllMotors();
- return WebSocketMCP::ToolResponse("{"success":true,"message":"已停止"}");
- }
-
- setRobotMotion(direction, speed);
- moveStartTime = millis();
- moveDuration = duration;
- isMoving = true;
-
- return WebSocketMCP::ToolResponse(
- "{"success":true,"direction":"" + direction +
- "","duration":" + String(duration) + "}"
- );
- }
- );
- }
-
- /********** 电机控制函数 ***********/
- void setMotor(int dirPin, int pwmPin, int speed, bool forward) {
- digitalWrite(dirPin, forward ? HIGH : LOW);
- analogWrite(pwmPin, speed);
- }
-
- void stopAllMotors() {
- analogWrite(PWM_LB, 0); analogWrite(PWM_LF, 0);
- analogWrite(PWM_RF, 0); analogWrite(PWM_RB, 0);
- }
-
- void setRobotMotion(const String &direction, int speed) {
- int spLB=0, spLF=0, spRF=0, spRB=0;
- bool dirLB=true, dirLF=true, dirRF=true, dirRB=true;
-
- if (direction == "forward") {
- spLB = spLF = spRF = spRB = speed;
- } else if (direction == "backward") {
- spLB = spLF = spRF = spRB = speed;
- dirLB = dirLF = dirRF = dirRB = false;
- } else if (direction == "left") { // 原地左转
- spLB = spLF = spRF = spRB = speed;
- dirLB = dirLF = false; // 左轮反转
- dirRF = dirRB = true; // 右轮正转
- } else if (direction == "right") { // 原地右转
- spLB = spLF = spRF = spRB = speed;
- dirLB = dirLF = true; // 左轮正转
- dirRF = dirRB = false; // 右轮反转
- } else if (direction == "left_shift") { // 水平左移
- spLF = spRB = speed; spLB = spRF = speed;
- dirLF = false; dirRB = false;
- dirLB = true; dirRF = true;
- } else if (direction == "right_shift") { // 水平右移
- spLF = spRB = speed; spLB = spRF = speed;
- dirLF = true; dirRB = true;
- dirLB = false; dirRF = false;
- } else if (direction == "left_forward") { // 左前斜移
- spLF = spLB = speed; spRF = spRB = 0;
- dirLF = dirLB = true;
- } else if (direction == "right_forward") { // 右前斜移
- spRF = spRB = speed; spLF = spLB = 0;
- dirRF = dirRB = true;
- } else if (direction == "left_backward") { // 左后斜移
- spLF = spLB = speed; spRF = spRB = 0;
- dirLF = dirLB = false;
- } else if (direction == "right_backward") {// 右后斜移
- spRF = spRB = speed; spLF = spLB = 0;
- dirRF = dirRB = false;
- } else { stopAllMotors(); return; }
-
- setMotor(DIR_LB, PWM_LB, spLB, dirLB);
- setMotor(DIR_LF, PWM_LF, spLF, dirLF);
- setMotor(DIR_RF, PWM_RF, spRF, dirRF);
- setMotor(DIR_RB, PWM_RB, spRB, dirRB);
- }
复制代码 【三、麦克纳姆轮运动原理】
麦克纳姆轮的设计原理是在车轮外环中安装了与轴心成45度角排列的辊子。当轮子旋转时,辊子与地面接触产生与轮轴呈45度的反推力,这个斜向推力可以被分解为纵向和横向两个向量。
通过控制四个轮子的旋转速度和方向,可以将各轮的斜向推力叠加或抵消,从而实现:
运动模式 | 左前轮 | 右前轮 | 左后轮 | 右后轮 | | 前进 | 正转 | 正转 | 正转 | 正转 | | 后退 | 反转 | 反转 | 反转 | 反转 | | 左移 | 正转 | 反转 | 反转 | 正转 | | 右移 | 反转 | 正转 | 正转 | 反转 | | 原地左转 | 反转 | 正转 | 反转 | 正转 | | 原地右转 | 正转 | 反转 | 正转 | 反转 |
注意:具体转向取决于麦克纳姆轮的安装方向(左旋/右旋),上述表格可能需要根据实际轮子方向微调。 【四、MCP通信原理】
MCP(Model Context Protocol) 是小智AI实现设备控制的核心协议。其工作流程如下:设备注册:MCP服务掌控板通过WebSocket连接到小智AI云端MCP接入点,注册move_robot工具及其参数 语音解析:用户对小智AI说“前进3秒”,ASR转为文本,LLM解析意图 工具调用:小智AI根据意图生成JSON参数{"direction":"forward","duration":3},通过WebSocket发送给MCP服务掌控板 执行反馈:MCP服务掌控板解析参数,驱动电机,返回执行结果
这种架构的优势在于:小智AI只负责“听懂”和“决策”,具体的硬件控制逻辑由MCP服务掌控板完成,两者通过标准协议解耦,便于扩展和维护。
【五、组装与调试】
5.1 组装步骤安装麦克纳姆轮:将四个麦克纳姆轮安装到直流电机上,注意左右两侧轮子的辊子方向应镜像对称 固定电机:将电机安装到小车底盘上 安装扩展板:将行空板K10 IO扩展板插入掌控板(MCP服务端) 连接电机驱动:按引脚表连接两块L298N模块与扩展板 连接小智AI外设:按引脚表连接I2S麦克风、I2S扬声器、OLED屏幕 供电:7.4V锂电池为L298N模块供电,掌控板通过USB或电池供电
5.2 调试要点【六、演示效果】
通过语音指令,小车可以实现: “前进3秒” → 直线前进 “后退2秒” → 直线后退 “左移5秒” → 水平向左平移 “右移5秒” → 水平向右平移 “左转” → 原地逆时针旋转 “右转” → 原地顺时针旋转 “左前” → 向左前方斜移 “右后” → 向右后方斜移
【七、注意事项】- 电源独立:L298N模块的电机电源(7.4V)与掌控板的逻辑电源(5V/USB)需分开供电,但必须共地
引脚电平:掌控板GPIO为3.3V逻辑,L298N模块兼容3.3V信号 轮子安装:麦克纳姆轮有左旋和右旋之分,安装错误会导致运动方向混乱 安全第一:调试时先卸下轮子,确认电机转向正确后再安装
【八、项目总结】
本项目实现了双掌控板分工协作的语音控制麦克纳姆轮小车: 小智AI掌控板:通过ESP-IDF编译小智AI固件,适配掌控板引脚,实现语音唤醒、语音识别、大模型对话 MCP服务掌控板:通过Arduino IDE + xiaozhi-mcp库,注册MCP工具,实现麦克纳姆轮的全向运动控制 双板协作:小智AI负责“听懂”,MCP服务负责“执行”,通过WebSocket MCP协议通信
这一方案将小智AI强大的语音交互能力与麦克纳姆轮独特的全向移动特性完美结合,为创客教育提供了一个兼具AI语音交互与 机器人运动控制的综合实践项目。 全部代码已在文中开源,欢迎大家一起折腾!如有问题,欢迎在评论区交流讨论。
|