目录
[一、 准备工作](#一、 准备工作)
[2.1 es8311.h](#2.1 es8311.h)
[2.2 es8311.c](#2.2 es8311.c)
[2.3 mian.c](#2.3 mian.c)
[2.4 工程日志](#2.4 工程日志)
前言
经过第八章的学习,相信大家已经音频信号的传输的各种方式有了体系的了解。那么本章来实现基于外部音频解码器播放flash中存储的音乐的工程。
开发板是微雪的**ESP32-P4-Module-DEV-KIT。**ESP-IDF版本是6.0。基于第一章的模板工程。
一、 准备工作
音频解码器是ES8311,上一章也简单介绍了这款芯片的构成。
首先我们要先制作一个.wav格式的音乐文件。其他格式当然也可以,不过先以该格式测试。因为WAV 格式本质是PCM 原始数据 + 一个 44 字节的文件头(最接近原生 PCM)。所以解码器可以直接使用。
我这使用软件 "Audacity" 截取了歌曲《错位时空》前一小段得到了《cwsk.wav》,转换时采样频率为48K,数据位为32位,得到大小大约9MB。注意查看在自己flash的大小,我的flash为16MB。
另外,注意去设置中把Flash size大小改为自己的实际大小。

接着去分区设置中类型选择自定义分区表 "Custom partition table CSV",因为系统默认分区表的固件程序烧录分区只有1MB,无法存储音频文件。

在项目根目录下创建自定义分区文件partitions.csv, 添加下列内容。关于各种分区的解释可以去看《分区表》,其中factory是固件烧录分区,一定要大于音频文件。注意各分区对齐要求。

bash
# ESP-IDF Partition Table
# Name , Type, SubType, Offset , Size , Flags
nvs , data, nvs , 0x9000 , 0x6000,
phy_init , data, phy , 0xf000 , 0x1000,
factory , app , factory, 0x10000, 10M , ,
| 分区类型 | 对齐要求 | 原因 |
|---|---|---|
data(如 nvs、phy_init) |
4KB(0x1000) |
存储配置数据,Flash 擦除最小单位是 4KB |
app(如 factory、ota_0/ota_1) |
64KB(0x10000) |
ESP32 的 Flash MMU(内存管理单元)映射粒度是 64KB,代码加载时必须按 64KB 建立地址映射 |
二、代码编写
首先添加官方音频解码器驱动,这里面包含很多音频解码芯片的底层驱动。其中就有我使用的ES8311。
打开终端,输入idf.py add-dependency "espressif/esp_codec_dev^1.5.8",重新编译组件就添加成功了。

接着自定义组件es8311,把上面准备的音频文件cwsk.wav粘贴组件es8311根目录下,完整工程如图所示

2.1 es8311.h
cpp
#ifndef __ES8311_H__
#define __ES8311_H__
#include "esp_log.h" // ESP32日志函数
#include "FreeRTOS/FreeRTOS.h" // FreeRTOS函数
#include "FreeRTOS/task.h" // FreeRTOS任务管理函数
#include "FreeRTOS/semphr.h" // FreeRTOS信号量管理函数
#define I2S_num 0 // I2S端口号
#define role I2S_ROLE_MASTER // I2S工作模式,设置为主机模式
#define I2S_MCLK GPIO_NUM_13 // I2S主时钟引脚,连接到ES8311的MCLK引脚
#define I2S_BCLK GPIO_NUM_12 // I2S位时钟引脚,连接到ES8311的BCLK引脚
#define I2S_DOUT GPIO_NUM_9 // I2S数据输出引脚,连接到ES8311的SDOUT引脚
#define I2S_WS GPIO_NUM_10 // I2S帧同步引脚,连接到ES8311的LRCK引脚
#define I2S_DIN GPIO_NUM_11 // I2S数据输入引脚,连接到ES8311的SDIN引脚
#define sample_hz 48000 // 采样率,设置为48000Hz
#define I2S_bits 32 // 每个采样的位数,设置为32位以兼容ES8311的24位数据格式
#define MCLK_multiple 256 // MCLK倍频,设置为256倍以满足ES8311的时钟要求
#define I2C_SDA GPIO_NUM_7 // I2C数据引脚,连接到ES8311的SDA引脚
#define I2C_SCL GPIO_NUM_8 // I2C时钟引脚,连接到ES8311的SCL引脚
#define NS4150_PA GPIO_NUM_53 // NS4150功放使能引脚,连接到NS4150的EN引脚
#define CONFIG_MODE_MUSIC 1 // 1表示启用音乐模式,0表示关闭
#define CONFIG_MODE_ECHO 0 // 1表示启用回声模式,0表示关闭
void audio_start(void);
#endif
2.2 es8311.c
cpp
#include <stdio.h>
#include "es8311.h"
#include "driver/i2s_std.h"
#include "driver/i2c_master.h"
#include "driver/gpio.h"
#include "esp_codec_dev_defaults.h"
#include "esp_codec_dev.h"
static char *TAG = "es8311";
static i2s_chan_handle_t tx_handle = NULL;
static i2s_chan_handle_t rx_handle = NULL;
void I2S_driver_init(void)
{
i2s_chan_config_t i2s_chan_config = I2S_CHANNEL_DEFAULT_CONFIG(I2S_num, role);
ESP_ERROR_CHECK(i2s_new_channel(&i2s_chan_config, &tx_handle, &rx_handle)); // 创建I2S通道
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(sample_hz),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_bits, I2S_SLOT_MODE_STEREO),
.gpio_cfg = {
.mclk = I2S_MCLK,
.bclk = I2S_BCLK,
.ws = I2S_WS,
.dout = I2S_DOUT,
.din = I2S_DIN,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
},
};
std_cfg.clk_cfg.clk_src = I2S_CLK_SRC_APLL;
std_cfg.clk_cfg.mclk_multiple = 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));
}
i2c_master_bus_handle_t I2C_driver_init(void)
{
// I2C主机初始化代码
i2c_master_bus_handle_t i2c_bus_handle = NULL;
i2c_master_bus_config_t bus_config = {
.i2c_port = I2C_NUM_0,
.sda_io_num = I2C_SDA,
.scl_io_num = I2C_SCL,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
};
ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, &i2c_bus_handle));
return i2c_bus_handle;
}
首先进行I2S设备初始化,各API可以看我整理的《ESP32实用API指南3》上面代码实现了I2S通道的创建和I2S标准模式的创建。
查看ES8311,发现支持所有标准模式下所有格式的传输,所以我这用了最广泛使用的Philips 格式。
接着初始化ES8311控制接口I2C设备的初始化。
完成后会有以下日志(我在设置中开启了I2S调试日志),
bash
D (439) i2s_common: tx channel is registered on I2S0 successfully
Desp> ) i2s_common: rx channel is registered on I2S0 successfully
D (441) i2s_common: DMA malloc info: dma_desc_num = 6, dma_desc_buf_size = dma_frame_num * slot_num * data_bit_width = 1920
D (443) i2s_common: APLL expected frequency is 24576000 Hz, real frequency is 24575996 Hz
D (444) i2s_std: Clock division info: [sclk] 24575996 Hz [mdiv] 2 0/2 [mclk] 12287998 Hz [bdiv] 4 [bclk] 3072000 Hz
D (445) i2s_common: MCLK is pinned to GPIO13 on I2S0
D (446) i2s_std: The tx channel on I2S0 has been initialized to STD mode successfully
D (447) i2s_common: DMA malloc info: dma_desc_num = 6, dma_desc_buf_size = dma_frame_num * slot_num * data_bit_width = 1920
W (448) i2s_common: APLL is occupied already, it is working at 24575996 Hz while the expected frequency is 24576000 Hz
W (449) i2s_common: Trying to work at 24575996 Hz...
D (450) i2s_common: APLL expected frequency is 24576000 Hz, real frequency is 24575996 Hz
D (451) i2s_std: Clock division info: [sclk] 24575996 Hz [mdiv] 2 0/2 [mclk] 12287998 Hz [bdiv] 4 [bclk] 3072000 Hz
D (452) i2s_common: MCLK is pinned to GPIO13 on I2S0
D (452) i2s_std: The rx channel on I2S0 has been initialized to STD mode successfully
D (453) i2s_common: i2s tx channel enabled
D (454) i2s_common: i2s rx channel enabled
从日志中可以看出:
I2S0 的发送 (TX) 和 接收 (RX) 通道已成功注册到硬件控制器
开启了6 个 DMA 描述符,每个DMA缓冲区大小 1920 字节
音频时钟APLL期望 24.576MHz,由采样率自动计算得到,实际 24575996Hz(仅4Hz 误差 )ESP32 APLL 是小数分频硬件,微小误差是**正常硬件特性,**完全不影响音频功能。
MCLK期望值:48000*256 = 12288000,实际值:12287998
BCLK期望值:48000*32*2 = 3072000,实际值:3072000
cpp
void ES8311_coder_init(void)
{
i2c_master_bus_handle_t i2c_bus_handle = I2C_driver_init();
// es8311 控制接口设置
audio_codec_i2c_cfg_t i2c_cfg = {
.port = I2C_NUM_0,
.addr = ES8311_CODEC_DEFAULT_ADDR,
.bus_handle = i2c_bus_handle,
};
const audio_codec_ctrl_if_t *ctrl_if = audio_codec_new_i2c_ctrl(&i2c_cfg);
assert(ctrl_if); // 判断控制接口是否创建成功
// es8311 数据接口设置
audio_codec_i2s_cfg_t i2s_cfg = {
.port = I2S_num,
.tx_handle = tx_handle,
.rx_handle = rx_handle,
.clk_src = I2S_CLK_SRC_APLL,
};
const audio_codec_data_if_t *data_if = audio_codec_new_i2s_data(&i2s_cfg);
assert(data_if);
// es8311 GPIO接口设置
const audio_codec_gpio_if_t *gpio_if = audio_codec_new_gpio();
assert(gpio_if);
// es8311 编码器配置
es8311_codec_cfg_t codec_cfg = {
.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH,
.ctrl_if = ctrl_if,
.gpio_if = gpio_if,
.pa_pin = NS4150_PA,
.pa_reverted = false,
.master_mode = false,
.digital_mic = false,
.use_mclk = true,
// 查看实际电路获得
.hw_gain = {
.pa_voltage = 5.0,
.codec_dac_voltage = 3.3,
.pa_gain = 1.6,
},
.mclk_div = MCLK_multiple,
};
const audio_codec_if_t *es8311_if = es8311_codec_new(&codec_cfg);
assert(es8311_if);
// es8311 配置
esp_codec_dev_cfg_t codec_dev_cfg = {
.codec_if = es8311_if,
.data_if = data_if,
.dev_type = ESP_CODEC_DEV_TYPE_IN_OUT,
};
esp_codec_dev_handle_t codec_handle = esp_codec_dev_new(&codec_dev_cfg);
assert(codec_handle);
// 启动es8311
esp_codec_dev_sample_info_t sample_info = {
.bits_per_sample = I2S_bits,
.channel = I2S_SLOT_MODE_STEREO,
.channel_mask =I2S_STD_SLOT_BOTH,
.sample_rate = sample_hz,
.mclk_multiple = MCLK_multiple,
};
esp_codec_dev_open(codec_handle, &sample_info);
// 设置输出音量
esp_codec_dev_set_out_vol(codec_handle, 30);
// 设置输入音量
//esp_codec_dev_set_in_gain(codec_handle, 30);
}
assert(_e); 断言,判断_e是不是非空指针
该组件将硬件行为抽象如下:

通讯通道抽象为两种接口:
audio_codec_ctrl_if_t控制接口: 主要提供read_reg和write_regAPI 来配置编解码器设备 常用控制通道包括 I2C, SPI 等audio_codec_data_if_t数据接口: 主要提供read和writeAPI 用来交换音频数据 常用数据通道包括 I2S, SPI 等
esp_codec_dev 为用户提供便捷的上层 API 来实现播放和录音功能。我们这使用了esp_codec_dev_new和esp_codec_dev_open。
音量统一通过 API **esp_codec_dev_set_out_vol**进行设定。 默认的音量调节区间是 0 - 100,音量 100 对应为 0 dB,每个刻度对应 0.5 dB,音量 0 被特殊映射为 -96 dB。
cpp
#if CONFIG_MODE_MUSIC
extern const uint8_t* music_start asm("_binary_cwsk_wav_start");
extern const uint8_t* music_end asm("_binary_cwsk_wav_end");
#endif
void music_task(void* args);
void audio_start(void)
{
I2S_driver_init();
ES8311_coder_init();
#if CONFIG_MODE_MUSIC
xTaskCreate(music_task, "music_task", 4096, NULL, 5, NULL);
#elif CONFIG_MODE_ECHO
xTaskCreate(echo_task, "echo_task", 4096, NULL, 5, NULL);
#endif
}
void music_task(void* args)
{
size_t bytes_write = 0;
const uint8_t* data_ptr = music_start+44;
ESP_ERROR_CHECK(i2s_channel_disable(tx_handle));
ESP_ERROR_CHECK(i2s_channel_preload_data(tx_handle, data_ptr, music_end - data_ptr, &bytes_write));
data_ptr += bytes_write;
ESP_LOGI(TAG, "music len: %d", music_end - data_ptr);
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));
while(1)
{
i2s_channel_write(tx_handle, data_ptr, music_end - data_ptr, &bytes_write, portMAX_DELAY);
data_ptr = (uint8_t *)music_start+44;
ESP_LOGI(TAG, "i2s music played, %d bytes are written", bytes_write);
}
}
添加依赖访问如下:
cpp
idf_component_register(SRCS "es8311.c"
INCLUDE_DIRS "include"
PRIV_REQUIRES esp_driver_i2s esp_driver_i2c esp_driver_gpio
EMBED_FILES "cwsk.wav")
EMBED_FILES "cwsk.wav"是 ESP-IDF 工程中专门用于嵌入二进制文件到固件的 CMake 配置。表示将 cwsk.wav 音频文件以原始二进制数据的形式烧录到芯片 Flash 中。并自动生成链接器符号:
_binary_cwsk_wav_start(文件起始地址)
_binary_cwsk_wav_end(文件结束地址)
_binary_cwsk_wav_size(文件总大小)
注意: 链接器生成的这些不是一个 "存储地址的指针变量" ,而是一个直接代表 Flash 地址的标签(常量地址)。
在开头也有两个很特殊的用法
bash
extern const uint8_t music_start[] asm("_binary_canon_pcm_start");
extern const uint8_t music_end[] asm("_binary_canon_pcm_end");
**extern表示:**这两个符号不在该 C 文件中定义,由外部提供。
**const:**强制只读,避免修改
**asm(x):**GCC 特殊扩展语法,修改变量x在链接层的符号名。
这是固定搭配操作,相当于给_binary_cwsk_wav_start重命名为music_start ,给_binary_cwsk_wav_end重命名为music_end,所以必须加extern,因为不是新建指针。
**注意:**只有数组名能直接映射这个地址标签,如果换成指针变量会因为「间接寻址」出错。
因为I2S只传输PCM 原始数据,所以首地址data_ptr为music_start+44,避开文件头。
在music任务内,首先先关闭通道,将通道状态变为就绪态,方便调用函数i2s_channel_preload_data,该函数会提前储存一段音频信息在DMA缓冲区,接着启动通道,通道状态变为运行态。
**注意:**此时首地址data_ptr要下移,覆盖提前储存的数据
**作用:**启动播放时无静音、无爆音,无缝开始。可以删除
在while循环中,会不断地重复播放音乐,注意此时预存储的音频数据,所以首地址要恢复过来。另外函数i2s_channel_write是DMA阻塞,阻塞过程可以运行其他任务,对CPU负担小。
2.3 mian.c
cpp
#include <stdio.h>
#include "user.h"
#include "es8311.h"
void app_main(void)
{
CONSOLE_REPL_INIT(); // 初始化控制台REPL环境
//ESP_LDOV4_SET(3300);
audio_start();
while(1)
{
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
2.4 工程日志
bash
D (909) i2s_common: tx channel is registered on I2S0 successfully
D (909) i2s_common: rx channel is registered on I2S0 successfully
D (910) i2s_common: DMA malloc info: dma_desc_num = 6, dma_desc_buf_size = dma_frame_num * slot_num * data_bit_width = 960
D (912) i2s_common: APLL expected frequency is 24576000 Hz, real frequency is 24575996 Hz
D (913) i2s_std: Clock division info: [sclk] 24575996 Hz [mdiv] 2 0/2 [mclk] 12287998 Hz [bdiv] 8 [bclk] 1536000 Hz
D (914) i2s_common: MCLK is pinned to GPIO13 on I2S0
D (915) i2s_std: The tx channel on I2S0 has been initialized to STD mode successfully
D (916) i2s_common: DMA malloc info: dma_desc_num = 6, dma_desc_buf_size = dma_frame_num * slot_num * data_bit_width = 960
W (917) i2s_common: APLL is occupied already, it is working at 24575996 Hz while the expected frequency is 24576000 Hz
W (919) i2s_common: Trying to work at 24575996 Hz...
D (919) i2s_common: APLL expected frequency is 24576000 Hz, real frequency is 24575996 Hz
D (920) i2s_std: Clock division info: [sclk] 24575996 Hz [mdiv] 2 0/2 [mclk] 12287998 Hz [bdiv] 8 [bclk] 1536000 Hz
D (921) i2s_common: MCLK is pinned to GPIO13 on I2S0
D (922) i2s_std: The rx channel on I2S0 has been initialized to STD mode successfully
esp> D (923) i2s_common: i2s tx channel enabled
D (924) i2s_common: i2s rx channel enabled
I (931) ES8311: Work in Slave mode
D (934) i2s_common: i2s tx channel disabled
D (935) i2s_common: i2s rx channel disabled
W (935) i2s_common: APLL is occupied already, it is working at 24575996 Hz while the expected frequency is 24576000 Hz
W (936) i2s_common: Trying to work at 24575996 Hz...
D (937) i2s_common: APLL expected frequency is 24576000 Hz, real frequency is 24575996 Hz
D (938) i2s_std: Clock division info: [sclk] 24575996 Hz [mdiv] 2 0/2 [mclk] 12287998 Hz [bdiv] 8 [bclk] 1536000 Hz
I (939) I2S_IF: STD: TX, data_bit: 16, slot_bit: 16, ws_width: 16, slot_mode: STEREO, slot_mask: 0x3
I (940) I2S_IF: STD: TX, sample_rate_hz: 48000, mclk_multiple: 256, clk_src: 20
W (941) i2s_common: APLL is occupied already, it is working at 24575996 Hz while the expected frequency is 24576000 Hz
W (942) i2s_common: Trying to work at 24575996 Hz...
D (943) i2s_common: APLL expected frequency is 24576000 Hz, real frequency is 24575996 Hz
D (944) i2s_std: Clock division info: [sclk] 24575996 Hz [mdiv] 2 0/2 [mclk] 12287998 Hz [bdiv] 8 [bclk] 1536000 Hz
I (945) I2S_IF: STD: RX, data_bit: 16, slot_bit: 16, ws_width: 16, slot_mode: STEREO, slot_mask: 0x3
I (946) I2S_IF: STD: RX, sample_rate_hz: 48000, mclk_multiple: 256, clk_src: 20
D (947) i2s_common: i2s tx channel enabled
D (947) i2s_common: i2s rx channel enabled
I (963) Adev_Codec: Open codec device OK
D (964) i2s_common: i2s tx channel disabled
I (965) es8311: music len: 3731254
D (965) i2s_common: i2s tx channel enabled
I (20401) es8311: i2s music played, 3731254 bytes are written
I (39865) es8311: i2s music played, 3737014 bytes are written
I (59330) es8311: i2s music played, 3737014 bytes are written
I (78791) es8311: i2s music played, 3737014 bytes are written
I (98255) es8311: i2s music played, 3737014 bytes are written
I (117720) es8311: i2s music played, 3737014 bytes are written