
一、软硬件环境说明
硬件环境: ESP32-S3+INMP441+MAX98357A
软件环境:Arduino 开发环境
1. 硬件说明
ESP32-S3-DevKitC-1 是一款入门级开发板,搭载 Wi-Fi + Bluetooth® LE 模组 ESP32-S3-WROOM-1、ESP32-S3-WROOM-1U 或 ESP32-S3-WROOM-2。
|--------------|-------------------|
| 参数项 | 规格说明 |
| Flash 存储 | 16MB,用于存放固件与音频资源 |
| PSRAM | 8MB,用于高速音频缓冲与数据处理 |
| 核心架构 | 双核 240MHz,确保实时响应 |

输入端 - INMP441 数字麦克风
高灵敏度与数字信号直传特性,降低噪声干扰。

输出端 - MAX98357A D 类功放
高效率驱动能力,适合直接驱动扬声器。

2. 依赖库安装
依赖:ESP32 内置 driver/i2s.h、SPIFFS.h、Adafruit_NeoPixel 库(一键安装即可)
Arduino 库管理器搜索安装:Adafruit NeoPixel(作者 Adafruit)
安装方法(Arduino IDE)
- 顶部菜单:项目 → 加载库 → 管理库
- 搜索:
Adafruit NeoPixel - 作者:Adafruit
- 点击安装(建议安装最新稳定版)
3. ESP32-S3 N16R8开发板配置
- 开发板:
ESP32S3 Dev Module - Flash Size:
16MB (QIO) - PSRAM:
8MB PSRAM - USB Mode:
USB OTG
二、硬件接线说明
1. INMP441(I2S0 输入录音)
|-------------|-------------------|
| INMP441 | ESP32-S3 GPIO |
| SCK | 4 |
| WS | 5 |
| SD | 6 |
| VCC | 3.3V |
| GND | GND |
2. MAX98357A(I2S1 输出播放)
|--------------|-------------------|
| MAX98357 | ESP32-S3 GPIO |
| BCLK | 15 |
| LRCLK | 16 |
| DIN | 7 |
| SD | 3.3V(必须拉高使能) |
| VIN | 5V |
| GND | GND |
关键:三个模块共 GND,否则电流噪声巨大;MAX98357 SD 脚悬空无声。
3. 板载外设
- BOOT 按键:GPIO0(无需外接)
- WS2812:GPIO48(开发板自带)
三、程序功能说明
1. 短按播放功能
- 松手后触发播放当前
recordN.wav - WS2812 亮绿色,播放完毕熄灭
- 文件序号自动 + 1,5 之后回到 1 循环
2. 长按录音功能
- 按住 BOOT 键不放,内部计时走到 500ms立刻启动录音,红灯点亮
- 只要手指不松开就一直录,松开按键瞬间停止录音、保存 WAV、红灯熄灭
- 保护限制:最长连续录制 60 秒,到点强制结束
- 保存成功后文件序号 + 1,满 5 个覆盖第一个
3. LED 状态显示功能
- 空闲:全灭
- 按住录音中:红色常亮
- 文件播放中:绿色常亮
4. 其他功能
- 文件:
record1.wav ~ record5.wav循环覆盖 - 每次录制 / 播放结束自动打印 SPIFFS 总容量、已用、剩余空间
- I2S 每次使用完毕卸载端口,杜绝 ESP32-S3 内核 Guru 崩溃
- 硬件接线完全沿用之前即可
四、程序代码
-
I2S0 硬件通道专门做录音 RX读取 INMP441 的 I2S PCM 数字音频;
-
I2S1 硬件通道专门做播放 TX推送数据给 MAX98357;
-
采样率 16kHz、16bit 单声道,是语音最低开销标准;
-
环形数组做短时缓冲,避免 S3 任务调度造成断音、卡顿;
-
纯硬件 I2S DMA 搬运,CPU 占用极低。
#include <Arduino.h>
#include <driver/i2s.h>
#include <SPIFFS.h>
#include <Adafruit_NeoPixel.h>// ===================== 硬件引脚 =====================
#define I2S0_BCLK 4
#define I2S0_WS 5
#define I2S0_IN 6#define I2S1_BCLK 15
#define I2S1_WS 16
#define I2S1_OUT 7#define BOOT_KEY_PIN 0
#define WS2812_PIN 48
#define NUMPIXELS 1// 音频参数
#define SAMPLE_RATE 16000
#define BITS_PER_SAMPLE 16
#define CHANNELS 1
#define BUF_SAMPLES 128// 业务规则
#define MAX_RECORD_NUM 5
#define LONG_PRESS_THR 500 // 按住超过500ms判定为录音模式
#define MAX_RECORD_DUR 60000 // 最长录音60秒(ms)// WAV文件头结构体
typedef struct {
char riff[4];
uint32_t file_len;
char wave[4];
char fmt[4];
uint32_t fmt_len;
uint16_t audio_fmt;
uint16_t channels;
uint32_t sample_rate;
uint32_t byte_rate;
uint16_t block_align;
uint16_t bits_per_sample;
char data_tag[4];
uint32_t data_len;
} WavHeader;Adafruit_NeoPixel led(NUMPIXELS, WS2812_PIN, NEO_GRB + NEO_KHZ800);
File wavFile;
int16_t audioBuf[BUF_SAMPLES];
uint8_t currentFileIndex = 1;// ===================== I2S 配置 =====================
const i2s_config_t i2sCfgRx = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = 0,
.dma_buf_count = 4,
.dma_buf_len = BUF_SAMPLES,
.use_apll = true,
.tx_desc_auto_clear = false,
.fixed_mclk = 0
};const i2s_pin_config_t pinCfgRx = {
.mck_io_num = I2S_PIN_NO_CHANGE,
.bck_io_num = I2S0_BCLK,
.ws_io_num = I2S0_WS,
.data_out_num = I2S_PIN_NO_CHANGE,
.data_in_num = I2S0_IN
};const i2s_config_t i2sCfgTx = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = 0,
.dma_buf_count = 4,
.dma_buf_len = BUF_SAMPLES,
.use_apll = true,
.tx_desc_auto_clear = true,
.fixed_mclk = 0
};const i2s_pin_config_t pinCfgTx = {
.mck_io_num = I2S_PIN_NO_CHANGE,
.bck_io_num = I2S1_BCLK,
.ws_io_num = I2S1_WS,
.data_out_num = I2S1_OUT,
.data_in_num = I2S_PIN_NO_CHANGE
};// ===================== 工具函数 =====================
void setLed(uint8_t r, uint8_t g, uint8_t b) {
led.setPixelColor(0, led.Color(r, g, b));
led.show();
}String getFileName(uint8_t idx) {
return "/record" + String(idx) + ".wav";
}void writeWavHeader(File &f, uint32_t dataLen) {
WavHeader hdr;
uint32_t byteRate = SAMPLE_RATE * CHANNELS * (BITS_PER_SAMPLE / 8);
uint16_t blockAlign = CHANNELS * (BITS_PER_SAMPLE / 8);memcpy(hdr.riff, "RIFF", 4); hdr.file_len = 36 + dataLen; memcpy(hdr.wave, "WAVE", 4); memcpy(hdr.fmt, "fmt ", 4); hdr.fmt_len = 16; hdr.audio_fmt = 1; hdr.channels = CHANNELS; hdr.sample_rate = SAMPLE_RATE; hdr.byte_rate = byteRate; hdr.block_align = blockAlign;hdr.bits_per_sample = BITS_PER_SAMPLE;
memcpy(hdr.data_tag, "data", 4);
hdr.data_len = dataLen;f.write((uint8_t *)&hdr, sizeof(WavHeader));
}void printSPIFFSInfo() {
size_t totalBytes = SPIFFS.totalBytes();
size_t usedBytes = SPIFFS.usedBytes();
size_t freeBytes = totalBytes - usedBytes;Serial.println("==================== SPIFFS 空间 ====================");
Serial.printf("总容量: %d (%.2f KB)\n", totalBytes, totalBytes / 1024.0);
Serial.printf("已使用: %d (%.2f KB)\n", usedBytes, usedBytes / 1024.0);
Serial.printf("剩余: %d (%.2f KB)\n", freeBytes, freeBytes / 1024.0);
Serial.println("====================================================\n");
}// 按住录音,松开停止,最长60秒
uint32_t holdRecording(uint8_t fileIdx) {
String filePath = getFileName(fileIdx);
Serial.printf("开始录音流程,保存至 %s,最长60秒上限\n", filePath.c_str());setLed(255, 0, 0); // 红灯 录音中
i2s_driver_install(I2S_NUM_0, &i2sCfgRx, 0, NULL);
i2s_set_pin(I2S_NUM_0, &pinCfgRx);
i2s_zero_dma_buffer(I2S_NUM_0);if (SPIFFS.exists(filePath)) {
SPIFFS.remove(filePath);
Serial.printf("覆盖旧文件: %s\n", filePath.c_str());
}
wavFile = SPIFFS.open(filePath, FILE_WRITE);
if (!wavFile) {
Serial.println("文件创建失败");
i2s_driver_uninstall(I2S_NUM_0);
setLed(0, 0, 0);
return 0;
}wavFile.seek(sizeof(WavHeader));
uint32_t totalDataLen = 0;
unsigned long startMs = millis();// 循环条件:按键仍按住 并且 未超60秒最大时长
while (digitalRead(BOOT_KEY_PIN) == LOW && (millis() - startMs) < MAX_RECORD_DUR) {
size_t readLen = 0;
i2s_read(I2S_NUM_0, audioBuf, sizeof(audioBuf), &readLen, 10);
if (readLen > 0) {
wavFile.write((uint8_t *)audioBuf, readLen);
totalDataLen += readLen;
}
}// 收尾保存WAV
unsigned long recTime = millis() - startMs;
wavFile.seek(0);
writeWavHeader(wavFile, totalDataLen);
wavFile.close();
i2s_driver_uninstall(I2S_NUM_0);
setLed(0, 0, 0);Serial.printf("录音结束,实际时长:%d ms,音频数据长度:%d Bytes\n", recTime, totalDataLen);
printSPIFFSInfo();
return recTime;
}// 播放指定序号文件
void playAudio(uint8_t fileIdx) {
String filePath = getFileName(fileIdx);
Serial.printf("播放文件: %s\n", filePath.c_str());if (!SPIFFS.exists(filePath)) {
Serial.printf("文件 %s 不存在,跳过播放\n", filePath.c_str());
return;
}setLed(0, 255, 0);
wavFile = SPIFFS.open(filePath, FILE_READ);
if (!wavFile) {
Serial.println("打开播放文件失败");
setLed(0, 0, 0);
return;
}wavFile.seek(sizeof(WavHeader));
i2s_driver_install(I2S_NUM_1, &i2sCfgTx, 0, NULL);
i2s_set_pin(I2S_NUM_1, &pinCfgTx);
i2s_zero_dma_buffer(I2S_NUM_1);while (wavFile.available()) {
size_t rlen = wavFile.read((uint8_t *)audioBuf, sizeof(audioBuf));
if (rlen > 0) {
size_t wlen;
i2s_write(I2S_NUM_1, audioBuf, rlen, &wlen, portMAX_DELAY);
}
}wavFile.close();
i2s_driver_uninstall(I2S_NUM_1);
setLed(0, 0, 0);
Serial.println("播放完成\n");
printSPIFFSInfo();
}void setup() {
Serial.begin(115200);led.begin();
led.setBrightness(80);
setLed(0, 0, 0);pinMode(BOOT_KEY_PIN, INPUT_PULLUP);
if (!SPIFFS.begin(true)) {
Serial.println("SPIFFS 挂载失败");
} else {
Serial.println("SPIFFS 挂载成功");
printSPIFFSInfo();
}
}void loop() {
// 检测按键按下
if (digitalRead(BOOT_KEY_PIN) == LOW) {
unsigned long pressStart = millis();
bool isRecordMode = false;// 持续按住循环检测 while (digitalRead(BOOT_KEY_PIN) == LOW) { unsigned long heldTime = millis() - pressStart; // 按住超过500ms,进入录音模式 if (heldTime >= LONG_PRESS_THR && !isRecordMode) { isRecordMode = true; holdRecording(currentFileIndex); } delay(10); } // 松手后总按下时长 unsigned long totalHold = millis() - pressStart; if (isRecordMode) { // 有效录音,序号轮转 currentFileIndex++; if (currentFileIndex > MAX_RECORD_NUM) { currentFileIndex = 1; } Serial.printf("下次录音将保存至 record%d.wav\n\n", currentFileIndex); } else { // 按住不足500ms,短按 = 切换播放 playAudio(currentFileIndex); currentFileIndex++; if (currentFileIndex > MAX_RECORD_NUM) { currentFileIndex = 1; } Serial.printf("下次播放将指向 record%d.wav\n\n", currentFileIndex); }}
delay(20);
}
五、 I2S 完整解析
适用:ESP-IDF Legacy I2S API,driver/i2s.h
1. 关键 API 汇总
|----------------------|---------------------|
| 函数 | 作用 |
| i2s_driver_install | 注册初始化 I2S 硬件外设 |
| i2s_set_pin | I2S 外设绑定 GPIO 引脚 |
| i2s_zero_dma_buffer | 清空 DMA 残留音频,消除上电杂音 |
| i2s_read | I2S 接收:读取麦克风 PCM 数据 |
| i2s_write | I2S 发送:推送 PCM 数据到功放 |
| i2s_driver_uninstall | 释放 I2S 驱动资源 |
2. 两个核心结构体
① i2s_config_t I2S 通用时序配置(RX/_TX 分别定义)
const i2s_config_t i2sCfgRx = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX), // 主机模式 + I2S接收(麦克风)
.sample_rate = SAMPLE_RATE, // 采样率 16000Hz
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // 每个采样16bit
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, // 仅左声道(INMP441单声道输出)
.communication_format = I2S_COMM_FORMAT_STAND_I2S, // 标准I2S格式
.intr_alloc_flags = 0, // 中断默认配置
.dma_buf_count = 4, // DMA缓冲区数量 =4
.dma_buf_len = BUF_SAMPLES, // 每个DMA缓冲区采样点数=128
.use_apll = true, // 使用APLL高精度时钟,音频采样更稳定,减少噪声、频偏
.tx_desc_auto_clear = false, // RX接收:不自动清理发送描述符
.fixed_mclk = 0
};
const i2s_config_t i2sCfgTx = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX), // 主机模式 + I2S发送(功放)
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = 0,
.dma_buf_count = 4,
.dma_buf_len = BUF_SAMPLES,
.use_apll = true,
.tx_desc_auto_clear = true, // TX开启:发送完成自动清空DMA缓冲区,避免杂音残留
.fixed_mclk = 0
};
DMA 缓冲说明(音频缓冲核心)
dma_buf_count=4、dma_buf_len=128
- ESP32 I2S 依靠DMA 搬运音频数据,CPU 不需要逐字节搬运,降低占用
- 总 DMA 缓存采样:4 × 128 = 512 个采样点
- 作用:平滑数据流,防止 CPU 读写文件卡顿造成音频断音、爆音
② i2s_pin_config_t I2S 引脚映射
const i2s_pin_config_t pinCfgRx = {
.mck_io_num = I2S_PIN_NO_CHANGE, // 不使用MCLK主时钟(INMP441无需MCLK)
.bck_io_num = I2S0_BCLK, // BCLK位时钟
.ws_io_num = I2S0_WS, // WS左右声道帧时钟
.data_out_num = I2S_PIN_NO_CHANGE, // RX无数据输出引脚
.data_in_num = I2S0_IN // 麦克风数据输入引脚
};
TX 结构体相反:data_out_num指定输出引脚,data_in_num悬空
3. I2S 初始化流程
// 1. 安装I2S0驱动,传入配置
i2s_driver_install(I2S_NUM_0, &i2sCfgRx, 0, NULL);
// 2. 绑定GPIO引脚到I2S硬件外设
i2s_set_pin(I2S_NUM_0, &pinCfgRx);
// 3. 清空DMA缓存残留数据,防止上电杂音
i2s_zero_dma_buffer(I2S_NUM_0);
⚠️ 本程序设计:每次录音开始才安装驱动,录音结束 i2s_driver_uninstall 卸载 优点:空闲释放外设资源;缺点:频繁安装卸载有微小耗时,适合低频录音场景,不适合持续实时对讲。 播放函数 playAudio 对 I2S_NUM_1 使用完全一致初始化流程。
4. I2S 音频采集
size_t readLen = 0;
// 从I2S0 DMA缓冲区读取音频到内存数组 audioBuf
i2s_read(I2S_NUM_0, audioBuf, sizeof(audioBuf), &readLen, 10);
参数解释:
I2S_NUM_0:指定 I2S 硬件端口audioBuf:应用层软件缓冲区(int16_t audioBuf 128),存放读到的 16bit PCM 采样sizeof(audioBuf):期望读取字节数readLen:函数执行完成后,实际读到的有效字节10:超时时间 (ms),超时未读到数据直接返回
采集数据流链路
INMP441麦克风 → I2S0硬件 → DMA环形缓冲区 → i2s_read → audioBuf(软件缓存) → SPIFFS写入文件
分层缓冲:硬件 DMA 缓冲 + 上层数组 audioBuf 二级缓冲,保障音频连续
5. I2S 音频播放
size_t wlen;
i2s_write(I2S_NUM_1, audioBuf, rlen, &wlen, portMAX_DELAY);
- 读取 SPIFFS 中 WAV 的 PCM 原始数据放入 audioBuf
i2s_write将内存 PCM 数据写入 I2S1 的 DMA 缓冲区- I2S 硬件自动按照 16kHz 时序输出给 MAX98357 功放
portMAX_DELAY:永久阻塞等待缓冲区可写入(播放场景,保证音频不中断)
播放数据流链路:SPIFFS WAV文件 → audioBuf软件缓存 → i2s_write → I2S1 DMA缓冲区 → I2S硬件 → MAX98357 → 扬声器
6. I2S 资源释放
i2s_driver_uninstall(I2S_NUM_0);
录音 / 播放完成后卸载驱动,释放 I2S 外设、DMA 内存,避免多次重复安装驱动报错。