9浏览
查看: 9|回复: 1

[项目] 从“点动”到“对话”:我的步进电机控制四部曲

[复制链接]
本帖最后由 云天 于 2026-2-20 16:44 编辑

**前言**

       在创客项目中,让东西“动起来”总是最迷人的一步。步进电机以其精准的定位能力,成为很多机械臂、3D打印机、云台的首选。但在实践中,从让它转,到让它精准地转,再到让它听懂指令转,这中间的路程充满探索。本文将分享我在步进电机控制上的三次迭代:从 **micro:bit 的初步尝试**,到 **ESP32 的网页遥控**,再到最终结合 **小智AI(DF K10)实现语音对话控制** 的完整过程与心得。

## 第一章:初体验 – Micro:bit 与扩展板的“温柔一颤”

       我的第一个想法很简单:用最熟悉的 Micro:bit 搭配电机扩展板来驱动一个42步进电机。
从“点动”到“对话”:我的步进电机控制四部曲图1

从“点动”到“对话”:我的步进电机控制四部曲图2


      **硬件连接:**       Micro:bit 插在扩展板上,扩展板M1+、M1-、M2+、M2-直接连接步进电机。软件上,使用 Mind+ 图形化编程,拖拽“电机转动...”积木块。
从“点动”到“对话”:我的步进电机控制四部曲图9

从“点动”到“对话”:我的步进电机控制四部曲图7

       **实际测试:**

       **结果与分析:**
       电机动了,但问题也很明显:
       1.  **转速“无法控制”**:电机启动时也会有明显的顿挫感,无法控制转速。
       2.  **位置“对不上”**:让电机转一圈(例如5.9*1圈),它总是多转或少转一点,精度无法满足要求。

       **问题出在哪?**
       问题原因已经明确:使用的Micro:bit电机驱动扩展板(DFR0548)的步进电机控制库,是通过延时来实现角度控制的,而不是精确的步进脉冲计数。这种方法精度较差,容易受电压、负载等因素影响,导致实际转动角度与预期存在偏差。
  1. void Microbit_Motor::stepperDegree42(Steppers index, Dir direction, int degree)
  2. {
  3.   ...
  4.   uint32_t Degree = abs(degree);
  5.   delay( (50000 * Degree) / (360 * 50) + 80);
  6.   ...
  7. }
复制代码

       它通过一个固定的延时公式来近似转动角度,没有考虑电机实际转速、细分设置或减速比。因此,当您通过减速比计算期望圈数时,库只是简单地延时相应的时间,但实际电机转动速度可能因电压、负载等因素而偏离预期,导致最终角度误差。
       **小结:**
       这次尝试让我明白,对于要求稍高的控制,Micro:bit 这类教育主板加基础扩展板的方案,更适合快速验证运动逻辑,而非追求精密控制。

插曲:用 Arduino UNO R3 快速验证 TB6600 驱动器
       在抛弃 Micro:bit 扩展板方案后,我首先用最熟悉的 Arduino UNO R3 搭配 TB6600 驱动器 进行了一次基础测试。这一步的目的很简单:确认驱动器、电机、电源和接线都正常,并亲身体验 细分 带来的平滑效果。
       硬件连接

Arduino UNO 引脚
TB6600 信号端
说明
D7PUL- (脉冲)发送脉冲信号
D6DIR- (方向)控制旋转方向
D5ENA- (使能)高电平有效
5VPUL+、DIR+、ENA+共阳极接法,所有正端接 UNO 5V
从“点动”到“对话”:我的步进电机控制四部曲图4



从“点动”到“对话”:我的步进电机控制四部曲图5

       注意: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 步),暂停一秒,再反转一圈,如此循环。

  1. // 定义引脚连接
  2. // 请根据您的实际接线修改引脚号
  3. #define PUL_PIN 7  // 脉冲信号引脚,连接到驱动器的PUL+
  4. #define DIR_PIN 6  // 方向信号引脚,连接到驱动器的DIR+
  5. #define ENA_PIN 5  // 使能信号引脚(可选),连接到驱动器的ENA+
  6. void setup() {
  7.   // 初始化引脚模式
  8.   pinMode(PUL_PIN, OUTPUT);
  9.   pinMode(DIR_PIN, OUTPUT);
  10.   pinMode(ENA_PIN, OUTPUT);
  11.   // 初始状态设置
  12.   digitalWrite(PUL_PIN, LOW);
  13.   digitalWrite(DIR_PIN, LOW); // 设置一个旋转方向,例如LOW为正转
  14.   digitalWrite(ENA_PIN, HIGH); // HIGH使能驱动器(具体请参考您的驱动器说明)
  15.   // 开启串口监视器,用于调试
  16.   Serial.begin(9600);
  17.   Serial.println("TB6600 8细分测试开始");
  18. }
  19. void loop() {
  20.   // 示例1:让电机连续旋转
  21.   // 速度由脉冲间隔决定,这里每转需要1600个脉冲(基于1.8°电机,8细分)
  22.   // 以下代码将使电机以一个较慢的速度连续旋转
  23.   for(int i = 0; i < 1600; i++) { // 发送一圈所需的脉冲数
  24.     digitalWrite(PUL_PIN, HIGH);
  25.     delayMicroseconds(500); // 高电平持续时间,配合下一个延时控制脉冲周期
  26.     digitalWrite(PUL_PIN, LOW);
  27.     delayMicroseconds(500); // 低电平持续时间
  28.     // 整个周期约1000微秒,脉冲频率为1kHz,对应转速约为 (1kHz / 1600脉冲/圈) * 60秒 = 37.5 RPM
  29.   }
  30.   // 延时一下,然后反转方向
  31.   delay(1000);
  32.   digitalWrite(DIR_PIN, HIGH); // 改变方向
  33.   // 再次发送一圈脉冲
  34.   for(int i = 0; i < 1600; i++) {
  35.     digitalWrite(PUL_PIN, HIGH);
  36.     delayMicroseconds(500);
  37.     digitalWrite(PUL_PIN, LOW);
  38.     delayMicroseconds(500);
  39.   }
  40.   delay(1000);
  41.   digitalWrite(DIR_PIN, LOW); // 恢复方向
  42.   // 或者,可以使用更精确的定时方法,如利用millis()或使用AccelStepper库
  43. }
复制代码

       测试演示视频


       测试流程与感受
  • 上传代码:将程序烧录到 Arduino UNO。
  • 上电观察:接通驱动器电源后,电机应立即按设定转动。如果方向反了,交换 DIR 的高低电平逻辑即可;如果不转,检查使能引脚电平。
  • 感受细分效果:与之前无细分的 Micro:bit 驱动相比,8 细分下的电机运转明显更加平滑,低频振动几乎消失,噪音也大大降低。用手轻捏电机轴,能感受到均匀的力矩,而非一顿一顿的冲击。

       为什么先做这个测试?
  • 排除硬件故障:确保电机、驱动器和电源都工作正常,为后续 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轴与模式切换**:保留了圈数、步数、角度等传统控制模式,方便进行精确的定位控制。
       **代码:**
  1. #include <WiFi.h>
  2. #include <ESPAsyncWebServer.h>
  3. // ========== WiFi 配置(请修改为你的热点信息) ==========
  4. const char* ssid = "sxs";
  5. const char* password = "#######";
  6. // ========== 步进电机引脚 ==========
  7. #define PUL_PIN  4
  8. #define DIR_PIN  17
  9. #define ENA_PIN  16
  10. // ========== 电机参数 ==========
  11. // 假设步进电机步距角1.8°(200步/转),驱动器细分拨码设为8细分
  12. const float STEPS_PER_OUTPUT_REV = 1600.0; // 200步/圈 * 8细分 = 1600
  13. // ========== 非阻塞电机控制变量 ==========
  14. volatile long targetSteps = 0;          // 剩余脉冲数(正数为CW,负数为CCW)
  15. volatile bool motorRunning = false;
  16. unsigned long lastPulseTime = 0;
  17. unsigned long pulseInterval = 100;       // 脉冲周期(微秒),初始默认100us(10kHz)
  18. bool continuousMode = false;              // 连续运转模式(用于摇杆速度控制)
  19. // ========== 创建异步服务器 ==========
  20. AsyncWebServer server(80);
  21. // ========== 内嵌 HTML 页面(摇杆界面,含CSS) ==========
  22. const char index_html[] PROGMEM = R"rawliteral(
  23. ……
  24. )rawliteral";
  25. // ========== 函数声明 ==========
  26. void handleControl(AsyncWebServerRequest *request);
  27. void updateMotor();
  28. unsigned long mapSpeedToInterval(float speedFactor);
  29. void setup() {
  30.   Serial.begin(115200);
  31.   pinMode(PUL_PIN, OUTPUT);
  32.   pinMode(DIR_PIN, OUTPUT);
  33.   pinMode(ENA_PIN, OUTPUT);
  34.   digitalWrite(ENA_PIN, HIGH); // 使能驱动器
  35.   WiFi.begin(ssid, password);
  36.   Serial.print("正在连接 WiFi");
  37.   while (WiFi.status() != WL_CONNECTED) {
  38.     delay(500);
  39.     Serial.print(".");
  40.   }
  41.   Serial.println("\nWiFi 连接成功");
  42.   Serial.print("IP 地址: ");
  43.   Serial.println(WiFi.localIP());
  44.   server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
  45.     request->send_P(200, "text/html", index_html);
  46.   });
  47.   server.on("/control", HTTP_GET, handleControl);
  48.   server.begin();
  49.   Serial.println("HTTP 服务器已启动");
  50. }
  51. void loop() {
  52.   updateMotor();
  53. }
  54. // ========== 处理 /control GET 请求 ==========
  55. void handleControl(AsyncWebServerRequest *request) {
  56.   String message = "OK";
  57.   // 1. 检查停止指令
  58.   if (request->hasParam("action")) {
  59.     String action = request->getParam("action")->value();
  60.     if (action == "stop") {
  61.       targetSteps = 0;
  62.       motorRunning = false;
  63.       continuousMode = false;
  64.       digitalWrite(ENA_PIN, LOW);
  65.       delay(10);
  66.       digitalWrite(ENA_PIN, HIGH);
  67.       request->send(200, "text/plain", "STOPPED");
  68.       return;
  69.     }
  70.   }
  71.   // 2. 解析方向
  72.   bool dirCW = true;
  73.   if (request->hasParam("dir")) {
  74.     String dir = request->getParam("dir")->value();
  75.     dirCW = (dir == "CW");
  76.   }
  77.   // 3. 解析模式和数值
  78.   if (request->hasParam("mode") && request->hasParam("value")) {
  79.     String mode = request->getParam("mode")->value();
  80.     float value = request->getParam("value")->value().toFloat();
  81.     Serial.println(mode);
  82.     Serial.println( request->getParam("value")->value());
  83.     // 摇杆模式(速度控制)
  84.     if (mode == "joystick") {
  85.         float speedFactor = value; // 此时 value 即为速度因子 (0~1)
  86.         if (speedFactor == 0) {
  87.             targetSteps = 0;
  88.             motorRunning = false;
  89.             continuousMode = false;
  90.             message = "Joystick stopped";
  91.         } else {
  92.             // 立即设置方向引脚
  93.             digitalWrite(DIR_PIN, dirCW ? HIGH : LOW);
  94.             // 根据速度因子更新脉冲间隔
  95.             pulseInterval = mapSpeedToInterval(speedFactor);
  96.             Serial.println(pulseInterval);
  97.             // 进入连续模式:用一个大数表示方向,实际步数不减少
  98.             targetSteps = dirCW ? 1000000 : -1000000;
  99.             motorRunning = true;
  100.             continuousMode = true;
  101.             message = "Joystick set: speed=" + String(speedFactor) + ", interval=" + String(pulseInterval) + "us";
  102.             Serial.println(message);
  103.         }
  104.     }
  105.     else {
  106.         // 原有的位置控制模式(圈数/步数/角度)
  107.         long steps = 0;
  108.         if (mode == "turn") {
  109.             steps = (long)(value * STEPS_PER_OUTPUT_REV);
  110.         } else if (mode == "steps") {
  111.             steps = (long)value;
  112.         } else if (mode == "angle") {
  113.             steps = (long)((value / 360.0) * STEPS_PER_OUTPUT_REV);
  114.         }
  115.         if (steps > 0) {
  116.             targetSteps = dirCW ? steps : -steps;
  117.             motorRunning = true;
  118.             continuousMode = false; // 退出连续模式
  119.             message = "Started: " + String(steps) + " pulses";
  120.         } else {
  121.             message = "Invalid value (must be >0)";
  122.         }
  123.     }
  124.   } else {
  125.     message = "Missing mode or value";
  126.   }
  127.   request->send(200, "text/plain", message);
  128. }
  129. // ========== 非阻塞脉冲发送 ==========
  130. void updateMotor() {
  131.   if (!motorRunning || targetSteps == 0) {
  132.     return;
  133.   }
  134.   
  135.   unsigned long now = micros();
  136.   if (now - lastPulseTime >= pulseInterval) {
  137.     lastPulseTime = now;
  138.     Serial.println(targetSteps);
  139.     // 根据目标步数的符号设置方向引脚(连续模式下符号仍然有效)
  140.     if (targetSteps > 0) {
  141.       digitalWrite(DIR_PIN, HIGH);
  142.     } else {
  143.       digitalWrite(DIR_PIN, LOW);
  144.     }
  145.     // 产生一个50us的高电平脉冲(确保驱动器可靠检测)
  146.     digitalWrite(PUL_PIN, HIGH);
  147.     delayMicroseconds(50);
  148.     digitalWrite(PUL_PIN, LOW);
  149.     // 如果是连续模式,不减少 targetSteps(保持方向标记)
  150.     if (!continuousMode) {
  151.       if (targetSteps > 0) {
  152.         targetSteps--;
  153.       } else if (targetSteps < 0) {
  154.         targetSteps++;
  155.       }
  156.     }
  157.   }
  158. }
  159. // ========== 速度因子映射到脉冲间隔 ==========
  160. unsigned long mapSpeedToInterval(float speedFactor) {
  161.     // 限制速度因子在有效范围内
  162.     if (speedFactor < 0.01) speedFactor = 0.01;
  163.     if (speedFactor > 1.0) speedFactor = 1.0;
  164.     // 定义速度范围:最大速度对应最小间隔 100us (10kHz),最小速度对应最大间隔 2000us (500Hz)
  165.     const unsigned long minInterval = 100;   // 最快
  166.     const unsigned long maxInterval = 2000;  // 最慢
  167.     // 线性映射:速度越快(speedFactor越大),间隔越小
  168.     unsigned long interval = maxInterval - (unsigned long)((maxInterval - minInterval) * speedFactor);
  169.     // 确保不越界
  170.     if (interval < minInterval) interval = minInterval;
  171.     if (interval > maxInterval) interval = maxInterval;
  172.     return interval;
  173. }
复制代码
      注:代码省略部分在留言附件中。
       效果:
       当我在手机浏览器上打开 ESP32 的 IP 地址,手指在摇杆上轻轻一推,电机就平滑地加速旋转,没有丝毫抖动。那种“指哪打哪”的精准感,是第一次尝试完全无法比拟的。
从“点动”到“对话”:我的步进电机控制四部曲图3

从“点动”到“对话”:我的步进电机控制四部曲图10
      演示视频:
       小结: 这次升级让我深刻体会到“专业的事交给专业的设备”。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 可以理解的“指令格式”:
    1. {
    2.   "direction": "CW",   // 方向
    3.   "mode": "steps",      // 模式 (steps, turns, angle, speed)
    4.   "value": 200          // 数值
    5. }
    复制代码

  • 工作流程:我说“转一圈” → DF K10 将语音转为文字,通过 AI 大模型理解意图 → 大模型通过 MCP 协议,向 ESP32 的 motor_control 工具发送一个包含参数 {"direction":"CW","mode":"turns","value":1} 的请求 → ESP32 收到请求,调用对应的回调函数,解析参数并驱动电机完成动作。
    代码:
  1. #include <Arduino.h>
  2. #include <WiFi.h>
  3. #include <WebSocketMCP.h>
  4. /********** 步进电机配置 ***********/
  5. // 引脚定义
  6. #define PUL_PIN  4
  7. #define DIR_PIN  17
  8. #define ENA_PIN  16
  9. // 电机参数(根据实际细分调整)
  10. const float STEPS_PER_OUTPUT_REV = 1600.0; // 200步/圈 * 8细分
  11. // 非阻塞电机控制变量
  12. volatile long targetSteps = 0;          // 剩余脉冲数(正数CW,负数CCW)
  13. volatile bool motorRunning = false;
  14. unsigned long lastPulseTime = 0;
  15. unsigned long pulseInterval = 100;       // 脉冲周期(微秒),用于速度控制
  16. bool continuousMode = false;              // 连续运转模式(用于摇杆/速度控制)
  17. /********** WiFi配置 ***********/
  18. const char* WIFI_SSID = "Xiaomi_10EE";
  19. const char* WIFI_PASS = "moto19tes84";
  20. /********** MCP服务器地址 ***********/
  21. const char* MCP_ENDPOINT = "wss://api.xiaozhi.me/mcp/?token=eyJhbGciOiJFUzI11NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEzODMzOCwiYWdlbnRJZCI6ODQyNDAsImVuZHBvaW50SWQiOiJhZ2VudF84NDI0MCIsInB1cnBvc2UiOiJtY3AtZW5kcG9pbnQiLCJpYXQiOjE3NTE3OTE4NDF9.U4-FWlCgqUUaKtq6gB6HwczogI9eUhZXIm7aaSsmND6vVbvB7TP0shu4XdSYHMUuwUcOZrJX2M6YlS3yJNsdeA";
  22. /********** 调试 ***********/
  23. #define DEBUG_SERIAL Serial
  24. #define DEBUG_BAUD_RATE 115200
  25. /********** LED引脚(板载)***********/
  26. #define LED_PIN 2
  27. /********** 全局变量 ***********/
  28. WebSocketMCP mcpClient;
  29. // 缓冲区管理(用于串口命令)
  30. #define MAX_INPUT_LENGTH 1024
  31. char inputBuffer[MAX_INPUT_LENGTH];
  32. int inputBufferIndex = 0;
  33. // 连接状态
  34. bool wifiConnected = false;
  35. bool mcpConnected = false;
  36. /********** 函数声明 ***********/
  37. void setupWifi();
  38. void onMcpOutput(const String &message);
  39. void onMcpError(const String &error);
  40. void onMcpConnectionChange(bool connected);
  41. void processSerialCommands();
  42. void blinkLed(int times, int delayMs);
  43. void registerMcpTools();
  44. void updateMotor();                     // 步进电机脉冲发送
  45. unsigned long mapSpeedToInterval(float speedFactor); // 速度因子转脉冲间隔
  46. // 步进电机工具回调函数
  47. WebSocketMCP::ToolResponse motorControlCallback(const String& args);
  48. void setup() {
  49.   DEBUG_SERIAL.begin(DEBUG_BAUD_RATE);
  50.   DEBUG_SERIAL.println("\n\n[ESP32 MCP客户端 + 步进电机] 初始化...");
  51.   // 初始化步进电机引脚
  52.   pinMode(PUL_PIN, OUTPUT);
  53.   pinMode(DIR_PIN, OUTPUT);
  54.   pinMode(ENA_PIN, OUTPUT);
  55.   digitalWrite(ENA_PIN, HIGH); // 使能驱动器
  56.   // 初始化LED
  57.   pinMode(LED_PIN, OUTPUT);
  58.   digitalWrite(LED_PIN, LOW);
  59.   // 连接WiFi
  60.   setupWifi();
  61.   // 初始化MCP客户端
  62.   if (mcpClient.begin(MCP_ENDPOINT, onMcpConnectionChange)) {
  63.     DEBUG_SERIAL.println("[MCP] 初始化成功,尝试连接...");
  64.   } else {
  65.     DEBUG_SERIAL.println("[MCP] 初始化失败!");
  66.   }
  67.   DEBUG_SERIAL.println("\n使用说明:");
  68.   DEBUG_SERIAL.println("- 串口输入命令并回车发送到MCP服务器");
  69.   DEBUG_SERIAL.println("- 输入"help"查看可用命令");
  70.   DEBUG_SERIAL.println("- 已集成步进电机控制工具:motor_control");
  71.   DEBUG_SERIAL.println();
  72. }
  73. void loop() {
  74.   // 处理MCP客户端(维持心跳、接收消息)
  75.   mcpClient.loop();
  76.   // 处理来自串口的命令
  77.   processSerialCommands();
  78.   // 步进电机非阻塞脉冲发送
  79.   updateMotor();
  80.   // 状态LED指示
  81.   if (!wifiConnected) {
  82.     blinkLed(1, 100);   // WiFi未连接:快速闪烁
  83.   } else if (!mcpConnected) {
  84.     blinkLed(1, 500);   // MCP未连接:慢闪
  85.   } else {
  86.     digitalWrite(LED_PIN, HIGH); // 全部连接:常亮
  87.   }
  88. }
  89. /**
  90. * 设置WiFi连接
  91. */
  92. void setupWifi() {
  93.   DEBUG_SERIAL.print("[WiFi] 连接到 ");
  94.   DEBUG_SERIAL.println(WIFI_SSID);
  95.   WiFi.mode(WIFI_STA);
  96.   WiFi.begin(WIFI_SSID, WIFI_PASS);
  97.   int attempts = 0;
  98.   while (WiFi.status() != WL_CONNECTED && attempts < 20) {
  99.     delay(500);
  100.     DEBUG_SERIAL.print(".");
  101.     attempts++;
  102.   }
  103.   if (WiFi.status() == WL_CONNECTED) {
  104.     wifiConnected = true;
  105.     DEBUG_SERIAL.println();
  106.     DEBUG_SERIAL.println("[WiFi] 连接成功!");
  107.     DEBUG_SERIAL.print("[WiFi] IP地址: ");
  108.     DEBUG_SERIAL.println(WiFi.localIP());
  109.   } else {
  110.     wifiConnected = false;
  111.     DEBUG_SERIAL.println();
  112.     DEBUG_SERIAL.println("[WiFi] 连接失败!");
  113.   }
  114. }
  115. /**
  116. * MCP输出回调(来自服务器的标准输出)
  117. */
  118. void onMcpOutput(const String &message) {
  119.   DEBUG_SERIAL.print("[MCP输出] ");
  120.   DEBUG_SERIAL.println(message);
  121. }
  122. /**
  123. * MCP错误回调(来自服务器的错误输出)
  124. */
  125. void onMcpError(const String &error) {
  126.   DEBUG_SERIAL.print("[MCP错误] ");
  127.   DEBUG_SERIAL.println(error);
  128. }
  129. /**
  130. * MCP连接状态变化回调
  131. */
  132. void onMcpConnectionChange(bool connected) {
  133.   mcpConnected = connected;
  134.   if (connected) {
  135.     DEBUG_SERIAL.println("[MCP] 已连接到服务器");
  136.     // 连接成功后注册所有工具
  137.     registerMcpTools();
  138.   } else {
  139.     DEBUG_SERIAL.println("[MCP] 与服务器断开连接");
  140.   }
  141. }
  142. /**
  143. * 注册MCP工具
  144. */
  145. void registerMcpTools() {
  146.   DEBUG_SERIAL.println("[MCP] 注册工具...");
  147.   // 1. LED控制工具(原有)
  148.   mcpClient.registerTool(
  149.     "led_blink",
  150.     "控制ESP32 LED状态",
  151.     "{"properties":{"state":{"title":"LED状态","type":"string","enum":["on","off","blink"]}},"required":["state"],"title":"ledControlArguments","type":"object"}",
  152.     [](const String& args) -> WebSocketMCP::ToolResponse {
  153.       DEBUG_SERIAL.println("[工具] LED控制: " + args);
  154.       DynamicJsonDocument doc(256);
  155.       DeserializationError error = deserializeJson(doc, args);
  156.       if (error) {
  157.         return WebSocketMCP::ToolResponse("{"success":false,"error":"无效参数格式"}", true);
  158.       }
  159.       String state = doc["state"].as<String>();
  160.       if (state == "on") digitalWrite(LED_PIN, HIGH);
  161.       else if (state == "off") digitalWrite(LED_PIN, LOW);
  162.       else if (state == "blink") {
  163.         for (int i=0; i<5; i++) {
  164.           digitalWrite(LED_PIN, HIGH); delay(200);
  165.           digitalWrite(LED_PIN, LOW); delay(200);
  166.         }
  167.       }
  168.       String resultJson = "{"success":true,"state":"" + state + ""}";
  169.       return WebSocketMCP::ToolResponse(resultJson);
  170.     }
  171.   );
  172.   // 2. 系统信息工具(原有)
  173.   mcpClient.registerTool(
  174.     "system-info",
  175.     "获取ESP32系统信息",
  176.     "{"properties":{},"title":"systemInfoArguments","type":"object"}",
  177.     [](const String& args) -> WebSocketMCP::ToolResponse {
  178.       String chipModel = ESP.getChipModel();
  179.       uint32_t chipId = ESP.getEfuseMac() & 0xFFFFFFFF;
  180.       uint32_t flashSize = ESP.getFlashChipSize() / 1024;
  181.       uint32_t freeHeap = ESP.getFreeHeap() / 1024;
  182.       String resultJson = "{"success":true,"model":"" + chipModel + "","chipId":"" + String(chipId, HEX) +
  183.                          "","flashSize":" + String(flashSize) + ","freeHeap":" + String(freeHeap) +
  184.                          ","wifiStatus":"" + (WiFi.status() == WL_CONNECTED ? "connected" : "disconnected") +
  185.                          "","ipAddress":"" + WiFi.localIP().toString() + ""}";
  186.       return WebSocketMCP::ToolResponse(resultJson);
  187.     }
  188.   );
  189.   // 3. 计算器工具(原有)
  190.   mcpClient.registerTool(
  191.     "calculator",
  192.     "简单计算器",
  193.     "{"properties":{"expression":{"title":"表达式","type":"string"}},"required":["expression"],"title":"calculatorArguments","type":"object"}",
  194.     [](const String& args) -> WebSocketMCP::ToolResponse {
  195.       DynamicJsonDocument doc(256);
  196.       deserializeJson(doc, args);
  197.       String expr = doc["expression"].as<String>();
  198.       int result = 0;
  199.       if (expr.indexOf('+') > 0) {
  200.         int plusPos = expr.indexOf('+');
  201.         int a = expr.substring(0, plusPos).toInt();
  202.         int b = expr.substring(plusPos+1).toInt();
  203.         result = a + b;
  204.       } else if (expr.indexOf('-') > 0) {
  205.         int minusPos = expr.indexOf('-');
  206.         int a = expr.substring(0, minusPos).toInt();
  207.         int b = expr.substring(minusPos+1).toInt();
  208.         result = a - b;
  209.       }
  210.       String resultJson = "{"success":true,"expression":"" + expr + "","result":" + String(result) + "}";
  211.       return WebSocketMCP::ToolResponse(resultJson);
  212.     }
  213.   );
  214.   // 4. 新增:步进电机控制工具
  215.   mcpClient.registerTool(
  216.     "motor_control",
  217.     "控制步进电机(支持方向、步数、圈数、角度、速度)",
  218.     "{"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"}",
  219.     motorControlCallback
  220.   );
  221.   DEBUG_SERIAL.println("[MCP] 工具注册完成,共 " + String(mcpClient.getToolCount()) + " 个工具");
  222. }
  223. /**
  224. * 步进电机控制工具的回调函数
  225. * 参数示例:{"direction":"CW","mode":"steps","value":200}
  226. * 支持模式:steps(步数)、turns(圈数)、angle(角度)、speed(速度因子 0~1)
  227. */
  228. WebSocketMCP::ToolResponse motorControlCallback(const String& args) {
  229.   DEBUG_SERIAL.println("[工具] 电机控制: " + args);
  230.   DynamicJsonDocument doc(256);
  231.   DeserializationError error = deserializeJson(doc, args);
  232.   if (error) {
  233.     return WebSocketMCP::ToolResponse("{"success":false,"error":"无效的JSON参数"}", true);
  234.   }
  235.   // 解析参数
  236.   String dir = doc["direction"].as<String>();
  237.   String mode = doc["mode"].as<String>();
  238.   float value = doc["value"].as<float>();
  239.   bool dirCW = (dir == "CW");
  240.   if (mode == "steps") {
  241.     // 步数模式
  242.     long steps = (long)value;
  243.     if (steps <= 0) {
  244.       return WebSocketMCP::ToolResponse("{"success":false,"error":"步数必须大于0"}", true);
  245.     }
  246.     targetSteps = dirCW ? steps : -steps;
  247.     motorRunning = true;
  248.     continuousMode = false; // 定位模式
  249.     DEBUG_SERIAL.printf("[电机] 步数模式: %ld 步\n", steps);
  250.   }
  251.   else if (mode == "turns") {
  252.     // 圈数模式
  253.     if (value <= 0) {
  254.       return WebSocketMCP::ToolResponse("{"success":false,"error":"圈数必须大于0"}", true);
  255.     }
  256.     long steps = (long)(value * STEPS_PER_OUTPUT_REV);
  257.     targetSteps = dirCW ? steps : -steps;
  258.     motorRunning = true;
  259.     continuousMode = false;
  260.     DEBUG_SERIAL.printf("[电机] 圈数模式: %.2f 圈 -> %ld 步\n", value, steps);
  261.   }
  262.   else if (mode == "angle") {
  263.     // 角度模式
  264.     if (value < 0 || value > 360) {
  265.       return WebSocketMCP::ToolResponse("{"success":false,"error":"角度必须在0~360之间"}", true);
  266.     }
  267.     long steps = (long)((value / 360.0) * STEPS_PER_OUTPUT_REV);
  268.     targetSteps = dirCW ? steps : -steps;
  269.     motorRunning = true;
  270.     continuousMode = false;
  271.     DEBUG_SERIAL.printf("[电机] 角度模式: %.1f° -> %ld 步\n", value, steps);
  272.   }
  273.   else if (mode == "speed") {
  274.     // 速度模式(连续运转)
  275.     if (value < 0 || value > 1) {
  276.       return WebSocketMCP::ToolResponse("{"success":false,"error":"速度因子必须在0~1之间"}", true);
  277.     }
  278.     if (value == 0) {
  279.       // 停止
  280.       targetSteps = 0;
  281.       motorRunning = false;
  282.       continuousMode = false;
  283.       DEBUG_SERIAL.println("[电机] 速度0:停止");
  284.     } else {
  285.       // 设置方向
  286.       digitalWrite(DIR_PIN, dirCW ? HIGH : LOW);
  287.       // 根据速度因子调整脉冲间隔
  288.       pulseInterval = mapSpeedToInterval(value);
  289.       // 使用一个大数作为方向标记,连续模式不减少步数
  290.       targetSteps = dirCW ? 1000000 : -1000000;
  291.       motorRunning = true;
  292.       continuousMode = true;
  293.       DEBUG_SERIAL.printf("[电机] 速度模式: 方向=%s, 因子=%.2f, 间隔=%lu us\n",
  294.                           dirCW ? "CW" : "CCW", value, pulseInterval);
  295.     }
  296.   }
  297.   else {
  298.     return WebSocketMCP::ToolResponse("{"success":false,"error":"未知的模式"}", true);
  299.   }
  300.   // 返回成功响应
  301.   String resultJson = "{"success":true,"mode":"" + mode + "","value":" + String(value) + "}";
  302.   return WebSocketMCP::ToolResponse(resultJson);
  303. }
  304. /**
  305. * 速度因子映射到脉冲间隔(微秒)
  306. */
  307. unsigned long mapSpeedToInterval(float speedFactor) {
  308.   if (speedFactor < 0.01) speedFactor = 0.01;
  309.   if (speedFactor > 1.0) speedFactor = 1.0;
  310.   const unsigned long minInterval = 100;   // 最快 10kHz
  311.   const unsigned long maxInterval = 2000;  // 最慢 500Hz
  312.   unsigned long interval = maxInterval - (unsigned long)((maxInterval - minInterval) * speedFactor);
  313.   if (interval < minInterval) interval = minInterval;
  314.   if (interval > maxInterval) interval = maxInterval;
  315.   return interval;
  316. }
  317. /**
  318. * 非阻塞脉冲发送(需在 loop 中循环调用)
  319. */
  320. void updateMotor() {
  321.   if (!motorRunning || targetSteps == 0) {
  322.     return;
  323.   }
  324.   unsigned long now = micros();
  325.   if (now - lastPulseTime >= pulseInterval) {
  326.     lastPulseTime = now;
  327.     // 设置方向引脚(基于 targetSteps 符号)
  328.     if (targetSteps > 0) {
  329.       digitalWrite(DIR_PIN, HIGH);  // CW
  330.     } else {
  331.       digitalWrite(DIR_PIN, LOW);   // CCW
  332.     }
  333.     // 产生脉冲(高电平50us)
  334.     digitalWrite(PUL_PIN, HIGH);
  335.     delayMicroseconds(50);
  336.     digitalWrite(PUL_PIN, LOW);
  337.     // 如果不是连续模式,则减少剩余步数
  338.     if (!continuousMode) {
  339.       if (targetSteps > 0) {
  340.         targetSteps--;
  341.       } else if (targetSteps < 0) {
  342.         targetSteps++;
  343.       }
  344.     }
  345.   }
  346. }
  347. /**
  348. * 处理串口命令(用于调试)
  349. */
  350. void processSerialCommands() {
  351.   while (DEBUG_SERIAL.available() > 0) {
  352.     char inChar = (char)DEBUG_SERIAL.read();
  353.     if (inChar == '\n' || inChar == '\r') {
  354.       if (inputBufferIndex > 0) {
  355.         inputBuffer[inputBufferIndex] = '\0';
  356.         String command = String(inputBuffer);
  357.         command.trim();
  358.         if (command.length() > 0) {
  359.           if (command == "help") {
  360.             DEBUG_SERIAL.println("可用命令: help, status, reconnect, tools, stop (停止电机)");
  361.           } else if (command == "status") {
  362.             DEBUG_SERIAL.print("WiFi: "); DEBUG_SERIAL.println(wifiConnected ? "已连接" : "未连接");
  363.             DEBUG_SERIAL.print("MCP: "); DEBUG_SERIAL.println(mcpConnected ? "已连接" : "未连接");
  364.             DEBUG_SERIAL.print("电机运行: "); DEBUG_SERIAL.println(motorRunning ? "是" : "否");
  365.             DEBUG_SERIAL.print("剩余步数: "); DEBUG_SERIAL.println(targetSteps);
  366.           } else if (command == "reconnect") {
  367.             DEBUG_SERIAL.println("重新连接MCP...");
  368.             mcpClient.disconnect();
  369.           } else if (command == "tools") {
  370.             DEBUG_SERIAL.println("已注册工具数量: " + String(mcpClient.getToolCount()));
  371.           } else if (command == "stop") {
  372.             targetSteps = 0;
  373.             motorRunning = false;
  374.             continuousMode = false;
  375.             DEBUG_SERIAL.println("电机已停止");
  376.           } else {
  377.             // 将其他命令发送到MCP服务器
  378.             if (mcpClient.isConnected()) {
  379.               mcpClient.sendMessage(command);
  380.               DEBUG_SERIAL.println("[发送] " + command);
  381.             } else {
  382.               DEBUG_SERIAL.println("未连接到MCP服务器");
  383.             }
  384.           }
  385.         }
  386.         inputBufferIndex = 0;
  387.       }
  388.     } else if (inChar == '\b' || inChar == 127) {
  389.       if (inputBufferIndex > 0) {
  390.         inputBufferIndex--;
  391.         DEBUG_SERIAL.print("\b \b");
  392.       }
  393.     } else if (inputBufferIndex < MAX_INPUT_LENGTH - 1) {
  394.       inputBuffer[inputBufferIndex++] = inChar;
  395.       DEBUG_SERIAL.print(inChar);
  396.     }
  397.   }
  398. }
  399. /**
  400. * LED闪烁函数(用于状态指示)
  401. */
  402. void blinkLed(int times, int delayMs) {
  403.   static int blinkCount = 0;
  404.   static unsigned long lastBlinkTime = 0;
  405.   static bool ledState = false;
  406.   static int lastTimes = 0;
  407.   if (times == 0) {
  408.     digitalWrite(LED_PIN, LOW);
  409.     blinkCount = 0;
  410.     lastTimes = 0;
  411.     return;
  412.   }
  413.   if (lastTimes != times) {
  414.     blinkCount = 0;
  415.     lastTimes = times;
  416.     ledState = false;
  417.     lastBlinkTime = millis();
  418.   }
  419.   unsigned long now = millis();
  420.   if (blinkCount < times * 2) {
  421.     if (now - lastBlinkTime > delayMs) {
  422.       lastBlinkTime = now;
  423.       ledState = !ledState;
  424.       digitalWrite(LED_PIN, ledState ? HIGH : LOW);
  425.       blinkCount++;
  426.     }
  427.   } else {
  428.     digitalWrite(LED_PIN, LOW);
  429.     blinkCount = 0;
  430.     lastTimes = 0;
  431.   }
  432. }
复制代码


       成果:
       “小智,让电机转起来,正转3圈。”电机应声启动,正转3圈。
       “小智,反转。”电机立刻反转。
       电机不再只是一个冷冰冰的执行器,它变成了一个能与 AI 对话、能听懂人类指令的智能终端。从单纯的网页“遥控”,进化到了自然语言“交互”。
从“点动”到“对话”:我的步进电机控制四部曲图6
       演示视频:
总结与展望
       回顾这段历程,我的收获远不止让一个电机转起来:
  • 硬件选型是关键:了解步距角、细分、驱动器能力,是保证精度的基础。
  • 软件设计要分层:将底层硬件控制(ESP32 脉冲输出)与上层业务逻辑(网页控制、AI 指令解析)分离,让系统更健壮、易于扩展。
  • AI 赋予设备灵魂:通过 MCP 协议将物理设备接入 AI 生态,是未来智能硬件的发展方向。DF K10 和小智AI 的低门槛,让创客也能轻松拥抱这个未来。

从“点动”到“对话”:我的步进电机控制四部曲图8

       下一步,我计划为这个系统加入传感器反馈(比如限位开关或编码器),并让 AI 能够读取传感器数据,实现更复杂的闭环控制逻辑。甚至可以让 AI 理解“从A点走到B点”这样更抽象的命令。
       创客之路,就是不断折腾、不断迭代的过程。希望我的“三部曲”能给你带来一些灵感。如果你也有有趣的步进电机控制项目,欢迎在评论区交流!


云天  中级技神
 楼主|

发表于 4 小时前

省略的网页代码部分(原文加不进去)

本帖最后由 云天 于 2026-2-20 16:29 编辑

省略的网页代码部分(原文加不进去)下载附件html.zip




回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4 备案 沪公网安备31011502402448

© 2013-2026 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail