
1. 从手动指令到自动巡航ESP32 语音小车要解决的真实问题语音控制智能小车做到“说前进就前进”其实不难难的是让它自己跑起来还不撞墙。我这次要聊的就是 ESP32 语音控制智能小车从手动指令升级到自动巡航的完整链路你对着小车说一句“开始自动巡航”它进入自主前进状态靠 HC-SR04 超声波持续测距前方 20cm 内出现障碍物就自动后退加转向绕开巡航时间到了自己停下。这套东西适合谁适合已经用 ESP32-S3 跑通语音对话、手里有 L298N 双驱动板和四轮底盘、想再加一个超声波模块做避障的玩家。核心检索词就三个语音控制、自动巡航、超声波避障。整条链路的关键不在语音识别本身而在于把“语音指令”翻译成 MCP 工具调用再由一个独立 FreeRTOS 任务去执行运动逻辑这样语音线程和电机控制线程互不阻塞。我踩过的坑是一开始把巡航逻辑直接塞进语音回调里结果语音一说话小车就卡住不动。后来改成队列加独立任务语音只负责往队列里丢事件运动任务自己消费才彻底顺了。下面按硬件、TaoToken 统一 Key 配置、可复制代码、串口验证、排错、CTA 六段走一遍。2. TaoToken 统一 Key 前置给语音大脑配一个稳定入口语音小车要“听懂话”背后得有大模型做意图解析。小智 AI 这套固件本身支持配置大模型服务我这里用 TaoToken 做统一入口一个 Key 就能覆盖对话模型和后续可能用到的编码模型省得每个服务单独申请。先到官网注册并进入控制台在 API Keys 页面创建一个 Key。地址是 https://taotoken.net/api 控制台入口在 https://taotoken.net/console 。创建完把 Key 复制出来形如sk-xxxxxxxx后面写进配置。TaoToken 在这里的角色是“统一网关”你的 ESP32 固件只需要认一个 base_url 和一个 Key模型切换、额度查看都在控制台完成。对嵌入式项目来说少维护几套鉴权逻辑就是少几个半夜调试的理由。需要注意Key 只放在本地配置文件里别提交到 Git。我一般放在main/private_config.toml并加进.gitignore。如果你还想在电脑上先验证模型能不能正常对话可以直接用模型对话页面 https://taotoken.net/models 试一句确认 Key 有效再烧录能省不少排查时间。3. 可复制配置config.toml 骨架与统一 Key 片段小智 AI 固件读取的是 TOML 配置。下面这份骨架你可以直接抄重点是[llm]段里的 base_url 和 api_key。模型名按你控制台里可用的填这里用通用占位。# main/private_config.toml [device] name esp32-smart-car board bread-compact-wifi [wifi] ssid 你的WiFi名 password 你的WiFi密码 [llm] type openai base_url https://taotoken.net/api/v1 api_key sk-你的TaoTokenKey model gpt-4o-mini temperature 0.7 max_tokens 512 [audio] sample_rate 16000如果你用的是 Anthropic 协议通道base_url 换成对应的 Anthropic 兼容地址Key 还是同一个。配置里type字段决定走哪套请求格式别填错。超声波和电机的引脚不写在 TOML 里而是写死在senor_control.h和motor_control.h的宏定义中。我这次用的引脚是TRIGGPIO12ECHOGPIO13左电机 PWMGPIO9、方向 GPIO10/11右电机 PWMGPIO45、方向 GPIO40/39。你按自己接线改宏就行改完重新编译。配置写完先别急着烧用idf.py build过一遍编译确认 TOML 没语法错误。TOML 对引号和缩进敏感少一个引号编译期就会报解析失败。4. 自动巡航核心代码队列 独立任务 超声波避障这一节是全文重点。整体设计是语音通过 MCP 工具调用car_auto_cruise(duration)这个函数不直接驱动电机而是往 FreeRTOS 队列里发一个CAR_STATE_AUTO_CRUISE事件一个名为car_setup的任务循环消费队列进入巡航分支后按 tick 计时每 60ms 调一次避障逻辑。先看状态与事件定义放在smart_car_run.henum car_state { CAR_STATE_STOP, CAR_STATE_FORWARD, CAR_STATE_BACKWARD, CAR_STATE_TURN_LEFT, CAR_STATE_TURN_RIGHT, CAR_STATE_AUTO_CRUISE, }; struct smart_car_event { car_state state; int delay_ms; };巡航入口只做入队不做实际运动这是不阻塞语音的关键void SmartCarControl::car_auto_cruise(int seconds) { CHECK_SEC_AUTO_CRUISE(seconds) // 限制在 10~80 秒 ev_data.state CAR_STATE_AUTO_CRUISE; ev_data.delay_ms seconds; BaseType_t xStatus xQueueSend(control_evt_queue, ev_data, pdMS_TO_TICKS(200)); if (xStatus ! pdPASS) { ESP_LOGI(TAG, car_auto_cruise xQueueSend fail); } }避障逻辑是巡航的心脏。每轮先读超声波距离小于 20cm 就后退再随机左右转否则低速前进void SmartCarControl::_logic_auto_cruise() { int distance getSenorManager()-get_senor_distance( Senor_Ultrasonic, Senor_Direction_Forward); if (distance 0) { ESP_LOGE(TAG, get_senor_distance fail); return; } if (distance 20) { _car_backward(STANDARD_ACTION_MS, false); if (xTaskGetTickCount() 1) { _car_turn_left(STANDARD_ACTION_MS, false); } else { _car_turn_right(STANDARD_ACTION_MS, false); } } else { _auto_cruise_forward(AUTO_CRUISE_ACTION_MS); // 60ms } }巡航前进用低速避免冲太快来不及避障void SmartCarControl::_auto_cruise_forward(int ms) { auto motor getMotorControl(); motor-car_control(CAR_FORWARD, AUTO_CRUISE_SET_SPEED(car_state_is_stop)); vTaskDelay(pdMS_TO_TICKS(ms)); CHANGE_CAR_STATE_TO_RUN(car_state_is_stop) }任务主循环里巡航分支用 tick 计时判断是否超时超时就停车退出巡航case CAR_STATE_AUTO_CRUISE: { start_ticks xTaskGetTickCount() / configTICK_RATE_HZ; CHECK_SEC_AUTO_CRUISE(ev.delay_ms) end_ticks start_ticks ev.delay_ms; bAuto_cruise true; } break;巡航过程中如果收到非巡航事件比如用户喊“停”立即退出巡航并执行新指令if (ev.state ! CAR_STATE_AUTO_CRUISE) { bAuto_cruise false; _car_stop(); switch (ev.state) { case CAR_STATE_FORWARD: _car_forward(ev.delay_ms); break; case CAR_STATE_STOP: _car_stop(); break; default: break; } }超声波测距用esp_timer_get_time()做微秒级计时超时 60ms 返回 -1避免死等int ultrasonic_control::ultrasonic_measure_distance() { int result -1; gpio_set_level(TRIG_PIN, 1); esp_rom_delay_us(10); gpio_set_level(TRIG_PIN, 0); int64_t start_time esp_timer_get_time(); int64_t timeout_time start_time 1000 * 60; while (gpio_get_level(ECHO_PIN) 0) { if (esp_timer_get_time() timeout_time) { ESP_LOGW(TAG, Timeout waiting for echo start); return result; } esp_rom_delay_us(2); } int64_t echo_start esp_timer_get_time(); timeout_time echo_start TIMEOUT_US; while (gpio_get_level(ECHO_PIN) 1) { if (esp_timer_get_time() timeout_time) { ESP_LOGW(TAG, Timeout waiting for echo end); return result; } esp_rom_delay_us(2); } int64_t echo_end esp_timer_get_time(); uint32_t duration_us (uint32_t)(echo_end - echo_start); uint32_t distance_cm (duration_us * SOUND_SPEED_CM_US) / 2.0f; return (int)distance_cm; }MCP 工具注册时把self.car.auto_cruise暴露给大模型参数是秒数范围 5~50AddTool(self.car.auto_cruise, Control the car to enter automatic cruise mode with obstacle avoidance., PropertyList({ Property(duration, kPropertyTypeInteger, 5, 50) }), [carcontrol](const PropertyList properties) - ReturnValue { int duration properties[duration].valueint(); carcontrol-car_auto_cruise(duration); ESP_LOGI(TAG, self.car.auto_cruise %d, duration); return true; });调试阶段建议把TEST_MODE设为 1轮子不动但日志照打方便确认指令链路通不通。确认没问题再改回 0。5. 串口日志验证确认巡航触发与避障响应烧录后打开串口监视器波特率 115200idf.py -p COM3 flash monitor对着小车说“开始自动巡航 30 秒”正常应该看到这样的日志序列I (5230) SMARTCAR: #######car_run state5,time30 I (5290) SMARTCAR: #######[auto]_logic_auto_cruise get_senor_distance156 cm I (5350) SMARTCAR: #######[auto]_logic_auto_cruise get_senor_distance148 cm I (5410) SMARTCAR: #######[auto]_logic_auto_cruise get_senor_distance23 cm I (5470) SMARTCAR: #######[auto]_logic_auto_cruise get_senor_distance18 cm I (5530) MOTORCON: Car moving BACKWARD at speed: 800 I (5590) MOTORCON: Car turning LEFT at speed: 800看到state5说明进入了CAR_STATE_AUTO_CRUISE距离从 156 递减到 18说明超声波在实时测距出现 BACKWARD 和 LEFT 说明避障被触发。30 秒后应该看到I (35230) MOTORCON: Car STOPPED如果中途喊“停”日志会立刻跳出巡航分支打印state0并停车。这一步验证通过说明语音到运动整条链路是通的。6. 本篇常见错排查超声波一直返回 -1先查 ECHO 引脚有没有下拉代码里配的是GPIO_PULLDOWN_ENABLE悬空会一直读到高电平导致超时。再确认 TRIG 和 ECHO 没接反HC-SR04 的 Trig 是输入脉冲、Echo 是输出回响。巡航启动后小车不动检查TEST_MODE是不是还是 1。另外确认car_init_config()在Application::Start()里被调用了队列没创建的话xQueueSend会失败日志里会有xQueueSend fail。语音说“自动巡航”没反应MCP 工具名和描述要写清楚大模型靠描述判断何时调用。确认self.car.auto_cruise已经注册进AddCommonTools()并且carcontrol指针非空。编译报 espressif2022 依赖找不到清理工程后重新编译会遇到依赖版本变化去 GitHub 查对应组件的最新版本号更新idf_component.yml里的版本约束再编译。巡航中语音卡顿说明运动逻辑还在语音线程里跑。确认所有car_xxx函数都是入队而非直接调_car_xxxDIRECT_SHELL宏不要定义。避障转向太频繁把 20cm 阈值调小到 15cm或者把AUTO_CRUISE_ACTION_MS从 60 调到 80让每次前进多走一点再判断。7. 下一步把统一 Key 用到长期编码与 Agent 场景小车跑通之后如果你还想继续折腾固件、写更多 MCP 工具或者把语音 Agent 接到更复杂的任务流里TaoToken 的 Coding Plan 会更合适长期编码场景下额度更耐用入口在 https://taotoken.net/coding-plan 。接入文档和 API 细节都在 https://taotoken.net/doc 遇到鉴权或请求格式问题先翻这里。Key 管理统一在 https://taotoken.net/api-keys 需要新增或轮换 Key 时直接操作。我自己的习惯是小车调试阶段用模型对话页面快速验证意图解析对不对正式烧录前再确认 Key 和 base_url 没写错。这套流程跑顺之后加舵机做云台测距、加第二路超声波做左右避障都是在这个队列任务上继续扩状态机的事不会推翻现有结构。