ESP32-A1S-AudioKit录音排障实录

本文记录一次旧款安信可 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

实际过程却分成了四个阶段:

  1. 电脑识别不到可用串口;
  2. 老工程与新电脑的开发环境不兼容;
  3. 能生成 WAV,但播放完全没有声音;
  4. 能录到人声,但有周期性的“嗒嗒”声和轻微变声。

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. 最终修复清单

回顾整个过程,真正有效的修复可以归纳为:

  1. 安装 CP210x VCP 驱动,使 Windows 出现稳定的 COM3;
  2. 使用与旧 ESP-ADF 匹配的 ESP-IDF 3.3 工具链和独立 Python 环境;
  3. 将 A149 的默认 Codec 从 AC101 改为 ES8388;
  4. 修正 I2S WS/DOUT 引脚定义;
  5. 将 ES8388 ADC 输入从 LINE1 改为板载麦克风所在的 LINE2;
  6. 显式启动 Codec 的 ENCODE/ADC 路径和 MCLK;
  7. 固定为 16 kHz、16-bit PCM,并安全结束录音;
  8. 使用 8 位随机数字命名,避免覆盖旧文件;
  9. 录音时关闭扬声器功放 PA;
  10. 将麦克风 PGA 从约 33 dB 降至 24 dB;
  11. 用双声道诊断确认两路高度相关,选择信噪比略好的通道 0;
  12. 将音频先存入 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 文件。

发表评论