ESP32-S3 + ES8311 录音到 SD 卡调试记录:从全 0 到成功采集声音
这次调试的目标很简单:让 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”到“成功录音”的根本区别。
更多推荐
所有评论(0)