8.12
【目标任务】
小智语音控制ESP32 S3,想控制双电机,做小车,控制舵机,然后可以控制我的仰望小车了。
在这个帖子中,记录的是双电机小车的探索。所用的电机驱动模块是经典的红色电机驱动模块L298N。

小车做成样子:

编程环境配置教程:
材料清单
- K10小智AI X1
- L298N电机驱动模块 X1
- TT电机 X2
- 车架及车轮 X1
- ESP32 S3-DevKitC-1开发板(N16R8) X1
步骤1 L298N的基本参数和两轮小车电机接线
L298N参数:
参数一般只关心如下两个:
逻辑电压(控制端口电压):5V
驱动电压(电机电压):5V-35V (我计划用7V锂电供电,在这个范围中)
驱动电流(电机电流):2A(MAX单桥)
意味着这个板子适合用5V引脚的主控板驱动(例如arudino系列),而3.3V引脚的主控板可能就不能用了(例如mcirobit、掌控板、esp32)。这个板子驱动的一般都是小电流的电机,例如TT电机(小黄马达)、N20电机这一类的。
接下来看L298N的控制方法:

引脚说明:
输出A、输出B:接两个电机,不分正负,反了只会反转。(换线头可以调方向)
GND:电源地,需要与主控板的GND用一根杜邦线连接到一起(共地)。(这一点很重要的)
12V供电:给电机供电的接口,此处要独立供电。
5V供电:此为电压输出口,可以使用这个口给主控板供5V电源,但不推荐这么用,所以就让它空着好了。
板载5V使能:此跳线帽接起来之后上面的“5V供电”口才有5V电压输出。
通道A、通道B使能(ENA、ENB):此接口高电平则电机可以运行,低电平则电机停止运行,一般直接跳线帽短接到高电平,即一直使能,可以拔掉跳线帽接到主控板pwm模拟输入端口实现调速(我要调速,就要拔掉跳帽了)。
逻辑输入(IN1、IN2、IN3、IN4):这两个接口控制电机正反转,IN1和IN2控制电机M1,IN3和IN4控制电机M2.
具体控制信号如下图(重点图片):简单理解(亮工神总结):
电源接线:
从12V和GND供6-12V的电压。
L298N和主控板的GND使用一根线连接起来。
板载5V使能跳线帽不动。
电机接线:
两个电机分别接到M1和M2上。
信号控制:
ENA和ENB跳线帽拔掉,使用杜邦线分别接到主控板两个PWM模拟输出口。
IN1和IN2,IN3和IN4分别接到主控板数字口。
一图说明:
以Arduino uno为例
我用L298N_红色直流电机驱动模块和ESP32 S3的接线方式:

L298N电源

ESP32单独供电
提示:记得L298N和主控板的GND使用一根杜邦线连接起来
电机1、2分别接电机接口 不分正负,换线头可以调方向
7V锂电正极 接 12V接口 负极接GND口
ESP32 S3用3.7v锂电供电
步骤2 L298N库安装和电机转动测试
工具-库管理:安装L298N库(轻量级基础控制)
特点:专为L298N设计,接口简单(如DCMotor motor1(L298N::A, L298N::B)),支持速度(setSpeed())和方向控制。
适用场景:基础正反转、调速需求,代码简洁易上手。
优点:
无需复杂配置,直接兼容ESP32的PWM引脚。
资源占用低,适合实时性要求高的小车项目。
缺点:功能较基础,不支持高级运动控制(如闭环调速)。
安装:通过Arduino库管理器搜索安装。

先用示例测试一个电机能否运行。
#include <L298N.h>
// 定义电机引脚(以ESP32 S3为例)
#define ENA 14 // PWM使能引脚
#define IN1 12 // 方向引脚1
#define IN2 13 // 方向引脚2
L298N motor(ENA, IN1, IN2); // 初始化电机对象
void setup() {
motor.setSpeed(150); // 初始速度(0-255)
}
void loop() {
motor.forward(); // 前进
delay(1000);
motor.backward(); // 后退
delay(1000);
}
通过了,再测试双电机能否运行。
#include <L298N.h>
// 定义电机引脚(以ESP32 S3为例)
#define ENA 14
#define IN1 12
#define IN2 13
#define ENB 17
#define IN3 15
#define IN4 16
// 初始化电机对象
L298N leftMotor(ENA, IN1, IN2);
L298N rightMotor(ENB, IN3, IN4);
void setup() {
leftMotor.setSpeed(150); // 初始速度(0-255)
rightMotor.setSpeed(150);
}
void loop() {
leftMotor.forward(); // 前进
rightMotor.forward();
delay(1000);
leftMotor.backward(); // 后退
rightMotor.backward();
delay(1000);
}
步骤3 小车控制基本版
整合基础示例和L298N基本功能,得到小车控制基础版代码,简单控制小车的前进、后退、左转、右转和停止,并且可以调节速度。
#include <WiFi.h>
#include <WebSocketMCP.h>
#include <L298N.h>
#include <ArduinoJson.h>
// 硬件配置(根据实际接线修改)
#define ENA 14 // 左侧电机PWM
#define IN1 12 // 左侧电机方向1
#define IN2 13 // 左侧电机方向2
#define ENB 17 // 右侧电机PWM
#define IN3 15 // 右侧电机方向1
#define IN4 16 // 右侧电机方向2
// WiFi配置
const char* ssid = "******";
const char* password = "******";
const char* mcpEndpoint = "ws://api.xiaozhi.me/mcp/?token=*******";
WebSocketMCP mcpClient;
// 电机对象初始化
L298N leftMotor(ENA, IN1, IN2);
L298N rightMotor(ENB, IN3, IN4);
// 全局变量
int currentSpeed = 150; // 默认速度(0-255)
// 方向枚举
enum Direction { STOP, FORWARD, BACKWARD, LEFT, RIGHT };
// 电机控制函数(简化版)
void controlMotors(Direction dir) {
switch(dir) {
case FORWARD:
leftMotor.forward(); // 左轮正转
rightMotor.forward(); // 右轮正转
break;
case BACKWARD:
leftMotor.backward(); // 左轮反转
rightMotor.backward();// 右轮反转
break;
case LEFT:
leftMotor.backward(); // 左轮反转 → 向右转弯
rightMotor.forward(); // 右轮正转
break;
case RIGHT:
leftMotor.forward(); // 左轮正转
rightMotor.backward();// 右轮反转 → 向左转弯
break;
default: // STOP
leftMotor.stop();
rightMotor.stop();
}
// 应用当前速度
leftMotor.setSpeed(currentSpeed);
rightMotor.setSpeed(currentSpeed);
}
// 连接状态回调
void onConnectionStatus(bool connected) {
Serial.println(connected ? "[MCP] 已连接" : "[MCP] 断开");
if (connected) registerMcpTools();
}
// 注册MCP工具(语义化参数)
void registerMcpTools() {
// 运动控制工具
mcpClient.registerTool(
"car_move",
"控制小车运动",
"{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"enum\":[\"FORWARD\",\"BACKWARD\",\"LEFT\",\"RIGHT\",\"STOP\"]}}}",
[](const String& args) {
DynamicJsonDocument doc(256);
deserializeJson(doc, args);
String action = doc["action"].as<String>();
if (action == "FORWARD") controlMotors(FORWARD);
else if (action == "BACKWARD") controlMotors(BACKWARD);
else if (action == "LEFT") controlMotors(LEFT);
else if (action == "RIGHT") controlMotors(RIGHT);
else controlMotors(STOP);
return WebSocketMCP::ToolResponse("{\"status\":\"OK\",\"action\":\"" + action + "\"}");
}
);
// 速度调节工具
mcpClient.registerTool(
"car_speed",
"调节电机速度",
"{\"type\":\"object\",\"properties\":{\"speed\":{\"type\":\"integer\",\"minimum\":0,\"maximum\":255}}}",
[](const String& args) {
DynamicJsonDocument doc(256);
deserializeJson(doc, args);
currentSpeed = doc["speed"];
// 立即应用新速度
leftMotor.setSpeed(currentSpeed);
rightMotor.setSpeed(currentSpeed);
return WebSocketMCP::ToolResponse("{\"status\":\"OK\",\"speed\":" + String(currentSpeed) + "}");
}
);
Serial.println("[MCP] 控制工具已注册");
}
void setup() {
Serial.begin(115200);
// 电机安全初始化
leftMotor.stop();
rightMotor.stop();
leftMotor.setSpeed(currentSpeed);
rightMotor.setSpeed(currentSpeed);
// 连接WiFi
Serial.printf("连接WiFi: %s\n", ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.printf("\nWiFi已连接\nIP地址: %s\n", WiFi.localIP().toString());
// 启动MCP客户端
mcpClient.begin(mcpEndpoint, onConnectionStatus);
}
void loop() {
mcpClient.loop(); // 处理网络通信
}
步骤4 控制进阶,运动时间可控
功能说明
运动控制:
支持五种指令:前进(forward)、后退(backward)、左转(left)、右转(right)、停止(stop)
可指定持续时间(如前进2秒)
转向采用差速控制:左转时右轮前进/左轮后退,右转时左轮前进/右轮后退
速度调节:
独立的速度控制工具(set_speed)
速度范围0-255(PWM占空比)
实时生效,不影响当前运动状态
控制协议:
基于JSON的WebSocket通信
支持同步响应和状态反馈
超时自动断开连接处理
使用示例
前进2秒:
{ "action": "forward", "duration": 2000 }
左转1秒:
{ "action": "left", "duration": 1000 }
停止小车:
{ "action": "stop" }
调节速度:
{ "speed": 200 }
注意事项
实际接线需根据电机转向调整IN1-IN4的相位
转向灵敏度可通过调整controlCar()中的差速逻辑优化
若需要更精确的转向控制,可扩展为独立设置左右电机速度
确保L298N的使能引脚(ENA/ENB)已连接至ESP32的PWM引脚
#include <WiFi.h>
#include <WebSocketMCP.h>
#include <L298N.h>
#include <ArduinoJson.h>
// 电机引脚定义 (ESP32 S3)
#define ENA 14
#define IN1 12
#define IN2 13
#define ENB 17
#define IN3 15
#define IN4 16
// 初始化电机对象
L298N leftMotor(ENA, IN1, IN2);
L298N rightMotor(ENB, IN3, IN4);
// WiFi配置
const char* ssid = "******";
const char* password = "******";
const char* mcpEndpoint = "ws://api.xiaozhi.me/mcp/?token=******";
// 创建WebSocketMCP实例
WebSocketMCP mcpClient;
// 小车控制状态
enum CarState { STOP, FORWARD, BACKWARD, LEFT, RIGHT };
CarState currentState = STOP;
int currentSpeed = 150; // 默认速度(0-255)
// 连接状态回调
void onConnectionStatus(bool connected) {
if (connected) {
Serial.println("[MCP] 已连接到服务器");
registerMcpTools();
} else {
Serial.println("[MCP] 与服务器断开连接");
}
}
// 控制小车动作
void controlCar(CarState state, int speed = -1) {
if (speed >= 0) currentSpeed = speed;
switch (state) {
case FORWARD:
leftMotor.setSpeed(currentSpeed);
rightMotor.setSpeed(currentSpeed);
leftMotor.forward();
rightMotor.forward();
break;
case BACKWARD:
leftMotor.setSpeed(currentSpeed);
rightMotor.setSpeed(currentSpeed);
leftMotor.backward();
rightMotor.backward();
break;
case LEFT:
leftMotor.setSpeed(currentSpeed);
rightMotor.setSpeed(currentSpeed);
leftMotor.backward(); // 差速转向
rightMotor.forward();
break;
case RIGHT:
leftMotor.setSpeed(currentSpeed);
rightMotor.setSpeed(currentSpeed);
leftMotor.forward();
rightMotor.backward(); // 差速转向
break;
case STOP:
leftMotor.stop();
rightMotor.stop();
break;
}
currentState = state;
}
// 注册MCP控制工具
void registerMcpTools() {
// 运动控制工具
mcpClient.registerTool(
"car_control",
"控制小车运动(forward/backward/left/right/stop)",
"{\"type\":\"object\",\"properties\":{"
"\"action\":{\"type\":\"string\",\"enum\":[\"forward\",\"backward\",\"left\",\"right\",\"stop\"]},"
"\"duration\":{\"type\":\"integer\",\"description\":\"持续时间(ms)\"},"
"\"speed\":{\"type\":\"integer\",\"minimum\":0,\"maximum\":255}}"
",\"required\":[\"action\"]}",
[](const String& args) {
DynamicJsonDocument doc(256);
deserializeJson(doc, args);
String action = doc["action"].as<String>();
int duration = doc["duration"] | 0; // 可选参数
int speed = doc["speed"] | -1; // 可选参数
// 执行动作
if (action == "forward") controlCar(FORWARD, speed);
else if (action == "backward") controlCar(BACKWARD, speed);
else if (action == "left") controlCar(LEFT, speed);
else if (action == "right") controlCar(RIGHT, speed);
else if (action == "stop") controlCar(STOP);
// 延时执行(非阻塞)
if (duration > 0) {
unsigned long start = millis();
while (millis() - start < duration) {
mcpClient.loop();
delay(10);
}
controlCar(STOP);
}
return WebSocketMCP::ToolResponse("{\"success\":true,\"action\":\"" + action + "\",\"speed\":" + String(currentSpeed) + "}");
}
);
// 速度调节工具
mcpClient.registerTool(
"set_speed",
"设置小车速度(0-255)",
"{\"type\":\"object\",\"properties\":{\"speed\":{\"type\":\"integer\",\"minimum\":0,\"maximum\":255}},\"required\":[\"speed\"]}",
[](const String& args) {
DynamicJsonDocument doc(128);
deserializeJson(doc, args);
currentSpeed = doc["speed"];
// 更新当前运动状态的速度
if (currentState != STOP) {
controlCar(currentState);
}
return WebSocketMCP::ToolResponse("{\"success\":true,\"speed\":" + String(currentSpeed) + "}");
}
);
}
void setup() {
Serial.begin(115200);
// 初始停止电机
controlCar(STOP);
// 连接WiFi
Serial.print("连接到WiFi: ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi已连接");
Serial.println("IP地址: " + WiFi.localIP().toString());
// 初始化MCP客户端
mcpClient.begin(mcpEndpoint, onConnectionStatus);
}
void loop() {
mcpClient.loop();
delay(10);
}
上传,全部功能测试通过。

【提示】
这个帖子中的代码有一个bug,它可以用带速度和时间的指令,但是如果只是前进、后退、左转、右转,它只会执行两秒。所以,后面还会有优化,另一个帖子见。
评论