尧图精选

基于小智ESP32S3的智能控制小车:用MCP打通GPIO与L298N的TaoToken实践

🕒 发布时间:2026/10/2 20:20:05 📁 来源:尧图网络
1. 小智 ESP32S3 智能小车为什么需要 L298N 与 MCP 协议小智 ESP32S3 智能控制小车本质是把「语音/文本指令」翻译成「GPIO 电平」再通过 L298N 驱动四个直流电机完成前进、后退、转向、调速的闭环。很多刚上手的朋友会问ESP32S3 的 GPIO 不是能输出高低电平吗为什么不直接把电机接上去原因很直接——ESP32S3 的 GPIO 输出电流只有几十毫安级别而一个 130 或 TT 马达堵转电流轻松上到几百毫安直接接上去要么电机不转要么把主控拉挂。L298N 的作用就是「低压控制高压」它用独立电池两节 3.7V 串联约 7.4V给电机供电同时接收 ESP32S3 的 3.3V 逻辑信号两边互不干扰。那 MCP 又是什么角色MCPModel Context Protocol在小智项目里是「工具调用协议」。简单说你对着小智说「往前走两秒」语音识别把这句话交给大模型大模型判断需要调用一个叫self.car.control的工具参数是actionforward, duration2000这个调用请求通过 MCP 协议下发到 ESP32S3固件里的回调函数再去操作 GPIO。整条链路是语音 → ASR → LLM → MCP tools/call → GPIO → L298N → 电机。理解了这条链路后面接线和改代码就不会迷路。这套方案适合谁适合已经跑通小智语音助手、想加一个「能动的身体」的玩家也适合想学 MCP 协议在嵌入式端怎么落地的开发者。成本上小智开发套件加亚克力底板、四驱电机、L298N、杜邦线和电池整体可控。我试过用最朴素的四驱底板搭接线半小时、改代码十分钟就能动起来难点主要在 GPIO 上电自转和引脚映射这两块。需要提前说明的是本文聚焦「GPIO 指令下发到 L298N」这一段闭环语音唤醒、麦克风、功放这些属于小智基础搭建按官方百科走通即可。下面从硬件映射、MCP 工具注册、编译烧录到串口验证一步步给可复制的配置。2. TaoToken 前置准备拿到 Base URL、API Key 与 Model ID在动手接线之前先把「大脑」这一侧准备好。小智的语音对话和工具调用需要一个大模型服务来解析意图这里用 TaoToken 提供的统一接入。它的价值在于一个 Base URL 加一个 Key就能在模型对话、Coding Plan、API 调用之间切换不用为每个模型单独配环境。对小智这种需要频繁调试 prompt 和工具描述的场景省事很多。第一步打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册并登录。登录后进入控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 在「API Keys」页面创建一个新 Key。创建时给它起个能认出来的名字比如xiaozhi-car方便后面区分。Key 只在创建时完整显示一次复制下来存好别截图发群里。第二步确认你要用的 Model ID。小智的工具调用对模型的 function calling 能力有要求选一个支持工具调用的模型即可。在模型对话页面 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 可以先手动测一下模型能不能正确返回工具调用结构确认没问题再写进固件配置。第三步记下三个关键值后面配置里会反复用到配置项值说明Base URLhttps://taotoken.net/api统一接入地址不加 UTMAPI Keysk-xxxxxxxx控制台创建仅显示一次Model ID按控制台可选模型填写需支持 function calling如果你打算长期做编码和 Agent 类调试可以了解下 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 它更适合高频调用场景。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 遇到参数格式问题先查这里。这里要提醒一句小智固件里配置的是「服务端地址 Key」不是让你在 ESP32 上跑模型。ESP32S3 只负责采集语音、发请求、执行工具回调推理在云端完成。所以 Key 的权限和额度要留够调试阶段工具调用会比较频繁。3. 可复制配置GPIO 映射表与 MCP 工具注册代码这一节是全文的核心给你可以直接抄的接线映射和mcp_server.cc修改片段。先说接线。我用的是四驱亚克力底板四个直流电机L298N 四路驱动模块。ESP32S3 的 GPIO 到 L298N 的 IN 引脚映射如下这张表建议打印出来贴在桌边电机编号电机位置L298N IN 引脚ESP32 GPIO电机1左前轮IN1GPIO19电机1左前轮IN2GPIO20电机2右前轮IN3GPIO3电机2右前轮IN4GPIO46电机3左后轮IN5GPIO9电机3左后轮IN6GPIO10电机4右后轮IN7GPIO11电机4右后轮IN8GPIO12接线注意三点L298N 的 GND 必须和 ESP32S3 的 GND 共地否则逻辑电平没有参考电机电源用两节 3.7V 串联约 7.4V接 L298N 的 12V 输入和 GNDESP32S3 的 3.3V 只给 L298N 的逻辑供电参考不要用它去驱动电机。左侧的 ESP32S3 开发板除了主控还要有数字麦克风和功放喇叭否则语音链路不完整。接下来是代码。小智源码在 v1.7.0 起把 MCP 作为默认控制协议之前的 IoT framework 已删除所以务必用支持 MCP 的版本我用的是 v2.0.3。修改文件是main/mcp_server.cc在AddCommonTools()里注册一个控制小车的工具。核心片段如下可直接粘贴// 小车整体控制工具 - 基于L298N四电机模块 AddTool(self.car.control, 控制小车的整体运动。支持前进、后退、左转、右转、停止等基本动作。\n Use this tool to control the car movement when user asks to move the car.\n Args:\n action: 运动动作可选值: forward(前进), backward(后退), left(左转), right(右转), stop(停止)\n duration: 持续时间(毫秒)0表示持续运动默认0\n Return:\n 返回控制结果和状态信息, PropertyList({ Property(action, kPropertyTypeString), Property(duration, kPropertyTypeInteger, 0, 0, 100000) }), [](const PropertyList properties) - ReturnValue { auto action properties[action].valuestd::string(); int duration properties[duration].valueint(); static bool gpio_initialized false; static const gpio_num_t motor_pins[4][2] { {GPIO_NUM_19, GPIO_NUM_20}, // 电机1: 左前轮 {GPIO_NUM_3, GPIO_NUM_46}, // 电机2: 右前轮 {GPIO_NUM_9, GPIO_NUM_10}, // 电机3: 左后轮 {GPIO_NUM_11, GPIO_NUM_12} // 电机4: 右后轮 }; if (!gpio_initialized) { for (int i 0; i 4; i) { gpio_set_direction(motor_pins[i][0], GPIO_MODE_OUTPUT); gpio_set_direction(motor_pins[i][1], GPIO_MODE_OUTPUT); gpio_set_level(motor_pins[i][0], 0); gpio_set_level(motor_pins[i][1], 0); } vTaskDelay(pdMS_TO_TICKS(50)); for (int i 0; i 4; i) { gpio_config_t config { .pin_bit_mask (1ULL motor_pins[i][0]) | (1ULL motor_pins[i][1]), .mode GPIO_MODE_OUTPUT, .pull_up_en GPIO_PULLUP_DISABLE, .pull_down_en GPIO_PULLDOWN_DISABLE, .intr_type GPIO_INTR_DISABLE, }; ESP_ERROR_CHECK(gpio_config(config)); gpio_set_level(motor_pins[i][0], 0); gpio_set_level(motor_pins[i][1], 0); } gpio_initialized true; ESP_LOGI(TAG, Car GPIO initialized, all motors stopped); } auto control_motor [](int idx, int dir) { if (dir 0) { gpio_set_level(motor_pins[idx][0], 0); gpio_set_level(motor_pins[idx][1], 1); } else if (dir 0) { gpio_set_level(motor_pins[idx][0], 1); gpio_set_level(motor_pins[idx][1], 0); } else { gpio_set_level(motor_pins[idx][0], 0); gpio_set_level(motor_pins[idx][1], 0); } }; if (action forward) { control_motor(0, -1); control_motor(1, 1); control_motor(2, -1); control_motor(3, 1); } else if (action backward) { control_motor(0, 1); control_motor(1, -1); control_motor(2, 1); control_motor(3, -1); } else if (action left) { control_motor(0, -1); control_motor(1, 1); control_motor(2, 1); control_motor(3, -1); } else if (action right) { control_motor(0, 1); control_motor(1, -1); control_motor(2, -1); control_motor(3, 1); } else if (action stop) { control_motor(0, 0); control_motor(1, 0); control_motor(2, 0); control_motor(3, 0); } else { throw std::runtime_error(Invalid action: action); } if (duration 0) { auto app Application::GetInstance(); app.Schedule([duration]() { static const gpio_num_t pins[4][2] { {GPIO_NUM_19, GPIO_NUM_20}, {GPIO_NUM_3, GPIO_NUM_46}, {GPIO_NUM_9, GPIO_NUM_10}, {GPIO_NUM_11, GPIO_NUM_12} }; vTaskDelay(pdMS_TO_TICKS(duration)); for (int i 0; i 4; i) { gpio_set_level(pins[i][0], 0); gpio_set_level(pins[i][1], 0); } ESP_LOGI(TAG, Car auto-stopped after %d ms, duration); }); } cJSON* result cJSON_CreateObject(); cJSON_AddStringToObject(result, action, action.c_str()); cJSON_AddNumberToObject(result, duration, duration); cJSON_AddBoolToObject(result, success, true); return result; });这段代码有两个关键设计。一是构造函数里先把所有电机引脚拉低防止上电瞬间 GPIO19、GPIO20、GPIO3、GPIO46 这些可能默认高电平的引脚让电机自转。二是duration参数配合Application::Schedule做定时停止这样「往前走两秒」这种指令能自动收尾不用再喊一句「停」。如果你用的是 Cline MCP 或 Codex 这类外部工具来调试工具描述记得三件套要写全Base URL 填https://taotoken.net/apiKey 填控制台创建的Model ID 填支持 function calling 的模型。三者缺一工具调用就会失败。4. 编译烧录与串口验证确认 GPIO 真的动了代码改完接下来编译烧录。我用的是 ESP-IDF VS Code或 Cursor的组合环境搭建按乐鑫官方教程走一遍即可。烧录前先确认idf.py set-target esp32s3选对芯片然后idf.py build编译idf.py -p /dev/ttyUSB0 flash monitor烧录并打开串口监视器。Windows 下端口可能是COM3之类按设备管理器实际显示填。烧录成功后串口会打印初始化日志。重点看这几行I (xxx) MCP: Car GPIO initialized, all motors stopped I (xxx) MCP: Motor1(左前): IN1GPIO19, IN2GPIO20 I (xxx) MCP: Motor2(右前): IN3GPIO3, IN4GPIO46 I (xxx) MCP: Motor3(左后): IN5GPIO9, IN6GPIO10 I (xxx) MCP: Motor4(右后): IN7GPIO11, IN8GPIO12看到这些说明 GPIO 配置生效、电机已处于停止状态。如果上电瞬间轮子转了一下才停说明构造函数里的拉低逻辑起作用了属于正常现象如果一直转不停检查是不是漏了gpio_config那一段。接下来做动作验证。唤醒小智说「往前走两秒」串口应该打印类似I (xxx) MCP: Car moving forward - all motors rotating forward I (xxx) MCP: Car auto-stopped after 2000 ms同时观察四个轮子是否同向转动。如果某个轮子反向把对应电机的两根 IN 线对调或者改代码里control_motor的正负号。转向验证说「左转」应该是左侧两轮反转、右侧两轮正转形成原地左旋。调速这块L298N 的 ENA/ENB 使能脚如果接了 PWM可以在代码里用ledc输出占空比来调速如果没接就是全速运行先用动作验证跑通再考虑调速。轮速验证有个土办法在轮子上贴一小段胶带用手机慢动作拍一秒数胶带转了几圈粗略估算转速。四个轮子转速差异大说明电机个体差异或供电不足检查电池电压是否掉到 6V 以下。验证模型侧是否正常可以打开模型对话 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 手动发一条工具调用请求看返回结构里tool_calls字段是否正确。这一步能把「模型不返回工具调用」和「固件不执行」两类问题分开定位。5. 本篇常见错误排查401、local proxy failed 与 choices 解析失败调试过程中最容易卡在几个报错上这里逐个对照。401 Unauthorized。这个基本是 Key 的问题。检查三处Key 是否复制完整有没有漏字符、Base URL 是否写成https://taotoken.net/api不要多加斜杠或路径、请求头里Authorization: Bearer sk-xxx格式对不对。如果 Key 在控制台被删过旧 Key 会立即失效重新创建一个换上。local proxy failed / connection refused。这类报错通常出现在本地调试工具链里意思是请求没发出去。先确认网络能正常访问taotoken.net再检查是不是把 Base URL 填成了带 UTM 的官网地址——API 调用要用https://taotoken.net/api不带任何查询参数。如果用了 Cline MCP 或类似工具检查它的代理配置有没有指向一个不存在的本地端口。reading choices / choices 字段为空。这是响应解析阶段的报错说明请求发出去了但返回结构不符合预期。常见原因有两个一是 Model ID 填错用了一个不支持 function calling 的模型返回里没有choices或tool_calls二是请求体里tools字段格式不对模型没识别出工具定义。解决办法是先用模型对话页面手动发一次带工具的请求确认返回结构再对照固件里的请求格式改。OAuth 相关报错。如果你在 Claude Code 或类似工具里接入遇到 OAuth 报错说明认证方式选错了。这类工具应该用 API Key 认证不是 OAuth 流程。把认证方式切到 KeyBase URL 填https://taotoken.net/apiModel ID 填对三件套齐了再试。电机不转但串口有日志。说明 MCP 调用成功、GPIO 也设置了问题在硬件侧。检查 L298N 的电机电源是否接上、共地是否做了、使能跳线帽是否插好。L298N 板子上通常有 5V-EN 跳线帽拔掉才能用外部逻辑电平控制插着的话使能脚被拉高可能一直转或一直不转。电机自转停不下来。回到构造函数那段确认gpio_set_level拉低在所有gpio_config之前和之后都执行了。GPIO19、GPIO20、GPIO3、GPIO46 这几个引脚在 ESP32S3 上电时有默认状态必须显式拉低。排障时建议开两个窗口一个看串口日志一个看模型对话页面的请求记录。哪一侧断了一眼能看出来。接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里有完整的请求示例对照着改比自己猜快。6. 从能跑到好用把小车接入你的日常调试流跑通「前进两秒」只是起点。真正让这台小车好用是把 MCP 工具描述写得更贴合你的说话习惯。比如你习惯说「冲一下」可以在工具描述里加同义词提示让模型更容易命中forward。工具描述写得越具体模型误判越少。另一个实用技巧是给动作加组合。现在只有单步动作你可以在self.car.control之外再注册一个self.car.patrol内部循环调用前进、转向、停止实现简单的巡逻路径。这样一句「巡逻一圈」就能触发一串动作比一步步喊高效。调速方面如果 L298N 的 ENA/ENB 接了 PWM 引脚可以用ledc_timer_config和ledc_channel_config输出不同占空比把速度也做成工具参数。比如self.car.control加一个speed参数0 到 100映射到占空比。这样「慢慢往前」和「快速后退」就能区分开。长期做这类嵌入式 Agent 调试调用频率会上去可以关注 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 它更适合高频工具调用场景。API Key 管理在控制台 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 建议给不同项目建不同的 Key方便排查和限额。最后说个我踩过的坑一开始我把四个电机的正负号写反了两个结果「前进」变成了原地打转。后来在代码里加了一行日志把每个电机实际设置的电平打出来对照接线表一查就找到了。所以别嫌日志多调试阶段串口打印是你最好的朋友。把这套跑顺之后你可以试着让小智控制小车去撞倒一个纸杯或者沿着胶带线走一圈这些具体任务比单纯「前进后退」更能暴露工具描述和参数设计的问题。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →