1、组装好小车
1)L198N连接两个轮子,然后用胶带粘到车架上的,比较丑陋,先组装起来
2)供电:我是用充电宝给ESP32-S3供电;用电池仓给L298N供电
3)L298N接线:
左轮:图中黑色胶带对应的那边
左轮:OUT3 接左电机左侧金属片 和OUT4 接左电机右侧金属片,控制线 接IN3 和IN4 分别插入G5和G6引脚
右轮:OUT1 接右电机左侧金属片 和OUT2 接右电机右侧金属片,控制线 接IN1 和IN2 分别插入G17和G18引脚
板子GND和电池仓负极接L298N的接地(图中灰褐色线与电池仓黑色负极)
接好后的小车如下:

2、写程序控制小车
第一步:语音转文字逻辑
已经在第四篇中搞定,代码可以直接复制ESP32-S3-CAM:豆包语音识别文字后控制小车(四)------增加mic拾音后通过豆包语音识别后转文字输出
第二步:增加前进后退控制逻辑代码
ESP32-S3-CAM:豆包语音识别文字后控制小车(五)------认识L298N驱动模块
我们在第五篇的时候,已经用kimi写了一个电机的控制逻辑,再让豆包实现两个电机的控制逻辑
前进,后退的逻辑。
2.1 先实现两个电机前进,后退,左转,右转
接着第五篇的kimi对话,继续给提示词:
现在我接了两个电机,左电机:控制线 接IN3 和IN4 分别插入G5和G6引脚
右电机:控制线 接IN1 和IN2 分别插入G17和G18引脚,
每次输入命令后,前进,后退,运行3秒,左转,右转 只运行 1秒
更新下代码
然后就可以实现 :"命令: 1=前进(3秒) | 0=后退(3秒) | 2=左转(1秒) | 3=右转(1秒) | s=立即停止"
代码如下:
cpp
// ESP32-S3-CAM + L298N 双电机控制(定时自动停止)
// 左电机: IN3->GPIO5, IN4->GPIO6
// 右电机: IN1->GPIO17, IN2->GPIO18
#define LEFT_IN3 5 // 左电机 IN3 -> GPIO5
#define LEFT_IN4 6 // 左电机 IN4 -> GPIO6
#define RIGHT_IN1 17 // 右电机 IN1 -> GPIO17
#define RIGHT_IN2 18 // 右电机 IN2 -> GPIO18
unsigned long stopTime = 0; // 记录停止时间点
bool isRunning = false; // 电机运行状态
void setup() {
Serial.begin(115200);
pinMode(LEFT_IN3, OUTPUT);
pinMode(LEFT_IN4, OUTPUT);
pinMode(RIGHT_IN1, OUTPUT);
pinMode(RIGHT_IN2, OUTPUT);
stopAll();
Serial.println("ESP32-S3 双电机控制已启动");
Serial.println("命令: 1=前进(3秒) | 0=后退(3秒) | 2=左转(1秒) | 3=右转(1秒) | s=立即停止");
}
void loop() {
// 检查是否需要自动停止
if (isRunning && millis() >= stopTime) {
stopAll();
isRunning = false;
Serial.println("■ 自动停止");
}
// 处理串口命令
if (Serial.available() > 0) {
char cmd = Serial.read();
if (cmd == '\n' || cmd == '\r') return;
switch (cmd) {
case '1':
forward();
setTimer(3000); // 3秒后停止
Serial.println("↑ 前进 3秒");
break;
case '0':
backward();
setTimer(3000); // 3秒后停止
Serial.println("↓ 后退 3秒");
break;
case '2':
turnLeft();
setTimer(1000); // 1秒后停止
Serial.println("↺ 左转 1秒");
break;
case '3':
turnRight();
setTimer(1000); // 1秒后停止
Serial.println("↻ 右转 1秒");
break;
case 's':
case 'S':
stopAll();
isRunning = false;
Serial.println("■ 立即停止");
break;
default:
Serial.println("未知命令");
Serial.println("1=前进(3秒) | 0=后退(3秒) | 2=左转(1秒) | 3=右转(1秒) | s=立即停止");
}
}
}
// 设置定时器
void setTimer(unsigned long duration) {
stopTime = millis() + duration;
isRunning = true;
}
// 前进: 双电机正转
void forward() {
digitalWrite(LEFT_IN3, HIGH); digitalWrite(LEFT_IN4, LOW);
digitalWrite(RIGHT_IN1, HIGH); digitalWrite(RIGHT_IN2, LOW);
}
// 后退: 双电机反转
void backward() {
digitalWrite(LEFT_IN3, LOW); digitalWrite(LEFT_IN4, HIGH);
digitalWrite(RIGHT_IN1, LOW); digitalWrite(RIGHT_IN2, HIGH);
}
// 左转: 左退右进
void turnLeft() {
digitalWrite(LEFT_IN3, LOW); digitalWrite(LEFT_IN4, HIGH);
digitalWrite(RIGHT_IN1, HIGH); digitalWrite(RIGHT_IN2, LOW);
}
// 右转: 左进右退
void turnRight() {
digitalWrite(LEFT_IN3, HIGH); digitalWrite(LEFT_IN4, LOW);
digitalWrite(RIGHT_IN1, LOW); digitalWrite(RIGHT_IN2, HIGH);
}
// 停止
void stopAll() {
digitalWrite(LEFT_IN3, LOW); digitalWrite(LEFT_IN4, LOW);
digitalWrite(RIGHT_IN1, LOW); digitalWrite(RIGHT_IN2, LOW);
}
测试一下:代码编译上传到板子后,按一下复位按钮,板子上那个RST那个黑色按钮
bash
ESP32-S3 双电机控制已启动
命令: 1=前进(3秒) | 0=后退(3秒) | 2=左转(1秒) | 3=右转(1秒) | s=立即停止
↑ 前进 3秒
■ 自动停止
↺ 左转 1秒
■ 自动停止
↻ 右转 1秒
■ 自动停止
↓ 后退 3秒
■ 自动停止
2.2 实现按boot按钮后自动旋转
提示词:现在修改下代码,不用从终端输入,改成 按boot 按键后 自动前进3秒,左转2秒,前进3秒,右转2秒,前进3秒,然后后退3秒
这样就不用一直插着USB了,用充电宝供电后,按一下 板子上的boot按键,就可以自动跑了。
这一步,主要是为了给后续第三步,按boot后开始自动录音转文字功能做准备。
cpp
// ESP32-S3-CAM + L298N 自动序列控制(Boot键触发)
// 左电机: IN3->GPIO5, IN4->GPIO6
// 右电机: IN1->GPIO17, IN2->GPIO18
// Boot键: GPIO0(ESP32-S3-CAM开发板上的Boot按钮)
#define LEFT_IN3 5 // 左电机 IN3 -> GPIO5
#define LEFT_IN4 6 // 左电机 IN4 -> GPIO6
#define RIGHT_IN1 17 // 右电机 IN1 -> GPIO17
#define RIGHT_IN2 18 // 右电机 IN2 -> GPIO18
#define BOOT_BUTTON 0 // Boot按键接GPIO0
// 状态定义
enum State {
IDLE, // 待机,等待按键
FORWARD_1, // 第一次前进3秒
TURN_LEFT, // 左转2秒
FORWARD_2, // 第二次前进3秒
TURN_RIGHT, // 右转2秒
FORWARD_3, // 第三次前进3秒
BACKWARD // 后退3秒
};
State currentState = IDLE;
unsigned long stateStartTime = 0;
bool sequenceRunning = false;
void setup() {
Serial.begin(115200);
pinMode(LEFT_IN3, OUTPUT);
pinMode(LEFT_IN4, OUTPUT);
pinMode(RIGHT_IN1, OUTPUT);
pinMode(RIGHT_IN2, OUTPUT);
pinMode(BOOT_BUTTON, INPUT_PULLUP); // Boot键,内部上拉
stopAll();
Serial.println("ESP32-S3 自动序列控制已启动");
Serial.println("按Boot键开始执行序列");
Serial.println("序列: 前进3秒→左转2秒→前进3秒→右转2秒→前进3秒→后退3秒");
}
void loop() {
// 检测Boot按键(低电平触发,带消抖)
if (currentState == IDLE && digitalRead(BOOT_BUTTON) == LOW) {
delay(50); // 消抖
if (digitalRead(BOOT_BUTTON) == LOW) {
startSequence();
// 等待按键释放
while (digitalRead(BOOT_BUTTON) == LOW);
delay(50);
}
}
// 执行状态机
if (sequenceRunning) {
runStateMachine();
}
}
// 启动序列
void startSequence() {
sequenceRunning = true;
currentState = FORWARD_1;
stateStartTime = millis();
forward();
Serial.println("▶ 序列开始: 前进3秒");
}
// 状态机控制
void runStateMachine() {
unsigned long elapsed = millis() - stateStartTime;
switch (currentState) {
case FORWARD_1:
if (elapsed >= 3000) { // 3秒到,转左转
currentState = TURN_LEFT;
stateStartTime = millis();
turnLeft();
Serial.println("↺ 左转2秒");
}
break;
case TURN_LEFT:
if (elapsed >= 2000) { // 2秒到,转前进
currentState = FORWARD_2;
stateStartTime = millis();
forward();
Serial.println("↑ 前进3秒");
}
break;
case FORWARD_2:
if (elapsed >= 3000) { // 3秒到,转右转
currentState = TURN_RIGHT;
stateStartTime = millis();
turnRight();
Serial.println("↻ 右转2秒");
}
break;
case TURN_RIGHT:
if (elapsed >= 2000) { // 2秒到,转前进
currentState = FORWARD_3;
stateStartTime = millis();
forward();
Serial.println("↑ 前进3秒");
}
break;
case FORWARD_3:
if (elapsed >= 3000) { // 3秒到,转后退
currentState = BACKWARD;
stateStartTime = millis();
backward();
Serial.println("↓ 后退3秒");
}
break;
case BACKWARD:
if (elapsed >= 3000) { // 3秒到,序列结束
stopAll();
sequenceRunning = false;
currentState = IDLE;
Serial.println("■ 序列完成,等待再次按键");
}
break;
default:
break;
}
}
// 电机控制函数
void forward() {
digitalWrite(LEFT_IN3, HIGH); digitalWrite(LEFT_IN4, LOW);
digitalWrite(RIGHT_IN1, HIGH); digitalWrite(RIGHT_IN2, LOW);
}
void backward() {
digitalWrite(LEFT_IN3, LOW); digitalWrite(LEFT_IN4, HIGH);
digitalWrite(RIGHT_IN1, LOW); digitalWrite(RIGHT_IN2, HIGH);
}
void turnLeft() {
digitalWrite(LEFT_IN3, LOW); digitalWrite(LEFT_IN4, HIGH);
digitalWrite(RIGHT_IN1, HIGH); digitalWrite(RIGHT_IN2, LOW);
}
void turnRight() {
digitalWrite(LEFT_IN3, HIGH); digitalWrite(LEFT_IN4, LOW);
digitalWrite(RIGHT_IN1, LOW); digitalWrite(RIGHT_IN2, HIGH);
}
void stopAll() {
digitalWrite(LEFT_IN3, LOW); digitalWrite(LEFT_IN4, LOW);
digitalWrite(RIGHT_IN1, LOW); digitalWrite(RIGHT_IN2, LOW);
}
2.3 实现语音转文字后控制电机的功能
用之前语音转文字的代码和前面电机控制的代码,让kimi综合写了一份控制代码
本来也是计划让他可以通过按boot按键后就录音转文字然后执行动作的,但始终没调通
最后放弃了,还是先确保输入audio命令后,每次都能够正常运行,最终调试通的代码如下
调试过程中发现,网络不稳定会严重导致websocket连接速度变慢,程序容易出错,尽量先确保网络稳定,后续可以优化代码逻辑。
cpp
/*
* ESP32-S3 豆包ASR语音识别 - 双电机语音控制小车版(定时版)
* 支持: 1) SD卡文件识别 2) 麦克风实时识别 3) 语音控制小车
*
* 硬件: GOOUUU ESP32-S3-CAM v1.3
* SD卡: CLK=39, CMD=38, D0=40
* 麦克风(I2S): WS=GPIO1, SCK=GPIO2, SD=GPIO42
* 左电机(L298N): IN3=GPIO5, IN4=GPIO6
* 右电机(L298N): IN1=GPIO17, IN2=GPIO18
*/
#include <WiFi.h>
#include <WebSocketsClient.h>
#include <SD_MMC.h>
#include <ArduinoJson.h>
#include <driver/i2s.h>
// ============== 配置区域 输入你的信息==============
const char* ssid = "";
const char* password = "";
// 豆包ASR配置
const char* appid = "";
const char* token = "";
const char* cluster = "volcengine_input_common";
const char* ws_host = "openspeech.bytedance.com";
const int ws_port = 443;
const char* ws_path = "/api/v2/asr";
// SD卡引脚
#define SD_CLK 39
#define SD_CMD 38
#define SD_D0 40
// 麦克风I2S引脚
#define I2S_WS 1
#define I2S_SCK 2
#define I2S_SD 42
#define I2S_PORT I2S_NUM_0
// ===== 双电机控制引脚定义 =====
#define LEFT_IN3 5 // 左电机IN3
#define LEFT_IN4 6 // 左电机IN4
#define RIGHT_IN1 17 // 右电机IN1
#define RIGHT_IN2 18 // 右电机IN2
// ===== 动作持续时间配置(毫秒) =====
#define DURATION_FORWARD 3000 // 前进3秒
#define DURATION_BACKWARD 3000 // 后退3秒
#define DURATION_TURN 2000 // 转向2秒
// 音频配置
const int audio_rate = 16000;
const int audio_bits = 16;
const int audio_channel = 2;
// 分片大小
const size_t CHUNK_SIZE = 64000;
// 协议常量
#define PROTOCOL_VERSION 0x01
#define DEFAULT_HEADER_SIZE 0x01
#define CLIENT_FULL_REQUEST 0x01
#define CLIENT_AUDIO_ONLY_REQUEST 0x02
#define SERVER_FULL_RESPONSE 0x09
#define SERVER_ACK 0x0B
#define SERVER_ERROR_RESPONSE 0x0F
#define NO_SEQUENCE 0x00
#define NEG_SEQUENCE 0x02
#define JSON_SERIALIZATION 0x01
#define NO_COMPRESSION 0x00
// ============== 全局变量 ==============
WebSocketsClient webSocket;
bool ws_connected = false;
bool asr_completed = false;
bool asr_running = false;
bool result_displayed = false;
String recognition_result = "";
// SD卡文件模式
File audio_file;
size_t audio_total_size = 0;
size_t audio_sent_size = 0;
String target_filename = "";
// 麦克风模式
bool mic_mode = false;
int16_t* mic_buffer = nullptr;
const int MIC_BUFFER_SAMPLES = 1024;
unsigned long mic_start_time = 0;
const unsigned long MIC_RECORD_DURATION = 5000;
// 小车控制状态
enum CarCommand {
CMD_NONE,
CMD_FORWARD,
CMD_BACKWARD,
CMD_LEFT,
CMD_RIGHT,
CMD_STOP
};
CarCommand last_command = CMD_NONE;
// ===== 新增:定时控制变量 =====
bool car_is_moving = false; // 小车是否正在执行动作
unsigned long car_action_start = 0; // 动作开始时间
unsigned long car_action_duration = 0;// 动作持续时间
// ============== 函数声明 ==============
bool initSD();
void listSDFiles();
void showPrompt();
void processCommand(String cmd);
bool loadAndRecognizeFile(String filename);
bool startMicRecognition();
void initWebSocket();
void webSocketEvent(WStype_t type, uint8_t * payload, size_t length);
void sendFullClientRequest(bool from_mic);
void sendNextChunk();
void sendMicAudio();
void sendLastMicChunk();
void parseResponse(uint8_t* data, size_t len);
void generateHeader(uint8_t* header, uint8_t msg_type, uint8_t flags);
bool initI2SMic();
void stopI2SMic();
// ===== 双电机控制函数声明 =====
void initMotor();
void leftMotorForward();
void leftMotorBackward();
void leftMotorStop();
void rightMotorForward();
void rightMotorBackward();
void rightMotorStop();
void carForward();
void carBackward();
void carTurnLeft();
void carTurnRight();
void carStop();
CarCommand parseVoiceCommand(String text);
void executeCarCommand(CarCommand cmd);
void checkCarActionTimeout(); // 新增:检查动作是否超时
// ============== 协议头生成 ==============
void generateHeader(uint8_t* header, uint8_t msg_type, uint8_t flags) {
header[0] = (PROTOCOL_VERSION << 4) | DEFAULT_HEADER_SIZE;
header[1] = (msg_type << 4) | flags;
header[2] = (JSON_SERIALIZATION << 4) | NO_COMPRESSION;
header[3] = 0x00;
}
// ===== 双电机控制实现 =====
void initMotor() {
pinMode(LEFT_IN3, OUTPUT);
pinMode(LEFT_IN4, OUTPUT);
pinMode(RIGHT_IN1, OUTPUT);
pinMode(RIGHT_IN2, OUTPUT);
carStop();
Serial.println("[MOTOR] 双电机控制初始化完成 (左轮:5/6, 右轮:17/18)");
}
// 左电机控制
void leftMotorForward() {
digitalWrite(LEFT_IN3, HIGH);
digitalWrite(LEFT_IN4, LOW);
}
void leftMotorBackward() {
digitalWrite(LEFT_IN3, LOW);
digitalWrite(LEFT_IN4, HIGH);
}
void leftMotorStop() {
digitalWrite(LEFT_IN3, LOW);
digitalWrite(LEFT_IN4, LOW);
}
// 右电机控制
void rightMotorForward() {
digitalWrite(RIGHT_IN1, HIGH);
digitalWrite(RIGHT_IN2, LOW);
}
void rightMotorBackward() {
digitalWrite(RIGHT_IN1, LOW);
digitalWrite(RIGHT_IN2, HIGH);
}
void rightMotorStop() {
digitalWrite(RIGHT_IN1, LOW);
digitalWrite(RIGHT_IN2, LOW);
}
// ===== 修改:带定时的动作控制 =====
void carForward() {
leftMotorForward();
rightMotorForward();
car_is_moving = true;
car_action_start = millis();
car_action_duration = DURATION_FORWARD;
Serial.printf("🚗 [小车] 前进 ⬆️ (持续%d秒)\n", DURATION_FORWARD/1000);
}
void carBackward() {
leftMotorBackward();
rightMotorBackward();
car_is_moving = true;
car_action_start = millis();
car_action_duration = DURATION_BACKWARD;
Serial.printf("🚗 [小车] 后退 ⬇️ (持续%d秒)\n", DURATION_BACKWARD/1000);
}
void carTurnLeft() {
leftMotorStop();
rightMotorForward();
car_is_moving = true;
car_action_start = millis();
car_action_duration = DURATION_TURN;
Serial.printf("🚗 [小车] 左转 ↪️ (持续%d秒)\n", DURATION_TURN/1000);
}
void carTurnRight() {
leftMotorForward();
rightMotorStop();
car_is_moving = true;
car_action_start = millis();
car_action_duration = DURATION_TURN;
Serial.printf("🚗 [小车] 右转 ↩️ (持续%d秒)\n", DURATION_TURN/1000);
}
void carStop() {
leftMotorStop();
rightMotorStop();
car_is_moving = false;
car_action_duration = 0;
Serial.println("🚗 [小车] 停止 ⏹️");
}
// 检查动作是否超时(在loop中调用)
void checkCarActionTimeout() {
if (car_is_moving && (millis() - car_action_start >= car_action_duration)) {
Serial.println("[小车] 动作时间到,自动停止");
carStop();
}
}
// 解析语音命令
CarCommand parseVoiceCommand(String text) {
text.toLowerCase();
if (text.indexOf("前进") >= 0 || text.indexOf("向前") >= 0 ||
text.indexOf("往前") >= 0 || text.indexOf("走") >= 0 ||
text.indexOf("直走") >= 0) {
return CMD_FORWARD;
}
if (text.indexOf("后退") >= 0 || text.indexOf("向后") >= 0 ||
text.indexOf("往后") >= 0 || text.indexOf("退") >= 0) {
return CMD_BACKWARD;
}
if (text.indexOf("左转") >= 0 || text.indexOf("向左") >= 0 ||
text.indexOf("往左") >= 0 || text.indexOf("左") >= 0) {
return CMD_LEFT;
}
if (text.indexOf("右转") >= 0 || text.indexOf("向右") >= 0 ||
text.indexOf("往右") >= 0 || text.indexOf("右") >= 0) {
return CMD_RIGHT;
}
if (text.indexOf("停止") >= 0 || text.indexOf("停") >= 0 ||
text.indexOf("别动") >= 0 || text.indexOf("站住") >= 0 ||
text.indexOf("站") >= 0) {
return CMD_STOP;
}
return CMD_NONE;
}
// 执行小车命令
void executeCarCommand(CarCommand cmd) {
// 如果正在执行动作,先停止
if (car_is_moving) {
carStop();
delay(100); // 短暂停顿
}
switch(cmd) {
case CMD_FORWARD: carForward(); break;
case CMD_BACKWARD: carBackward(); break;
case CMD_LEFT: carTurnLeft(); break;
case CMD_RIGHT: carTurnRight(); break;
case CMD_STOP: carStop(); break;
default: break;
}
}
// ============== 设置 ==============
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("\n========================================");
Serial.println("ESP32-S3 豆包ASR语音识别(双电机定时版)");
Serial.println("========================================");
Serial.println("硬件配置:");
Serial.println(" 左电机: GPIO5(IN3) / GPIO6(IN4)");
Serial.println(" 右电机: GPIO17(IN1) / GPIO18(IN2)");
Serial.println("----------------------------------------");
Serial.println("动作时长:");
Serial.println(" 前进/后退: 3秒");
Serial.println(" 左转/右转: 2秒");
Serial.println("----------------------------------------");
Serial.println("命令:");
Serial.println(" r <文件名> - 识别SD卡中的音频文件");
Serial.println(" audio - 从麦克风实时识别");
Serial.println(" list - 列出SD卡文件");
Serial.println(" help - 显示帮助");
Serial.println("----------------------------------------");
Serial.println("语音控制指令:");
Serial.println(" \"前进/向前/走\" - 小车前进3秒");
Serial.println(" \"后退/向后/退\" - 小车后退3秒");
Serial.println(" \"左转/向左\" - 小车左转2秒");
Serial.println(" \"右转/向右\" - 小车右转2秒");
Serial.println(" \"停止/停/别动\" - 立即停止");
Serial.println("========================================\n");
// 初始化电机控制
initMotor();
// 初始化SD卡
if (!initSD()) {
Serial.println("⚠️ SD卡初始化失败,文件模式不可用");
}
// 连接WiFi
Serial.print("[WiFi] 连接 ");
Serial.print(ssid);
Serial.print(" ...");
WiFi.begin(ssid, password);
int retry = 0;
while (WiFi.status() != WL_CONNECTED && retry < 30) {
delay(500);
Serial.print(".");
retry++;
}
if (WiFi.status() != WL_CONNECTED) {
Serial.println(" ❌ 失败");
while (1) delay(1000);
}
Serial.println(" ✅ 已连接");
Serial.print("[WiFi] IP: ");
Serial.println(WiFi.localIP());
// 初始化麦克风缓冲区
mic_buffer = (int16_t*)malloc(MIC_BUFFER_SAMPLES * sizeof(int16_t));
if (!mic_buffer) {
Serial.println("⚠️ 麦克风缓冲区分配失败");
}
showPrompt();
}
// ============== 主循环 ==============
void loop() {
webSocket.loop();
// ===== 新增:检查小车动作超时 =====
checkCarActionTimeout();
// 处理串口命令
if (Serial.available()) {
String cmd = Serial.readStringUntil('\n');
cmd.trim();
if (cmd.length() > 0) {
processCommand(cmd);
}
}
// 麦克风模式:采集5秒后自动结束
if (mic_mode && ws_connected && !asr_completed) {
if (millis() - mic_start_time >= MIC_RECORD_DURATION) {
Serial.println("\n[MIC] 5秒录音时间到,停止采集");
sendLastMicChunk();
mic_mode = false;
} else {
sendMicAudio();
}
}
// 识别完成处理
if (asr_completed && !result_displayed) {
result_displayed = true;
Serial.println("\n========================================");
Serial.println("🎉 ASR识别完成!");
Serial.println("----------------------------------------");
Serial.print("📢 识别文本: ");
Serial.println(recognition_result);
// 解析并执行语音命令
CarCommand cmd = parseVoiceCommand(recognition_result);
if (cmd != CMD_NONE) {
Serial.println("----------------------------------------");
Serial.println("🎮 检测到控制指令!");
executeCarCommand(cmd);
last_command = cmd;
} else {
Serial.println("💬 非控制指令,忽略");
}
Serial.println("========================================");
webSocket.setReconnectInterval(1000);
if (audio_file) audio_file.close();
if (mic_buffer) stopI2SMic();
asr_running = false;
mic_mode = false;
showPrompt();
}
}
// ============== 显示提示符 ==============
void showPrompt() {
Serial.println("\n[等待命令] > ");
}
// ============== 处理命令 ==============
void processCommand(String cmd) {
cmd.toLowerCase();
if (cmd.startsWith("r ")) {
String filename = cmd.substring(2);
filename.trim();
if (!filename.startsWith("/")) filename = "/" + filename;
result_displayed = false;
loadAndRecognizeFile(filename);
}
else if (cmd == "audio") {
result_displayed = false;
initWebSocket();
startMicRecognition();
}
else if (cmd == "list") {
listSDFiles();
showPrompt();
}
else if (cmd == "forward" || cmd == "f") {
carForward();
showPrompt();
}
else if (cmd == "backward" || cmd == "b") {
carBackward();
showPrompt();
}
else if (cmd == "left" || cmd == "l") {
carTurnLeft();
showPrompt();
}
else if (cmd == "right" || cmd == "r") {
carTurnRight();
showPrompt();
}
else if (cmd == "stop" || cmd == "s") {
carStop();
showPrompt();
}
else if (cmd == "help") {
Serial.println("命令:");
Serial.println(" r <文件名> - 识别SD卡音频文件 (如: r audio1.wav)");
Serial.println(" audio - 从麦克风实时识别");
Serial.println(" list - 列出SD卡文件");
Serial.println(" forward/f - 前进3秒");
Serial.println(" backward/b - 后退3秒");
Serial.println(" left/l - 左转2秒");
Serial.println(" right/r - 右转2秒");
Serial.println(" stop/s - 立即停止");
Serial.println(" help - 显示帮助");
showPrompt();
}
else {
Serial.println("❌ 未知命令,输入 help 查看帮助");
showPrompt();
}
}
// ============== SD卡初始化 ==============
bool initSD() {
Serial.println("[SD] 初始化SD卡...");
SD_MMC.setPins(SD_CLK, SD_CMD, SD_D0);
if (!SD_MMC.begin("/sdcard", true)) {
Serial.println("[SD] ❌ 失败");
return false;
}
uint8_t type = SD_MMC.cardType();
Serial.print("[SD] 类型: ");
if (type == CARD_SD) Serial.println("SD");
else if (type == CARD_SDHC) Serial.println("SDHC");
else if (type == CARD_MMC) Serial.println("MMC");
else Serial.println("未知");
Serial.printf("[SD] 容量: %llu MB\n", SD_MMC.cardSize() / 1048576);
return true;
}
// ============== 列出SD卡文件 ==============
void listSDFiles() {
File root = SD_MMC.open("/");
if (!root || !root.isDirectory()) {
Serial.println("[SD] ❌ 无法打开目录");
return;
}
Serial.println("\n[SD] 文件列表:");
int count = 0;
File f = root.openNextFile();
while (f) {
if (!f.isDirectory()) {
Serial.printf(" [FILE] %-20s %6d KB\n", f.name(), f.size() / 1024);
count++;
}
f = root.openNextFile();
}
Serial.printf("[SD] 共 %d 个文件\n", count);
}
// ============== 加载并识别文件 ==============
bool loadAndRecognizeFile(String filename) {
if (asr_running) {
Serial.println("❌ 当前有识别任务在运行,请等待完成");
return false;
}
Serial.printf("\n[文件] 准备识别: %s\n", filename.c_str());
if (!SD_MMC.exists(filename)) {
Serial.println("❌ 文件不存在");
showPrompt();
return false;
}
audio_file = SD_MMC.open(filename, FILE_READ);
if (!audio_file) {
Serial.println("❌ 无法打开文件");
showPrompt();
return false;
}
audio_total_size = audio_file.size();
audio_sent_size = 0;
target_filename = filename;
mic_mode = false;
Serial.printf("[文件] 大小: %d bytes (%.1f KB)\n", audio_total_size, audio_total_size / 1024.0);
asr_completed = false;
asr_running = true;
recognition_result = "";
Serial.println("[ASR] 连接服务器...");
initWebSocket();
return true;
}
// ============== 初始化麦克风 ==============
bool initI2SMic() {
Serial.println("[MIC] 初始化I2S麦克风...");
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = audio_rate,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 4,
.dma_buf_len = 1024,
.use_apll = false,
.tx_desc_auto_clear = false,
.fixed_mclk = 0
};
i2s_pin_config_t pin_config = {
.bck_io_num = I2S_SCK,
.ws_io_num = I2S_WS,
.data_out_num = I2S_PIN_NO_CHANGE,
.data_in_num = I2S_SD
};
esp_err_t err = i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL);
if (err != ESP_OK) {
Serial.printf("[MIC] ❌ I2S驱动安装失败: %d\n", err);
return false;
}
err = i2s_set_pin(I2S_PORT, &pin_config);
if (err != ESP_OK) {
Serial.printf("[MIC] ❌ I2S引脚设置失败: %d\n", err);
i2s_driver_uninstall(I2S_PORT);
return false;
}
Serial.println("[MIC] ✅ I2S麦克风初始化成功");
return true;
}
// ============== 停止麦克风 ==============
void stopI2SMic() {
i2s_driver_uninstall(I2S_PORT);
Serial.println("[MIC] I2S已停止");
mic_mode = false;
}
// ============== 启动麦克风识别 ==============
bool startMicRecognition() {
if (asr_running) {
Serial.println("❌ 当前有识别任务在运行,请等待完成");
return false;
}
Serial.println("\n[MIC] 启动麦克风识别...");
if (!initI2SMic()) {
showPrompt();
return false;
}
mic_mode = true;
asr_completed = false;
asr_running = true;
recognition_result = "";
mic_start_time = millis();
Serial.println("[MIC] 请说话(5秒自动停止)...");
return true;
}
// ============== WebSocket初始化 ==============
void initWebSocket() {
Serial.println("[ASR] 连接服务器...");
webSocket.beginSSL(ws_host, ws_port, ws_path);
String auth = "Authorization: Bearer; ";
auth += token;
webSocket.setExtraHeaders(auth.c_str());
webSocket.onEvent(webSocketEvent);
webSocket.setReconnectInterval(5000);
}
// ============== WebSocket事件处理 ==============
void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) {
switch(type) {
case WStype_DISCONNECTED:
Serial.println("[WS] ❌ 断开");
ws_connected = false;
break;
case WStype_CONNECTED:
Serial.println("[WS] ✅ 已连接");
ws_connected = true;
sendFullClientRequest(mic_mode);
break;
case WStype_BIN:
parseResponse(payload, length);
break;
case WStype_ERROR:
Serial.println("[WS] ❌ 错误");
break;
default:
break;
}
}
// ============== 发送配置 ==============
void sendFullClientRequest(bool from_mic) {
Serial.println("[ASR] 📤 发送配置...");
JsonDocument doc;
doc["app"]["appid"] = appid;
doc["app"]["token"] = token;
doc["app"]["cluster"] = cluster;
doc["user"]["uid"] = "esp32_asr";
doc["request"]["reqid"] = "esp32-" + String(millis());
doc["request"]["nbest"] = 1;
doc["request"]["workflow"] = "audio_in,resample,partition,vad,fe,decode,itn,nlu_punctuate";
doc["request"]["show_utterances"] = false;
doc["request"]["result_type"] = "full";
doc["request"]["sequence"] = 1;
doc["audio"]["format"] = from_mic ? "raw" : "wav";
doc["audio"]["rate"] = audio_rate;
doc["audio"]["language"] = "zh-CN";
doc["audio"]["bits"] = audio_bits;
doc["audio"]["channel"] = from_mic ? 1 : audio_channel;
doc["audio"]["codec"] = "raw";
String json;
serializeJson(doc, json);
size_t msg_len = 4 + 4 + json.length();
uint8_t* msg = (uint8_t*)malloc(msg_len);
generateHeader(msg, CLIENT_FULL_REQUEST, NO_SEQUENCE);
msg[4] = (json.length() >> 24) & 0xFF;
msg[5] = (json.length() >> 16) & 0xFF;
msg[6] = (json.length() >> 8) & 0xFF;
msg[7] = json.length() & 0xFF;
memcpy(msg + 8, json.c_str(), json.length());
webSocket.sendBIN(msg, msg_len);
free(msg);
Serial.println("[ASR] ✅ 配置已发送");
if (!from_mic && audio_file) {
delay(100);
sendNextChunk();
}
}
// ============== 发送文件下一片 ==============
void sendNextChunk() {
if (!audio_file || !asr_running) return;
size_t remaining = audio_total_size - audio_sent_size;
if (remaining == 0) {
Serial.println("[ASR] 文件读取完成,等待最终结果...");
return;
}
size_t chunk = (remaining > CHUNK_SIZE) ? CHUNK_SIZE : remaining;
bool is_last = (remaining <= CHUNK_SIZE);
uint8_t* buffer = (uint8_t*)malloc(chunk);
if (!buffer) {
Serial.println("[ERR] ❌ 内存不足");
return;
}
size_t read = audio_file.read(buffer, chunk);
if (read != chunk) {
Serial.println("[ERR] ❌ 读取失败");
free(buffer);
return;
}
size_t msg_len = 4 + 4 + chunk;
uint8_t* msg = (uint8_t*)malloc(msg_len);
uint8_t flags = is_last ? NEG_SEQUENCE : NO_SEQUENCE;
generateHeader(msg, CLIENT_AUDIO_ONLY_REQUEST, flags);
msg[4] = (chunk >> 24) & 0xFF;
msg[5] = (chunk >> 16) & 0xFF;
msg[6] = (chunk >> 8) & 0xFF;
msg[7] = chunk & 0xFF;
memcpy(msg + 8, buffer, chunk);
webSocket.sendBIN(msg, msg_len);
free(buffer);
free(msg);
audio_sent_size += chunk;
Serial.printf("[ASR] 📤 已发送 %d/%d KB (%d%%)%s\n",
audio_sent_size / 1024, audio_total_size / 1024,
audio_sent_size * 100 / audio_total_size,
is_last ? " [最后一片]" : "");
}
// ============== 发送最后一片麦克风音频 ==============
void sendLastMicChunk() {
if (!mic_buffer) return;
Serial.println("[MIC] 发送结束标记...");
size_t msg_len = 4 + 4;
uint8_t* msg = (uint8_t*)malloc(msg_len);
if (!msg) return;
generateHeader(msg, CLIENT_AUDIO_ONLY_REQUEST, NEG_SEQUENCE);
msg[4] = 0;
msg[5] = 0;
msg[6] = 0;
msg[7] = 0;
webSocket.sendBIN(msg, msg_len);
free(msg);
Serial.println("[MIC] 结束标记已发送,等待识别结果...");
}
// ============== 发送麦克风音频 ==============
void sendMicAudio() {
if (!mic_buffer || !asr_running) return;
size_t bytes_read = 0;
esp_err_t err = i2s_read(I2S_PORT, mic_buffer, MIC_BUFFER_SAMPLES * sizeof(int16_t), &bytes_read, portMAX_DELAY);
if (err != ESP_OK || bytes_read == 0) return;
size_t msg_len = 4 + 4 + bytes_read;
uint8_t* msg = (uint8_t*)malloc(msg_len);
if (!msg) return;
generateHeader(msg, CLIENT_AUDIO_ONLY_REQUEST, NO_SEQUENCE);
msg[4] = (bytes_read >> 24) & 0xFF;
msg[5] = (bytes_read >> 16) & 0xFF;
msg[6] = (bytes_read >> 8) & 0xFF;
msg[7] = bytes_read & 0xFF;
memcpy(msg + 8, mic_buffer, bytes_read);
webSocket.sendBIN(msg, msg_len);
free(msg);
static int mic_packets = 0;
mic_packets++;
if (mic_packets % 50 == 0) {
Serial.print(".");
}
}
// ============== 解析响应 ==============
void parseResponse(uint8_t* data, size_t len) {
if (len < 8) return;
uint8_t msg_type = data[1] >> 4;
uint8_t serialization = data[2] >> 4;
uint8_t header_size = data[0] & 0x0F;
size_t payload_offset = header_size * 4;
if (len < payload_offset + 4) return;
int32_t payload_size = ((int32_t)data[payload_offset] << 24) |
((int32_t)data[payload_offset + 1] << 16) |
((int32_t)data[payload_offset + 2] << 8) |
((int32_t)data[payload_offset + 3]);
if (payload_size <= 0 || len < payload_offset + 4 + payload_size) return;
uint8_t* payload = data + payload_offset + 4;
if (serialization == JSON_SERIALIZATION) {
JsonDocument doc;
DeserializationError err = deserializeJson(doc, payload, payload_size);
if (err) return;
int code = doc["code"] | -1;
int sequence = doc["sequence"] | 0;
if (code == 1000) {
JsonArray arr = doc["result"].as<JsonArray>();
if (arr.size() > 0) {
const char* txt = arr[0]["text"];
if (txt && strlen(txt) > 0) {
recognition_result = String(txt);
Serial.printf("\n[识别] %s\n", txt);
}
}
if (sequence < 0) {
asr_completed = true;
Serial.println("[ASR] 收到最终响应标记(sequence<0)");
return;
}
if (!mic_mode && audio_file) {
sendNextChunk();
}
} else {
const char* msg = doc["message"] | "Unknown";
Serial.printf("[ERR] %d: %s\n", code, msg);
asr_running = false;
asr_completed = true;
}
}
}
2.4 测试结果
终端输入:audio
然后对着mic说话 "小车前进"
日志输出如下:
bash
[等待命令] >
[ASR] 连接服务器...
[MIC] 启动麦克风识别...
[MIC] 初始化I2S麦克风...
[MIC] ✅ I2S麦克风初始化成功
[MIC] 请说话(5秒自动停止)...
[WS] ✅ 已连接
[ASR] 📤 发送配置...
[ASR] ✅ 配置已发送
[识别] 前
.
[识别] 前
[识别] 前
[识别] 前
[识别] 前
[识别] 前
[识别] 前
[识别] 前进
[识别] 前进
[识别] 前进
[识别] 前进
[识别] 前进
[识别] 前进小
[识别] 前进小
[识别] 前进小
[识别] 前进小
[识别] 前进小车
[识别] 前进小车
[识别] 前进小车
[识别] 前进小车
[识别] 前进小车
[识别] 前进小车
[识别] 前进小车前
[识别] 前进小车前
[识别] 前进小车前
[识别] 前进小车前进
[识别] 前进小车前进
[识别] 前进小车前进
[识别] 前进小车前进
[识别] 前进小车前进
[识别] 前进小车前进
[识别] 前进小车前进
[MIC] 5秒录音时间到,停止采集
[MIC] 发送结束标记...
[MIC] 结束标记已发送,等待识别结果...
[识别] 前进小车前进
[识别] 前进小车前进
[识别] 前进小车前进
[识别] 前进小车前进
[ASR] 收到最终响应标记(sequence<0)
========================================
🎉 ASR识别完成!
----------------------------------------
📢 识别文本: 前进小车前进
----------------------------------------
🎮 检测到控制指令!
🚗 [小车] 前进 ⬆️ (持续3秒)
========================================
[MIC] I2S已停止
[等待命令] >
[小车] 动作时间到,自动停止
🚗 [小车] 停止 ⏹️
小车前进