ESP32 实战教程:用 I2S 麦克风采集并保存 WAV 音频
这篇不做“音量条跳一跳”就结束,而是认真录一段能下载、能播放、能分析的声音。项目会把 I2S 的三根线、DMA 缓冲、PCM 数据和 WAV 文件头串起来。录音过程中,ESP32 终于有机会把“我听到了”变成一个电脑真正认识的文件。

一、为什么选择数字 I2S 麦克风
INMP441、SPH0645 这类麦克风内部已经完成了模拟放大和模数转换,输出的是 I2S 数字音频。相比模拟麦克风加 ADC,I2S 方案少受模拟电源噪声、ADC 参考电压和地线布局影响,数据也更容易直接送入 DMA。
当然,数字并不等于“插上就有声音”。I2S 至少包含 BCLK、WS/LRCLK 和 DIN 三根线,ESP32 作为主机时负责产生时钟。INMP441 的 L/R 选择脚决定它输出左声道还是右声道,如果软件读取了另一个槽,就会得到一串接近安静的数字。这个坑特别有教育意义,因为它告诉我们:协议正确,不代表数据一定在你以为的位置。
二、接线和供电注意事项
以 INMP441 为例,常见接线如下(GPIO 可以根据板子调整):
- SCK/BCLK -> GPIO 26;
- WS/LRCLK -> GPIO 25;
- SD -> GPIO 33;
- VDD -> 3.3 V;
- GND -> 共地;
- L/R -> GND 或 3.3 V,决定左/右槽。
麦克风供电一定以模块规格为准。长杜邦线、USB 线材和 SD 卡同时工作时,电源噪声会让录音出现“电流声”和周期性咔哒声。布线时把数字麦克风和 SD 卡的地线回路做短,必要时在电源旁加去耦电容。
三、Arduino Core 3.x 的 I2S 初始化
当前 Arduino-ESP32 的推荐接口是 ESP_I2S.h 中的 I2SClass,可以先设置引脚,再调用 begin()。下面示例采用 16 kHz、16 bit、单声道接收,适合语音记录和简单声音检测。
#include <Arduino.h>
#include <ESP_I2S.h>
I2SClass I2S;
constexpr int PIN_BCLK = 26;
constexpr int PIN_WS = 25;
constexpr int PIN_DIN = 33;
void setupI2S() {
I2S.setPins(PIN_BCLK, PIN_WS, -1, PIN_DIN);
bool ok = I2S.begin(I2S_MODE_STD, 16000,
I2S_DATA_BIT_WIDTH_16BIT,
I2S_SLOT_MODE_MONO);
if (!ok) {
Serial.println("I2S begin failed");
while (true) delay(1000);
}
}
void setup() {
Serial.begin(115200);
setupI2S();
}
void loop() {
int available = I2S.available();
if (available > 0) {
uint8_t buffer[512];
size_t n = I2S.readBytes((char*)buffer, min(available, 512));
Serial.printf("read %u bytes\\n", (unsigned)n);
}
}
旧教程中常见 driver/i2s.h、i2s_driver_install() 等写法,在旧版 Arduino Core 和 ESP-IDF 项目里仍然有价值,但不能和新的 I2SClass 接口混用。升级开发环境时,先确认头文件和示例对应同一套 API。原版 ESP32 的 16 位单声道还存在硬件槽位限制,Arduino 库会在底层做兼容转换,但这也是为什么文档强调要确认声道模式。
四、WAV 文件到底是什么
WAV 不是一种神秘的“音频格式”,最常见的是 RIFF 容器加 PCM 裸数据。文件开头 44 字节描述采样率、位宽、通道数和数据长度,后面紧跟一串 little-endian 的采样值。
RIFF + fileSize + WAVE
fmt + 16 + PCM 参数
data + dataSize + PCM 样本
录音开始时我们还不知道最终文件有多大,所以先写一个占位头;录音结束后回到文件开头,把 fileSize 和 dataSize 回填。这个过程很像先写发票抬头,最后数清楚购物车里有几件东西再填总价。
#include <FS.h>
void writeLE16(File& f, uint16_t v) {
uint8_t b[2] = {(uint8_t)(v & 0xff), (uint8_t)(v >> 8)};
f.write(b, 2);
}
void writeLE32(File& f, uint32_t v) {
uint8_t b[4] = {(uint8_t)(v & 0xff), (uint8_t)(v >> 8),
(uint8_t)(v >> 16), (uint8_t)(v >> 24)};
f.write(b, 4);
}
void writeWavHeader(File& f, uint32_t sampleRate,
uint16_t bits, uint16_t channels,
uint32_t dataSize) {
f.seek(0);
f.write((const uint8_t*)"RIFF", 4);
writeLE32(f, 36 + dataSize);
f.write((const uint8_t*)"WAVEfmt ", 8);
writeLE32(f, 16); // PCM fmt chunk size
writeLE16(f, 1); // PCM
writeLE16(f, channels);
writeLE32(f, sampleRate);
writeLE32(f, sampleRate * channels * bits / 8);
writeLE16(f, channels * bits / 8);
writeLE16(f, bits);
f.write((const uint8_t*)"data", 4);
writeLE32(f, dataSize);
}
五、录音循环:按块读、按块写
录音时不要每读两个字节就 File.write() 一次,那会放大文件系统开销。让 I2S DMA 一次准备 512 或 1024 字节,程序批量写入,既减少调用次数,也能让任务调度更平稳。

File wav;
uint32_t dataBytes = 0;
const uint32_t MAX_BYTES = 16000UL * 2 * 10; // 10 秒单声道 16 bit
void recordWav(fs::FS& fs, const char* path) {
wav = fs.open(path, FILE_WRITE);
if (!wav) return;
writeWavHeader(wav, 16000, 16, 1, 0);
uint8_t buffer[1024];
uint32_t start = millis();
dataBytes = 0;
while (millis() - start < 10000 && dataBytes < MAX_BYTES) {
int available = I2S.available();
if (available <= 0) { delay(1); continue; }
size_t want = min((int)sizeof(buffer), available);
size_t got = I2S.readBytes((char*)buffer, want);
if (got == 0) continue;
wav.write(buffer, got);
dataBytes += got;
}
writeWavHeader(wav, 16000, 16, 1, dataBytes);
wav.close();
}
示例中使用 fs::FS,可以接 LittleFS,也可以接 SD。LittleFS 适合保存短录音和配置,SD 卡适合多段长录音。无论选择哪种介质,都要检查剩余空间,并给文件名加时间戳或递增编号,避免每次开机都覆盖同一段“最新录音”。
六、如何判断录到的声音是否正常
第一步看文件能否被播放器打开;第二步看文件时长是否接近预期;第三步用 Audacity 或 ffplay 查看波形。完全平直通常是声道槽位或 DIN 接线错误,持续满幅则可能是位宽解析错误或数据没有右移。底噪很大时,先短接/遮住麦克风不是好方法,应检查电源、地线和 I2S 线长。
INMP441 常见的 24 bit 数据会放在 32 bit 槽内,软件最后取哪 16 位取决于驱动配置。如果把 32 位样本直接当成 16 位写入 WAV,播放器虽然能打开,听起来会像机器人在水里说话。建议先用串口打印几帧原始值,再确认有效位所在位置。
七、加一个网页下载录音
当录音已经保存到 LittleFS 或 SD,ESP32 可以提供一个简单的下载接口:设置正确的 audio/wav Content-Type,使用文件流式发送,不要把整段文件读进 String。录音按钮则负责开始任务,任务完成后在网页显示文件大小和下载链接。
如果还要同时采集摄像头视频,建议把 I2S 采集、JPEG 编码和 HTTP 流分成独立任务,并严格控制 DMA 缓冲和堆内存。摄像头项目里“声音能录,视频一开就重启”,往往不是麦克风坏了,而是多个高吞吐任务同时抢内存。
八、延伸实验和避坑总结
可以继续加入 VAD(语音活动检测)、按音量自动分段、录音前后各保留一秒缓冲、上传到 NAS、或把 WAV 转成 Opus 后再传输。学习时建议先固定 16 kHz/16 bit/mono 这一组参数,等文件链路稳定后再提高采样率;一次改变太多变量,排错会变成“每个变量都嫌疑很大”。
本项目的核心不是做出录音笔,而是理解一条完整数据链:I2S 时钟 -> DMA 缓冲 -> PCM 样本 -> WAV 头 -> 文件系统。链路上任何一环的单位不一致,最终就会表现为噪声、速度不对或文件无法播放。把每一环单独验证,声音自然会从“数字噪音”变成一段像样的录音。
参考资料
打赏
如果这篇学习记录对你有帮助,欢迎扫码支持后续整理。

打赏支持:王庆岩
浙公网安备 33010602011771号