基于 ESP32-S3 的离线语音助手:从麦克风到扬声器的完整闭环
你有没有想过,一个能听懂你说"打开台灯"并立刻执行的小盒子,其实不需要联网?也不需要云服务器?甚至成本还不到一杯奶茶?
这不再是科幻。借助 ESP32-S3 和乐鑫官方推出的 ESP-SR 语音识别 SDK ,我们完全可以在一块不到 20 元的开发板上,实现一套真正意义上的本地智能语音助手系统------它能听见你说话、理解你的命令,并用声音回应你,全过程都在芯片内部完成,延迟低至 200ms 以内,且不上传任何音频数据。
听起来有点不可思议?但这就是边缘 AI 正在发生的真实变革。而今天我们要做的,就是亲手把这个"魔法"变成现实 🧪✨
为什么是 ESP32-S3?不是 STM32 或者树莓派?
市面上做嵌入式项目的 MCU 多如牛毛,那为啥偏偏选了 ESP32-S3 来搞语音交互?
先说结论: 它是目前性价比最高的支持 Wi-Fi/蓝牙 + 神经网络加速 + 音频接口三位一体的 RISC-V 替代方案之一。
我们来拆解一下它的硬实力:
- 双核 Xtensa LX7,主频高达 240MHz ------ 足够跑轻量级 DNN 模型;
- 支持外部 PSRAM(最高 16MB)------ 让你能加载中文唤醒词模型和几十条命令词;
- 内置 I2S、PDM、DAC 接口 ------ 直接对接数字麦克风和功放芯片,省掉一堆转接电路;
- 关键!有 AI 指令集扩展 ,比如 MAC(乘加)、VECTORED INTERRUPT(向量化中断),这对 MFCC 特征提取和神经网络推理速度提升非常关键;
- 官方提供完整的 ESP-SR SDK ,连模型都给你训练好了,直接调 API 就行。
相比之下,STM32F4 虽然性能也不错,但没 Wi-Fi、没蓝牙、没有专用语音框架,想做联网语音助手得外挂模块;树莓派倒是全能,可功耗高、体积大、价格贵,不适合电池供电或小型化产品。
所以如果你的目标是在一个小设备里塞进"听得见 + 会思考 + 能说话"的能力,ESP32-S3 几乎是现阶段最合理的选择 💡
听得清:麦克风怎么接?I2S 还是 PDM?
语音系统的起点永远是采集声音。对于 ESP32-S3 来说,主流方案有两种:
- 模拟麦克风 + 外部 ADC
- 数字麦克风(INMP441 / SPH0645LM4H)通过 PDM 或 I2S 输入
显然,第二种更优------毕竟数字信号抗干扰强、采样稳定,而且 ESP32-S3 原生支持 PDM 和 I2S 接收模式。
我们推荐使用 INMP441 数字麦克风
这是一款基于 PDM 编码的 MEMS 麦克风,工作电压 1.6~3.6V,与 ESP32-S3 完全兼容。只需要两个引脚:
-
CLK(时钟输入)
-
DAT(数据输出)
接线方式如下:
| ESP32-S3 | INMP441 |
|---|---|
| GPIO12 | CLK |
| GPIO13 | DAT |
然后在代码中配置为 PDM 模式即可开始录音 👇
c
i2s_config_t i2s_config = {
.mode = I2S_MODE_MASTER | I2S_MODE_RX,
.sample_rate = 16000,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = I2S_COMM_FORMAT_STAND_PDM,
.dma_buf_count = 8,
.dma_buf_len = 64,
.use_apll = true
};
// 注意:PDM 需要特殊引脚映射
i2s_pin_config_t pin_config = {
.bck_io_num = 12, // BCK 实际连接到 PDM CLK
.ws_io_num = -1, // WS 不使用
.data_out_num = -1,
.data_in_num = 13 // PDM 数据线
};
i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
i2s_set_pin(I2S_NUM_0, &pin_config);
📌 小贴士 :
-
PDM 是单线数据传输,靠 CLK 控制采样节奏,适合空间受限的设计;
-
如果你要用多个麦克风组成阵列做波束成形,建议改用 I2S 差分麦克风;
-
一定要注意电源去耦!给麦克风加个 10μF + 0.1μF 电容组合,否则容易引入电源噪声。
听得懂:本地语音识别到底怎么做到的?
很多人以为"语音识别=必须上云",但实际上,像"小爱同学"、"Hey Siri"这些产品的第一道关卡------ 唤醒检测 ------都是在本地完成的。
我们的目标也一样:让设备只在听到"小助手"之后才开始认真听你说话,其余时间保持低功耗待机。
这就轮到 ESP-SR SDK 登场了!
ESP-SR 是什么?
这是 Espressif 官方推出的一套专为 ESP32 系列优化的语音识别工具包,包含两个核心组件:
- WakeNet :深度学习模型,专门用来检测唤醒词(例如:"你好小智"、"Hi Bot")
- MultiNet :命令词识别模型,最多支持 50 个离散指令(如"开灯"、"播放音乐")
所有模型都已经做了量化压缩,可以直接烧录进 Flash,运行时不依赖文件系统,内存占用极低。
⚙️ 数据参考:WakeNet 模型大小约 40KB,运行 RAM 占用 < 80KB,在无 PSRAM 的 ESP32-S3 上也能跑。
识别流程长什么样?
整个过程可以分为四个阶段:
① 音频采集
每 20ms 获取一次 PCM 数据块(16kHz × 0.02s ≈ 320 样本),缓存起来准备处理。
② 前端处理(Audio Front-End)
这部分是语音识别的关键预处理步骤,主要包括:
-
预加重(Pre-emphasis) :增强高频成分,补偿发音中的自然衰减;
-
分帧加窗(Hamming Window) :把连续信号切成短段,减少频谱泄漏;
-
FFT + Mel 滤波器组 → MFCC 提取 :将时域信号转换为 13 维 MFCC 特征向量,作为模型输入。
这一系列操作原本很耗 CPU,但在 ESP32-S3 上得益于其 MAC 指令加速,MFCC 提取可在 10ms 内完成 ✅
③ 模型推理
将 MFCC 特征送入 WakeNet 模型进行前向传播,输出一个置信度分数(confidence score)。如果超过阈值(默认 0.8),就判定为有效唤醒。
一旦唤醒成功,立即切换到 MultiNet 模式,继续监听后续命令词。
④ 结果回调
返回识别出的命令 ID 或字符串,交由主逻辑处理。
整个流程完全离线,无需联网,也没有隐私泄露风险 🔐
如何集成到你的项目?
首先,在 idf.py menuconfig 中启用 ESP-SR 支持:
Component config --->
ESP-SR Configuration --->
[*] Enable WakeNet support
[*] Enable MultiNet support
(1) Default WakeNet model index
然后在代码中初始化并启动识别任务:
c
#include "wakenet/wakenet.h"
#include "multinet/multinet.h"
#include "wakenet/wakenet_model_0.h"
#include "multinet/multinet_model_zh_cn.h"
static const wake_word_list_t *wn_model = &g_wakenet_model_table[WAKENET_MODEL_0];
static const multinet_model_t *mn_model = &g_multinet_model_zh_cn;
void task_speech_recognition(void *arg) {
wakenet_handle_t wn_handle = wakenet_init(wn_model);
multinet_handle_t mn_handle = multinet_init(mn_model);
if (!wn_handle || !mn_handle) {
ESP_LOGE(TAG, "Failed to initialize speech engine");
vTaskDelete(NULL);
}
int16_t pcm_buffer[1024]; // ~64ms audio at 16kHz
size_t bytes_read;
while (1) {
// 读取音频流
i2s_read(I2S_NUM_0, pcm_buffer, sizeof(pcm_buffer), &bytes_read, portMAX_DELAY);
int len = bytes_read / sizeof(int16_t);
// 先尝试唤醒检测
int cmd_id = wakenet_process(wn_handle, pcm_buffer, len);
if (cmd_id >= 0) {
ESP_LOGI(TAG, "✅ Wake-up detected! CMD: %d", cmd_id);
trigger_response("已唤醒,请说命令");
// 切换到命令识别模式(持续监听 1~2 秒)
for (int i = 0; i < 5; i++) {
i2s_read(I2S_NUM_0, pcm_buffer, sizeof(pcm_buffer), &bytes_read, portMAX_DELAY);
len = bytes_read / sizeof(int16_t);
int result = multinet_process(mn_handle, pcm_buffer, len);
if (result > 0) {
handle_command(result); // 执行对应动作
break;
}
}
}
}
wakenet_destroy(wn_handle);
multinet_destroy(mn_handle);
vTaskDelete(NULL);
}
🎯 重点说明 :
-
wakenet_process()是非阻塞调用,每次传入新的音频块即可; -
模型索引
WAKENET_MODEL_0对应的是英文"Hi Alexa",如果你想用中文"小助手",需要选择其他模型编号或者自定义训练; -
实际部署时建议加入状态机管理(待机 → 唤醒 → 命令识别 → 回复 → 返回待机),避免误触发。
怎么让设备"开口说话"?
光听还不算完整交互。真正的语音助手还得能"回答"你。
比如你说:"现在几点?" 它应该回一句:"现在是下午三点二十分。"
这就涉及到音频播放系统了。
ESP32-S3 提供了三种主要方式:
| 方式 | 是否推荐 | 适用场景 |
|---|---|---|
| 内置 DAC 输出 | ❌ | 仅用于蜂鸣提示音 |
| I2S + 外部 DAC(如 MAX98357A) | ✅✅✅ | 高质量语音播报 |
| I2S + 音频编解码器(如 WM8978) | ✅ | 高保真需求 |
我们当然选第二种: I2S + MAX98357A ,因为它简单、便宜、效果好!
MAX98357A 是谁?为什么这么香?
这是一颗由 Maxim(现 ADI)推出的 I2S 接口 Class-D 数字功放芯片,特点如下:
- 支持标准 I2S、Left-Justified 输入格式
- 16~32bit 字长,最高支持 48kHz 采样率
- 内置放大器,最大输出功率 3.2W @ 4Ω
- THD+N < 1%,声音清晰干净
- 单电源供电(3.0--5.5V),完美匹配 ESP32-S3
最关键的是: 它不需要额外的控制信号!只要数据和时钟对上了,插上喇叭就能响!
接线也很简单:
| ESP32-S3 | MAX98357A |
|---|---|
| GPIO26 | BCLK |
| GPIO25 | LRCLK |
| GPIO27 | DIN |
| GND | GND |
| 5V | VIN |
扬声器直接接到 SPK+ 和 SPK− 就行。
播放 WAV 文件:如何跳过头部直接输出 PCM?
大多数 WAV 文件都有一个 44 字节的头部信息(RIFF header),里面记录了采样率、声道数等元数据。但我们不需要解析它------只需要把后面的 PCM 数据一股脑写进 I2S 就行。
示例函数如下:
c
extern const uint8_t welcome_wav_start[] asm("_binary_welcome_wav_start");
extern const uint8_t welcome_wav_end[] asm("_binary_welcome_wav_end");
void play_wav(const uint8_t *wav_data, size_t total_size) {
// 跳过 WAV header (通常 44 字节)
const uint8_t *audio_pos = wav_data + 44;
const uint8_t *end = wav_data + total_size;
size_t bytes_written;
i2s_channel_enable(I2S_NUM_1, I2S_CHANNEL_TX, 1000);
while (audio_pos < end) {
size_t chunk = ((end - audio_pos) > 1024) ? 1024 : (end - audio_pos);
i2s_write(I2S_NUM_1, (void*)audio_pos, chunk, &bytes_written, portMAX_DELAY);
audio_pos += bytes_written;
}
i2s_channel_disable(I2S_NUM_1, I2S_CHANNEL_TX, 1000);
}
💡 技巧 :利用 components/esptool_py/pycopy/data_bin_embed.py 工具,可以把 .wav 文件自动打包进固件,通过链接器符号访问,省去了 SPIFFS 文件系统的开销。
调用方式超简单:
c
play_wav(welcome_wav_start, welcome_wav_end - welcome_wav_start);
能不能让设备自己"生成"语音?TTS 上场!
上面播的是预先录制好的语音。但如果命令很多(比如天气、时间、温度),不可能每句都提前录好。
这时候就需要 TTS(Text-to-Speech) 技术,让设备实时"朗读"文本。
遗憾的是,ESP32-S3 上跑不了 Google TTS 或阿里云语音合成------太重了。
但我们有一个轻量级替代方案: NanoTTS !
这是一个专为嵌入式设计的极简中文 TTS 引擎,基于共振峰合成原理,代码仅几百行,内存占用 <50KB,完全可以跑在 ESP32-S3 上。
虽然音质不如真人,但胜在小巧可控,适合播报固定模板语句,例如:
"当前室温:26 度,湿度:54%。"
"明天晴转多云,气温 18 到 25 摄氏度。"
你可以把它想象成老式电子词典的声音 😂
集成方式大致如下:
c
// 输入文本 → 生成 PCM 流 → 写入 I2S
const char *text = "您好,我是本地语音助手";
int16_t *pcm_output;
size_t len = nanotts_synthesize(text, &pcm_output, 16000);
for (int i = 0; i < len; i += 1024) {
size_t chunk = (len - i > 1024) ? 1024 : (len - i);
i2s_write(I2S_NUM_1, pcm_output + i, chunk * 2, &bytes_written, portMAX_DELAY);
}
free(pcm_output);
📌 当前局限:NanoTTS 主要支持普通话,不支持语调变化和情感表达;更适合做工业级播报而非消费级助手。
未来如果有更强的芯片(比如 ESP32-H2 或 ESP32-P4),或许能跑更高级的 Tacotron 微型版本。
整体架构:从硬件到软件的全链路协同
现在让我们把所有模块串起来,看看整个系统是怎么运作的。
硬件连接概览
[INMP441 Mic]
│ PDM CLK/DATA
▼
[ESP32-S3 Module]
├─ I2S RX → Audio Input
├─ WakeNet/MultiNet → Local ASR
├─ GPIO → Relay / LED / Sensor
└─ I2S TX → MAX98357A → Speaker
电源部分建议分开处理:
-
数字部分(ESP32-S3)用 AMS1117-3.3V LDO 供电;
-
模拟部分(MAX98357A)单独用另一个 LDO 或滤波电感供电,避免开关噪声影响音质。
软件任务划分
FreeRTOS 下建议创建以下任务:
| 任务名称 | 功能 | 优先级 |
|---|---|---|
mic_capture_task |
持续采集音频,送入 WakeNet | 高 |
main_control_task |
处理识别结果,控制外设 | 中 |
response_playback_task |
播放语音反馈(防止阻塞主线程) | 高 |
ota_update_task (可选) |
支持远程升级固件和模型 | 低 |
通信机制可以用队列传递事件:
c
typedef enum {
EVT_WAKEUP,
EVT_COMMAND_OPEN_LIGHT,
EVT_COMMAND_QUERY_WEATHER,
} event_type_t;
QueueHandle_t event_queue;
// 在识别任务中发送事件
event_type_t evt = EVT_COMMAND_OPEN_LIGHT;
xQueueSend(event_queue, &evt, 0);
// 在主控任务中接收并处理
if (xQueueReceive(event_queue, &evt, portMAX_DELAY)) {
switch (evt) {
case EVT_COMMAND_OPEN_LIGHT:
gpio_set_level(GPIO_LED, 1);
play_wav(confirm_wav_start, ...);
break;
// ...
}
}
这样实现了松耦合设计,便于后期扩展新功能。
实战中踩过的坑,我都帮你试过了 ⚠️
再完美的理论也会被现实毒打。以下是我在实际调试过程中总结的一些经验教训:
❗ 问题 1:总是误唤醒?
环境噪音太大?隔壁同事说话都被识别成"唤醒"?
✅ 解决方案:
-
启用 AGC(自动增益控制),避免突发响声触发;
-
提高置信度阈值(从 0.8 提到 0.9);
-
使用双麦克风波束成形抑制侧向噪声(需硬件支持);
-
添加唤醒后延时关闭机制:比如 5 秒内无命令则自动休眠。
❗ 问题 2:播放语音卡顿、爆音?
DMA buffer 太小 or 任务调度不及时?
✅ 解决方案:
-
增大
dma_buf_count至 8~10,dma_buf_len至 128; -
将播放任务设为高优先级;
-
使用双缓冲机制,一边填充数据一边播放;
-
启用 APLL 锁定精确时钟源,避免采样率漂移导致失真。
❗ 问题 3:模型加载慢,开机要等好几秒?
Flash 读取速度跟不上?
✅ 解决方案:
-
把模型常量放在
.iram0.text段,强制加载到 IRAM; -
使用
__attribute__((section(".rodata")))控制布局; -
若使用 PSRAM,确保启用 cache 加速访问。
❗ 问题 4:功耗太高,无法电池供电?
一直在跑 I2S 录音?
✅ 解决方案:
-
非唤醒时段关闭 I2S 接收通道;
-
使用 ULP 协处理器监听特定 GPIO(如按键唤醒);
-
进入深度睡眠模式,仅保留 RTC 存储上下文;
-
触发源可以是定时唤醒 or 外部中断。
自定义唤醒词:我能把自己的名字设成"唤醒口令"吗?
当然可以!虽然 ESP-SR 提供了几种预训练模型(如"Hi Alexa"、"こんにちは"),但你也完全可以训练属于自己的唤醒词。
方法有两种:
方法一:使用 Espressif 提供的在线训练工具(推荐新手)
访问 https://speech.ai-service.dev (需注册账号),上传至少 30 条你自己念"小助手"的录音样本(每条约 1~2 秒),系统会自动生成一个 .bin 模型文件,下载后替换原有模型即可。
要求:
-
录音清晰,背景安静;
-
发音一致,避免夸张语调;
-
格式为 16kHz/16bit 单声道 WAV。
方法二:本地训练(进阶玩家)
使用 PyTorch 搭建 TinySpeech 模型结构,配合 ESP-IDF 的模型转换工具链导出为 C 头文件。
优点是可以完全控制模型大小和精度;缺点是需要一定的 ML 基础。
示例项目参考:GitHub 上搜索
esp-sr custom wakeword training
成本核算:这套系统到底多少钱?
别被"AI"吓住,实际上整套硬件成本相当亲民:
| 模块 | 型号 | 单价(人民币) |
|---|---|---|
| 主控芯片 | ESP32-S3-WROOM-1-N8 | ¥18 |
| 数字麦克风 | INMP441 | ¥3 |
| 功放芯片 | MAX98357A(SOIC封装) | ¥6 |
| 扬声器 | Φ30mm 8Ω 0.5W | ¥2 |
| 辅助元件(电容电阻等) | ------ | ¥1 |
| 合计 | ¥30 左右 |
再加上外壳和 PCB 板,总量产的话单价还能压到 ¥25 以内。
对比市面上动辄上百元的儿童故事机或智能插座,这个方案极具竞争力 💥
能做什么有趣的产品?灵感来了 🚀
别只停留在"点灯关灯",我们可以玩得更野一点:
🎯 场景 1:儿童语音故事机
- 孩子说:"讲个恐龙的故事"
- 设备播放预存的音频:"很久以前,在白垩纪......"
- 支持 OTA 更新故事库,家长可通过手机 App 添加新内容
🎯 场景 2:工厂设备语音面板
- 工人戴着手套喊:"启动 3 号机床"
- PLC 接收 GPIO 信号执行动作
- 设备回复:"3 号机床已启动,请注意安全"
免去了触摸屏操作的麻烦,特别适合油污、潮湿环境。
🎯 场景 3:盲人辅助终端
- 用户问:"现在几点?"
- 设备通过 NTP 获取时间后 TTS 播报
- 结合温湿度传感器:"当前温度 26 度,天气晴朗"
无需联网,保护隐私的同时提供基础信息服务。
最后的思考:边缘语音的边界在哪里?
当我们能在一块 20 块钱的芯片上实现"听 + 思考 + 说"的完整闭环时,意味着什么?
意味着 智能不再集中于云端,而是分散到每一个角落 。
你家的台灯、水杯、门锁、玩具熊......都可以拥有"耳朵"和"嘴巴"。
更重要的是,这种智能是 私有的、即时的、可靠的 ------不会因为断网而瘫痪,也不会因为服务器宕机而沉默。
当然,现在的 ESP32-S3 还做不到理解复杂语义,也无法进行多轮对话。但它已经足够胜任"关键词触发 + 动作响应"这类典型任务。
而这,正是万物互联时代最需要的基础能力。
也许几年后,我们会回头看今天这个项目,就像当年看第一个点亮 LED 的 "Hello World" 一样------简单,却意义非凡。
而现在,轮到你动手了。
要不要试试看,让你的第一个设备,叫出你的名字?🎤🔥