【立创·实战派ESP32-S3】文档教程
第 7 章 音频输入-ES7210
实战派ESP32-C3 开发板的音频输入和输出,使用一个芯片 es8310 完成。实战派ESP32-S3 开发板的音频输入和输出则使用了 2 个芯片,es7210 连接 MIC 负责音频输入,es8311 只负责音频输出。
es7210 可以连接 4 个 MIC,开发板上连接了 3 个 MIC,MIC1 和 MIC2 接收人说话的声音,而 MIC3 连接了 es8311 的输出,用于回声消除。
S3 芯片可以做 AI 语音识别对话,需要用到回声消除,就是音响在播放声音的时候,我们也可以说话打断它。这个原理就是 es8311 输出的信号,不仅给了喇叭,还给了 es7210 的 MIC3 输入,ESP32 在接收到 MIC1 MIC2 和 MIC3 的声音后,可以分离出 MIC3,从而进行识别。
以上回声消除的算法,乐鑫已经写好代码,我们直接调用就可以。
本例程还没有涉及到 AI 语音对话(后面会有 AI 语音对话例程),只是对 MIC1 和 MIC2 的声音进行采集。本例程将实现录制人的声音,并把录制好的声音存储到 SD 卡中,格式为 wav。
7.1 使用例程
本例程主要实现的功能是录音,声音文件格式保存为 wav,写到 SD 卡里面。
把开发板提供的【04-audio_es7210】例程复制到你的实验文件夹当中,并使用 VSCode 打开工程。
准备一张 microSD 卡,把重要的文件拷贝走,以免做实验的时候损坏文件。把 microSD 卡插入开发板卡槽,再把开发板插入电脑。在 VSCode 上选择串口号,选择目标芯片为 esp32s3,串口下载方式,然后点击“一键三联”按钮,等待编译下载打开终端。
终端自动打开后,输出结果中会提示开始录音的倒计时,一共可以录制 10 秒钟。
I (358) main_task: Calling app_main()
I (358) example: Create I2S receive channel
I (368) example: Configure I2S receive channel to TDM mode
I (378) example: Init I2C used to configure ES7210
I (378) example: Configure ES7210 codec parameters
I (388) ES7210: format: standard i2s, bit width: 16, tdm mode enabled
I (398) ES7210: sample rate: 48000Hz, mclk frequency: 12288000Hz
I (398) example: Mounting SD card
I (398) example: Initializing SD card
I (408) example: Using SDMMC peripheral
I (408) example: Mounting filesystem
I (418) gpio: GPIO[47]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
I (428) gpio: GPIO[48]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
I (438) gpio: GPIO[21]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
I (478) example: Card size: 30726MB, speed: 20MHz
I (478) example: Opening file /RECORD.WAV
I (488) example: Recording: 1/10s
I (1498) example: Recording: 2/10s
I (2498) example: Recording: 3/10s
I (3498) example: Recording: 4/10s
I (4508) example: Recording: 5/10s
I (5508) example: Recording: 6/10s
I (6508) example: Recording: 7/10s
I (7508) example: Recording: 8/10s
I (8488) example: Recording: 9/10s
I (9498) example: Recording: 10/10s
I (10498) example: Recording done! Flushing file buffer
I (10508) example: Audio was successfully recorded into /RECORD.WAV. You can now remove the SD card safely
I (10508) main_task: Returned from app_main()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
当看到 Recording: 1/10s 出现时,就可以开始对着开发板说话了,10 秒钟后录制结束。保存到 SD 卡上的文件名称为 RECORD.WAV。
按 Ctrl+]退出终端后,开发板断电,把开发板上的 SD 卡插入读卡器,在电脑上可以看到 SD 卡里面多了一个 RECORD.WAV 文件,如下图所示:
双击使用你电脑上的音频播放器就可以播放。
7.2 例程讲解
本例程和之前的例程相比,在 main 文件夹下多了一个 yml 文件。
此文件是组件依赖项清单文件,里面包含了工程需要的组件名称和版本号,在配置目标芯片时,会自动清单中的组件从乐鑫组件管理器官网下载到工程中。点击打开此文件,可以看到本例程需要的组件名称和版本号,如下所示:
## IDF Component Manager Manifest File
dependencies:
espressif/es7210: '^1.0.0'
## Required IDF version
idf:
version: '>=4.4.0'
# # Put list of dependencies here
# # For components maintained by Espressif:
# component: "~1.0.0"
# # For 3rd party components:
# username/component: ">=1.0.0,<2.0.0"
# username2/component2:
# version: "~1.0.0"
# # For transient dependencies `public` flag can be set.
# # `public` flag doesn't have an effect dependencies of the `main` component.
# # All dependencies of `main` are public by default.
# public: true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
其中#符号后面的都是注释,不需要的话删除也可以。在文件中,指定了需要的组件只有一个,就是es7210组件,idf后面是指定了本例程需要的idf编译器版本号。
乐鑫组件管理器官网:https://components.espressif.com/
你可以把组件理解为别人写好的驱动代码,只要下载到工程中调用即可。
下载组件会自动创建一个名称为managed_components的文件夹,然后把组件代码放到里面。
本例程主要代码都在main.c中,format_wav.h文件在存储文件的时候调用。
点击打开main.c文件,找到app_main函数。
void app_main(void)
{
/* 初始化I2S接口 */
i2s_chan_handle_t i2s_rx_chan = es7210_i2s_init();
/* 初始化es7210芯片 */
es7210_codec_init();
/* 挂载SD卡 */
sdmmc_card_t *sdmmc_card = mount_sdcard();
/* 录音 */
esp_err_t err = record_wav(i2s_rx_chan);
/* 弹出SD卡 */
esp_vfs_fat_sdcard_unmount(EXAMPLE_SD_MOUNT_POINT, sdmmc_card);
if(err == ESP_OK) {
ESP_LOGI(TAG, "Audio was successfully recorded into "EXAMPLE_RECORD_FILE_PATH
". You can now remove the SD card safely");
} else {
ESP_LOGE(TAG, "Record failed, "EXAMPLE_RECORD_FILE_PATH" on SD card may not be playable.");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在app_main函数里面,使用了4个函数。分别是:
- 初始化I2S总线 es7210_i2s_init()
- 初始化es7210芯片 es7210_codec_init()
- 加载SD卡 mount_sdcard()
- 录制声音 record_wav()
这4个函数都位于app_main()主函数的前面,接下来我们看一下这4个函数的具体实现过程。
es7210_i2s_init()函数
static i2s_chan_handle_t es7210_i2s_init(void)
{
i2s_chan_handle_t i2s_rx_chan = NULL; // 定义接收通道句柄
ESP_LOGI(TAG, "Create I2S receive channel");
i2s_chan_config_t i2s_rx_conf = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER); // 配置接收通道
ESP_ERROR_CHECK(i2s_new_channel(&i2s_rx_conf, NULL, &i2s_rx_chan)); // 创建i2s通道
ESP_LOGI(TAG, "Configure I2S receive channel to TDM mode");
// 定义接收通道为I2S TDM模式 并配置
i2s_tdm_config_t i2s_tdm_rx_conf = {
.slot_cfg = I2S_TDM_PHILIPS_SLOT_DEFAULT_CONFIG(EXAMPLE_I2S_SAMPLE_BITS, I2S_SLOT_MODE_STEREO, EXAMPLE_I2S_TDM_SLOT_MASK),
.clk_cfg = {
.clk_src = I2S_CLK_SRC_DEFAULT,
.sample_rate_hz = EXAMPLE_I2S_SAMPLE_RATE,
.mclk_multiple = EXAMPLE_I2S_MCLK_MULTIPLE
},
.gpio_cfg = {
.mclk = EXAMPLE_I2S_MCK_IO,
.bclk = EXAMPLE_I2S_BCK_IO,
.ws = EXAMPLE_I2S_WS_IO,
.dout = -1, // ES7210 only has ADC capability
.din = EXAMPLE_I2S_DI_IO
},
};
ESP_ERROR_CHECK(i2s_channel_init_tdm_mode(i2s_rx_chan, &i2s_tdm_rx_conf)); // 初始化I2S通道为TDM模式
return i2s_rx_chan;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
这个函数中,最主要的是倒数第2行代码,就是i2s_channel_init_tdm_mode(i2s_rx_chan, &i2s_tdm_rx_conf)函数,这个函数用来初始化i2s通道为tdm模式,函数里面的2个参数,i2s_rx_chan和i2s_tdm_rx_conf,分别是通道句柄,和通道配置结构体,这两个在前面分别定义。
上面代码,开通了I2S接收通道,没有开通I2S发送通道,因为ES7210只用于接收声音,不用于发送声音。
这里简单介绍一下标准I2S和TDM_I2S模式的区别。
下图是ES7210工作在I2S模式下的时序图:
下图是ES7210工作在TDM_I2S模式下的时序图:
通过上面两张图对比发现,ES7210工作在I2S模式时,只能采集2个通道,而工作在TDM_I2S模式时,可以采集4个通道。
es7210_codec_init()函数
es7210芯片的初始化函数代码如下所示:
static void es7210_codec_init(void)
{
// 初始化I2C接口
ESP_LOGI(TAG, "Init I2C used to configure ES7210");
i2c_config_t i2c_conf = {
.sda_io_num = EXAMPLE_I2C_SDA_IO,
.scl_io_num = EXAMPLE_I2C_SCL_IO,
.mode = I2C_MODE_MASTER,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = EXAMPLE_ES7210_I2C_CLK,
};
ESP_ERROR_CHECK(i2c_param_config(EXAMPLE_I2C_NUM, &i2c_conf));
ESP_ERROR_CHECK(i2c_driver_install(EXAMPLE_I2C_NUM, i2c_conf.mode, 0, 0, 0));
// 配置es7210器件句柄
es7210_dev_handle_t es7210_handle = NULL;
es7210_i2c_config_t es7210_i2c_conf = {
.i2c_port = EXAMPLE_I2C_NUM,
.i2c_addr = EXAMPLE_ES7210_I2C_ADDR
};
ESP_ERROR_CHECK(es7210_new_codec(&es7210_i2c_conf, &es7210_handle));
// 初始化es7210芯片
ESP_LOGI(TAG, "Configure ES7210 codec parameters");
es7210_codec_config_t codec_conf = {
.i2s_format = EXAMPLE_I2S_TDM_FORMAT,
.mclk_ratio = EXAMPLE_I2S_MCLK_MULTIPLE,
.sample_rate_hz = EXAMPLE_I2S_SAMPLE_RATE,
.bit_width = (es7210_i2s_bits_t)EXAMPLE_I2S_SAMPLE_BITS,
.mic_bias = EXAMPLE_ES7210_MIC_BIAS,
.mic_gain = EXAMPLE_ES7210_MIC_GAIN,
.flags.tdm_enable = true
};
ESP_ERROR_CHECK(es7210_config_codec(es7210_handle, &codec_conf));
ESP_ERROR_CHECK(es7210_config_volume(es7210_handle, EXAMPLE_ES7210_ADC_VOLUME));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
上述代码中的第3到14行,初始化了I2C总线,因为ES7210需要使用I2C通信进行寄存器的配置。
第35行,es7210_config_codec()函数,用来初始化配置ES7210的各个寄存器。它的两个参数,在它前面已经配置好。
第36行,es7210_config_volume()函数,用来配置ADC的增益。
这里调用的几个开头为es7210的函数,都位于es7210组件中,鼠标点击函数后按F12可以跳转过去。
mount_sdcard()函数
加载SD卡的函数,函数中内容都是在上一章说过的,这里就不多说了。
record_wav()函数
录制声音的函数代码如下所示:
static esp_err_t record_wav(i2s_chan_handle_t i2s_rx_chan)
{
ESP_RETURN_ON_FALSE(i2s_rx_chan, ESP_FAIL, TAG, "invalid i2s channel handle pointer");
esp_err_t ret = ESP_OK;
uint32_t byte_rate = EXAMPLE_I2S_SAMPLE_RATE * EXAMPLE_I2S_CHAN_NUM * EXAMPLE_I2S_SAMPLE_BITS / 8;
uint32_t wav_size = byte_rate * EXAMPLE_RECORD_TIME_SEC;
const wav_header_t wav_header =
WAV_HEADER_PCM_DEFAULT(wav_size, EXAMPLE_I2S_SAMPLE_BITS, EXAMPLE_I2S_SAMPLE_RATE, EXAMPLE_I2S_CHAN_NUM);
ESP_LOGI(TAG, "Opening file %s", EXAMPLE_RECORD_FILE_PATH);
FILE *f = fopen(EXAMPLE_SD_MOUNT_POINT EXAMPLE_RECORD_FILE_PATH, "w");
ESP_RETURN_ON_FALSE(f, ESP_FAIL, TAG, "error while opening wav file");
/* Write wav header */
ESP_GOTO_ON_FALSE(fwrite(&wav_header, sizeof(wav_header_t), 1, f), ESP_FAIL, err,
TAG, "error while writing wav header");
/* Start recording */
size_t wav_written = 0;
static int16_t i2s_readraw_buff[4096];
ESP_GOTO_ON_ERROR(i2s_channel_enable(i2s_rx_chan), err, TAG, "error while starting i2s rx channel");
while (wav_written < wav_size) {
if(wav_written % byte_rate < sizeof(i2s_readraw_buff)) {
ESP_LOGI(TAG, "Recording: %"PRIu32"/%ds", wav_written/byte_rate + 1, EXAMPLE_RECORD_TIME_SEC);
}
size_t bytes_read = 0;
/* Read RAW samples from ES7210 */
ESP_GOTO_ON_ERROR(i2s_channel_read(i2s_rx_chan, i2s_readraw_buff, sizeof(i2s_readraw_buff), &bytes_read,
pdMS_TO_TICKS(1000)), err, TAG, "error while reading samples from i2s");
/* Write the samples to the WAV file */
ESP_GOTO_ON_FALSE(fwrite(i2s_readraw_buff, bytes_read, 1, f), ESP_FAIL, err,
TAG, "error while writing samples to wav file");
wav_written += bytes_read;
}
err:
i2s_channel_disable(i2s_rx_chan);
ESP_LOGI(TAG, "Recording done! Flushing file buffer");
fclose(f);
return ret;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
WAV文件由header和数据构成,详细的文件格式协议,可以去参考其它资料。
上述代码,用来配置wav_header,以及写入wav数据。
代码中,出现了1个fopen函数和2个fwrite函数。fopen函数用来在SD卡上创建名称为RECORD.WAV的文件。第1个fwirte函数用来给文件中写音频header,第2个fwirte函数用来给文件中写音频数据。
7.3 例程制作过程
我们还是使用sample project作为模板,复制sample_project这个工程到我们的实验文件夹,然后把这个文件夹的名称修改为04-audio_es7210,修改后我的工程路径为D:\esp32s3\04-audio_es7210。
使用VSCode打开audio_es7210工程。再打开一个VSCode打开esp-idf整个文件夹,我们需要参考官方的i2s_es7210_tdm例程,该例程在esp-idf中的路径为examples\peripherals\i2s\i2s_codec\i2s_es7210_tdm。
我们先点击打开audio_es7210工程目录下的CMakeList.txt文件,修改工程的名称为audio_es7210,然后保存关闭此文件。
project(audio_es7210)
点击打开官方i2s_es7210_tdm例程的主c文件,直接把这个i2s_es7210_record_example.c文件中的全部代码复制到实验例程的main.c里面,然后在实验例程的main.c中进行修改。
按照从上往下的顺序,最开始是I2C、I2S、SDIO相关的引脚和外设序号定义,我们把条件编译语句删除,并且依据开发板的引脚进行修改,修改之后的代码如下所示。
/* I2C port and GPIOs */
#define EXAMPLE_I2C_NUM (0)
#define EXAMPLE_I2C_SDA_IO (1)
#define EXAMPLE_I2C_SCL_IO (2)
/* I2S port and GPIOs */
#define EXAMPLE_I2S_NUM (0)
#define EXAMPLE_I2S_MCK_IO (38)
#define EXAMPLE_I2S_BCK_IO (14)
#define EXAMPLE_I2S_WS_IO (13)
#define EXAMPLE_I2S_DI_IO (12)
/* SD card GPIOs */
#define EXAMPLE_SD_CMD_IO (48)
#define EXAMPLE_SD_CLK_IO (47)
#define EXAMPLE_SD_DAT0_IO (21)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
再往下是I2S的配置参数,把CHAN_NUM改成2,把TDM_SLOT_MASK改成SLOT0|SLOT1,因为开发板上只有MIC1和MIC2接收声音,最后如下所示:
/* I2S configurations */
#define EXAMPLE_I2S_TDM_FORMAT (ES7210_I2S_FMT_I2S)
#define EXAMPLE_I2S_CHAN_NUM (2)
#define EXAMPLE_I2S_SAMPLE_RATE (48000)
#define EXAMPLE_I2S_MCLK_MULTIPLE (I2S_MCLK_MULTIPLE_256)
#define EXAMPLE_I2S_SAMPLE_BITS (I2S_DATA_BIT_WIDTH_16BIT)
#define EXAMPLE_I2S_TDM_SLOT_MASK (I2S_TDM_SLOT0 | I2S_TDM_SLOT1)
2
3
4
5
6
7
再往下,把es7210的I2C地址由原来的0x40改成0x41。I2C地址依据es7210芯片的AD0和AD1引脚决定,当AD0和AD1都接GND时,地址是0x40。开发板的AD0接高电平,AD1接低电平,I2C地址是0x41。
#define EXAMPLE_ES7210_I2C_ADDR (0x41)
再往下修改es7210_i2s_init()函数,ES7210工作在TDM模式时,又可以分为TDM_I2S、TDM_LJ、TDM_DSP_A和TDM_DSP_B,所以会有下述代码中的4个条件编译,这4种的具体区别,可以看ES7210数据手册对应的4种时序图。现在我们只需要让它工作在ES7210_I2S_FMT_I2S模式就可以。把条件编译语句去掉,只留下ES7210_I2S_FMT_I2S的语句,结果如下所示:
static i2s_chan_handle_t es7210_i2s_init(void)
{
i2s_chan_handle_t i2s_rx_chan = NULL;
ESP_LOGI(TAG, "Create I2S receive channel");
i2s_chan_config_t i2s_rx_conf = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
ESP_ERROR_CHECK(i2s_new_channel(&i2s_rx_conf, NULL, &i2s_rx_chan));
ESP_LOGI(TAG, "Configure I2S receive channel to TDM mode");
i2s_tdm_config_t i2s_tdm_rx_conf = {
.slot_cfg = I2S_TDM_PHILIPS_SLOT_DEFAULT_CONFIG(EXAMPLE_I2S_SAMPLE_BITS, I2S_SLOT_MODE_STEREO, EXAMPLE_I2S_TDM_SLOT_MASK),
.clk_cfg = {
.clk_src = I2S_CLK_SRC_DEFAULT,
.sample_rate_hz = EXAMPLE_I2S_SAMPLE_RATE,
.mclk_multiple = EXAMPLE_I2S_MCLK_MULTIPLE
},
.gpio_cfg = {
.mclk = EXAMPLE_I2S_MCK_IO,
.bclk = EXAMPLE_I2S_BCK_IO,
.ws = EXAMPLE_I2S_WS_IO,
.dout = -1, // ES7210 only has ADC capability
.din = EXAMPLE_I2S_DI_IO
},
};
ESP_ERROR_CHECK(i2s_channel_init_tdm_mode(i2s_rx_chan, &i2s_tdm_rx_conf));
return i2s_rx_chan;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
接下来修改mount_sdcard函数的内容,官方例程使用的是SPI方式与SD卡通信,现在我们需要改成1-SD模式,在上一章的SD卡例程中,app_main中的前几行,就是1-SD模式SD卡挂载的代码,参考那里的代码,就可以把这里修改好。修改好以后的mount_sdcard函数如下所示:
sdmmc_card_t * mount_sdcard(void)
{
sdmmc_card_t *sdmmc_card = NULL;
ESP_LOGI(TAG, "Mounting SD card");
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = true,
.max_files = 2,
.allocation_unit_size = 8 * 1024
};
ESP_LOGI(TAG, "Initializing SD card");
ESP_LOGI(TAG, "Using SDMMC peripheral");
sdmmc_host_t sdmmc_host = SDMMC_HOST_DEFAULT(); // SDMMC主机接口配置
sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); // SDMMC插槽配置
slot_config.width = 1; // 设置为1线SD模式
slot_config.clk = EXAMPLE_SD_CLK_IO;
slot_config.cmd = EXAMPLE_SD_CMD_IO;
slot_config.d0 = EXAMPLE_SD_DAT0_IO;
slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP; // 打开内部上拉电阻
ESP_LOGI(TAG, "Mounting filesystem");
esp_err_t ret;
while (1) {
ret = esp_vfs_fat_sdmmc_mount(EXAMPLE_SD_MOUNT_POINT, &sdmmc_host, &slot_config, &mount_config, &sdmmc_card);
if (ret == ESP_OK) {
break;
} else if (ret == ESP_FAIL) {
ESP_LOGE(TAG, "Failed to mount filesystem.");
} else {
ESP_LOGE(TAG, "Failed to initialize the card (%s). ", esp_err_to_name(ret));
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
ESP_LOGI(TAG, "Card size: %lluMB, speed: %dMHz",
(((uint64_t)sdmmc_card->csd.capacity) * sdmmc_card->csd.sector_size) >> 20,
sdmmc_card->max_freq_khz / 1000);
return sdmmc_card;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
接下来,需要给文件添加两个sdmmc的头文件,原例程使用的是SPI,我们改成了SD模式,所以需要增加这两个头文件。
#include "sdmmc_cmd.h"
#include "driver/sdmmc_host.h"
2
app_main函数中的其它代码,就不需要修改了。
把官方例程main文件夹下的idf_component.yml文件复制到实验例程的main文件夹下。这个文件用来加载乐鑫的es7210组件。
例程还需要用到format_wav.h这个文件,这个文件的路径位于examples/peripherals/i2s/common,我们找到这个文件,把它复制到实验例程的main文件夹里面。
到这里,程序代码就全部修改好了。
接下来,先选择目标芯片,再修改menuconfig,顺序不能反!!!注意每个例程都是如此。
在menuconfig里面,把FLASH大小修改为16MB,然后把FAT文件系统的Block size修改为4096,然后保存关闭。
接下来就可以编译下载看结果了。
如果你保存的文件需要支持长文件名、支持中文等,请参考第6章的menuconfig配置。
如果没有问题的话,使用idf.py save-defconfig命令生成sdkconfig.defaults文件,此文件保存了你在menuconfig中做的所有改动配置,不包含默认的配置。