【立创·实战派ESP32-S3】文档教程
第 8 章 音频输出-ES8311
开发板上使用 ES8311 作为音频输出控制芯片,ES8311 的输出,同时接到了 ES7210 的 MIC3 和音频功放芯片。
本例程,实现播放一小段格式为 PCM 的音乐,后面的例程,我们会学习播放 MP3 格式的音乐。
8.1 使用例程
把开发板提供的【05-audio_es8311】例程复制到你的实验文件夹当中,并使用 VSCode 打开工程。
连接开发板到电脑,在 VSCode 上选择串口号,选择目标芯片为 esp32s3,串口下载方式,然后点击“一键三联”按钮,等待编译下载打开终端。
终端输出内容如下:
I (421) main_task: Calling app_main()
i2s es8311 codec example start
-----------------------------
I (431) i2s_es8311: i2s driver init success
I (451) ES8311: ES8311 in Slave mode and I2S format
I (461) i2s_es8311: es8311 codec init success
I (461) main_task: Returned from app_main()
2
3
4
5
6
7
同时开发板开始播放声音。
每播放完一段音乐,终端都会输出提示:
I (10381) i2s_es8311: [music] i2s music played, 634240 bytes are written.
音乐循环播放。
8.2 例程讲解
本例程需要用到 es8311 组件,在 idf.component.yml 文件中指定了名称和版本号。
canon.pcm 是要播放的音乐。
点击打开 main.c 文件,找到 app_main 函数。
void app_main(void)
{
printf("i2s es8311 codec example start\n-----------------------------\n");
/* 初始化I2S外设 */
if (i2s_driver_init() != ESP_OK) {
ESP_LOGE(TAG, "i2s driver init failed");
abort();
} else {
ESP_LOGI(TAG, "i2s driver init success");
}
/* 初始化I2C 以及初始化es8311芯片 */
if (es8311_codec_init() != ESP_OK) {
ESP_LOGE(TAG, "es8311 codec init failed");
abort();
} else {
ESP_LOGI(TAG, "es8311 codec init success");
}
pca9557_init(); //初始化IO扩展芯片
pa_en(1); // 打开音频
/* 创建播放音乐任务 */
xTaskCreate(i2s_music, "i2s_music", 4096, NULL, 5, NULL);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
主函数中使用了 5 个函数,分别是:
i2s_driver_init()
函数初始化 i2s 接口。es8311_codec_init()
函数初始化 i2c 接口并初始化 es8311 芯片。pca9557_init()
函数初始化 IO 扩展芯片 pca9557。pa_en()
函数用于控制音频功放的打开和关闭。i2s_music()
函数是创建的任务函数,用于播放音乐。
上面的第 1、2、5 个函数都位于 main.c 文件中,第 3、4 个函数位于 esp32_s3_szp.c 文件中。接下来分别看看它们是怎么实现的。
i2s_driver_init()函数
i2s_driver_init()
函数代码如下所示:
// 初始化I2S外设
static esp_err_t i2s_driver_init(void)
{
/* 配置i2s发送通道 */
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM, I2S_ROLE_MASTER);
chan_cfg.auto_clear = true; // Auto clear the legacy data in the DMA buffer
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle, NULL));
/* 初始化i2s为std模式 并打开i2s发送通道 */
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(EXAMPLE_SAMPLE_RATE),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
.gpio_cfg = {
.mclk = I2S_MCK_IO,
.bclk = I2S_BCK_IO,
.ws = I2S_WS_IO,
.dout = I2S_DO_IO,
.din = I2S_DI_IO,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
},
};
std_cfg.clk_cfg.mclk_multiple = EXAMPLE_MCLK_MULTIPLE;
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle, &std_cfg));
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));
return ESP_OK;
}
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
函数中,前 3 行代码,创建 i2s 发送通道,接下来,初始化 i2s 为 std 模式,并打开了 i2s 发送通道。
es8311_codec_init()函数
es8311_codec_init()
函数代码如下所示:
// 初始化I2C接口 并初始化es8311芯片
static esp_err_t es8311_codec_init(void)
{
/* 初始化I2C接口 */
const i2c_config_t es_i2c_cfg = {
.sda_io_num = BSP_I2C_SDA,
.scl_io_num = BSP_I2C_SCL,
.mode = I2C_MODE_MASTER,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = 100000,
};
ESP_RETURN_ON_ERROR(i2c_param_config(BSP_I2C_NUM, &es_i2c_cfg), TAG, "config i2c failed");
ESP_RETURN_ON_ERROR(i2c_driver_install(BSP_I2C_NUM, I2C_MODE_MASTER, 0, 0, 0), TAG, "install i2c driver failed");
/* 初始化es8311芯片 */
es8311_handle_t es_handle = es8311_create(BSP_I2C_NUM, ES8311_ADDRRES_0);
ESP_RETURN_ON_FALSE(es_handle, ESP_FAIL, TAG, "es8311 create failed");
const es8311_clock_config_t es_clk = {
.mclk_inverted = false,
.sclk_inverted = false,
.mclk_from_mclk_pin = true,
.mclk_frequency = EXAMPLE_MCLK_FREQ_HZ,
.sample_frequency = EXAMPLE_SAMPLE_RATE
};
ESP_ERROR_CHECK(es8311_init(es_handle, &es_clk, ES8311_RESOLUTION_16, ES8311_RESOLUTION_16));
ESP_RETURN_ON_ERROR(es8311_sample_frequency_config(es_handle, EXAMPLE_SAMPLE_RATE * EXAMPLE_MCLK_MULTIPLE, EXAMPLE_SAMPLE_RATE), TAG, "set es8311 sample frequency failed");
ESP_RETURN_ON_ERROR(es8311_voice_volume_set(es_handle, EXAMPLE_VOICE_VOLUME, NULL), TAG, "set es8311 volume failed");
ESP_RETURN_ON_ERROR(es8311_microphone_config(es_handle, false), TAG, "set es8311 microphone failed");
return ESP_OK;
}
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
因为 es8311 需要使用 i2c 接口与 esp32 通信,所以在初始化 es8311 之前,需要先初始化 i2c 接口。i2c 初始化,也可以使用 esp32_s3_szp.c 文件中的 bsp_i2c_init()函数。不管使用哪个,初始化一次就可以了。
es8311_init()函数初始化了芯片的基本功能。
es8311_sample_frequency_config()函数配置采样率。
es8311_voice_volume_set()函数设置输出声音大小,EXAMPLE_VOICE_VOLUME 的有效值是 0~100。
es8311_microphone_config()函数配置麦克风。
pca9557_init()函数
pca9557 是 IO 扩展芯片,由于 esp32s3 芯片的引脚少的可怜,这里必须要借助 IO 扩展芯片来完成一些功能了。该芯片采用 I2C 接口,可以扩展 8 个 IO。在我们开发板上,扩展了 3 个引脚,分别是 LCS_CS、PA_EN、DVP_PWDN。其中,LCD_CS 控制液晶屏,PA_EN 控制音频功放,DVP_PWDN 控制摄像头。
因为这里需要控制音频输出,所以必须要驱动 pca9557 芯片才行。
pca9557_init()
函数代码如下:
// 初始化PCA9557 IO扩展芯片
void pca9557_init(void)
{
// 写入控制引脚默认值 DVP_PWDN=1 PA_EN = 0 LCD_CS = 1
pca9557_register_write_byte(PCA9557_OUTPUT_PORT, 0x05);
// 把PCA9557芯片的IO1 IO1 IO2设置为输出 其它引脚保持默认的输入
pca9557_register_write_byte(PCA9557_CONFIGURATION_PORT, 0xf8);
}
2
3
4
5
6
7
8
从原理图中可以看到,LCD_CS 对应 PCA9557 的 IO0 引脚,PA_EN 对应 IO1 引脚,DVP_PWDN 对应 IO2 引脚。
第 1 条语句的作用是让 DVP_PWDN 和 LCD_CS 引脚输出高电平,让 PA_EN 输出低电平,对应它们的初始状态,让 LCD、音频功放、摄像头都处于禁能状态。PCA9557_OUTPUT_PORT 寄存器的对应位为 1,对应引脚输出高电平,对应位为 0,对应引脚输出低电平,所以这里需要写入 0x05。
第 2 条语句的作用是控制对应的 IO0、IO1、IO2 为输出引脚。PCA9557_CONFIGURATION_PORT 寄存器的对应位为 1,表示对应引脚为输入引脚,对应位为 0,表示对应引脚为输出引脚,所以这里需要写入 0xf8。
这个函数中用到的 pca9557_register_write_byte()函数,位于这个函数的前面,代码如下所示:
// 给PCA9557芯片的寄存器写值
esp_err_t pca9557_register_write_byte(uint8_t reg_addr, uint8_t data)
{
int ret;
uint8_t write_buf[2] = {reg_addr, data};
ret = i2c_master_write_to_device(BSP_I2C_NUM, PCA9557_SENSOR_ADDR, write_buf, sizeof(write_buf), 1000 / portTICK_PERIOD_MS);
return ret;
}
2
3
4
5
6
7
8
9
10
调用了 esp-idf 文件中的 i2c 写函数。
pa_en()函数
pa_en()
函数代码如下所示:
// 控制 PCA9557_PA_EN 引脚输出高低电平 参数0输出低电平 参数1输出高电平
void pa_en(uint8_t level)
{
pca9557_set_output_state(PA_EN_GPIO, level);
}
2
3
4
5
此函数用来控制 PA_EN 引脚输出高低电平。PA_EN 引脚拉低,关闭音频输出,PA_EN 引脚拉高,开启音频输出。
PA_EN_GPIO 的定义在 esp32_s3_szp.h 文件中,如下所示,这里定义了我们使用的 3 个引脚。
#define LCD_CS_GPIO BIT(0) // PCA9557_GPIO_NUM_1
#define PA_EN_GPIO BIT(1) // PCA9557_GPIO_NUM_2
#define DVP_PWDN_GPIO BIT(2) // PCA9557_GPIO_NUM_3
2
3
pa_en()函数中用到了 pca9557_set_output_state()函数,pca9557_set_output_state()函数也在 esp32_s3_szp.c 文件中,定义如下:
// 设置PCA9557芯片的某个IO引脚输出高低电平
esp_err_t pca9557_set_output_state(uint8_t gpio_bit, uint8_t level)
{
char data;
esp_err_t res = ESP_FAIL;
data = pca9557_register_read(PCA9557_OUTPUT_PORT);
res = pca9557_register_write_byte(PCA9557_OUTPUT_PORT, SET_BITS(data, gpio_bit, level));
return res;
}
2
3
4
5
6
7
8
9
10
11
该函数中,先读取“输出寄存器”,然后再给“输出寄存器”写值。这是因为,“输出寄存器”不能按位写,只能一次写一个寄存器。PCA9557 的 8 个 IO 的引脚电平,使用 1 个 8 位寄存器控制,所以我们需要考虑在控制某一个引脚电平的时候,不应该影响其它引脚的当前电平状态。所以需要先读出来这个寄存器,然后给这个读出来的值的某个位赋值 0 或者 1,其它位不动,最后把这个寄存器值写入到输出寄存器中。
这里面用到了 SET_BITS(data, gpio_num, level),这个其实是一个宏定义函数。这个宏定义函数位于 esp32_s3_szp.h 文件中,它的作用是,设置某个 IO 为低电平或者高电平,且不影响其它 IO 的值。第 1 个参数是 data,就是读取到的输出寄存器值。第 2 个参数是你要控制的 IO 序号。第 3 个参数是你要让这个 IO 序号的引脚输出 0 还是 1。
#define SET_BITS(_m, _s, _v) ((_v) ? (_m)|((_s)) : (_m)&~((_s)))
这个宏定义函数是怎么运行的,只需要带个数进去算一次就会明白。
假设读到的 data 值是 0x04(0000 0100),即目前的 IO2 是高电平,其它全是低电平。现在我要让 IO0 输出高电平,那么第 2 个参数值就是 BIT(0),BIT(0)就是(1<<0),即 0x01,这个 BIT 的定义在 esp_bit_defs.h 文件中定义,这个文件的路径是:esp-idf\components\esp_common\include。第 3 个参数值就是 1。我们可以把这几个数值代入计算一下。_m 是 0x04,_s 是 0,_v 是 1。此函数的内容是一个三目运算符,如果“?”号前面的内容为真,就执行“:”号前面的,否则执行“:”号后面的。现在的(_v)是 1,为真,所以执行“:”号前面的,即(_m)|((_s),即 0x04|0x01,结果是 0x05(0000 0101),即让 IO0 变成高电平,且没有影响 IO2 的电平。
i2s_music()函数
i2s_music()
函数代码如下所示:
static void i2s_music(void *args)
{
esp_err_t ret = ESP_OK;
size_t bytes_write = 0;
uint8_t *data_ptr = (uint8_t *)music_pcm_start;
/* (Optional) Disable TX channel and preload the data before enabling the TX channel,
* so that the valid data can be transmitted immediately */
ESP_ERROR_CHECK(i2s_channel_disable(tx_handle));
ESP_ERROR_CHECK(i2s_channel_preload_data(tx_handle, data_ptr, music_pcm_end - data_ptr, &bytes_write));
data_ptr += bytes_write; // Move forward the data pointer
/* Enable the TX channel */
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));
while (1) {
/* Write music to earphone */
ret = i2s_channel_write(tx_handle, data_ptr, music_pcm_end - data_ptr, &bytes_write, portMAX_DELAY);
if (ret != ESP_OK) {
/* Since we set timeout to 'portMAX_DELAY' in 'i2s_channel_write'
so you won't reach here unless you set other timeout value,
if timeout detected, it means write operation failed. */
ESP_LOGE(TAG, "[music] i2s write failed, %s", err_reason[ret == ESP_ERR_TIMEOUT]);
abort();
}
if (bytes_write > 0) {
ESP_LOGI(TAG, "[music] i2s music played, %d bytes are written.", bytes_write);
} else {
ESP_LOGE(TAG, "[music] i2s music play failed.");
abort();
}
data_ptr = (uint8_t *)music_pcm_start;
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}
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
上面代码中的第 5 行,定义了一个 data_ptr 指针,指向了 music_pcm_start,music_pcm_start 是音乐的最开始处,在 main.c 文件中的开始处定义。第 10 行使用 i2s_channel_preload_data()函数中的第 3 个参数出现的 music_pcm_end,是音乐文件的结尾。
while 循环里面,i2s_channel_write()函数用于写入 I2S 数据,其实就是播放音乐。从这里可以看到,每隔 1 秒钟播放一次音乐。
音乐文件的处理
pcm 音乐文件可以由 mp3 音乐转换和剪切制作完成,具体制作方法,请看官方例程 i2s_es8311 中的 ReadMe.md 文件中的 Customize your own music 小节。
i2s_es8311 例程的路径:examples\peripherals\i2s\i2s_codec\i2s_es8311
需要注意的是,官方教程第 5 步把 mp3 转换成 pcm 的命令,里面写错一个字母,你制作的时候需要改过来。
它原来的命令:
ffmpeg -i a_cut.mp3 -f s16ls -ar 16000 -ac -1 -acodec pcm_s16le a.pcm
改成:
ffmpeg -i a_cut.mp3 -f s16le -ar 16000 -ac -1 -acodec pcm_s16le a.pcm
把制作好的 pcm 音乐放到 main 文件夹下,然后打开 main 文件夹下的 CMakeLists.txt 文件,如下所示:
idf_component_register(SRCS "esp32_s3_szp.c" "main.c"
INCLUDE_DIRS "."
EMBED_FILES "canon.pcm")
2
3
最后面的 EMBED_FILES 可以把文件加入到工程编译,canon.pcm 是音乐文件的名称,如果你的音乐文件名称是 xxx.pcm,就把这里的 canon 替换成 xxx。
在 main.c 的开始处,找到如下定义:
extern const uint8_t music_pcm_start[] asm("_binary_canon_pcm_start");
extern const uint8_t music_pcm_start[] asm("_binary_canon_pcm_end");
2
把 asm 里面的 canon 替换成你的 xxx 名称,例如 asm("_binary_xxx_pcm_start")
。
然后就可以在程序中使用 music_pcm_start 和 music_pcm_start 了。
8.3 例程制作过程
我们还是使用 sample project 作为模板,复制 sample_project 这个工程到我们的实验文件夹,然后把这个文件夹的名称修改为 05-audio_es8311,修改后我的工程路径为 D:\esp32s3\05-audio_es8311。使用 VSCode 打开 audio_es8311 工程。
再打开一个 VSCode 打开 esp-idf 整个文件夹,我们需要参考官方的 i2s_es8311 例程,该例程在 esp-idf 中的路径为 examples\peripherals\i2s\i2s_codec\i2s_es8311。
我们先点击打开 audio_es8311 工程目录下的 CMakeList.txt 文件,修改工程的名称为 audio_es8311,然后保存关闭此文件。
project(audio_es8311)
pca9557 的驱动函数,我们写到 esp32_s3_szp.c 文件中,把之前的“02-attitude”例程中写好的 esp32_s3_szp.c 和 esp32_s3_szp.h 文件复制粘贴到本例程的 main 文件夹里面。
然后在 main 下的 CMakeLists.txt 文件中,把源文件名称 esp32_s3_szp.c 添加进去。
idf_component_register(SRCS "esp32_s3_szp.c" "main.c"
INCLUDE_DIRS ".")
2
点击打开官方 i2s_es8311_example.c 文件,把这个.c 文件中的全部内容复制粘贴到我们的 main.c 里面,然后再进行修改。
复制好以后,我们以从上到下的顺序修改。
找到如下代码,把 rx_handle 去掉语句去掉,因为这里不需要接收通道,只需要发送通道。
static i2s_chan_handle_t tx_handle = NULL;
static i2s_chan_handle_t rx_handle = NULL;
2
修改好后的代码如下所示:
static i2s_chan_handle_t tx_handle = NULL;
找到如下代码,把条件编译语句去掉,因为我们将只让它工作在音乐模式。
/* Import music file as buffer */
#if CONFIG_EXAMPLE_MODE_MUSIC
extern const uint8_t music_pcm_start[] asm("_binary_canon_pcm_start");
extern const uint8_t music_pcm_end[] asm("_binary_canon_pcm_end");
#endif
2
3
4
5
修改后代码如下所示:
/* Import music file as buffer */
extern const uint8_t music_pcm_start[] asm("_binary_canon_pcm_start");
extern const uint8_t music_pcm_end[] asm("_binary_canon_pcm_end");
2
3
再往下是 es8311_codec_init 函数,代码如下所示:
static esp_err_t es8311_codec_init(void)
{
/* Initialize I2C peripheral */
#if !defined(CONFIG_EXAMPLE_BSP)
const i2c_config_t es_i2c_cfg = {
.sda_io_num = I2C_SDA_IO,
.scl_io_num = I2C_SCL_IO,
.mode = I2C_MODE_MASTER,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = 100000,
};
ESP_RETURN_ON_ERROR(i2c_param_config(I2C_NUM, &es_i2c_cfg), TAG, "config i2c failed");
ESP_RETURN_ON_ERROR(i2c_driver_install(I2C_NUM, I2C_MODE_MASTER, 0, 0, 0), TAG, "install i2c driver failed");
#else
ESP_ERROR_CHECK(bsp_i2c_init());
#endif
/* Initialize es8311 codec */
es8311_handle_t es_handle = es8311_create(I2C_NUM, ES8311_ADDRRES_0);
ESP_RETURN_ON_FALSE(es_handle, ESP_FAIL, TAG, "es8311 create failed");
const es8311_clock_config_t es_clk = {
.mclk_inverted = false,
.sclk_inverted = false,
.mclk_from_mclk_pin = true,
.mclk_frequency = EXAMPLE_MCLK_FREQ_HZ,
.sample_frequency = EXAMPLE_SAMPLE_RATE
};
ESP_ERROR_CHECK(es8311_init(es_handle, &es_clk, ES8311_RESOLUTION_16, ES8311_RESOLUTION_16));
ESP_RETURN_ON_ERROR(es8311_sample_frequency_config(es_handle, EXAMPLE_SAMPLE_RATE * EXAMPLE_MCLK_MULTIPLE, EXAMPLE_SAMPLE_RATE), TAG, "set es8311 sample frequency failed");
ESP_RETURN_ON_ERROR(es8311_voice_volume_set(es_handle, EXAMPLE_VOICE_VOLUME, NULL), TAG, "set es8311 volume failed");
ESP_RETURN_ON_ERROR(es8311_microphone_config(es_handle, false), TAG, "set es8311 microphone failed");
#if CONFIG_EXAMPLE_MODE_ECHO
ESP_RETURN_ON_ERROR(es8311_microphone_gain_set(es_handle, EXAMPLE_MIC_GAIN), TAG, "set es8311 microphone gain failed");
#endif
return ESP_OK;
}
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
把这里面的条件编译语句都去掉。第 1 个条件编译是询问是否使用乐鑫官方开发板 bsp 的 i2c 初始化函数,这里我们不需要。第 2 个条件编译是询问是否工作在回声模式。这里面涉及到的 I2C 参数,I2C_SDA_IO、I2C_SCL_IO、I2C_NUM,依次修改成 BSP_I2C_SDA、BSP_I2C_SCL、 BSP_I2C_NUM,因为它原来的这个名称定义,是在它的 examle_config.h 文件中,在我们的 esp32_s3_szp.h 文件中,名称是修改后的。修改后的代码如下所示:
static esp_err_t es8311_codec_init(void)
{
/* Initialize I2C peripheral */
const i2c_config_t es_i2c_cfg = {
.sda_io_num = BSP_I2C_SDA,
.scl_io_num = BSP_I2C_SCL,
.mode = I2C_MODE_MASTER,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = 100000,
};
ESP_RETURN_ON_ERROR(i2c_param_config(BSP_I2C_NUM, &es_i2c_cfg), TAG, "config i2c failed");
ESP_RETURN_ON_ERROR(i2c_driver_install(BSP_I2C_NUM, I2C_MODE_MASTER, 0, 0, 0), TAG, "install i2c driver failed");
/* Initialize es8311 codec */
es8311_handle_t es_handle = es8311_create(I2C_NUM, ES8311_ADDRRES_0);
ESP_RETURN_ON_FALSE(es_handle, ESP_FAIL, TAG, "es8311 create failed");
const es8311_clock_config_t es_clk = {
.mclk_inverted = false,
.sclk_inverted = false,
.mclk_from_mclk_pin = true,
.mclk_frequency = EXAMPLE_MCLK_FREQ_HZ,
.sample_frequency = EXAMPLE_SAMPLE_RATE
};
ESP_ERROR_CHECK(es8311_init(es_handle, &es_clk, ES8311_RESOLUTION_16, ES8311_RESOLUTION_16));
ESP_RETURN_ON_ERROR(es8311_sample_frequency_config(es_handle, EXAMPLE_SAMPLE_RATE * EXAMPLE_MCLK_MULTIPLE, EXAMPLE_SAMPLE_RATE), TAG, "set es8311 sample frequency failed");
ESP_RETURN_ON_ERROR(es8311_voice_volume_set(es_handle, EXAMPLE_VOICE_VOLUME, NULL), TAG, "set es8311 volume failed");
ESP_RETURN_ON_ERROR(es8311_microphone_config(es_handle, false), TAG, "set es8311 microphone failed");
return ESP_OK;
}
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
再往后是 I2S 初始化函数,代码如下所示:
static esp_err_t i2s_driver_init(void)
{
#if !defined(CONFIG_EXAMPLE_BSP)
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM, I2S_ROLE_MASTER);
chan_cfg.auto_clear = true; // Auto clear the legacy data in the DMA buffer
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle, &rx_handle));
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(EXAMPLE_SAMPLE_RATE),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
.gpio_cfg = {
.mclk = I2S_MCK_IO,
.bclk = I2S_BCK_IO,
.ws = I2S_WS_IO,
.dout = I2S_DO_IO,
.din = I2S_DI_IO,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
},
};
std_cfg.clk_cfg.mclk_multiple = EXAMPLE_MCLK_MULTIPLE;
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle, &std_cfg));
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle, &std_cfg));
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));
ESP_ERROR_CHECK(i2s_channel_enable(rx_handle));
#else
ESP_LOGI(TAG, "Using BSP for HW configuration");
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(EXAMPLE_SAMPLE_RATE),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
.gpio_cfg = BSP_I2S_GPIO_CFG,
};
std_cfg.clk_cfg.mclk_multiple = EXAMPLE_MCLK_MULTIPLE;
ESP_ERROR_CHECK(bsp_audio_init(&std_cfg, &tx_handle, &rx_handle));
ESP_ERROR_CHECK(bsp_audio_poweramp_enable(true));
#endif
return ESP_OK;
}
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
我们需要把条件编译语句去掉,再把 rx 通道配置相关语句去掉。修改以后的代码如下所示:
static esp_err_t i2s_driver_init(void)
{
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM, I2S_ROLE_MASTER);
chan_cfg.auto_clear = true; // Auto clear the legacy data in the DMA buffer
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle, NULL));
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(EXAMPLE_SAMPLE_RATE),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
.gpio_cfg = {
.mclk = I2S_MCK_IO,
.bclk = I2S_BCK_IO,
.ws = I2S_WS_IO,
.dout = I2S_DO_IO,
.din = I2S_DI_IO,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
},
};
std_cfg.clk_cfg.mclk_multiple = EXAMPLE_MCLK_MULTIPLE;
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle, &std_cfg));
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));
return ESP_OK;
}
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
这其中用到的引脚名称等宏定义,位于官方例程 main 下面的 example_config.h 文件中,我们点击打开它,把 I2S 相关的宏定义,复制粘贴到我们例程的 esp32_s3_szp.h 文件中,并把引脚序号,按照开发板修改,修改后如下所示。
/* Example configurations */
#define EXAMPLE_RECV_BUF_SIZE (2400)
#define EXAMPLE_SAMPLE_RATE (16000)
#define EXAMPLE_MCLK_MULTIPLE (384) // If not using 24-bit data width, 256 should be enough
#define EXAMPLE_MCLK_FREQ_HZ (EXAMPLE_SAMPLE_RATE * EXAMPLE_MCLK_MULTIPLE)
#define EXAMPLE_VOICE_VOLUME (70)
/* I2S port and GPIOs */
#define I2S_NUM (0)
#define I2S_MCK_IO (GPIO_NUM_38)
#define I2S_BCK_IO (GPIO_NUM_14)
#define I2S_WS_IO (GPIO_NUM_13)
#define I2S_DO_IO (GPIO_NUM_45)
#define I2S_DI_IO (-1)
2
3
4
5
6
7
8
9
10
11
12
13
14
EXAMPLE_VOICE_VOLUME 用来定义输出的声音,范围是从 0~100,这里设置为了 70。
ES8311 没有连接 MIC,不需要从它这里读数据,所以 I2S_DI_IO 引脚没有连接,这里设置为-1。
再往下,分别是音乐模式和回声模式的任务函数代码,函数外层还有一个条件编译语句。我们把条件编译语句去掉,同时把回声模式的函数也删除,只留下音乐模式的函数就行,这里比较简单,就不贴代码了。
再往下就是 app_main 函数了,我们把条件编译语句去掉,只留下创建音乐任务的语句就可以了。修改后的代码如下所示:
void app_main(void)
{
printf("i2s es8311 codec example start\n-----------------------------\n");
/* Initialize i2s peripheral */
if (i2s_driver_init() != ESP_OK) {
ESP_LOGE(TAG, "i2s driver init failed");
abort();
} else {
ESP_LOGI(TAG, "i2s driver init success");
}
/* Initialize i2c peripheral and config es8311 codec by i2c */
if (es8311_codec_init() != ESP_OK) {
ESP_LOGE(TAG, "es8311 codec init failed");
abort();
} else {
ESP_LOGI(TAG, "es8311 codec init success");
}
/* Play a piece of music in music mode */
xTaskCreate(i2s_music, "i2s_music", 4096, NULL, 5, NULL);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
接下来,我们需要把官方例程中 main 文件夹下面的 canon.pcm、idf_component.yml 这 2 个文件复制到我们实验文件夹 main 的里面。
点击打开我们例程 main 下的 CMakeLists.txt 文件,使用 EMBED_FILES 把 canon.pcm 文件添加到编译器,如下所示:
idf_component_register(SRCS "esp32_s3_szp.c" "main.c"
INCLUDE_DIRS "."
EMBED_FILES "canon.pcm")
2
3
然后点击打开 idf_component.yml 文件,如下所示:
## IDF Component Manager Manifest File
dependencies:
idf: '^5.0'
espressif/es8311: '^1.0.0'
# After enabling Board Support Packages support in menuconfig, you can pick you specific BSP here
espressif/esp-box:
version: '^2.4.2'
rules:
- if: 'target in [esp32s3]'
#espressif/esp32_s2_kaluga_kit:
# version: "^2.1.1"
# rules:
# - if: "target in [esp32s2]"
#espressif/esp32_s3_lcd_ev_board:
# version: "^1"
# rules:
# - if: "target in [esp32s3]"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这个文件用来加载乐鑫组件,我们只需要定义idf的版本和es8311就可以了,其它的是为乐鑫的官方开发板准备的,这里我们把这些都删除,修改后的代码如下所示:
## IDF Component Manager Manifest File
dependencies:
idf: '^5.0'
espressif/es8311: '^1.0.0'
2
3
4
到这里为止,这个官方的例程代码就修改完毕了,但是还不能下载到开发板运行,因为要想有声音,还需要打开音频功放,控制音频功放的引脚,是IO扩展芯片PCA9557的引脚,所以还需要写好PCA9557的驱动程序才行。
不过,我们可以先编译一下目前的程序,看看修改过程中有没有发生错误。如果编译通过,就说明语法没有问题了,如果编译有错误,就根据终端提示的错误进行修改。
在编译之前,需要先设置目标芯片,设置完目标芯片后,去menuconfig中设置FLASH大小为16MB,然后再进行编译操作。
编译完之后,不用下载到开发板运行,需要先写好pca9557的驱动程序。
pca9557使用i2c通信,所以参照官方i2c_simple例程中的代码修改就可以。
首先需要写好读写PCA9557寄存器的函数,如下代码所示,放到esp32_s3_szp.c文件中。
static uint8_t pca9557_register_read(uint8_t reg_addr)
{
uint8_t data;
i2c_master_write_read_device(I2C_NUM, PCA9557_SENSOR_ADDR, ®_addr, 1, &data, 1, 1000 / portTICK_PERIOD_MS);
return data;
}
esp_err_t pca9557_register_write_byte(uint8_t reg_addr, uint8_t data)
{
int ret;
uint8_t write_buf[2] = {reg_addr, data};
ret = i2c_master_write_to_device(I2C_NUM, PCA9557_SENSOR_ADDR, write_buf, sizeof(write_buf), 1000 / portTICK_PERIOD_MS);
return ret;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
有了上面两个函数,就可以读取和配置PCA9557的寄存器值了。
再写一个PCA9557初始化的函数,放到esp32_s3_szp.c文件中,如下所示:
void pca9557_init(void)
{
// 写入控制引脚默认值 DVP_PWDN=1 PA_EN = 0 LCD_CS = 1
pca9557_register_write_byte(PCA9557_OUTPUT_PORT, 0x05);
// 把PCA9557芯片的IO1 IO1 IO2设置为输出 其它引脚保持默认的输入
pca9557_register_write_byte(PCA9557_CONFIGURATION_PORT, 0xf8);
}
2
3
4
5
6
7
再写一个控制PCA9557任意一个单独引脚变高变低的函数,放到esp32_s3_szp.c文件中,如下所示:
// 设置PCA9557芯片的某个IO引脚输出高低电平
esp_err_t pca9557_set_output_state(uint8_t gpio_bit, uint8_t level)
{
char data;
esp_err_t res = ESP_FAIL;
data = pca9557_register_read(PCA9557_OUTPUT_PORT);
res = pca9557_register_write_byte(PCA9557_OUTPUT_PORT, SET_BITS(data, gpio_bit, level));
return res;
}
2
3
4
5
6
7
8
9
10
11
这里面用到的SET_BITS(data, gpio_num, level),放到esp32_s3_szp.h文件中。
#define SET_BITS(_m, _s, _v) ((_v) ? (_m)|((_s)) : (_m)&~((_s)))
然后我们再写3个分别控制IO0、IO1、IO2的函数,如下所示:
// 控制 PCA9557_LCD_CS 引脚输出高低电平 参数0输出低电平 参数1输出高电平
void lcd_cs(uint8_t level)
{
pca9557_set_output_state(LCD_CS_GPIO, level);
}
// 控制 PCA9557_PA_EN 引脚输出高低电平 参数0输出低电平 参数1输出高电平
void pa_en(uint8_t level)
{
pca9557_set_output_state(PA_EN_GPIO, level);
}
// 控制 PCA9557_DVP_PWDN 引脚输出高低电平 参数0输出低电平 参数1输出高电平
void dvp_pwdn(uint8_t level)
{
pca9557_set_output_state(DVP_PWDN_GPIO, level);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这里面用到的LCD_CS_GPIO、PA_EN_GPIO、DVP_PWDN_GPIO,以及前面用到的寄存器地址等,以及各个函数的声明,放到esp32_s3_szp.h文件中,代码如下所示:
#define PCA9557_INPUT_PORT 0x00
#define PCA9557_OUTPUT_PORT 0x01
#define PCA9557_POLARITY_INVERSION_PORT 0x02
#define PCA9557_CONFIGURATION_PORT 0x03
#define LCD_CS_GPIO BIT(0) // PCA9557_GPIO_NUM_1
#define PA_EN_GPIO BIT(1) // PCA9557_GPIO_NUM_2
#define DVP_PWDN_GPIO BIT(2) // PCA9557_GPIO_NUM_3
#define PCA9557_SENSOR_ADDR 0x19 /*!< Slave address of the MPU9250 sensor */
#define SET_BITS(_m, _s, _v) ((_v) ? (_m)|((_s)) : (_m)&~((_s)))
void pca9557_init(void);
void lcd_cs(uint8_t level);
void pa_en(uint8_t level);
void dvp_pwdn(uint8_t level);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在main.c文件中添加esp32_s3_szp.h头文件。
#include "esp32_s3_szp.h"
在app_main函数中,创建任务的语句前面,添加pca9557初始化的代码,以及让PA_EN引脚拉高,如下所示:
void app_main(void)
{
printf("i2s es8311 codec example start\n-----------------------------\n");
/* Initialize i2s peripheral */
if (i2s_driver_init() != ESP_OK) {
ESP_LOGE(TAG, "i2s driver init failed");
abort();
} else {
ESP_LOGI(TAG, "i2s driver init success");
}
/* Initialize i2c peripheral and config es8311 codec by i2c */
if (es8311_codec_init() != ESP_OK) {
ESP_LOGE(TAG, "es8311 codec init failed");
abort();
} else {
ESP_LOGI(TAG, "es8311 codec init success");
}
pca9557_init(); //初始化IO扩展芯片
pa_en(1); // 打开音频
/* Play a piece of music in music mode */
xTaskCreate(i2s_music, "i2s_music", 4096, NULL, 5, NULL);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
到现在为止,程序就全部修改好了。
如果编译下载测试没有问题的话,使用idf.py save-defconfig命令生成sdkconfig.defaults文件,此文件保存了你在menuconfig中做的所有改动配置,不包含默认的配置。