最近 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 加速 | 语音生态 | 适合程度 |
|---|---|---|---|---|---|
| ESP32 | Xtensa 单核/双核 | 520KB SRAM | 无 | 基础 I2S 驱动 | 勉强够用,内存紧 |
| ESP32-S3 | Xtensa 双核 | 512KB SRAM + 8MB PSRAM | 向量指令加速 | ESP-SR 语音识别框架 | 最推荐 |
| RP2040 | Cortex-M0+ 双核 | 264KB SRAM | 无 | 无官方语音方案 | 不推荐做语音 |
| STM32F4 | Cortex-M4 | 192KB SRAM | DSP 指令 | 需要自己移植 | 难度大 |
我选 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. 环境搭建:从拿到开发板到点亮第一颗 LED
3.1 开发框架怎么选:IDF 还是 Arduino
microduck 的软件部分有两条开发路线: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 到几百 KB,Wi-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 这类项目真正的门槛不在代码,而在把"硬件、声学、模型、网络"串成一条完整链路的系统思维。你在任何一个环节卡住,都不要死磕那一处,先从最小可用的版本跑起来,再一层层往上加。做出来之后你会发现,那些在帖子里反复问"怎么训练""怎么跑通"的人,和真正动手的人之间,差距其实只有一块开发板和两周的晚上。