这次调试的目标很简单:让 ESP32-S3 通过板载麦克风录音,并把录音保存成 WAV 文件到 SD 卡中

但是在整个过程中有遇到很多的问题,并不是一开始就顺利成功,中间踩了很多坑,比如:

1. 一开始误以为是 I2S 数字麦克风直连 ESP32-S3
2. SD 卡 SPI 引脚确认和挂载问题
3. I2S 的 BCLK / WS / DIN 引脚配错
4. ES8311 没有真正打开 ADC / MIC 输入通道
5. MCLK 是否需要提供的问题
6. 裸读 i2s_channel_read() 一直读到 0
7. 最后改成 esp_codec_dev 打开 input_dev 后成功读到 PCM

一、硬件结构确认

在最开始看到麦克风原理图时,因为不同硬件版本的原因,导致看错原理图,以为I2S数字麦克风是直接连接ESP32-S3,把他当成普通麦克风处理。

但是在后面确认官方原理图后发现,这块板子的录音链路不是直接连接,而是:

模拟麦克风 MK1
    ↓
ES8311 音频 Codec
    ↓
I2S
    ↓
ESP32-S3

也就是麦克风本身是模拟输入,真正负责把模拟声音转成数字信号的是ES8311。

因此,esp32-s3不能直接通过I2S读麦克风,必须通过I2C配置ES8311,让它打开麦克风输入、ADC、增益和I2S输出,然后S3才能从I2S DIN线上读取音频数据。

二、SD 卡部分先跑通

直接使用sd卡的spi模式,复制之前的sd卡模块直接使用,然后从原理图中读取sd卡的引脚信息,进行配置。中间出现了:

sdmmc_card_init failed (0x107)
ESP_ERR_TIMEOUT

最后发现是卡没有插入,所以在测试时一定要注意sd卡是否有插入,否则可能出现无用功。

三、最开始的录音思路是错的

一开始我写了一个 andio.c,思路是:

初始化 I2S
直接 i2s_channel_read()
把读到的数据写成 WAV

也就是:

i2s_channel_read(rx_chan, raw_buf, sizeof(raw_buf), &bytes_read, portMAX_DELAY);

但是录出来的 WAV 文件完全没有声音。

第一次文件里的数据基本全是:

-1, -1, -1, -1...

后来改了 GPIO 后,又变成:

0, 0, 0, 0...

这里可以判断:这不是“声音太小”,而是 根本没有读到有效音频流

因为正常情况下,即使环境很安静,麦克风底噪也会让采样值有轻微波动,不应该整段数据完全固定为 0 或 -1。

四、I2S 引脚曾经配错

一开始根据原理图看错过一次 I2S 引脚,曾经配置成:

BCK = 14
WS  = 12
DIN = 47

后来从官方 atk-dnesp32s3-box 的板级配置确认,正确的是:

#define AUDIO_I2S_GPIO_WS    GPIO_NUM_13
#define AUDIO_I2S_GPIO_BCLK  GPIO_NUM_21
#define AUDIO_I2S_GPIO_DIN   GPIO_NUM_47
#define AUDIO_I2S_GPIO_DOUT  GPIO_NUM_14

也就是录音相关的关键引脚是:

#define ANDIO_I2S_BCK_GPIO   GPIO_NUM_21
#define ANDIO_I2S_WS_GPIO    GPIO_NUM_13
#define ANDIO_I2S_DIN_GPIO   GPIO_NUM_47

其中:

BCLK / BCK:位时钟
WS / LRCK:左右声道帧同步
DIN:ES8311 输出到 ESP32-S3 的录音数据
DOUT:ESP32-S3 输出到 ES8311 的播放数据

GPIO14 是 DOUT,也就是播放方向,不是 BCLK。

这个地方如果配错,即使 ES8311 初始化成功,I2S 时序也不对,读到的数据还是会异常。

五、MCLK 问题的确认

一开始看到 ES8311 原理图里有:

MCLK / SCLK 4.096MHz

所以怀疑是不是必须给 ES8311 提供 MCLK。

但后面看官方板级代码后确认,这块板子的官方配置是:

MCLK = GPIO_NUM_NC
use_mclk = false

也就是说,这块板子官方并没有使用独立 MCLK 引脚,而是让 ES8311 在无 MCLK 的模式下工作,依靠 I2S 的 BCLK / WS 相关时钟完成音频同步。

所以最后代码里保持:

#define ANDIO_I2S_MCLK_GPIO GPIO_NUM_NC

不需要飞线补 MCLK。

这个结论很重要:不要看到 MCLK 名字就立刻认为一定要外接时钟,要看官方板级配置和 codec 初始化方式。

六、真正的问题:ES8311 输入通道没有打开

之前的代码虽然调用了类似:

es8311_init();
es8311_microphone_config();
es8311_microphone_gain_set();

但是依然全 0。

后面对照官方代码后发现,官方并不是直接裸读 I2S,而是通过 esp_codec_dev 这一套流程打开音频输入设备。

官方关键流程大概是:

创建 I2S TX/RX 通道
    ↓
创建 I2S data interface
    ↓
创建 I2C control interface
    ↓
创建 ES8311 codec interface
    ↓
创建 input_dev
    ↓
esp_codec_dev_open(input_dev, &fs)
    ↓
esp_codec_dev_set_in_gain(input_dev, 40.0)
    ↓
esp_codec_dev_read(input_dev, ...)

也就是说,真正让 ES8311 开始输出录音数据的是这一步:

esp_codec_dev_open(s_input_dev, &fs);

它相当于官方 EnableInput(true) 的核心操作。

旧代码的问题就在于:ES8311 的 ADC / MIC 输入路径没有真正打开,就直接去读 I2S,所以读到的当然是 0。

七、最终使用的核心初始化流程

最终改成了下面这个结构:

andio_init()
    ↓
andio_i2c_init()
    ↓
andio_i2s_init()
    ↓
andio_codec_init()
        ↓
        audio_codec_new_i2s_data()
        audio_codec_new_i2c_ctrl()
        audio_codec_new_gpio()
        es8311_codec_new()
        esp_codec_dev_new(input)
        esp_codec_dev_open(input)
        esp_codec_dev_set_in_gain(input, 40.0)

这里的重点是:

I2C:负责配置 ES8311
I2S:负责传输 PCM 音频数据
esp_codec_dev:负责按正确流程打开 ES8311 输入设备

最终读取数据不再用:

i2s_channel_read()

而是使用:

esp_codec_dev_read(s_input_dev, buf, samples * sizeof(int16_t));

这一步非常关键。


八、成功后的日志

最终成功后的日志如下:

I (393) ANDIO: I2C initialized: SDA=48, SCL=45
I (393) ANDIO: I2S initialized: MCLK=-1, BCK=21, WS=13, DOUT=14, DIN=47, rate=24000
I (403) ES8311: Work in Slave mode
I (463) Adev_Codec: Open codec device OK
I (463) ANDIO: ES8311 input opened, gain=40.0, use_mclk=false
I (463) ANDIO: ANDIO init done

录音时可以看到 PCM 数据已经有变化:

I (583) ANDIO: pcm[0]=2475 pcm[1]=2464 pcm[2]=2390 pcm[3]=2417 samples=720
I (813) ANDIO: pcm[0]=562 pcm[1]=661 pcm[2]=502 pcm[3]=463 samples=720
I (843) ANDIO: pcm[0]=-15560 pcm[1]=-15623 pcm[2]=-15820 pcm[3]=-16004 samples=720
I (873) ANDIO: pcm[0]=32767 pcm[1]=32767 pcm[2]=32767 pcm[3]=32767 samples=720

这里已经能说明麦克风采集到了声音。

最终生成 WAV:

I (5853) ANDIO: WAV saved: /sdcard/record.wav
I (5853) ANDIO: WAV size: 240044 bytes

因为最终使用的是:

采样率:24000 Hz
声道:1
位宽:16 bit
时长:5 秒

所以文件大小大约是:

24000 × 1 × 2 × 5 + 44 = 240044 bytes

这个大小是正确的。

九、关于 pcm 出现 32767 / -32768

日志里有时出现:

32767
-32768

这说明信号可能出现了饱和,也就是音量增益偏大。

当前设置了:

esp_codec_dev_set_in_gain(s_input_dev, 40.0);

这个是参考官方输入增益,优点是声音比较容易采到,缺点是可能爆音。

如果后续播放 WAV 发现声音炸裂、失真、削顶,可以把增益降低,比如:

esp_codec_dev_set_in_gain(s_input_dev, 30.0);

或者:

esp_codec_dev_set_in_gain(s_input_dev, 24.0);

调试时建议先用高一点的增益确认能录到声音,确认通路成功后再调音质。

十、最后的 deinit 报错

日志最后有一条:

E i2s_common: i2s_channel_disable: the channel has not been enabled yet

这不是录音失败的原因。

原因是 esp_codec_dev 内部可能已经管理了 I2S channel 的打开/关闭状态,而外层 andio_deinit() 又手动调用了 i2s_channel_disable(),导致重复 disable。

这类问题可以后续优化:

1. deinit 时只关闭 esp_codec_dev
2. 或者记录 I2S channel 是否真的 enable
3. 或者忽略这个测试阶段错误

它不影响 WAV 已经成功保存。

十一、这次调试的关键心得

1. 先确认硬件链路,不要只看信号名

一开始看到 MIC 和 I2S 信号,很容易以为是数字 I2S 麦克风。

但实际链路是:

模拟麦克风 → ES8311 → I2S → ESP32-S3

两种方案的软件写法完全不一样。

如果是 I2S 数字麦克风,ESP32 可以直接读 I2S。

如果是 ES8311 这种 Codec,就必须先配置 Codec。


2. I2C 和 I2S 的作用不要混淆

I2C:配置 ES8311
I2S:传输音频数据

I2C 通了,只能说明 ESP32 能写 ES8311 寄存器,不代表声音已经出来。

I2S 能读到 bytes,也不代表数据有效。

真正判断是否成功,要看 PCM 数据是否随声音变化。


3. 全 0 / 全 -1 是非常有价值的现象

如果录音数据是:

0, 0, 0, 0...

或者:

-1, -1, -1, -1...

一般不是音量小,而是:

数据线固定电平
I2S 引脚错
Codec 没有输出
ADC 没有打开
时钟没对

正常麦克风即使没有人说话,也会有底噪,不应该完全不变。


4. 不要盲目复制官方完整工程

官方工程是完整 AI 对话系统,里面包括:

音频输入
音频输出
网络
协议
AI 对话
显示
按键
IoT

如果只是做录音,不需要复制整套。

但可以从官方代码里抽取关键流程:

板级引脚配置
Codec 初始化方式
use_mclk 参数
esp_codec_dev_open
esp_codec_dev_read

这样既不依赖整套 AI 项目,又能保持和官方硬件初始化逻辑一致。


5. 采样率要和官方板级配置保持一致

这块板子官方配置是:

AUDIO_INPUT_SAMPLE_RATE = 24000

所以最后录音也使用 24kHz。

如果随意改成 16kHz 或其他值,需要确认 ES8311、I2S 时钟、采样率配置都支持,否则可能初始化失败或者录音异常。

6. MCLK 不一定必须有

ES8311 支持不同的时钟模式。

这块板子的官方配置是:

MCLK = GPIO_NUM_NC
use_mclk = false

所以这块板子不用独立 MCLK。

调试时不要只根据原理图某个网络名判断,最好结合官方板级代码一起看。

十二、最终总结

这次录音成功的关键不是 SD 卡,也不是 WAV 文件头,而是终于把 ES8311 的输入通道按正确方式打开了。

最终可用链路是:

MK1 模拟麦克风
    ↓
ES8311 ADC
    ↓
esp_codec_dev input_dev
    ↓
I2S DIN GPIO47
    ↓
ESP32-S3
    ↓
PCM 16bit mono 24kHz
    ↓
WAV 文件
    ↓
SD 卡 /sdcard/record.wav

最终正确配置是:

#define ANDIO_SAMPLE_RATE        24000
#define ANDIO_CHANNELS           1
#define ANDIO_BITS_PER_SAMPLE    16

#define ANDIO_I2C_SDA_GPIO       GPIO_NUM_48
#define ANDIO_I2C_SCL_GPIO       GPIO_NUM_45

#define ANDIO_I2S_MCLK_GPIO      GPIO_NUM_NC
#define ANDIO_I2S_BCK_GPIO       GPIO_NUM_21
#define ANDIO_I2S_WS_GPIO        GPIO_NUM_13
#define ANDIO_I2S_DOUT_GPIO      GPIO_NUM_14
#define ANDIO_I2S_DIN_GPIO       GPIO_NUM_47

最关键的读取方式是:

esp_codec_dev_open(s_input_dev, &fs);
esp_codec_dev_set_in_gain(s_input_dev, 40.0);
esp_codec_dev_read(s_input_dev, pcm_buf, size);

而不是只初始化 I2S 后直接裸读。

这也是这次从“全 0”到“成功录音”的根本区别。

更多推荐