本帖最后由 云天 于 2026-2-20 16:44 编辑
**前言**
在创客项目中,让东西“动起来”总是最迷人的一步。步进电机以其精准的定位能力,成为很多机械臂、3D打印机、云台的首选。但在实践中,从让它转,到让它精准地转,再到让它听懂指令转,这中间的路程充满探索。本文将分享我在步进电机控制上的三次迭代:从 **micro:bit 的初步尝试**,到 **ESP32 的网页遥控**,再到最终结合 **小智AI(DF K10)实现语音对话控制** 的完整过程与心得。
## 第一章:初体验 – Micro:bit 与扩展板的“温柔一颤”
我的第一个想法很简单:用最熟悉的 Micro:bit 搭配电机扩展板来驱动一个42步进电机。
**硬件连接:** Micro:bit 插在扩展板上,扩展板M1+、M1-、M2+、M2-直接连接步进电机。软件上,使用 Mind+ 图形化编程,拖拽“电机转动...”积木块。
**实际测试:**
**结果与分析:**
电机动了,但问题也很明显:
1. **转速“无法控制”**:电机启动时也会有明显的顿挫感,无法控制转速。
2. **位置“对不上”**:让电机转一圈(例如5.9*1圈),它总是多转或少转一点,精度无法满足要求。
**问题出在哪?**
问题原因已经明确:使用的Micro:bit电机驱动扩展板(DFR0548)的步进电机控制库,是通过延时来实现角度控制的,而不是精确的步进脉冲计数。这种方法精度较差,容易受电压、负载等因素影响,导致实际转动角度与预期存在偏差。
- void Microbit_Motor::stepperDegree42(Steppers index, Dir direction, int degree)
- {
- ...
- uint32_t Degree = abs(degree);
- delay( (50000 * Degree) / (360 * 50) + 80);
- ...
- }
复制代码
它通过一个固定的延时公式来近似转动角度,没有考虑电机实际转速、细分设置或减速比。因此,当您通过减速比计算期望圈数时,库只是简单地延时相应的时间,但实际电机转动速度可能因电压、负载等因素而偏离预期,导致最终角度误差。
**小结:**
这次尝试让我明白,对于要求稍高的控制,Micro:bit 这类教育主板加基础扩展板的方案,更适合快速验证运动逻辑,而非追求精密控制。
插曲:用 Arduino UNO R3 快速验证 TB6600 驱动器 在抛弃 Micro:bit 扩展板方案后,我首先用最熟悉的 Arduino UNO R3 搭配 TB6600 驱动器 进行了一次基础测试。这一步的目的很简单:确认驱动器、电机、电源和接线都正常,并亲身体验 细分 带来的平滑效果。 硬件连接
Arduino UNO 引脚 | TB6600 信号端 | 说明 | | D7 | PUL- (脉冲) | 发送脉冲信号 | | D6 | DIR- (方向) | 控制旋转方向 | | D5 | ENA- (使能) | 高电平有效 | | 5V | PUL+、DIR+、ENA+ | 共阳极接法,所有正端接 UNO 5V |
注意:TB6600 支持共阴/共阳两种接法。我采用最常见的 共阳极接法:将驱动器侧的 PUL+、DIR+、ENA+ 全部连接到 Arduino 的 5V,正端接数字引脚。驱动器需要外接 9~42V 直流电源。 拨码开关设置:8 细分 TB6600 侧面有 6 位拨码开关(S1、S2、S3 用于设置细分,S4、S5、S6 用于设置电流)。要实现 8 细分,根据驱动器说明书,需要将 S1、S2、S3 设置为 ON、OFF、ON(不同品牌可能定义不同,请以实物标签为准)。细分后,步进电机转一圈所需的脉冲数变为 200 × 8 = 1600 步。 Arduino 测试代码 下面是一个简单的正反转循环程序。它会让电机先正转一圈(1600 步),暂停一秒,再反转一圈,如此循环。
- // 定义引脚连接
- // 请根据您的实际接线修改引脚号
- #define PUL_PIN 7 // 脉冲信号引脚,连接到驱动器的PUL+
- #define DIR_PIN 6 // 方向信号引脚,连接到驱动器的DIR+
- #define ENA_PIN 5 // 使能信号引脚(可选),连接到驱动器的ENA+
-
- void setup() {
- // 初始化引脚模式
- pinMode(PUL_PIN, OUTPUT);
- pinMode(DIR_PIN, OUTPUT);
- pinMode(ENA_PIN, OUTPUT);
-
- // 初始状态设置
- digitalWrite(PUL_PIN, LOW);
- digitalWrite(DIR_PIN, LOW); // 设置一个旋转方向,例如LOW为正转
- digitalWrite(ENA_PIN, HIGH); // HIGH使能驱动器(具体请参考您的驱动器说明)
-
- // 开启串口监视器,用于调试
- Serial.begin(9600);
- Serial.println("TB6600 8细分测试开始");
- }
-
- void loop() {
- // 示例1:让电机连续旋转
- // 速度由脉冲间隔决定,这里每转需要1600个脉冲(基于1.8°电机,8细分)
- // 以下代码将使电机以一个较慢的速度连续旋转
- for(int i = 0; i < 1600; i++) { // 发送一圈所需的脉冲数
- digitalWrite(PUL_PIN, HIGH);
- delayMicroseconds(500); // 高电平持续时间,配合下一个延时控制脉冲周期
- digitalWrite(PUL_PIN, LOW);
- delayMicroseconds(500); // 低电平持续时间
- // 整个周期约1000微秒,脉冲频率为1kHz,对应转速约为 (1kHz / 1600脉冲/圈) * 60秒 = 37.5 RPM
- }
-
- // 延时一下,然后反转方向
- delay(1000);
- digitalWrite(DIR_PIN, HIGH); // 改变方向
-
- // 再次发送一圈脉冲
- for(int i = 0; i < 1600; i++) {
- digitalWrite(PUL_PIN, HIGH);
- delayMicroseconds(500);
- digitalWrite(PUL_PIN, LOW);
- delayMicroseconds(500);
- }
-
- delay(1000);
- digitalWrite(DIR_PIN, LOW); // 恢复方向
-
- // 或者,可以使用更精确的定时方法,如利用millis()或使用AccelStepper库
- }
复制代码
测试演示视频
测试流程与感受 为什么先做这个测试?排除硬件故障:确保电机、驱动器和电源都工作正常,为后续 ESP32 的复杂控制扫清障碍。 理解细分原理:亲眼见证细分对精度的改善,让我更坚定地在 ESP32 项目中使用 8 细分。 积累基础代码:这段简单代码稍加改造,就可以嵌入到任何主控中,成为脉冲生成的“内核”。
通过这次基础测试,我确认了 TB6600 搭配 8 细分的组合完全能够满足我的精度需求,同时也为后续用 ESP32 实现网络控制和语音交互打下了坚实的硬件基础。
## 第二章:进阶 – ESP32 + TB6600 搭建网页“遥控器”
为了攻克精度问题,我决定换一套“专业班子”:采用**ESP32 主控 + TB6600 专业步进电机驱动器**的方案。
**为什么是这对组合?**
* **ESP32:** 强大的处理能力,自带 Wi-Fi 和蓝牙,可以轻松搭建 Web 服务器,实现无线控制。
* **TB6600 驱动器:** 这是关键。它支持**最高32细分**、**大电流输出(最高4.0A)**,并且自带光耦隔离,信号更稳定。通过拨码开关设置细分,可以将电机每 一步的转动角度细化(例如8细分下,200步/圈的电机就变成了1600步/圈),从根本上消除了共振,提升了精度和平稳性。
**搭建网页遥控器:**
我在 ESP32 上编写了异步 Web 服务器,并设计了一个包含**虚拟摇杆**的控制页面。这个摇杆不仅仅是一个开关:
* **Y轴控制速度与方向**:向上推为正转,向下为反转。推动幅度(0~1)被映射为脉冲间隔(100μs ~ 2000μs),实现了**无级调速**。松开摇杆,电机以设定速度持续运转;再次松开回中的摇杆,电机停止。这个过程完全是非阻塞的,ESP32 的后台定时器保证了脉冲的精准发送。
* **X轴与模式切换**:保留了圈数、步数、角度等传统控制模式,方便进行精确的定位控制。
**代码:**
- #include <WiFi.h>
- #include <ESPAsyncWebServer.h>
-
- // ========== WiFi 配置(请修改为你的热点信息) ==========
- const char* ssid = "sxs";
- const char* password = "#######";
-
- // ========== 步进电机引脚 ==========
- #define PUL_PIN 4
- #define DIR_PIN 17
- #define ENA_PIN 16
-
- // ========== 电机参数 ==========
- // 假设步进电机步距角1.8°(200步/转),驱动器细分拨码设为8细分
-
- const float STEPS_PER_OUTPUT_REV = 1600.0; // 200步/圈 * 8细分 = 1600
-
- // ========== 非阻塞电机控制变量 ==========
- volatile long targetSteps = 0; // 剩余脉冲数(正数为CW,负数为CCW)
- volatile bool motorRunning = false;
- unsigned long lastPulseTime = 0;
- unsigned long pulseInterval = 100; // 脉冲周期(微秒),初始默认100us(10kHz)
- bool continuousMode = false; // 连续运转模式(用于摇杆速度控制)
-
- // ========== 创建异步服务器 ==========
- AsyncWebServer server(80);
-
- // ========== 内嵌 HTML 页面(摇杆界面,含CSS) ==========
- const char index_html[] PROGMEM = R"rawliteral(
- ……
- )rawliteral";
-
- // ========== 函数声明 ==========
- void handleControl(AsyncWebServerRequest *request);
- void updateMotor();
- unsigned long mapSpeedToInterval(float speedFactor);
-
- void setup() {
- Serial.begin(115200);
-
- pinMode(PUL_PIN, OUTPUT);
- pinMode(DIR_PIN, OUTPUT);
- pinMode(ENA_PIN, OUTPUT);
- digitalWrite(ENA_PIN, HIGH); // 使能驱动器
-
- WiFi.begin(ssid, password);
- Serial.print("正在连接 WiFi");
- while (WiFi.status() != WL_CONNECTED) {
- delay(500);
- Serial.print(".");
- }
- Serial.println("\nWiFi 连接成功");
- Serial.print("IP 地址: ");
- Serial.println(WiFi.localIP());
-
- server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
- request->send_P(200, "text/html", index_html);
- });
-
- server.on("/control", HTTP_GET, handleControl);
-
- server.begin();
- Serial.println("HTTP 服务器已启动");
- }
-
- void loop() {
- updateMotor();
- }
-
- // ========== 处理 /control GET 请求 ==========
- void handleControl(AsyncWebServerRequest *request) {
- String message = "OK";
-
- // 1. 检查停止指令
- if (request->hasParam("action")) {
- String action = request->getParam("action")->value();
- if (action == "stop") {
- targetSteps = 0;
- motorRunning = false;
- continuousMode = false;
- digitalWrite(ENA_PIN, LOW);
- delay(10);
- digitalWrite(ENA_PIN, HIGH);
- request->send(200, "text/plain", "STOPPED");
- return;
- }
- }
-
- // 2. 解析方向
- bool dirCW = true;
- if (request->hasParam("dir")) {
- String dir = request->getParam("dir")->value();
- dirCW = (dir == "CW");
- }
-
- // 3. 解析模式和数值
- if (request->hasParam("mode") && request->hasParam("value")) {
- String mode = request->getParam("mode")->value();
- float value = request->getParam("value")->value().toFloat();
- Serial.println(mode);
- Serial.println( request->getParam("value")->value());
- // 摇杆模式(速度控制)
- if (mode == "joystick") {
- float speedFactor = value; // 此时 value 即为速度因子 (0~1)
- if (speedFactor == 0) {
- targetSteps = 0;
- motorRunning = false;
- continuousMode = false;
- message = "Joystick stopped";
- } else {
- // 立即设置方向引脚
- digitalWrite(DIR_PIN, dirCW ? HIGH : LOW);
- // 根据速度因子更新脉冲间隔
- pulseInterval = mapSpeedToInterval(speedFactor);
- Serial.println(pulseInterval);
- // 进入连续模式:用一个大数表示方向,实际步数不减少
- targetSteps = dirCW ? 1000000 : -1000000;
- motorRunning = true;
- continuousMode = true;
- message = "Joystick set: speed=" + String(speedFactor) + ", interval=" + String(pulseInterval) + "us";
- Serial.println(message);
- }
- }
- else {
- // 原有的位置控制模式(圈数/步数/角度)
- long steps = 0;
- if (mode == "turn") {
- steps = (long)(value * STEPS_PER_OUTPUT_REV);
- } else if (mode == "steps") {
- steps = (long)value;
- } else if (mode == "angle") {
- steps = (long)((value / 360.0) * STEPS_PER_OUTPUT_REV);
- }
-
- if (steps > 0) {
- targetSteps = dirCW ? steps : -steps;
- motorRunning = true;
- continuousMode = false; // 退出连续模式
- message = "Started: " + String(steps) + " pulses";
- } else {
- message = "Invalid value (must be >0)";
- }
- }
- } else {
- message = "Missing mode or value";
- }
-
- request->send(200, "text/plain", message);
- }
-
- // ========== 非阻塞脉冲发送 ==========
- void updateMotor() {
- if (!motorRunning || targetSteps == 0) {
- return;
- }
-
- unsigned long now = micros();
- if (now - lastPulseTime >= pulseInterval) {
- lastPulseTime = now;
- Serial.println(targetSteps);
- // 根据目标步数的符号设置方向引脚(连续模式下符号仍然有效)
- if (targetSteps > 0) {
- digitalWrite(DIR_PIN, HIGH);
- } else {
- digitalWrite(DIR_PIN, LOW);
- }
-
- // 产生一个50us的高电平脉冲(确保驱动器可靠检测)
- digitalWrite(PUL_PIN, HIGH);
- delayMicroseconds(50);
- digitalWrite(PUL_PIN, LOW);
-
- // 如果是连续模式,不减少 targetSteps(保持方向标记)
- if (!continuousMode) {
- if (targetSteps > 0) {
- targetSteps--;
- } else if (targetSteps < 0) {
- targetSteps++;
- }
- }
- }
- }
-
- // ========== 速度因子映射到脉冲间隔 ==========
- unsigned long mapSpeedToInterval(float speedFactor) {
- // 限制速度因子在有效范围内
- if (speedFactor < 0.01) speedFactor = 0.01;
- if (speedFactor > 1.0) speedFactor = 1.0;
-
- // 定义速度范围:最大速度对应最小间隔 100us (10kHz),最小速度对应最大间隔 2000us (500Hz)
- const unsigned long minInterval = 100; // 最快
- const unsigned long maxInterval = 2000; // 最慢
-
- // 线性映射:速度越快(speedFactor越大),间隔越小
- unsigned long interval = maxInterval - (unsigned long)((maxInterval - minInterval) * speedFactor);
-
- // 确保不越界
- if (interval < minInterval) interval = minInterval;
- if (interval > maxInterval) interval = maxInterval;
-
- return interval;
- }
复制代码 注:代码省略部分在留言附件中。
效果: 当我在手机浏览器上打开 ESP32 的 IP 地址,手指在摇杆上轻轻一推,电机就平滑地加速旋转,没有丝毫抖动。那种“指哪打哪”的精准感,是第一次尝试完全无法比拟的。
演示视频: 小结: 这次升级让我深刻体会到“专业的事交给专业的设备”。TB6600 驱动器的细分能力和 ESP32 精准的脉冲生成,是项目成功的基石。 第三章:未来 – 让小智AI(DF K10)听懂电机的“悄悄话” 电机已经能被我“遥控”了,但我的终极目标是让它听懂人话。这时,DFRobot 的 K10 人工智能主控 进入了我的视野。结合 小智AI 的 MCP(模型上下文协议)功能,我可以将电机封装成一个“工具”,让 AI 来调用。 思路是这样的: 硬件核心:ESP32 依然负责底层的电机脉冲控制(沿用第二章的稳定代码)。 AI 大脑:DF K10 作为 AI 语音交互端,运行小智AI 固件。它能拾取我的语音命令,例如“让电机正转100步”。 MCP 的桥梁作用:DF K10 通过 MCP 协议与 ESP32 通信。我在 ESP32 上注册了一个名为 motor_control 的工具,它定义了 AI 可以理解的“指令格式”: - {
- "direction": "CW", // 方向
- "mode": "steps", // 模式 (steps, turns, angle, speed)
- "value": 200 // 数值
- }
复制代码
工作流程:我说“转一圈” → DF K10 将语音转为文字,通过 AI 大模型理解意图 → 大模型通过 MCP 协议,向 ESP32 的 motor_control 工具发送一个包含参数 {"direction":"CW","mode":"turns","value":1} 的请求 → ESP32 收到请求,调用对应的回调函数,解析参数并驱动电机完成动作。 代码:
- #include <Arduino.h>
- #include <WiFi.h>
- #include <WebSocketMCP.h>
-
- /********** 步进电机配置 ***********/
- // 引脚定义
- #define PUL_PIN 4
- #define DIR_PIN 17
- #define ENA_PIN 16
-
- // 电机参数(根据实际细分调整)
- const float STEPS_PER_OUTPUT_REV = 1600.0; // 200步/圈 * 8细分
-
- // 非阻塞电机控制变量
- volatile long targetSteps = 0; // 剩余脉冲数(正数CW,负数CCW)
- volatile bool motorRunning = false;
- unsigned long lastPulseTime = 0;
- unsigned long pulseInterval = 100; // 脉冲周期(微秒),用于速度控制
- bool continuousMode = false; // 连续运转模式(用于摇杆/速度控制)
-
- /********** WiFi配置 ***********/
- const char* WIFI_SSID = "Xiaomi_10EE";
- const char* WIFI_PASS = "moto19tes84";
-
- /********** MCP服务器地址 ***********/
- const char* MCP_ENDPOINT = "wss://api.xiaozhi.me/mcp/?token=eyJhbGciOiJFUzI11NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEzODMzOCwiYWdlbnRJZCI6ODQyNDAsImVuZHBvaW50SWQiOiJhZ2VudF84NDI0MCIsInB1cnBvc2UiOiJtY3AtZW5kcG9pbnQiLCJpYXQiOjE3NTE3OTE4NDF9.U4-FWlCgqUUaKtq6gB6HwczogI9eUhZXIm7aaSsmND6vVbvB7TP0shu4XdSYHMUuwUcOZrJX2M6YlS3yJNsdeA";
-
- /********** 调试 ***********/
- #define DEBUG_SERIAL Serial
- #define DEBUG_BAUD_RATE 115200
-
- /********** LED引脚(板载)***********/
- #define LED_PIN 2
-
- /********** 全局变量 ***********/
- WebSocketMCP mcpClient;
-
- // 缓冲区管理(用于串口命令)
- #define MAX_INPUT_LENGTH 1024
- char inputBuffer[MAX_INPUT_LENGTH];
- int inputBufferIndex = 0;
-
- // 连接状态
- bool wifiConnected = false;
- bool mcpConnected = false;
-
- /********** 函数声明 ***********/
- void setupWifi();
- void onMcpOutput(const String &message);
- void onMcpError(const String &error);
- void onMcpConnectionChange(bool connected);
- void processSerialCommands();
- void blinkLed(int times, int delayMs);
- void registerMcpTools();
- void updateMotor(); // 步进电机脉冲发送
- unsigned long mapSpeedToInterval(float speedFactor); // 速度因子转脉冲间隔
-
- // 步进电机工具回调函数
- WebSocketMCP::ToolResponse motorControlCallback(const String& args);
-
- void setup() {
- DEBUG_SERIAL.begin(DEBUG_BAUD_RATE);
- DEBUG_SERIAL.println("\n\n[ESP32 MCP客户端 + 步进电机] 初始化...");
-
- // 初始化步进电机引脚
- pinMode(PUL_PIN, OUTPUT);
- pinMode(DIR_PIN, OUTPUT);
- pinMode(ENA_PIN, OUTPUT);
- digitalWrite(ENA_PIN, HIGH); // 使能驱动器
-
- // 初始化LED
- pinMode(LED_PIN, OUTPUT);
- digitalWrite(LED_PIN, LOW);
-
- // 连接WiFi
- setupWifi();
-
- // 初始化MCP客户端
- if (mcpClient.begin(MCP_ENDPOINT, onMcpConnectionChange)) {
- DEBUG_SERIAL.println("[MCP] 初始化成功,尝试连接...");
- } else {
- DEBUG_SERIAL.println("[MCP] 初始化失败!");
- }
-
- DEBUG_SERIAL.println("\n使用说明:");
- DEBUG_SERIAL.println("- 串口输入命令并回车发送到MCP服务器");
- DEBUG_SERIAL.println("- 输入"help"查看可用命令");
- DEBUG_SERIAL.println("- 已集成步进电机控制工具:motor_control");
- DEBUG_SERIAL.println();
- }
-
- void loop() {
- // 处理MCP客户端(维持心跳、接收消息)
- mcpClient.loop();
-
- // 处理来自串口的命令
- processSerialCommands();
-
- // 步进电机非阻塞脉冲发送
- updateMotor();
-
- // 状态LED指示
- if (!wifiConnected) {
- blinkLed(1, 100); // WiFi未连接:快速闪烁
- } else if (!mcpConnected) {
- blinkLed(1, 500); // MCP未连接:慢闪
- } else {
- digitalWrite(LED_PIN, HIGH); // 全部连接:常亮
- }
- }
-
- /**
- * 设置WiFi连接
- */
- void setupWifi() {
- DEBUG_SERIAL.print("[WiFi] 连接到 ");
- DEBUG_SERIAL.println(WIFI_SSID);
-
- WiFi.mode(WIFI_STA);
- WiFi.begin(WIFI_SSID, WIFI_PASS);
-
- int attempts = 0;
- while (WiFi.status() != WL_CONNECTED && attempts < 20) {
- delay(500);
- DEBUG_SERIAL.print(".");
- attempts++;
- }
-
- if (WiFi.status() == WL_CONNECTED) {
- wifiConnected = true;
- DEBUG_SERIAL.println();
- DEBUG_SERIAL.println("[WiFi] 连接成功!");
- DEBUG_SERIAL.print("[WiFi] IP地址: ");
- DEBUG_SERIAL.println(WiFi.localIP());
- } else {
- wifiConnected = false;
- DEBUG_SERIAL.println();
- DEBUG_SERIAL.println("[WiFi] 连接失败!");
- }
- }
-
- /**
- * MCP输出回调(来自服务器的标准输出)
- */
- void onMcpOutput(const String &message) {
- DEBUG_SERIAL.print("[MCP输出] ");
- DEBUG_SERIAL.println(message);
- }
-
- /**
- * MCP错误回调(来自服务器的错误输出)
- */
- void onMcpError(const String &error) {
- DEBUG_SERIAL.print("[MCP错误] ");
- DEBUG_SERIAL.println(error);
- }
-
- /**
- * MCP连接状态变化回调
- */
- void onMcpConnectionChange(bool connected) {
- mcpConnected = connected;
- if (connected) {
- DEBUG_SERIAL.println("[MCP] 已连接到服务器");
- // 连接成功后注册所有工具
- registerMcpTools();
- } else {
- DEBUG_SERIAL.println("[MCP] 与服务器断开连接");
- }
- }
-
- /**
- * 注册MCP工具
- */
- void registerMcpTools() {
- DEBUG_SERIAL.println("[MCP] 注册工具...");
-
- // 1. LED控制工具(原有)
- mcpClient.registerTool(
- "led_blink",
- "控制ESP32 LED状态",
- "{"properties":{"state":{"title":"LED状态","type":"string","enum":["on","off","blink"]}},"required":["state"],"title":"ledControlArguments","type":"object"}",
- [](const String& args) -> WebSocketMCP::ToolResponse {
- DEBUG_SERIAL.println("[工具] LED控制: " + args);
- DynamicJsonDocument doc(256);
- DeserializationError error = deserializeJson(doc, args);
- if (error) {
- return WebSocketMCP::ToolResponse("{"success":false,"error":"无效参数格式"}", true);
- }
- String state = doc["state"].as<String>();
- if (state == "on") digitalWrite(LED_PIN, HIGH);
- else if (state == "off") digitalWrite(LED_PIN, LOW);
- else if (state == "blink") {
- for (int i=0; i<5; i++) {
- digitalWrite(LED_PIN, HIGH); delay(200);
- digitalWrite(LED_PIN, LOW); delay(200);
- }
- }
- String resultJson = "{"success":true,"state":"" + state + ""}";
- return WebSocketMCP::ToolResponse(resultJson);
- }
- );
-
- // 2. 系统信息工具(原有)
- mcpClient.registerTool(
- "system-info",
- "获取ESP32系统信息",
- "{"properties":{},"title":"systemInfoArguments","type":"object"}",
- [](const String& args) -> WebSocketMCP::ToolResponse {
- String chipModel = ESP.getChipModel();
- uint32_t chipId = ESP.getEfuseMac() & 0xFFFFFFFF;
- uint32_t flashSize = ESP.getFlashChipSize() / 1024;
- uint32_t freeHeap = ESP.getFreeHeap() / 1024;
- String resultJson = "{"success":true,"model":"" + chipModel + "","chipId":"" + String(chipId, HEX) +
- "","flashSize":" + String(flashSize) + ","freeHeap":" + String(freeHeap) +
- ","wifiStatus":"" + (WiFi.status() == WL_CONNECTED ? "connected" : "disconnected") +
- "","ipAddress":"" + WiFi.localIP().toString() + ""}";
- return WebSocketMCP::ToolResponse(resultJson);
- }
- );
-
- // 3. 计算器工具(原有)
- mcpClient.registerTool(
- "calculator",
- "简单计算器",
- "{"properties":{"expression":{"title":"表达式","type":"string"}},"required":["expression"],"title":"calculatorArguments","type":"object"}",
- [](const String& args) -> WebSocketMCP::ToolResponse {
- DynamicJsonDocument doc(256);
- deserializeJson(doc, args);
- String expr = doc["expression"].as<String>();
- int result = 0;
- if (expr.indexOf('+') > 0) {
- int plusPos = expr.indexOf('+');
- int a = expr.substring(0, plusPos).toInt();
- int b = expr.substring(plusPos+1).toInt();
- result = a + b;
- } else if (expr.indexOf('-') > 0) {
- int minusPos = expr.indexOf('-');
- int a = expr.substring(0, minusPos).toInt();
- int b = expr.substring(minusPos+1).toInt();
- result = a - b;
- }
- String resultJson = "{"success":true,"expression":"" + expr + "","result":" + String(result) + "}";
- return WebSocketMCP::ToolResponse(resultJson);
- }
- );
-
- // 4. 新增:步进电机控制工具
- mcpClient.registerTool(
- "motor_control",
- "控制步进电机(支持方向、步数、圈数、角度、速度)",
- "{"properties":{"direction":{"title":"方向","type":"string","enum":["CW","CCW"]},"mode":{"title":"控制模式","type":"string","enum":["steps","turns","angle","speed"]},"value":{"title":"数值","type":"number"}},"required":["direction","mode","value"],"title":"motorControlArguments","type":"object"}",
- motorControlCallback
- );
-
- DEBUG_SERIAL.println("[MCP] 工具注册完成,共 " + String(mcpClient.getToolCount()) + " 个工具");
- }
-
- /**
- * 步进电机控制工具的回调函数
- * 参数示例:{"direction":"CW","mode":"steps","value":200}
- * 支持模式:steps(步数)、turns(圈数)、angle(角度)、speed(速度因子 0~1)
- */
- WebSocketMCP::ToolResponse motorControlCallback(const String& args) {
- DEBUG_SERIAL.println("[工具] 电机控制: " + args);
- DynamicJsonDocument doc(256);
- DeserializationError error = deserializeJson(doc, args);
- if (error) {
- return WebSocketMCP::ToolResponse("{"success":false,"error":"无效的JSON参数"}", true);
- }
-
- // 解析参数
- String dir = doc["direction"].as<String>();
- String mode = doc["mode"].as<String>();
- float value = doc["value"].as<float>();
-
- bool dirCW = (dir == "CW");
-
- if (mode == "steps") {
- // 步数模式
- long steps = (long)value;
- if (steps <= 0) {
- return WebSocketMCP::ToolResponse("{"success":false,"error":"步数必须大于0"}", true);
- }
- targetSteps = dirCW ? steps : -steps;
- motorRunning = true;
- continuousMode = false; // 定位模式
- DEBUG_SERIAL.printf("[电机] 步数模式: %ld 步\n", steps);
- }
- else if (mode == "turns") {
- // 圈数模式
- if (value <= 0) {
- return WebSocketMCP::ToolResponse("{"success":false,"error":"圈数必须大于0"}", true);
- }
- long steps = (long)(value * STEPS_PER_OUTPUT_REV);
- targetSteps = dirCW ? steps : -steps;
- motorRunning = true;
- continuousMode = false;
- DEBUG_SERIAL.printf("[电机] 圈数模式: %.2f 圈 -> %ld 步\n", value, steps);
- }
- else if (mode == "angle") {
- // 角度模式
- if (value < 0 || value > 360) {
- return WebSocketMCP::ToolResponse("{"success":false,"error":"角度必须在0~360之间"}", true);
- }
- long steps = (long)((value / 360.0) * STEPS_PER_OUTPUT_REV);
- targetSteps = dirCW ? steps : -steps;
- motorRunning = true;
- continuousMode = false;
- DEBUG_SERIAL.printf("[电机] 角度模式: %.1f° -> %ld 步\n", value, steps);
- }
- else if (mode == "speed") {
- // 速度模式(连续运转)
- if (value < 0 || value > 1) {
- return WebSocketMCP::ToolResponse("{"success":false,"error":"速度因子必须在0~1之间"}", true);
- }
- if (value == 0) {
- // 停止
- targetSteps = 0;
- motorRunning = false;
- continuousMode = false;
- DEBUG_SERIAL.println("[电机] 速度0:停止");
- } else {
- // 设置方向
- digitalWrite(DIR_PIN, dirCW ? HIGH : LOW);
- // 根据速度因子调整脉冲间隔
- pulseInterval = mapSpeedToInterval(value);
- // 使用一个大数作为方向标记,连续模式不减少步数
- targetSteps = dirCW ? 1000000 : -1000000;
- motorRunning = true;
- continuousMode = true;
- DEBUG_SERIAL.printf("[电机] 速度模式: 方向=%s, 因子=%.2f, 间隔=%lu us\n",
- dirCW ? "CW" : "CCW", value, pulseInterval);
- }
- }
- else {
- return WebSocketMCP::ToolResponse("{"success":false,"error":"未知的模式"}", true);
- }
-
- // 返回成功响应
- String resultJson = "{"success":true,"mode":"" + mode + "","value":" + String(value) + "}";
- return WebSocketMCP::ToolResponse(resultJson);
- }
-
- /**
- * 速度因子映射到脉冲间隔(微秒)
- */
- unsigned long mapSpeedToInterval(float speedFactor) {
- if (speedFactor < 0.01) speedFactor = 0.01;
- if (speedFactor > 1.0) speedFactor = 1.0;
- const unsigned long minInterval = 100; // 最快 10kHz
- const unsigned long maxInterval = 2000; // 最慢 500Hz
- unsigned long interval = maxInterval - (unsigned long)((maxInterval - minInterval) * speedFactor);
- if (interval < minInterval) interval = minInterval;
- if (interval > maxInterval) interval = maxInterval;
- return interval;
- }
-
- /**
- * 非阻塞脉冲发送(需在 loop 中循环调用)
- */
- void updateMotor() {
- if (!motorRunning || targetSteps == 0) {
- return;
- }
-
- unsigned long now = micros();
- if (now - lastPulseTime >= pulseInterval) {
- lastPulseTime = now;
-
- // 设置方向引脚(基于 targetSteps 符号)
- if (targetSteps > 0) {
- digitalWrite(DIR_PIN, HIGH); // CW
- } else {
- digitalWrite(DIR_PIN, LOW); // CCW
- }
-
- // 产生脉冲(高电平50us)
- digitalWrite(PUL_PIN, HIGH);
- delayMicroseconds(50);
- digitalWrite(PUL_PIN, LOW);
-
- // 如果不是连续模式,则减少剩余步数
- if (!continuousMode) {
- if (targetSteps > 0) {
- targetSteps--;
- } else if (targetSteps < 0) {
- targetSteps++;
- }
- }
- }
- }
-
- /**
- * 处理串口命令(用于调试)
- */
- void processSerialCommands() {
- while (DEBUG_SERIAL.available() > 0) {
- char inChar = (char)DEBUG_SERIAL.read();
-
- if (inChar == '\n' || inChar == '\r') {
- if (inputBufferIndex > 0) {
- inputBuffer[inputBufferIndex] = '\0';
- String command = String(inputBuffer);
- command.trim();
-
- if (command.length() > 0) {
- if (command == "help") {
- DEBUG_SERIAL.println("可用命令: help, status, reconnect, tools, stop (停止电机)");
- } else if (command == "status") {
- DEBUG_SERIAL.print("WiFi: "); DEBUG_SERIAL.println(wifiConnected ? "已连接" : "未连接");
- DEBUG_SERIAL.print("MCP: "); DEBUG_SERIAL.println(mcpConnected ? "已连接" : "未连接");
- DEBUG_SERIAL.print("电机运行: "); DEBUG_SERIAL.println(motorRunning ? "是" : "否");
- DEBUG_SERIAL.print("剩余步数: "); DEBUG_SERIAL.println(targetSteps);
- } else if (command == "reconnect") {
- DEBUG_SERIAL.println("重新连接MCP...");
- mcpClient.disconnect();
- } else if (command == "tools") {
- DEBUG_SERIAL.println("已注册工具数量: " + String(mcpClient.getToolCount()));
- } else if (command == "stop") {
- targetSteps = 0;
- motorRunning = false;
- continuousMode = false;
- DEBUG_SERIAL.println("电机已停止");
- } else {
- // 将其他命令发送到MCP服务器
- if (mcpClient.isConnected()) {
- mcpClient.sendMessage(command);
- DEBUG_SERIAL.println("[发送] " + command);
- } else {
- DEBUG_SERIAL.println("未连接到MCP服务器");
- }
- }
- }
- inputBufferIndex = 0;
- }
- } else if (inChar == '\b' || inChar == 127) {
- if (inputBufferIndex > 0) {
- inputBufferIndex--;
- DEBUG_SERIAL.print("\b \b");
- }
- } else if (inputBufferIndex < MAX_INPUT_LENGTH - 1) {
- inputBuffer[inputBufferIndex++] = inChar;
- DEBUG_SERIAL.print(inChar);
- }
- }
- }
-
- /**
- * LED闪烁函数(用于状态指示)
- */
- void blinkLed(int times, int delayMs) {
- static int blinkCount = 0;
- static unsigned long lastBlinkTime = 0;
- static bool ledState = false;
- static int lastTimes = 0;
-
- if (times == 0) {
- digitalWrite(LED_PIN, LOW);
- blinkCount = 0;
- lastTimes = 0;
- return;
- }
- if (lastTimes != times) {
- blinkCount = 0;
- lastTimes = times;
- ledState = false;
- lastBlinkTime = millis();
- }
- unsigned long now = millis();
- if (blinkCount < times * 2) {
- if (now - lastBlinkTime > delayMs) {
- lastBlinkTime = now;
- ledState = !ledState;
- digitalWrite(LED_PIN, ledState ? HIGH : LOW);
- blinkCount++;
- }
- } else {
- digitalWrite(LED_PIN, LOW);
- blinkCount = 0;
- lastTimes = 0;
- }
- }
复制代码
成果: “小智,让电机转起来,正转3圈。”电机应声启动,正转3圈。
“小智,反转。”电机立刻反转。 电机不再只是一个冷冰冰的执行器,它变成了一个能与 AI 对话、能听懂人类指令的智能终端。从单纯的网页“遥控”,进化到了自然语言“交互”。 演示视频: 总结与展望 回顾这段历程,我的收获远不止让一个电机转起来: 硬件选型是关键:了解步距角、细分、驱动器能力,是保证精度的基础。 软件设计要分层:将底层硬件控制(ESP32 脉冲输出)与上层业务逻辑(网页控制、AI 指令解析)分离,让系统更健壮、易于扩展。 AI 赋予设备灵魂:通过 MCP 协议将物理设备接入 AI 生态,是未来智能硬件的发展方向。DF K10 和小智AI 的低门槛,让创客也能轻松拥抱这个未来。
下一步,我计划为这个系统加入传感器反馈(比如限位开关或编码器),并让 AI 能够读取传感器数据,实现更复杂的闭环控制逻辑。甚至可以让 AI 理解“从A点走到B点”这样更抽象的命令。 创客之路,就是不断折腾、不断迭代的过程。希望我的“三部曲”能给你带来一些灵感。如果你也有有趣的步进电机控制项目,欢迎在评论区交流!
|