本文记录一次旧款安信可 ESP32-A1S Audio Kit 语音开发板的完整抢救过程:从 Windows 无法烧录、工程无法编译、生成的 WAV 完全无声,到成功录到人声,再到定位周期性“嗒嗒”杂音并改用 PSRAM 缓冲。
1. 硬件与最终目标
开发板丝印为:
ESP32 Audio Kit V2.2 A149- 核心模组:
ESP32-A1S - 音频 Codec:
ES8388 - 板载双麦克风、MicroSD 卡槽、耳机与线路接口
- USB 转串口芯片:Silicon Labs
CP210x(常被统称为 CP2102 驱动)
目标很简单:上电后录制约 10 秒语音,保存为标准 WAV 文件,文件名使用随机的 8 位数字,例如:
26825253.wav
40059840.wav
实际过程却分成了四个阶段:
- 电脑识别不到可用串口;
- 老工程与新电脑的开发环境不兼容;
- 能生成 WAV,但播放完全没有声音;
- 能录到人声,但有周期性的“嗒嗒”声和轻微变声。
2. 先打通烧录链路:安装 CP210x 驱动
这块板要使用标有 UART 的 Micro-USB 口烧录。Windows 最初只显示“CP2102 USB to UART Bridge Controller”,但没有可用 COM 口,因此先安装 Silicon Labs 官方 CP210x VCP 驱动。
驱动安装完成、重新插拔开发板后,在设备管理器和 PowerShell 中可看到:
Silicon Labs CP210x USB to UART Bridge (COM3)
VID_10C4 & PID_EA60
可用下面的 PowerShell 命令检查串口:
Get-CimInstance Win32_PnPEntity |
Where-Object { $_.Name -match '(COMd+)' } |
Select-Object Status, Name, DeviceID
这里有一个很实际的坑:这块板和 USB 线曾发生过插接瞬断。烧录程序报过:
could not open port 'COM3': FileNotFoundError
这不一定是驱动坏了,也不一定是端口被占用;如果 COM3 从系统中暂时消失,重新插稳 USB 后再扫描、重试即可。
3. 恢复旧版 ESP-ADF/ESP-IDF 编译环境
这个项目来自较早期的 ESP-ADF,不能直接套用今天的 ESP-IDF。为了避免新版 API、Python 包和工具链把旧工程越改越乱,我保留了项目自带的 ESP-IDF 3.3 系列环境,并单独建立 Python 虚拟环境:
D:projectsesp32toolsidf3-python
工程中的工具链位于:
D:projectsesp32ESP32-A1S-AudioKitesp_tools
随后编写了统一入口脚本 build-a1s-recorder.ps1,集中设置 IDF_PATH、ADF_PATH、IDF_TOOLS_PATH,自动寻找 USB 串口,并将构建输出放到独立的 build_codex 目录。
核心逻辑如下:
$env:IDF_PATH = $idfPath
$env:ADF_PATH = $adfPath
$env:IDF_TOOLS_PATH = $idfToolsPath
$exports = & $pythonExe "$idfPathtoolsidf_tools.py" export --format key-value
foreach ($line in $exports) {
if ($line -match '^([^=]+)=(.*)$') {
Set-Item -Path "Env:$($matches[1])" -Value $matches[2]
}
}
之后的日常操作被简化成三条命令:
# 编译
.build-a1s-recorder.ps1 build
# 烧录
.build-a1s-recorder.ps1 flash -Port COM3
# 查看启动和录音日志
.build-a1s-recorder.ps1 monitor -Port COM3
烧录使用 DIO、80 MHz Flash,并在写入 bootloader、分区表和应用后逐段校验 Hash。实测可稳定识别为 ESP32D0WDQ5 revision 1。
4. 最关键的一步:为什么一开始 WAV 完全没有声音
4.1 “文件存在”不等于“音频输入正常”
最初程序能够在 SD 卡上生成 WAV,因此一开始很容易怀疑 WAV 头、采样率或播放器。但 WAV 能正常打开,只能证明:
- SD 卡挂载成功;
- 文件写入链路成功;
- 程序确实运行了一段时间。
它不能证明 ES8388 的 ADC 已经收到麦克风信号。
沿着真实音频链路检查:
板载麦克风
→ ES8388 模拟输入选择和 PGA
→ ES8388 ADC
→ I2S BCLK / WS / DATA
→ ESP32 I2S DMA
→ WAV
→ SD 卡
最终发现旧工程对这块 V2.2 A149 存在三项关键配置不匹配。
4.2 错误一:板级初始化默认调用了 AC101,而实物是 ES8388
开发板上实际 Codec 是 ES8388,但旧板级文件默认使用:
audio_hal_init(&audio_codec_cfg, &AUDIO_CODEC_AC101_CODEC_HANDLE);
这会导致程序看似继续运行,实际却没有按 ES8388 的寄存器定义配置 ADC、输入选择和时钟。修复为优先初始化 ES8388:
extern audio_hal_func_t AUDIO_CODEC_ES8388_DEFAULT_HANDLE;
audio_hal_handle_t codec_hal = audio_hal_init(
&audio_codec_cfg,
&AUDIO_CODEC_ES8388_DEFAULT_HANDLE
);
工程中还保留了 AC101 回退,以兼容可能存在的其他硬件批次:
if (codec_hal == NULL) {
ESP_LOGW(TAG, "ES8388 init failed, trying AC101...");
i2c_driver_delete(I2C_NUM_0);
codec_hal = audio_hal_init(
&audio_codec_cfg,
&AUDIO_CODEC_AC101_CODEC_HANDLE
);
}
4.3 错误二:I2S 的 WS 和 DOUT 引脚定义反了
旧定义是:
i2s_config->bck_io_num = GPIO_NUM_27;
i2s_config->ws_io_num = GPIO_NUM_26;
i2s_config->data_out_num = GPIO_NUM_25;
i2s_config->data_in_num = GPIO_NUM_35;
根据这块 A149 的实际连接,修正为:
i2s_config->bck_io_num = GPIO_NUM_27;
i2s_config->ws_io_num = GPIO_NUM_25;
i2s_config->data_out_num = GPIO_NUM_26;
i2s_config->data_in_num = GPIO_NUM_35;
录音主要使用 data_in_num,但 WS 接错会直接破坏左右声道边界与采样时序;板级定义必须整体与原理图/实板一致,不能只盯着数据输入脚。
4.4 错误三:板载麦克风接在 ES8388 LINE2,而默认选了 LINE1
原始配置为:
.adc_input = AUDIO_HAL_ADC_INPUT_LINE1,
但这块 A149 的板载麦克风实际走 ES8388 的第二组模拟输入。将 ADC 输入改为 LINE2:
#define AUDIO_CODEC_DEFAULT_CONFIG() {
.adc_input = AUDIO_HAL_ADC_INPUT_LINE2,
.dac_output = AUDIO_HAL_DAC_OUTPUT_ALL,
.codec_mode = AUDIO_HAL_CODEC_MODE_BOTH,
/* ... */
}
这一步决定了 ADC 到底在“听”哪个物理输入。选择错误时,I2S 和文件系统都可能工作正常,但采到的只有接近零的样本,于是就出现“WAV 文件大小正常、播放却没有声音”的典型假象。
4.5 正确启动录音 Codec
板级驱动和输入通道修正后,还必须明确启动编码(ADC)方向:
audio_board_handle_t board_handle = audio_board_init();
ESP_ERROR_CHECK(audio_hal_ctrl_codec(
board_handle->audio_hal,
AUDIO_HAL_CODEC_MODE_ENCODE,
AUDIO_HAL_CTRL_START
));
同时显式设置 I2S 引脚和 MCLK:
i2s_pin_config_t pins = {0};
get_i2s_pins(I2S_NUM_0, &pins);
i2s_set_pin(I2S_NUM_0, &pins);
i2s_mclk_gpio_select(I2S_NUM_0, GPIO_NUM_0);
完成这三项板级修复后,录音终于从“完全无声”变成了“能清楚录到人声”。这也是整个排障过程中最重要的转折点。
5. 让录音文件可用:采样格式、随机命名与安全收尾
录音参数最终固定为:
#define RECORD_TIME_SECONDS 10
#define RECORD_SAMPLE_RATE 16000
#define RECORD_BITS 16
16 kHz、16-bit PCM 对普通语音足够,也能控制文件大小和内存占用。
为了避免每次录音覆盖同一个文件,使用 ESP32 硬件随机数生成 8 位数字文件名:
char wav_path[40];
snprintf(wav_path, sizeof(wav_path), "/sdcard/%08u.wav",
esp_random() % 100000000U);
%08u 会用前导零补齐,例如:
/sdcard/03924642.wav
早期版本使用 ADF pipeline:
I2S reader → WAV encoder → FATFS writer
并增大了 DMA 与环形缓冲:
i2s_cfg.out_rb_size = 32 * 1024;
i2s_cfg.i2s_config.dma_buf_count = 8;
i2s_cfg.i2s_config.dma_buf_len = 512;
录音结束时不能直接释放对象,必须先停止、等待停止并终止 pipeline,否则 WAV 尾部或头部长度可能来不及更新:
audio_pipeline_stop(pipeline);
audio_pipeline_wait_for_stop(pipeline);
audio_pipeline_terminate(pipeline);
这一版已经可以稳定产生约 9.984 秒的有效 WAV。
6. 从“有声音”到“声音能听”:分析嗒嗒声和变声
6.1 第一份问题录音的客观数据
对 60828894.wav 做波形和频谱分析,得到:
- 16 kHz、16-bit、单声道;
- 时长约 9.984 秒;
- 峰值约
-13.9 dBFS,没有数字削波; - RMS 约
-38.1 dBFS; - 存在明显的周期性脉冲/梳状噪声;
- 约每 10 ms 出现一次规律干扰。
因为没有削波,所以“变声”并非简单的 PCM 满幅截断,更像是模拟前端增益过高、周期性电源干扰叠加在人声上。
6.2 录音时关闭扬声器功放
录音不需要板载扬声器功放。功放的开关电流和电源噪声可能串入共地的麦克风模拟链路,因此初始化 Codec 后立即拉低 PA 使能:
audio_board_handle_t board_handle = audio_board_init();
gpio_set_level(PA_ENABLE_GPIO, 0);
这不会关闭 ES8388 ADC,只关闭外部扬声器功率放大部分。
6.3 将麦克风 PGA 从约 33 dB 降到 24 dB
旧 ES8388 初始化代码把左右通道 PGA 写成 0xbb,约 33 dB:
es_write_reg(ES8388_ADDR, ES8388_ADCCONTROL1, 0xbb);
增益太高会把人声放大,也会把电源纹波、SD 卡活动和模拟底噪一起放大。修改为 0x88,约 24 dB:
/* 24 dB is enough for the onboard microphones. */
es_write_reg(ES8388_ADDR, ES8388_ADCCONTROL1, 0x88);
6.4 先录双声道,用数据选麦克风通道
为了确认是否只有一只麦克风或一路模拟输入有问题,中间诊断版先保留左右双声道:
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
对带人声的 26825253.wav 分析后得到:
| 指标 | 通道 0 | 通道 1 |
|---|---|---|
| 全段 RMS | -38.78 dBFS | -39.34 dBFS |
| 峰值 | -17.74 dBFS | -18.81 dBFS |
| 开头安静段 RMS | -49.87 dBFS | -50.00 dBFS |
| 人声段 RMS | -36.71 dBFS | -37.29 dBFS |
| 结尾安静段 RMS | -49.43 dBFS | -49.58 dBFS |
两个通道相关系数约为 0.986,说明二者采到的内容和干扰几乎相同,不像是某一只麦克风单独损坏。通道 0 的人声相对底噪略占优势,因此最终单声道输出选择通道 0:
mono[i] = stereo[(pos + i) * 2];
7. 最后一个关键改进:录音时不再写 SD 卡
7.1 为什么怀疑 SD 卡写入干扰
第二版的人声已经正常,但仍能看到严格的周期性包络:约每 32 ms 重复一次,而且左右通道完全同步。
这组特征非常关键:
- 如果是单只麦克风坏,左右声道不会高度同步;
- 如果是随机环境噪声,不会严格按固定周期重复;
- 如果是数字削波,波形峰值应接近 0 dBFS,但实际没有;
- 固定周期恰好与 DMA、环形缓冲和 FATFS 分块写入活动相符。
因此推断:程序一边从 I2S 采样、一边向 MicroSD 写数据,SD 卡的脉冲电流通过供电或地线耦合进 ES8388 模拟输入,形成可听的“嗒嗒”声。
7.2 用 PSRAM 把采集和写卡彻底错开
ESP32-A1S 带有足够的 PSRAM。10 秒、16 kHz、16-bit、双声道原始数据只需:
10 × 16000 × 2 字节 × 2 通道 = 640000 字节
因此最终方案不再实时写卡:
阶段一:ES8388 → I2S → PSRAM(SD 卡不写入)
阶段二:停止 I2S → 双声道转单声道 → 写 WAV 到 SD 卡
分配 PSRAM:
#define CAPTURE_BYTES
(10 * 16000 * 2 * sizeof(int16_t))
int16_t *stereo = heap_caps_malloc(
CAPTURE_BYTES,
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT
);
录音阶段只做 I2S 读取:
size_t captured = 0;
while (captured < CAPTURE_BYTES) {
size_t got = 0;
size_t request = CAPTURE_BYTES - captured;
if (request > 4096) request = 4096;
ESP_ERROR_CHECK(i2s_read(
I2S_NUM_0,
(uint8_t *)stereo + captured,
request,
&got,
portMAX_DELAY
));
captured += got;
}
i2s_stop(I2S_NUM_0);
I2S 停止后才创建文件、写 WAV 头和 PCM 数据。这样即使 SD 卡写入仍会产生电源脉冲,也不会再污染已经完成的 ADC 采样。
串口验证日志如下:
Mounting SD card (no writes during capture)
Initializing ES8388 LINE2; PA off; mic PGA 24 dB
Capturing 10 seconds to PSRAM for /sdcard/40059840.wav
Capture complete; writing mono WAV to SD card
Recording finished safely: /sdcard/40059840.wav
注意:最终版录音期间约 10 秒不打印每秒进度,这是刻意减少其他周期性活动,并非程序卡死。
8. 手工生成标准 PCM WAV 头
最终版绕开实时 ADF WAV pipeline,采集结束后手工写入 44 字节 PCM WAV 头:
typedef struct __attribute__((packed)) {
char riff[4];
uint32_t file_size_minus_8;
char wave[4];
char fmt[4];
uint32_t fmt_size;
uint16_t format;
uint16_t channels;
uint32_t sample_rate;
uint32_t byte_rate;
uint16_t block_align;
uint16_t bits_per_sample;
char data[4];
uint32_t data_size;
} wav_header_t;
单声道 16 kHz/16-bit 的关键字段为:
.format = 1, // PCM
.channels = 1,
.sample_rate = 16000,
.byte_rate = 16000 * 2,
.block_align = 2,
.bits_per_sample = 16,
写完后一定要刷新并关闭文件:
fflush(out);
fclose(out);
9. 最终修复清单
回顾整个过程,真正有效的修复可以归纳为:
- 安装 CP210x VCP 驱动,使 Windows 出现稳定的 COM3;
- 使用与旧 ESP-ADF 匹配的 ESP-IDF 3.3 工具链和独立 Python 环境;
- 将 A149 的默认 Codec 从 AC101 改为 ES8388;
- 修正 I2S WS/DOUT 引脚定义;
- 将 ES8388 ADC 输入从 LINE1 改为板载麦克风所在的 LINE2;
- 显式启动 Codec 的 ENCODE/ADC 路径和 MCLK;
- 固定为 16 kHz、16-bit PCM,并安全结束录音;
- 使用 8 位随机数字命名,避免覆盖旧文件;
- 录音时关闭扬声器功放 PA;
- 将麦克风 PGA 从约 33 dB 降至 24 dB;
- 用双声道诊断确认两路高度相关,选择信噪比略好的通道 0;
- 将音频先存入 PSRAM,停止采样后再写 SD 卡,切断周期性写卡干扰。
10. 这次排障最值得记住的经验
第一,能生成音频文件不代表音频硬件已经工作。遇到“WAV 无声”,应该从模拟输入复用、Codec 型号、I2C 寄存器、MCLK 和 I2S 引脚逐层检查,而不是先在播放器和 WAV 格式上反复试错。
第二,开发板名称相同不代表不同批次的 Codec 和引脚定义相同。旧 SDK 的板级支持文件也可能写错或只适配某一批硬件,实板丝印和芯片型号比示例工程的默认宏更可信。
第三,音频中的规律杂音最好用波形、频谱、声道相关性和周期分析说话。此次正是“左右同步 + 32 ms 固定周期 + 无削波”把问题指向了 SD 卡写入,而不是麦克风损坏。
最后,在带 PSRAM 的嵌入式语音设备上,短时录音完全可以采用“先采集、后落盘”。用约 640 KB 内存换取采样期间安静的电源环境,往往比叠加复杂的数字降噪更直接,也不会损伤人声。
工程文件位置
本次修改的主要文件:
ESP32-A1S-AudioKit/
├─ components/audio_board/ai_thinker_audio_kit_v2_2/
│ ├─ board.c
│ ├─ board_def.h
│ └─ board_pins_config.c
├─ components/audio_hal/driver/es8388/es8388.c
└─ examples/recorder/element_wav_amr_sdcard/
├─ main/element_wav_amr_sdcard.c
└─ sdkconfig.defaults
build-a1s-recorder.ps1
至此,这块多年前购买、最初连一段有效声音都录不到的 ESP32-A1S 开发板,已经能够稳定生成随机命名、标准格式并显著降低周期性写卡干扰的语音 WAV 文件。