8.14
【任务目标】
前面有两个帖子,记录了用ESP32 S3开发板控制舵机和双电机的学习过程。其实目标是将两者整合起来实现仰望小车的控制。
硬件整合之后的仰望小车,电线布满了身体。
难看无所谓了,母不嫌子丑。

为了保证供电,电源不冲突,开发板、舵机、电机都是单独供电,线是真乱啊。

编程环境配置:
材料清单
- K10小智AI X1
- ESP32 S3-DevKitC-1开发板(N16R8) X1
- L298N电机驱动模块 X1
- 乐高电机 X2
- 乐高舵机 X1
- 仰望小车底盘 X1
- 锂电池组合 X1
步骤1 电路连接
基本电路是在上一帖子的电路上增加舵机接在IO5。
下图是ESP32 S3+小车双电机+L298N+舵机接线:
在IO5引脚增加舵机,舵机也是单独供电。



步骤2 代码编写
在原来双电机小车代码基础上修改,增加了舵机控制。
经过一番测试修改,得到如下代码。
实现预想的全部功能:
核心功能架构
硬件驱动层
电机控制:通过L298N库驱动双直流电机(ENA/IN1/IN2控制左轮,ENB/IN3/IN4控制右轮),支持前进、后退、转向和停止。
舵机控制:使用ESP32Servo库驱动舵机(SERVO_PIN),实现转向角度调节(0°-180°),转向时自动调整舵机角度(左转60°、右转120°、直行90°)。
网络通信层
WiFi连接:通过WiFi库连接指定AP(SSID: CMCC-QuYY),为WebSocket通信提供网络基础。
WebSocket协议:通过WebSocketMCP库与远程服务器(api.xiaozhi.me)建立长连接,实现低延迟双向指令传输。
控制逻辑层
状态机模型:定义CarState枚举(STOP/FORWARD/BACKWARD/LEFT/RIGHT),管理小车运动状态。
多模式执行:
持续模式:默认状态,需显式发送stop指令终止动作。
限时模式:通过duration参数设定动作持续时间(毫秒),超时后自动停止。
核心控制指令
car_control运动控制指令
功能:控制小车基础运动(前进、后退、转向、停止)。
指令格式(JSON):
{
"action": "forward/backward/left/right/stop",
"duration": 整数(单位:ms,0表示持续执行),
"speed": 整数(0-255,可选)
}
参数说明:
action:动作类型,必填。
forward:前进(舵机归中90°)
backward:后退(舵机归中90°)
left:左转(舵机转向60°)
right:右转(舵机转向120°)
stop:停止(清除超时标志)
duration:动作持续时间(毫秒),默认0(持续执行直至新指令)。
speed:临时速度值(0-255),缺省时使用当前全局速度值。
执行逻辑:
舵机角度随转向动作自动调整,直行/后退时归中。
若设置duration>0,启动超时计时器,到期自动停止。
set_speed速度调节指令
功能:动态调整电机PWM占空比(全局速度)。
指令格式(JSON):
{ "speed": 整数(0-255) }
参数说明:
speed:必填,范围0(停止)至255(全速)。
执行效果:
立即更新全局速度变量currentSpeed。
若小车正在运动(非STOP状态),实时调整电机转速。
上面视频中并没有展示出速度调节功能,运动中也可以调速并且马上执行。
#include <WiFi.h>
#include <WebSocketMCP.h>
#include <L298N.h>
#include <ArduinoJson.h>
#include <ESP32Servo.h>
// 电机引脚定义
#define ENA 14
#define IN1 12
#define IN2 13
#define ENB 17
#define IN3 15
#define IN4 16
#define SERVO_PIN 5
// 硬件对象初始化
L298N leftMotor(ENA, IN1, IN2);
L298N rightMotor(ENB, IN3, IN4);
Servo myServo;
// 网络配置
const char* ssid = "******";
const char* password = "******";
const char* mcpEndpoint = "ws://api.xiaozhi.me/mcp/?token=******";
WebSocketMCP mcpClient;
// 控制状态机
enum CarState { STOP, FORWARD, BACKWARD, LEFT, RIGHT };
CarState currentState = STOP;
int currentSpeed = 150; // 默认速度(0-255)
// 超时控制核心变量
unsigned long actionStartTime = 0; // 当前动作开始时间
int actionDuration = 0; // 用户设定持续时间(0=持续执行)
unsigned long lastCmdTime = 0; // 最后有效指令时间戳
// 函数前置声明
void registerMcpTools();
void controlCar(CarState state, int speed = -1);
void setServoAngle(int angle);
// 连接状态回调
void onConnectionStatus(bool connected) {
if (connected) {
Serial.println("[MCP] 已连接到服务器");
registerMcpTools();
} else {
Serial.println("[MCP] 与服务器断开连接");
}
}
// 舵机控制(带变化检测)
void setServoAngle(int angle) {
static int lastAngle = -1;
angle = constrain(angle, 0, 180);
if (angle != lastAngle) {
myServo.write(angle);
Serial.printf("舵机: %d°\n", angle);
lastAngle = angle;
}
}
// 核心控制函数
void controlCar(CarState state, int speed) {
if (speed >= 0) currentSpeed = speed;
// 舵机转向逻辑
switch (state) {
case FORWARD:
case BACKWARD:
case STOP: setServoAngle(90); break; // 直行方向
case LEFT: setServoAngle(60); break; // 左转角度
case RIGHT: setServoAngle(120); break; // 右转角度
}
// 电机驱动逻辑
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:
case RIGHT:
leftMotor.setSpeed(currentSpeed);
rightMotor.setSpeed(currentSpeed);
leftMotor.forward();
rightMotor.forward();
break;
case STOP:
leftMotor.stop();
rightMotor.stop();
actionDuration = 0; // 清除超时标志
break;
}
// 状态更新与调试输出
currentState = state;
const char* stateNames[] = {"停止", "前进", "后退", "左转", "右转"};
Serial.printf("动作: %s | 速度: %d | 模式: %s\n",
stateNames[state],
currentSpeed,
actionDuration > 0 ? "限时模式" : "持续模式");
}
// 注册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,0表示持续执行)\"},"
"\"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; // 默认0=持续执行
int speed = doc["speed"] | -1;
// 指令转换
CarState targetState = STOP;
if (action == "forward") targetState = FORWARD;
else if (action == "backward") targetState = BACKWARD;
else if (action == "left") targetState = LEFT;
else if (action == "right") targetState = RIGHT;
// 超时参数记录
if (duration > 0) {
actionStartTime = millis();
actionDuration = duration;
} else {
actionDuration = 0; // 持续执行模式
}
// 执行动作
controlCar(targetState, speed);
// 关键修复:收到指令时刷新安全计时器
lastCmdTime = millis();
return WebSocketMCP::ToolResponse("{\"status\":\"executing\",\"action\":\"" + action + "\"}");
}
);
// 速度调节工具
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);
}
// 刷新安全计时器
lastCmdTime = millis();
return WebSocketMCP::ToolResponse("{\"speed\":" + String(currentSpeed) + "}");
}
);
}
void setup() {
Serial.begin(115200);
// 硬件初始化序列
myServo.setPeriodHertz(50);
myServo.attach(SERVO_PIN, 500, 2500);
setServoAngle(90); // 舵机归中
delay(500); // 确保舵机稳定
// 电机初始状态
controlCar(STOP);
// WiFi连接
Serial.printf("连接WiFi: %s\n", ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.printf("\nWiFi已连接! IP: %s\n", WiFi.localIP().toString());
// MCP客户端启动
mcpClient.begin(mcpEndpoint, onConnectionStatus);
lastCmdTime = millis(); // 初始化安全计时器
}
void loop() {
mcpClient.loop();
// 模式1:限时动作检测
if (actionDuration > 0 && currentState != STOP) {
if (millis() - actionStartTime >= actionDuration) {
controlCar(STOP);
Serial.println("[系统] 动作时间到达");
actionDuration = 0;
}
}
// 安全保护:30秒无新指令停止
if (millis() - lastCmdTime > 30000 && currentState != STOP) {
controlCar(STOP);
Serial.println("[安全] 30秒无指令自动停车");
}
delay(10);
}
上传代码,测试通过。
小智后台:

【小结】
1、整体效果很满意,达到全部预设功能。
2、基础代码自己改写,瓶颈问题由DeepSeek协助优化。
3、实践是检验的唯一标准,反复修改与调试,看似枯燥无味却又乐趣无穷。
评论