ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

microduck嵌入式语音助手实战:从ESP32-S3选型到唤醒词训练全流程

microduck嵌入式语音助手实战:从ESP32-S3选型到唤醒词训练全流程 最近 microduck 这个项目在 GitHub 上热度涨得很快讨论区里问得最多的问题集中在硬件买哪些怎么训练怎么跑通。我花了两个礼拜把这套东西从零到一完整走了一遍踩了不少坑也摸清了整条链路。这篇文章就是一份完整路线图从元器件选型讲起到烧录第一行代码再到训练出唤醒词、跑通语音闭环每一步我都会把选择逻辑和实际经验写清楚。microduck 在 GitHub 上的各家分支硬件方案略有差异但大方向一致下文按最常见的主控方案来讲。适合手里有单片机基础的开发者也适合第一次接触嵌入式语音 AI 的新手照着复现。1. microduck是谁拆解一台能说话的芯片到底由哪几部分组成1.1 它到底是个什么东西microduck 本质上是一个跑在 MCU 上的微型语音助手体积比鸡蛋还小核心功能是上电之后待机听到预设的唤醒词比如小鸭小鸭然后录制你说的话识别语义或上传给云端大模型再把回答播出来。整台设备没有屏幕交互全靠语音成本控制在几十块钱级别。这项目有意思的地方在于它把语音 AI这个听起来很重的概念压进了一颗没有操作系统的单片机里。很多人一听到训练两个字就觉得需要服务器、需要 GPU实际上 microduck 的完整链路里真正需要训练的只有一部分唤醒词和命令词识别而且量级很小普通笔记本电脑就能完成模型体积通常只有几百 KB。1.2 一条语音指令的完整旅程如果你第一次接触这种项目先把下面这条链路记住后面所有硬件选型和代码都是为它服务的麦克风持续采集音频做音量检测VAD检测到人声后本地跑唤醒词模型判断是不是预设唤醒词唤醒后录制一段指令音频切成固定长度音频做特征提取送入识别模型得到文本或意图根据意图生成回复文本本地模板或调用大模型 API回复文本通过 TTS 合成音频从扬声器播放出来microduck 这类项目通常会根据开发者水平提供两种玩法一种是把第 2 到第 6 步全部跑在本地芯片上离线可用但能识别的指令有限另一种是只做唤醒 录音 播放识别和对话全部走云端 API。我下面讲的路线图两种玩法都覆盖因为它们的硬件选型和前几步代码完全一样差别只在后续模型部署方式。2. 硬件选型别一上来就买最贵的按链路需求反推2.1 主控芯片为什么多数项目选 ESP32-S3先说结论microduck 这类项目的主流主控是乐鑫的 ESP32-S3我这次也用的它。选它的理由不是因为它性能最强而是因为生态正好卡在需求的中间位置。看下几个主流候选的对比芯片架构内存AI 加速语音生态适合程度ESP32Xtensa 单核/双核520KB SRAM无基础 I2S 驱动勉强够用内存紧ESP32-S3Xtensa 双核512KB SRAM 8MB PSRAM向量指令加速ESP-SR 语音识别框架最推荐RP2040Cortex-M0 双核264KB SRAM无无官方语音方案不推荐做语音STM32F4Cortex-M4192KB SRAMDSP 指令需要自己移植难度大我选 ESP32-S3 的具体理由有三条。第一它带 PSRAM。语音识别过程中音频缓冲区、模型权重、特征矩阵加起来很容易超过 512KB 内部 SRAM外挂 8MB PSRAM 之后空间压力小很多。很多上电崩溃的问题根源就是默认把大数组放在内部 RAM 里导致溢出有了 PSRAM 至少能把音频缓存这类大块数据挪出去。第二官方维护的 ESP-SR 框架直接提供了唤醒词检测、语音命令识别、声学前端回声消除、降噪等组件集成起来比从零写一个神经网络推理引擎省事太多。后面要自定义唤醒词也只能在这个框架里做生态粘性很强。第三它原生支持 I2S 和 Wi-Fi。I2S 用来接数字麦克风和解码音频Wi-Fi 用来调云端大模型 API不需要额外买芯片。如果你手头已经有 ESP32 老款也不是不能做但模型得换成极小的版本而且缺 PSRAM 会导致很多现成组件用不了。我的建议是别折腾直接买 S3 开发板省下的时间远比省下的几十块钱值。2.2 麦克风与扬声器模拟方案尽量避开语音链路的第一个环节是采集。microduck 项目里用的麦克风有两种选择模拟麦克风如 MAX9814输出模拟电压和数字麦克风如 INMP441、MSM261直接输出 I2S 数字信号。我的建议是直接用 I2S 数字麦克风。原因很简单ESP32-S3 自带的 ADC 做音频采集效果很一般采样率和信噪比都不理想用模拟麦克风还需要额外的放大和偏置电路处理不好还会引入直流偏置问题。而 INMP441 这种数字麦把放大、量化、调制全做在芯片内部了只需要四根线就能接VDD、GND、SCK、SD再加一个 L/R 选择脚几乎不会翻车。扬声器一侧推荐 MAX98357A 功放模块加 3W/4Ω 小喇叭的组合。MAX98357A 同样是一个 I2S 设备直接接收数字音频流驱动喇叭不用自己搭功放电路。这样整套音频链路就是数字麦克风 I2S 进功放 I2S 出主控只负责数字信号处理所有模拟电平和功率放大的脏活都让专用芯片去干。我一开始图省事买过 PAM8403 那种模拟功放板还得自己焊电位器和滤波电容调试起来麻烦得多后来换回 MAX98357A 一次搞定。2.3 供电、结构与成本供电方面开发阶段直接 USB 供电就好但要注意 ESP32-S3 开启 Wi-Fi 后瞬态电流能到 500mA 左右USB 口供电没问题如果用电池必须选支持持续 1A 输出的方案比如 TP4056 充电板加 18650 电池别用小容量纽扣电池一开 Wi-Fi 电压就跌直接重启。结构方面microduck 的鸭形外壳基本都是 3D 打印的STL 模型通常跟着项目仓库一起发布。如果没有打印机找淘宝代打 PLA 材质一个壳子也就十几块钱。这里有个很多人忽略的点喇叭和麦克风之间要做物理隔离中间加隔音棉或者让两者朝向相反方向否则会出现严重的啸叫和回声表现在现象上就是唤醒词识别率暴跌、播放回复时嗡嗡响。这不是软件能完全解决的结构上先隔开比事后调算法有效得多。我这次的总成本大致是ESP32-S3 开发板 35 元INMP441 麦克风 8 元MAX98357A 功放 9 元3W 喇叭 5 元外壳代打 15 元加上杜邦线、铜柱之类的小零件总共不到 80 元。比起买现成的智能音箱这个价格还要啥自行车。3. 环境搭建从拿到开发板到点亮第一颗 LED3.1 开发框架怎么选IDF 还是 Arduinomicroduck 的软件部分有两条开发路线ESP-IDF乐鑫官方 SDK和 Arduino 框架。IDF 功能全、性能好、ESP-SR 官方支持最完善但学习曲线陡Arduino 上手快但很多语音组件要么没有要么版本滞后。我的建议是走 ESP-IDF。原因不只是功能完整更关键的是 ESP-SR 的自定义唤醒词训练工具只在 IDF 生态里完整支持。你在 Arduino 里可以跑通 Demo但想训练属于自己的唤醒词最后还是得回到 IDF。既然早晚要回来不如一开始就装 IDF省得中途切换环境再踩一遍配置的坑。安装 IDF 官网写得很详细这里只说几个容易踩坑的点不要用系统自带的 Python 环境装建议用 Python 虚拟环境venv避免和系统其他包版本冲突IDF 5.x 和 4.x 的安装方式不同装之前先确认项目仓库要求哪个版本。microduck 这类新项目很多基于 IDF 5.0 以上因为新的 I2S 驱动 API 在 5.x 才稳定Linux/macOS 用户记得把 ESP-IDF 的export.sh加到 shell 配置里Windows 用户直接用 IDF PowerShell 终端别在普通 CMD 里折腾网络不好的时候下载工具链可能会失败多试几次或者换镜像源都能解决3.2 第一步代码不是Hello World而是环境自检很多人急着写语音代码结果第一个程序就跑不通最后发现是开发板型号没选对或者串口驱动没装。我的习惯是先烧一个最基础的点灯程序把编译-烧录-串口输出这条链路验证通再往上加复杂度。用 ESP-IDF 创建新项目最简单的main.c长这样#include stdio.h #include freertos/FreeRTOS.h #include freertos/task.h #include driver/gpio.h #include esp_log.h #define LED_GPIO GPIO_NUM_2 void app_main(void) { gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT); int level 0; while (1) { gpio_set_level(LED_GPIO, level ^ 1); ESP_LOGI(main, LED level: %d, level); vTaskDelay(pdMS_TO_TICKS(500)); } }编译和烧录的命令就三条idf.py set-target esp32s3 idf.py build idf.py -p /dev/ttyACM0 flash monitor注意set-target一定要在build之前执行它决定整个工程的链接脚本和 SDK 配置。烧录完成后如果串口监视器每 500ms 打印一条日志说明工具链、驱动、烧录流程全部正常。到这一步第一行代码其实已经算跑通了。3.3 从点灯到语音先配好 menuconfig点灯程序通了之后别急着写 I2S 代码先去 menuconfig 里把内存和音频相关配置调好idf.py menuconfig需要关注的关键项Component config - ESP System Settings - 开启 PSRAM选择 Octal PSRAM 或 Quad PSRAM这条必须和你开发板上的型号一致选错了直接启动崩溃Component config - FreeRTOS - 把主任务的栈大小调大一点默认 3KB 偏小后面跑语音任务建议直接开到 8KB 以上如果项目要接 Wi-Fi对应的 Flash 分区表要调整成支持 OTA 的方案这一步看起来不起眼但很多程序编译过了、上电就崩溃的问题根源都在 PSRAM 没配置对。调试时最有效的手段就是打开串口监视器看启动日志PSRAM 初始化失败会有明确报错照着提示去改配置即可。4. 第一行真正意义上的语音代码麦克风采集与喇叭播放4.1 音频设备初始化IDF 5.x 的 I2S 新 API在 IDF 5.x 里老的i2s_driver_installAPI 已经废弃新 API 按标准模式重新设计。以驱动 INMP441 和 MAX98357A 为例初始化分三步先创建 I2S 通道配置再为通道指定收发引脚最后设置音频格式。#include driver/i2s_std.h #define I2S_MCK_PIN GPIO_NUM_NC #define I2S_BCK_PIN GPIO_NUM_4 #define I2S_WS_PIN GPIO_NUM_5 #define I2S_DIN_PIN GPIO_NUM_6 #define I2S_DOUT_PIN GPIO_NUM_7 i2s_chan_handle_t rx_chan, tx_chan; void audio_init(void) { i2s_chan_config_t chan_cfg { .id I2S_NUM_0, .role I2S_ROLE_MASTER, .dma_desc_num 6, .dma_frame_num 240, .auto_clear true, }; i2s_std_config_t std_cfg { .clk_cfg I2S_STD_CLK_DEFAULT_CONFIG(16000), // 16kHz 采样率 .slot_cfg I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_MONO), .gpio_cfg { .mclk I2S_MCK_PIN, .bclk I2S_BCK_PIN, .ws I2S_WS_PIN, .dout I2S_DOUT_PIN, .din I2S_DIN_PIN, .invert_flags { .mclk_inv false, .bclk_inv false, .ws_inv false, }, }, }; i2s_new_channel(chan_cfg, tx_chan, rx_chan); i2s_channel_init_std_mode(tx_chan, std_cfg); i2s_channel_init_std_mode(rx_chan, std_cfg); i2s_channel_enable(tx_chan); i2s_channel_enable(rx_chan); }这里几个关键参数解释一下。采样率 16kHz 是语音识别的标准采样率既能覆盖人声频段又不会让数据量太大16kHz 单声道 16bit 的数据率是 32KB/s一个 3 秒的音频才 96KB放 PSRAM 绰绰有余。数据位宽选 32bit 是因为 INMP441 输出 24bit 有效数据放在 32bit 容器里方便后续移位处理如果直接选 16bit低字节的截断规则很容易把人搞晕。4.2 录制一段音频验证麦克风活着麦克风初始化完成后先做一个最简单的录音测试录 3 秒音频通过串口输出音量信息。如果麦克风接线没问题、配置正确你会看到音量随时间变化如果音量恒为 0 或者恒为满值通常是 WS/LR 引脚接错或者 DIN/DOUT 接反了。#include math.h void record_sample(uint16_t duration_ms) { int16_t buffer[48000]; size_t bytes_read 0; int64_t frames (int64_t)duration_ms * 16000 / 1000; i2s_channel_read(rx_chan, buffer, frames * sizeof(int16_t), bytes_read, portMAX_DELAY); // 计算 RMS 电平 double sum_sq 0; int n bytes_read / sizeof(int16_t); for (int i 0; i n; i) { sum_sq (double)buffer[i] * buffer[i]; } double rms sqrt(sum_sq / n); ESP_LOGI(mic, read %d frames, RMS %.1f, n, rms); }注意i2s_channel_read最后一个参数是阻塞等待时间这里设为portMAX_DELAY表示一直等到读够数据为止。实际项目里你要把音频数据缓存到环形缓冲区不能让录音任务阻塞在读取上否则后面的唤醒检测任务没法并发运行。4.3 播放一段提示音验证喇叭通录音通了之后测播放往 TX 通道写一段 1kHz 正弦波。能听到滴的一声说明音频输出链路也正常。到这一步你的 microduck 已经是一台能听能说的硬件了。void play_tone(int freq_hz, int duration_ms) { int16_t tone[16000]; int n 16000 * duration_ms / 1000; for (int i 0; i n; i) { tone[i] (int16_t)(3000 * sinf(2 * M_PI * freq_hz * i / 16000)); } size_t bytes_written 0; i2s_channel_write(tx_chan, tone, n * sizeof(int16_t), bytes_written, portMAX_DELAY); }4.1 到 4.3 这三段代码合起来就是 microduck 软件架构的最底层。后面不管接唤醒词、接云端大模型还是接本地指令集都在这套收发框架之上加业务逻辑。我建议把这三段封装成一个audio_io模块对外提供audio_io_record()和audio_io_play()两个接口上层代码会干净很多后续调试也方便单独测某个环节。5. 训练环节唤醒词和命令词模型是怎么来的5.1 先厘清microduck 需要训练的是哪部分很多人被训练两个字吓住其实 microduck 的模型训练分两类要求和难度完全不同。第一类是唤醒词模型Wake Word这是对资源最苛刻的模型。它必须常驻在芯片上、每时每刻对麦克风数据做推理所以模型必须极小参数量通常在几十万以内延迟必须控制在几十毫秒内。这类模型可以用乐鑫官方的 ESP-SR 框架训练训练完转成自定义唤醒词数据集成到框架里。第二类是命令词识别模型Command Recognition走 ESP-SR 里的 Speech Commands Recognition 组件可以识别几十条固定短语比如开灯关灯查天气。这类模型也不需要你掌握深度学习的底层原理主要是准备数据和调训练参数剩下的交给训练脚本。如果你想走云端方案识别和对话交给大模型 API那么芯片上只需要第一类唤醒词模型命令词识别都不用训练录完音直接发云端接口返回文本本地用 TTS 播出来。这也是我推荐的入门路径先只训练唤醒词其余全部交给云端跑通整条链路后再考虑把更多模型搬到本地。毕竟本地跑的模型越多内存和 CPU 占用越高排查问题的面就越广入门阶段没必要一次全上。5.2 数据准备唤醒词训练质量的命门唤醒词训练的核心不在模型结构而在数据。ESP-SR 的唤醒词训练对数据的要求大致是正样本唤醒词音频至少 200 到 500 条。需要不同的人录制至少 5 到 10 人男女都有覆盖不同的语速、语气、距离麦克风的远近负样本各种非唤醒词音频日常对话、环境噪声、其他命令词数量一般是正样本的 3 到 5 倍采样率统一 16kHz单声道格式以训练工具要求为准我多说一句很多人拿自己一个人的声音录 20 条就去训练结果是自己喊破喉咙能唤醒别人一喊就断。唤醒词模型要在真实环境工作必须覆盖说话人差异和环境噪声差异。哪怕只是自己做着玩也尽量拉身边的家人朋友各录几十条效果会指数级变好。另外录制环境也不要太安静最好在正常客厅的环境下录几遍这样模型才见过真实噪声。5.3 训练与部署的完整流程以 ESP-SR 的唤醒词训练为例流程一般是把正负样本音频打包按平台要求上传或者用本地训练脚本处理平台训练完成后下载生成的唤醒词模型文件用esp_sr组件把训练好的词条导入工程在 menuconfig 里选择自定义唤醒词重新编译烧录上电测试如果你更习惯通用的 TinyML 流程也可以完全绕过 ESP-SR用 Edge Impulse 训练一个关键字识别模型比如识别唤醒词导出为 C/C 库然后在 IDF 工程里用 ESP-NN 做推理。这种方式更通用但工作量和调试成本高不少不推荐第一次就搞。训练环节最容易犯的错是正负样本混淆也就是把唤醒词音频顺手放到了负样本里导致模型直接学废了永远无法唤醒或者一唤醒就误触。我的建议是给每个音频文件按规范命名比如wakeword_xxx_01.wav、negative_noise_03.wav训练前用脚本检查一遍目录确保正负样本严格分离。这类错误通常是你半夜录完音随手一拖文件夹然后第二天训练出来的模型怎么都不对最后排查半天发现是数据集本身的问题。6. 跑通全流程从能听能说到喊一声就有回应6.1 最小闭环唤醒词 录音 播放 TTS把前面所有模块拼起来microduck 的最小可用版本就三条逻辑检测到唤醒词后播放一声叮作为反馈然后录 5 秒音频录音结束后播放一段预设回复。这段代码不涉及识别和云 API但已经把完整的事件循环跑通了。void voice_task(void *arg) { while (1) { if (wakeword_detect()) { // 唤醒词检测 play_tone(800, 100); // 提示音 record_sample(5000); // 录 5 秒指令 play_audio((int16_t *)reply_wav, reply_wav_len); // 播放预设回复 } vTaskDelay(pdMS_TO_TICKS(50)); } }很多第一次做的人卡在这里唤醒词检测函数和录音不能同时工作因为音频硬件只有一个 I2S 外设。解决思路是让音频驱动始终保持采集状态用 DMA 双缓冲循环写一个环形缓冲区唤醒词检测模块实时从环形缓冲区读数据推理唤醒后再把环形缓冲区里最近的数据连同后续数据拼成完整指令音频。这个边采边检的架构是 microduck 这类项目最关键的设计比检测完再录音的方式自然得多也不会丢掉唤醒词之后紧接着说出的话。6.2 接上云端让回复内容是活的预设回复跑通后可以接大模型 API 让回复内容变成动态的。这一步的核心是 Wi-Fi 连接和 HTTP 请求。ESP32-S3 用的 HTTP 客户端库是esp_http_client把录音文件通过 multipart/form-data 上传到服务端服务端转文字、调大模型、TTS 合成音频返回芯片收到后播放。这里有个性能取舍要提前想清楚TTS 音频如果是服务端合成好一次性返回文件可能是几十 KB 到几百 KBWi-Fi 传输加上解码播放会有几秒延迟。想降低延迟可以用流式播放边下载边喂给 I2S。另外ESP32-S3 没有硬件 MP3 解码器播 MP3 需要软件解码库乐鑫的esp-audio组件里有这会占不少 CPU。初期建议直接让服务端返回 WAV 或 PCM 数据跳过硬解环节等链路稳定了再优化格式和压缩。6.3 联调中我遇到的高频问题最后把我在跑通过程中遇到的高频问题按发生频率列个表方便你对照排查现象根因排查方法唤醒词完全没反应唤醒词模型没烧进去或选错词条menuconfig 里确认自定义唤醒词已启用打印 ESP-SR 初始化日志唤醒后录音全是噪声麦克风 DIN/DOUT 接反或 WS 相位不对检查 GPIO 定义用逻辑分析仪看 BCK/WS 时序播放声音很小且发闷MAX98357A 接 3.3V 电源导致功率不够给功放单独供 5V地线共地一上电无限重启PSRAM 配置与实际芯片不符看启动日志的 PSRAM 初始化段修改 menuconfig唤醒后偶尔无响应主任务栈溢出在 FreeRTOS 里开栈水印检查调大任务栈音频有回音啸叫麦克风与喇叭距离过近结构上物理隔开或用 ESP-SR 的 AEC 组件这些坑没有一个是玄学全部能通过日志和量化的方式定位。我特别想强调遇到问题先把现象量化比如唤醒率 10%和完全无反应是完全不同的两类问题前者大概率是模型或声学问题后者大概率是硬件或初始化问题。带着量化的现象去查资料效率会高很多而不是一上来就怀疑代码哪里写错了。6.4 再往后可以怎么扩展microduck 这个框架最吸引人的地方是扩展性极强。跑通最小闭环后可以往这几个方向继续深入增加本地命令词识别把高频指令开关灯、查时间从云端搬到本地实现离线可用用乐鑫的 AFE 组件做回声消除和降噪提升嘈杂环境下唤醒率给设备加按键、LED 灯环、小屏幕把交互从纯语音扩展到多模态接入 MQTT让 microduck 控制家里其他智能设备变成一个小的语音网关我自己的下一步计划是训练第二个唤醒词用来切换麦克风静音模式本质上就是再走一遍第 5 节的数据采集和训练流程。有了第一次的完整经验第二次从准备数据到模型上线只花了一个晚上。最后分享一个小体会microduck 这类项目真正的门槛不在代码而在把硬件、声学、模型、网络串成一条完整链路的系统思维。你在任何一个环节卡住都不要死磕那一处先从最小可用的版本跑起来再一层层往上加。做出来之后你会发现那些在帖子里反复问怎么训练怎么跑通的人和真正动手的人之间差距其实只有一块开发板和两周的晚上。
返回列表