第四十三章 音乐播放器实验
43.3 程序设计
43.3.1 I2S的IDF驱动
MYI2S外设驱动位于ESP-IDF的components\BSP\目录。该目录中的include文件夹存放MYI2S相关的头文件,声明了MYI2S函数和结构体等;而i2s_common.c文件是实现I2S操作函数。要使用MYI2S功能,必须先导入以下头文件。
#include "driver/i2s_std.h"
接下来,作者将介绍一些常用的MYI2S函数,这些函数的描述及其作用如下:
1,分配新的I2S通道i2s_new_channel
该函数用于分配新的I2S通道,其函数原型如下:
esp_err_t i2s_new_channel(const i2s_chan_config_t *chan_cfg,
i2s_chan_handle_t *ret_tx_handle,
i2s_chan_handle_t *ret_rx_handle);
函数形参:
表43.3.1.1 i2s_new_channel函数形参描述
返回值: ESP_OK表示成功分配新通道。 ESP_ERR_NOT_SUPPORTED表示当前芯片不支持该通信模式。 ESP_ERR_INVALID_ARG表示参数无效,例如NULL指针。 ESP_ERR_NOT_FOUND表示未找到可用的I2S通道。 chan_cfg为指向I2S通道配置结构体的指针。接下来,笔者将详细介绍i2s_chan_config_t结构体中的各个成员变量,如下代码所示:typedef struct {
i2s_port_t id; /* I2S端口ID */
i2s_role_t role; /* I2S角色,I2S_ROLE_MASTER或I2S_ROLE_SLAVE */
/* DMA配置 */
uint32_t dma_desc_num; /* I2S DMA缓冲区的数量,也即DMA描述符的数量 */
uint32_t dma_frame_num; /* 每个DMA缓冲区的帧数。每帧表示一次采样数据,*/
union {
bool auto_clear; /* `auto_clear_after_cb`的别名 */
bool auto_clear_after_cb; /* 设置在`on_sent`回调之后自动清除DMA TX缓冲区,
如果没有数据发送,I2S会自动发送零。
这样用户可以直接在回调中分配数据到DMA缓冲区,
且在退出回调后数据不会被清除。*/
};
bool auto_clear_before_cb; /* 设置在`on_sent`回调之前自动清除DMA TX缓冲区,
如果没有数据发送,I2S会自动发送零。
这样用户可以在回调中访问刚刚发送完的数据。*/
bool allow_pd; /* 允许I2S控制器进入低功耗模式。启用此标志后,
驱动会在进入睡眠模式前备份I2S寄存器,
并在退出时恢复这些寄存器。这样可以节省功耗,但会消耗更多的RAM。*/
int intr_priority; /* I2S中断优先级,范围为[0, 7],如果设置为0,
驱动会尝试分配一个相对较低的中断优先级(1,2,3) */
} i2s_chan_config_t;
i2s_chan_config_t结构体用于传递I2S的通道配置参数,以便在调用i2s_new_channel时进行初始化和设置。通过这个结构体,开发者可以灵活配置I2S的主从机模式,以满足不同的需求。
2,初始化I2S通道为标准模式i2s_channel_init_std_mode
该函数用于初始化I2S通道为标准模式,其函数原型如下:
esp_err_t i2s_channel_init_std_mode(i2s_chan_handle_t handle,
const i2s_std_config_t *std_cfg);
函数形参:
表43.3.1.2 i2s_channel_init_std_mode函数形参描述
返回值:
ESP_OK表示初始化成功。
ESP_ERR_NO_MEM表示没有足够的内存存储通道信息。
ESP_ERR_INVALID_ARG表示NULL指针或无效的配置。
ESP_ERR_INVALID_STATE表示此通道未注册。
std_cfg为指向I2S标准模式结构体的指针。接下来,笔者将详细 介绍i2s_std_config_t结构体中的各个成员变量,如下代码所示:
/**
* @brief I2S标准模式声道配置
*/
typedef struct {
/* 一般字段 */
i2s_data_bit_width_t data_bit_width; /* I2S采样数据位宽 */
i2s_slot_bit_width_t slot_bit_width; /* I2S声道位宽(每个声道的总位数) */
i2s_slot_mode_t slot_mode; /* 设置单声道或立体声模式, */
/* 特殊字段 */
i2s_std_slot_mask_t slot_mask; /* 选择左声道、右声道或两者的声道 */
uint32_t ws_width; /* WS信号宽度(即WS信号为高电平的BCLK时钟周期数) */
bool ws_pol; /* WS信号极性,设置为true时,表示先为高电平 */
bool bit_shift; /* 在Philips模式下启用位移 */
#if SOC_I2S_HW_VERSION_1 /* 对于ESP32/ESP32-S2 */
bool msb_right; /* 设置在FIFO中将右声道数据放置在MSB */
#else
bool left_align; /* 设置启用左对齐 */
bool big_endian; /* 设置启用大端字节序 */
bool bit_order_lsb; /* 设置启用LSB优先 */
#endif
} i2s_std_slot_config_t;
/**
* @brief I2S标准模式时钟配置
*/
typedef struct {
/* 一般字段 */
uint32_t sample_rate_hz; /* I2S采样率 */
i2s_clock_src_t clk_src; /* 选择时钟源 */
#if SOC_I2S_HW_VERSION_2
uint32_t ext_clk_freq_hz; /* 外部时钟源频率(单位Hz)*/
#endif
i2s_mclk_multiple_t mclk_multiple; /* MCLK与采样率的倍数 */
} i2s_std_clk_config_t;
/**
* @brief I2S标准模式GPIO引脚配置
*/
typedef struct {
gpio_num_t mclk; /* MCK引脚,默认输出 */
gpio_num_t bclk; /* BCK引脚,主机角色时为输出,Slave角色时为输入 */
gpio_num_t ws; /* WS引脚,主机角色时为输出,Slave角色时为输入 */
gpio_num_t dout; /* 数据输出引脚 */
gpio_num_t din; /* 数据输入引脚 */
struct {
uint32_t mclk_inv: 1; /* 设置为1时,反转MCLK输入/输出 */
uint32_t bclk_inv: 1; /* 设置为1时,反转BCLK输入/输出 */
uint32_t ws_inv: 1; /* 设置为1时,反转WS输入/输出 */
} invert_flags; /* GPIO引脚反转标志 */
} i2s_std_gpio_config_t;
/**
* @brief I2S标准模式主要配置,包括时钟、声道和GPIO配置
*/
typedef struct {
i2s_std_clk_config_t clk_cfg; /* 标准模式时钟配置 */
i2s_std_slot_config_t slot_cfg; /* 标准模式声道配置 */
i2s_std_gpio_config_t gpio_cfg; /* 标准模式GPIO配置 */
} i2s_std_config_t;
这些结构体在I2S配置中起着至关重要的作用,可以帮助用户定制I2S的数据传输、时钟源选择和GPIO引脚设置等。
3,启用I2S TX/RX通道i2s_channel_enable
该函数用于启用I2S TX/RX通道,其函数原型如下:
esp_err_t i2s_channel_enable(i2s_chan_handle_t handle);
函数形参:
表43.3.1.3 i2s_channel_enable函数形参描述
返回值:
ESP_OK表示启动成功。
ESP_ERR_INVALID_ARG表示NULL指针或无效的配置。
ESP_ERR_INVALID_STATE表示该通道尚未初始化或已启动。
4,禁用I2S TX/RX通道i2s_channel_disable
该函数用于禁用I2S TX/RX通道,其函数原型如下:
esp_err_t i2s_channel_disable(i2s_chan_handle_t handle);
函数形参:
表43.3.1.4 i2s_channel_disable函数形参描述
返回值:
ESP_OK表示禁用成功。
ESP_ERR_INVALID_ARG表示NULL指针或无效的配置。
ESP_ERR_INVALID_STATE表示该通道尚未初始化或已启动。
5,删除I2S通道i2s_del_channel
该函数用于删除I2S通道,其函数原型如下:
esp_err_t i2s_del_channel(i2s_chan_handle_t handle);
函数形参:
表43.3.1.5 i2s_del_channel函数形参描述
返回值:
ESP_OK表示删除成功。
ESP_ERR_INVALID_ARG表示NULL指针或无效的配置。
6,重新配置I2S时钟以适应标准模式i2s_channel_reconfig_std_clock
该函数用于重新配置I2S时钟以适应标准模式,其函数原型如下:
esp_err_t i2s_channel_reconfig_std_clock(i2s_chan_handle_t handle,
const i2s_std_clk_config_t *clk_cfg);
函数形参:
表43.3.1.6 i2s_channel_reconfig_std_clock函数形参描述
返回值:
ESP_OK表示时钟设置成功。
ESP_ERR_INVALID_ARG表示NULL指针或无效的配置。
ESP_ERR_INVALID_STATE表示通道未初始化或未停止。
7,重新配置I2S声道以适应标准模式i2s_channel_reconfig_std_slot
该函数用于重新配置I2S声道以适应标准模式,其函数原型如下:
esp_err_t i2s_channel_reconfig_std_slot(i2s_chan_handle_t handle,
const i2s_std_slot_config_t *slot_cfg);
函数形参:
表43.3.1.7 i2s_channel_reconfig_std_slot函数形参描述
返回值:
ESP_OK表示声道设置成功。
ESP_ERR_NO_MEM表示没有足够内存分配DMA缓冲区。
ESP_ERR_INVALID_ARG表示参数无效或通道不是标准模式。
ESP_ERR_INVALID_STATE表示通道未初始化或未处于停止状态。
8,I2S写数据i2s_channel_write
该函数用于I2S写数据,其函数原型如下:
esp_err_t i2s_channel_write(i2s_chan_handle_t handle, const void *src,
size_t size, size_t *bytes_written,
uint32_t timeout_ms);
函数形参:
表43.3.1.8 i2s_channel_write函数形参描述
返回值:
ESP_OK表示写入成功。
ESP_ERR_TIMEOUT表示写入超时。
ESP_ERR_INVALID_ARG表示NULL指针或该句柄不是TX句柄。
ESP_ERR_INVALID_STATE表示写入数据失败。
9,I2S读数据i2s_channel_read
该函数用于I2S读数据,其函数原型如下:
esp_err_t i2s_channel_read(i2s_chan_handle_t handle, const void *dest,
size_t size, size_t *bytes_read,
uint32_t timeout_ms);
函数形参:
表43.3.1.9 i2s_channel_read函数形参描述
返回值:
ESP_OK表示读取成功。
ESP_ERR_TIMEOUT表示写入超时。
ESP_ERR_INVALID_ARG表示NULL指针或该句柄不是RX句柄。
ESP_ERR_INVALID_STATE表示写入数据失败。
43.3.2 程序流程图
图43.3.2.1 音乐播放器实验程序流程图
音乐播放我们从SD卡的指定目前读取音乐文件,解析格式正确后,通过I2S不断向ES8388发送文件数据至播放完成,ES8388解码后通过选择扬声器或直接从耳机输出音乐。为了交互性,我们设置板载的按键用于控制播放的歌曲切换及开始/暂停播放。**
43.3.3 程序解析
在34_music例程中,作者在34_music\components\BSP路径下新建MYI2S和ES8388文件夹,并且需要更改CMakeLists.txt内容,以便在其他文件上调用。
1,I2S驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。MYI2S驱动源码包括两个文件:myi2s.c和myi2s.h。
下面先解析myi2s.h的程序。对I2S所需的管脚及采样率做了定义。
#define I2S_NUM (I2S_NUM_0) /* I2S port */
#define I2S_BCK_IO (GPIO_NUM_47) /* ES8388_SCLK */
#define I2S_WS_IO (GPIO_NUM_48) /* ES8388_LRCK */
#define I2S_DO_IO (GPIO_NUM_49) /* ES8388_SDIN */
#define I2S_DI_IO (GPIO_NUM_50) /* ES8388_SDOUT */
#define I2S_MCK_IO (GPIO_NUM_46) /* ES8388_MCLK */
#define I2S_RECV_BUF_SIZE (2400) /* 接收大小 */
#define I2S_SAMPLE_RATE (44100) /* 采样率 */
#define I2S_MCLK_MULTIPLE (384) /* 如果不使用24位数据宽度,256应该足够了 */
下面我们再解析myi2s.c的程序,该文件定义了myi2s_init、i2s_trx_start、i2s_trx_stop等相关I2S操作函数,这些函数实现代码如下所示:
1,myi2s_init函数
/*
* @brief 初始化I2S
* @param 无
* @retval ESP_OK:初始化成功;其他:失败
*/
esp_err_t myi2s_init(void)
{
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM,
I2S_ROLE_MASTER); /* 默认的通道配置(I2S0,主机) */
chan_cfg.auto_clear = true; /* 自动清除DMA缓冲区遗留的数据 */
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle, &rx_handle));
i2s_std_config_t std_cfg = { /* 标准通信模式配置 */
.clk_cfg = { /* 时钟配置 */
.sample_rate_hz = I2S_SAMPLE_RATE, /* I2S采样率 */
.clk_src = I2S_CLK_SRC_DEFAULT, /* I2S时钟源 */
.mclk_multiple = I2S_MCLK_MULTIPLE, /* I2S主时钟MCLK */
},
.slot_cfg = { /* 声道配置 */
.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, /* 声道支持16位宽 */
.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, /* 通道位宽 */
.slot_mode = I2S_SLOT_MODE_STEREO, /* 立体声 */
.slot_mask = I2S_STD_SLOT_BOTH, /* 启用通道 */
.ws_width = I2S_DATA_BIT_WIDTH_16BIT, /* WS信号位宽 */
.ws_pol = false, /* WS信号极性 */
.bit_shift = true, /* 位移位 */
.left_align = true, /* 左对齐 */
.big_endian = false, /* 小端模式 */
.bit_order_lsb = false /* MSB */
},
.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,
},
},
};
my_std_cfg = std_cfg;
/* 初始化TX通道 */
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle, &std_cfg));
/* 初始化RX通道 */
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle, &std_cfg));
/* 启用TX通道 */
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));
/* 启用RX通道 */
ESP_ERROR_CHECK(i2s_channel_enable(rx_handle));
return ESP_OK;
}
上述函数 myi2s_init用于初始化ESP32-P4的I2S外设,并配置其为标准通信模式。首先,函数设置了I2S通道的默认配置,包括通道角色(主机模式)和DMA数据自动清除功能。接着,函数定义了标准模式下的时钟、数据通道和GPIO配置,确保I2S信号的时钟、数据位宽、信号极性等符合应用要求。然后,通过调用i2s_new_channel创建新的TX和RX通道句柄,并分别初始化为标准模式。最后,函数启用TX和RX通道,启动数据传输。该函数通过ESP_ERROR_CHECK确保每个步骤成功执行,若有任何错误会终止执行并返回错误信息。
2,i2s_trx_start函数
/**
* @brief I2S TRX启动
* @param 无
* @retval 无
*/
void i2s_trx_start(void)
{
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));
ESP_ERROR_CHECK(i2s_channel_enable(rx_handle));
}
上述函数i2s_trx_start用于启动I2S的TX与RX通道。
3,i2s_trx_stop函数
/**
* @brief I2S TRX停止
* @param 无
* @retval 无
*/
void i2s_trx_stop(void)
{
ESP_ERROR_CHECK(i2s_channel_disable(tx_handle));
ESP_ERROR_CHECK(i2s_channel_disable(rx_handle));
}
上述函数i2s_trx_stop用于禁用I2S的TX与RX通道。
3,i2s_deinit函数
/**
* @brief I2S卸载
* @param 无
* @retval 无
*/
void i2s_deinit(void)
{
ESP_ERROR_CHECK(i2s_del_channel(tx_handle));
ESP_ERROR_CHECK(i2s_del_channel(rx_handle));
}
上述函数用于卸载I2S设备。
4,i2s_set_samplerate_bits_sample函数
/**
* @brief 设置采样率和位宽
* @param sampleRate :采样率
* @param bits_sample :位宽
* @retval 无
*/
void i2s_set_samplerate_bits_sample(int samplerate, int bits_sample)
{
i2s_trx_stop();
/* 如果需要更新声道或时钟配置,需要在更新前先禁用通道 */
my_std_cfg.slot_cfg.ws_width = bits_sample; /* 位宽 */
ESP_ERROR_CHECK(i2s_channel_reconfig_std_slot(tx_handle,
&my_std_cfg.slot_cfg));
my_std_cfg.clk_cfg.sample_rate_hz = samplerate; /* 设置采样率 */
ESP_ERROR_CHECK(i2s_channel_reconfig_std_clock(tx_handle,
&my_std_cfg.clk_cfg));
}
上述函数i2s_set_samplerate_bits_sample用于重新配置通道位宽和采样率,值得注意的是,必须先禁用Tx/Rx通道,方能重新配置这些参数。
5,i2s_tx_write函数
/**
* @brief I2S传输数据
* @param buffer: 数据存储区的首地址
* @param frame_size: 数据大小
* @retval 发送的数据长度
*/
size_t i2s_tx_write(uint8_t *buffer, uint32_t frame_size)
{
size_t bytes_written;
ESP_ERROR_CHECK(i2s_channel_write(tx_handle, buffer, frame_size,
&bytes_written, 1000));
return bytes_written;
}
上述函数i2s_tx_write将用于I2S音频数据传输至es8388当中,并返回实际发送的大小。
6,i2s_rx_read函数
/**
* @brief I2S读取数据
* @param buffer: 读取数据存储区的首地址
* @param frame_size: 读取数据大小
* @retval 接收的数据长度
*/
size_t i2s_rx_read(uint8_t *buffer, uint32_t frame_size)
{
size_t bytes_written;
ESP_ERROR_CHECK(i2s_channel_read(rx_handle, buffer, frame_size,
&bytes_written, 1000));
return bytes_written;
}
上述函数i2s_rx_read将用于读取es8388设备的音频数据,并返回实际接收的数据大小。
2,ES8388驱动代码
ES8388主要用来将音频信号转换为数字信号或将数字信号转换为音频信号,接下来,我们开始介绍ES8388的几个函数,代码如下:
i2c_master_dev_handle_t es8388_handle = NULL;
/**
* @brief ES8388写寄存器
* @param reg_addr:寄存器地址
* @param data:写入的数据
* @retval 无
*/
esp_err_t es8388_write_reg(uint8_t reg_addr, uint8_t data)
{
esp_err_t ret;
uint8_t *buf = malloc(2);
if (buf == NULL)
{
ESP_LOGE(es8388_tag, "%s memory failed", __func__);
return ESP_ERR_NO_MEM; /* 分配内存失败 */
}
buf[0] = reg_addr;
buf[1] = data; /* 拷贝数据至存储区当中 */
do
{
i2c_master_bus_wait_all_done(bus_handle, 1000);
ret = i2c_master_transmit(es8388_handle, buf, 2, 1000);
} while (ret != ESP_OK);
free(buf); /* 发送完成释放内存 */
return ret;
}
/**
* @brief ES8388读寄存器
* @param reg_add:寄存器地址
* @param p_data:读取的数据
* @retval 无
*/
esp_err_t es8388_read_reg(uint8_t reg_addr, uint8_t *pdata)
{
uint8_t reg_data = 0;
i2c_master_transmit_receive(es8388_handle, ®_addr, 1, ®_data, 1, -1);
return reg_data;
}
/**
* @brief ES8388初始化
* @param 无
* @retval 0,初始化正常
* 其他,错误代码
*/
uint8_t es8388_init(void)
{
uint8_t ret_val = 0;
/* 未调用myiic_init初始化IIC */
if (bus_handle == NULL)
{
ESP_ERROR_CHECK(myiic_init());
}
i2c_device_config_t es8388_i2c_dev_conf = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7, /* 从机地址长度 */
.scl_speed_hz = IIC_SPEED_CLK, /* 传输速率 */
.device_address = ES8388_ADDR, /* 从机7位的地址 */
};
/* I2C总线上添加ES8388设备 */
ESP_ERROR_CHECK(i2c_master_bus_add_device(bus_handle, &es8388_i2c_dev_conf,
&es8388_handle));
ret_val |= es8388_write_reg(0, 0x80); /* 软复位ES8388 */
ret_val |= es8388_write_reg(0, 0x00);
vTaskDelay(pdMS_TO_TICKS(100)); /* 等待复位 */
ret_val |= es8388_write_reg(0x01, 0x58);
ret_val |= es8388_write_reg(0x01, 0x50);
ret_val |= es8388_write_reg(0x02, 0xF3);
ret_val |= es8388_write_reg(0x02, 0xF0);
ret_val |= es8388_write_reg(0x03, 0x09); /* 麦克风偏置电源关闭 */
ret_val |= es8388_write_reg(0x00, 0x06); /* 使能参考 500K驱动使能 */
ret_val |= es8388_write_reg(0x04, 0x00); /* DAC电源管理,不打开任何通道 */
ret_val |= es8388_write_reg(0x08, 0x00); /* MCLK不分频 */
ret_val |= es8388_write_reg(0x2B, 0x80); /* DAC控制 DACLRC与ADCLRC相同 */
ret_val |= es8388_write_reg(0x09, 0x88); /* ADC L/R PGA增益配置为+24dB */
/* ADC数据选择为left data = left ADC, right data=left ADC 音频数据为16bit */
ret_val |= es8388_write_reg(0x0C, 0x4C);
ret_val |= es8388_write_reg(0x0D, 0x02); /* ADC配置 MCLK/采样率=256 */
/* ADC数字音量控制将信号衰减 L 设置为最小!!! */
ret_val |= es8388_write_reg(0x10, 0x00);
/* ADC数字音量控制将信号衰减 R 设置为最小!!! */
ret_val |= es8388_write_reg(0x11, 0x00);
ret_val |= es8388_write_reg(0x17, 0x18); /* DAC音频数据为16bit */
ret_val |= es8388_write_reg(0x18, 0x02); /* DAC配置 MCLK/采样率=256 */
/* DAC数字音量控制将信号衰减 L 设置为最小!!! */
ret_val |= es8388_write_reg(0x1A, 0x00);
/* DAC数字音量控制将信号衰减 R 设置为最小!!! */
ret_val |= es8388_write_reg(0x1B, 0x00);
ret_val |= es8388_write_reg(0x27, 0xB8); /* L混频器 */
ret_val |= es8388_write_reg(0x2A, 0xB8); /* R混频器 */
vTaskDelay(pdMS_TO_TICKS(100));
if (ret_val != ESP_OK)
{
while(1)
{
ESP_LOGI(es8388_tag, "ES8388 fail");
vTaskDelay(pdMS_TO_TICKS(500));
}
}
else
{
ESP_LOGI(es8388_tag, "ES8388 success");
}
es8388_adda_cfg(0, 0); /* 开启DAC关闭ADC */
es8388_input_cfg(0); /* 关闭录音输入 */
es8388_output_cfg(0, 0); /* DAC选择通道输出 */
es8388_hpvol_set(0); /* 设置耳机音量 */
es8388_spkvol_set(0); /* 设置喇叭音量 */
return 0;
}
/**
* @brief ES8388反初始化
* @param 无
* @retval 0,初始化正常
* 其他,错误代码
*/
esp_err_t es8388_deinit(void)
{
return es8388_write_reg(0x02, 0xFF); /* 复位和暂停ES8388 */
}
/**
* @brief 设置ES8388工作模式
* @param fmt : 工作模式
* @arg 0, 飞利浦标准I2S;
* @arg 1, MSB(左对齐);
* @arg 2, LSB(右对齐);
* @arg 3, PCM/DSP
* @param len : 数据长度
* @arg 0, 24bit
* @arg 1, 20bit
* @arg 2, 18bit
* @arg 3, 16bit
* @arg 4, 32bit
* @retval 无
*/
void es8388_i2s_cfg(uint8_t fmt, uint8_t len)
{
fmt &= 0x03;
len &= 0x07; /* 限定范围 */
es8388_write_reg(23, (fmt << 1) | (len << 3)); /* R23,ES8388工作模式设置 */
}
/**
* @brief 设置耳机音量
* @param volume : 音量大小(0 ~ 33)
* @retval 无
*/
void es8388_hpvol_set(uint8_t volume)
{
if (volume > 33)
{
volume = 33;
}
es8388_write_reg(0x2E, volume);
es8388_write_reg(0x2F, volume);
}
/**
* @brief 设置喇叭音量
* @param volume : 音量大小(0 ~ 33)
* @retval 无
*/
void es8388_spkvol_set(uint8_t volume)
{
if (volume > 33)
{
volume = 33;
}
es8388_write_reg(0x30, volume);
es8388_write_reg(0x31, volume);
}
/**
* @brief 设置3D环绕声
* @param depth : 0 ~ 7(3D强度,0关闭,7最强)
* @retval 无
*/
void es8388_3d_set(uint8_t depth)
{
depth &= 0x7; /* 限定范围 */
es8388_write_reg(0x1D, depth << 2); /* R7,3D环绕设置 */
}
/**
* @brief ES8388 DAC/ADC配置
* @param dacen : dac使能(1) / 关闭(0)
* @param adcen : adc使能(1) / 关闭(0)
* @retval 无
*/
void es8388_adda_cfg(uint8_t dacen, uint8_t adcen)
{
uint8_t tempreg = 0;
tempreg |= !dacen << 0;
tempreg |= !adcen << 1;
tempreg |= !dacen << 2;
tempreg |= !adcen << 3;
es8388_write_reg(0x02, tempreg);
}
/**
* @brief ES8388 DAC输出通道配置
* @param o1en : 通道1使能(1)/禁止(0)
* @param o2en : 通道2使能(1)/禁止(0)
* @retval 无
*/
void es8388_output_cfg(uint8_t o1en, uint8_t o2en)
{
uint8_t tempreg = 0;
tempreg |= o1en * (3 << 4);
tempreg |= o2en * (3 << 2);
es8388_write_reg(0x04, tempreg);
}
/**
* @brief ES8388 MIC增益设置(MIC PGA增益)
* @param gain : 0~8, 对应0~24dB 3dB/Step
* @retval 无
*/
void es8388_mic_gain(uint8_t gain)
{
gain &= 0x0F;
gain |= gain << 4;
es8388_write_reg(0x09, gain); /* R9,左右通道PGA增益设置 */
}
/**
* @brief ES8388 ALC设置
* @param sel
* @arg 0,关闭ALC
* @arg 1,右通道ALC
* @arg 2,左通道ALC
* @arg 3,立体声ALC
* @param maxgain : 0~7,对应-6.5~+35.5dB
* @param minigain: 0~7,对应-12~+30dB 6dB/STEP
* @retval 无
*/
void es8388_alc_ctrl(uint8_t sel, uint8_t maxgain, uint8_t mingain)
{
uint8_t tempreg = 0;
tempreg = sel << 6;
tempreg |= (maxgain & 0x07) << 3;
tempreg |= mingain & 0x07;
es8388_write_reg(0x12, tempreg); /* R18,ALC设置 */
}
/**
* @brief ES8388 ADC输出通道配置
* @param in : 输入通道
* @arg 0, 通道1输入
* @arg 1, 通道2输入
* @retval 无
*/
void es8388_input_cfg(uint8_t in)
{
es8388_write_reg(0x0A, (5 * in) << 4); /* ADC1 输入通道选择L/R INPUT1 */
}
以上代码中,es8388_init函数用于初始化es8388,这里只是通用配置(ADC&DAC),初始化完成后,并不能正常播放音乐,还需要通过es8388_adda_cfg函数使能DAC,然后通过设置es8388_output_cfg选择DAC输出,通过es8388_i2s_cfg配置I2S工作模式,最后设置音量才可以接收I2S音频数据,实现音乐播放。
3,wavplay代码
该文件用于wav格式的音频文件解码,接下来看看wavplay.c里面的几个函数,代码如下:
/**
* @brief WAV解析初始化
* @param fname : 文件路径+文件名
* @param wavx : 信息存放结构体指针
* @retval 0,打开文件成功
* 1,打开文件失败
* 2,非WAV文件
* 3,DATA区域未找到
*/
uint8_t wav_decode_init(uint8_t *fname, __wavctrl *wavx)
{
FIL *ftemp;
uint8_t *buf;
uint32_t br = 0;
uint8_t res = 0;
ChunkRIFF *riff;
ChunkFMT *fmt;
ChunkFACT *fact;
ChunkDATA *data;
ftemp = (FIL*)malloc(sizeof(FIL));
buf = malloc(512);
if (ftemp && buf)
{
res = f_open(ftemp, (TCHAR*)fname, FA_READ); /* 打开文件 */
if (res == FR_OK)
{
f_read(ftemp, buf, 512, (UINT *)&br); /* 读取512字节在数据 */
riff = (ChunkRIFF *)buf;
if (riff->Format == 0x45564157) /* 是WAV文件 */
{
fmt = (ChunkFMT *)(buf + 12);
fact = (ChunkFACT *)(buf + 12 + 8 + fmt->ChunkSize);
if (fact->ChunkID == 0x74636166 || fact->ChunkID == 0x5453494C)
{
wavx->datastart=12+8 + fmt->ChunkSize + 8 + fact->ChunkSize;
}
else
{
wavx->datastart = 12 + 8 + fmt->ChunkSize;
}
data = (ChunkDATA *)(buf + wavx->datastart);
if (data->ChunkID == 0x61746164) /* 解析成功 */
{
wavx->audioformat = fmt->AudioFormat; /* 音频格式 */
wavx->nchannels = fmt->NumOfChannels; /* 通道数 */
wavx->samplerate = fmt->SampleRate; /* 采样率 */
wavx->bitrate = fmt->ByteRate * 8;
wavx->blockalign = fmt->BlockAlign;
wavx->bps = fmt->BitsPerSample;
wavx->datasize = data->ChunkSize;
wavx->datastart = wavx->datastart + 8;
printf("wavx->audioformat:%d\r\n", wavx->audioformat);
printf("wavx->nchannels:%d\r\n", wavx->nchannels);
printf("wavx->samplerate:%ld\r\n", wavx->samplerate);
printf("wavx->bitrate:%ld\r\n", wavx->bitrate);
printf("wavx->blockalign:%d\r\n", wavx->blockalign);
printf("wavx->bps:%d\r\n", wavx->bps);
printf("wavx->datasize:%ld\r\n", wavx->datasize);
printf("wavx->datastart:%ld\r\n", wavx->datastart);
}
else
{
res = 3;
}
}
else
{
res = 2;
}
}
else
{
res = 1;
}
}
f_close(ftemp);
free(ftemp);
free(buf);
return 0;
}
/**
* @brief 获取当前播放时间
* @param fx : 文件指针
* @param wavx : wavx播放控制器
* @retval 无
*/
void wav_get_curtime(FIL *fx, __wavctrl *wavx)
{
long long fpos = 0;
wavx->totsec = wavx->datasize / (wavx->bitrate / 8); /* 歌曲总长度(单位:秒) */
fpos = fx->fptr-wavx->datastart; /* 得到当前文件播放到的地方 */
wavx->cursec = fpos * wavx->totsec / wavx->datasize;/* 当前播放到第多少秒了? */
}
/**
* @brief music任务
* @param pvParameters : 传入参数(未用到)
* @retval 无
*/
void music(void *pvParameters)
{
pvParameters = pvParameters;
/* ES8388初始化配置,有效降低启动时发出沙沙声 */
es8388_adda_cfg(1,0); /* 打开DAC,关闭ADC */
es8388_input_cfg(0); /* 录音关闭 */
es8388_output_cfg(1,1); /* 喇叭通道和耳机通道打开 */
es8388_hpvol_set(20); /* 设置喇叭 */
es8388_spkvol_set(20); /* 设置耳机 */
xl9555_pin_write(SPK_EN_IO,0); /* 打开喇叭 */
vTaskDelay(pdMS_TO_TICKS(20));
i2s_tx_write(g_audiodev.tbuf, WAV_TX_BUFSIZE); /* 先发送一段无声音的数据 */
while(1)
{
if ((g_audiodev.status & 0x0F) == 0x03) /* 打开了音频 */
{
for(uint16_t readTimes = 0; readTimes < (wavctrl.datasize
/ WAV_TX_BUFSIZE); readTimes++)
{
if ((g_audiodev.status & 0x0F) == 0x00) /* 暂停播放 */
{
file_read_pos = f_tell(g_audiodev.file); /* 记录暂停位置 */
while(1)
{
if ((g_audiodev.status & 0x0F) == 0x03) /* 重新打开了 */
{
break;
}
vTaskDelay(pdMS_TO_TICKS(5)); /* 死等 */
}
f_lseek(g_audiodev.file,file_read_pos);
}
/* 判断是否播放完成 */
if (i2s_table_size >= wavctrl.datasize
|| i2s_play_next_prev== ESP_OK)
{
audio_stop(); /* 先停止播放 */
i2s_deinit(); /* 卸载I2S */
i2s_table_size = 0; /* 总大小清零 */
i2s_play_end = ESP_OK; /* 已播放完成标志位 */
vTaskDelete(NULL); /* 删除当前任务 */
vTaskDelay(pdMS_TO_TICKS(5)); /* 适当延时(为了删除这个任务) */
break; /* 防止延时5ms未能删除音频任务 */
}
f_read(g_audiodev.file,g_audiodev.tbuf, WAV_TX_BUFSIZE,
(UINT*)&bytes_write);
i2s_table_size = i2s_table_size + i2s_tx_write(g_audiodev.tbuf,
WAV_TX_BUFSIZE);
}
}
vTaskDelay(pdMS_TO_TICKS(1));
}
vTaskDelete(NULL);
}
/**
* @brief 播放某个wav文件
* @param fname : 文件路径+文件名
* @retval KEY0_PRES : 下一首
* KEY1_PRES : 上一首
* KEY2_PRES : 停止/启动
* 其他,非WAV文件
*/
uint8_t wav_play_song(uint8_t *fname)
{
uint8_t key = 0;
uint8_t res = 0;
i2s_play_end = ESP_FAIL;
i2s_play_next_prev = ESP_FAIL;
g_audiodev.file = (FIL*)heap_caps_malloc(sizeof(FIL),MALLOC_CAP_DMA);
g_audiodev.tbuf = heap_caps_malloc(WAV_TX_BUFSIZE, MALLOC_CAP_DMA);
myi2s_init(); /* I2S初始化 */
vTaskDelay(pdMS_TO_TICKS(50)); /* 适当延时 */
if (g_audiodev.file || g_audiodev.tbuf)
{
memset(g_audiodev.file,0,sizeof(FIL)); /* 文件指针清零 */
memset(g_audiodev.tbuf,0,WAV_TX_BUFSIZE); /* buf清零 */
memset(&wavctrl,0,sizeof(__wavctrl)); /* 对WAV结构体相关参数清零 */
res = wav_decode_init(fname, &wavctrl); /* 对wav音频文件解码 */
if (res == 0) /* 解码成功 */
{
if (wavctrl.bps == 16) /* 根据解码文件重新配置采样率和位宽 */
{
i2s_set_samplerate_bits_sample(wavctrl.samplerate,
I2S_BITS_PER_SAMPLE_16BIT);
}
else if (wavctrl.bps == 24)
{
i2s_set_samplerate_bits_sample(wavctrl.samplerate,
I2S_BITS_PER_SAMPLE_24BIT);
}
res = f_open(g_audiodev.file, (TCHAR*)fname, FA_READ);
if (res == FR_OK)
{
audio_start(); /* 开启I2S */
/* 打开成功后,才创建音频任务 */
if (MUSICTask_Handler == NULL && res == FR_OK)
{
taskENTER_CRITICAL(&my_spinlock);
xTaskCreate(music, "music",4096,&MUSICTask_Handler,5, NULL);
taskEXIT_CRITICAL(&my_spinlock);
}
while (res == FR_OK)
{
while (1)
{
/* 播放结束,下一首 */
if (i2s_play_end == ESP_OK)
{
res = KEY0_PRES;
break;
}
key = xl9555_key_scan(0);
switch (key)
{
/* 下一首/上一首 */
case KEY0_PRES:
case KEY1_PRES:
i2s_play_next_prev = ESP_OK;
break;
/* 暂停/开启 */
case KEY2_PRES:
if ((g_audiodev.status & 0x0F) == 0x03)
{
audio_stop();
}
else if ((g_audiodev.status & 0x0F) == 0x00)
{
audio_start();
}
break;
}
if ((g_audiodev.status & 0x0F) == 0x03)
{
wav_get_curtime(g_audiodev.file, &wavctrl);
audio_msg_show(wavctrl.totsec, wavctrl.cursec,
wavctrl.bitrate);
}
vTaskDelay(pdMS_TO_TICKS(10));
}
if (key == KEY1_PRES || key == KEY0_PRES)
{
res = key;
break;
}
}
}
else
{
res = 0xFF;
}
}
else
{
res = 0xFF;
}
}
else
{
res = 0xFF;
}
heap_caps_free(g_audiodev.file);
heap_caps_free(g_audiodev.tbuf);
g_audiodev.tbuf = NULL;
g_audiodev.file = NULL;
MUSICTask_Handler = NULL;
return res;
}
这段代码实现了基于ESP32-P4平台的WAV音频文件播放功能。核心包括wav_decode_init、wav_get_curtime、music和wav_play_song函数,分别负责WAV文件的解析、播放时间的获取、音频数据的传输以及整体音频播放的控制。
wav_decode_init函数解析WAV文件头,提取音频格式、采样率、位宽等信息,并计算数据的起始位置。music函数则用于在FreeRTOS环境下运行音频播放任务,控制音频数据的读取与传输,支持暂停、播放、切换歌曲等功能。wav_play_song函数是音频播放的主控制逻辑,负责初始化I2S播放器,处理用户输入并响应控制命令,如切换歌曲或暂停播放。通过这些函数,系统能够实现高效的 WAV 音频文件播放,并根据用户输入进行实时控制。
4,audioplay代码
该文件主要实现了对 SD 卡中的文件进行扫描,检查是否包含音频文件,并识别这些文件是否为有效的音频文件,如下代码。
/**
* @brief 播放音乐
* @param 无
* @retval 无
*/
void audio_play(void)
{
uint8_t res;
FF_DIR wavdir;
FILINFO *wavfileinfo;
uint8_t *pname;
uint16_t totwavnum;
uint16_t curindex;
uint8_t key;
uint32_t temp;
uint32_t *wavoffsettbl;
while (f_opendir(&wavdir, "0:/MUSIC"))
{
text_show_string(30, 190, 240, 16, "MUSIC文件夹错误!", 16, 0, BLUE);
vTaskDelay(200);
lcd_fill(30, 190, 240, 206, WHITE);
vTaskDelay(200);
}
totwavnum = audio_get_tnum((uint8_t *)"0:/MUSIC"); /* 得到总有效文件数 */
while (totwavnum == 0)
{
text_show_string(30, 190, 240, 16, "没有音乐文件!", 16, 0, BLUE);
vTaskDelay(200);
lcd_fill(30, 190, 240, 146, WHITE);
vTaskDelay(200);
}
wavfileinfo = (FILINFO*)malloc(sizeof(FILINFO));
pname = malloc(255 * 2 + 1);
wavoffsettbl = malloc(4 * totwavnum);
while (!wavfileinfo || !pname || !wavoffsettbl)
{
text_show_string(30, 190, 240, 16, "内存分配失败!", 16, 0, BLUE);
vTaskDelay(200);
lcd_fill(30, 190, 240, 146, WHITE);
vTaskDelay(200);
}
res = f_opendir(&wavdir, "0:/MUSIC");
if (res == FR_OK)
{
curindex = 0; /* 当前索引为0 */
while (1)
{
temp = wavdir.dptr; /* 记录当前index */
res = f_readdir(&wavdir, wavfileinfo); /* 读取目录下的一个文件 */
if ((res != FR_OK) || (wavfileinfo->fname[0] == 0))
{
break; /* 错误了/到末尾了,退出 */
}
res = exfuns_file_type(wavfileinfo->fname);
if ((res & 0xF0) == 0x40)
{
wavoffsettbl[curindex] = temp; /* 记录索引 */
curindex++;
}
}
}
curindex = 0; /* 从0开始显示 */
res = f_opendir(&wavdir, (const TCHAR*)"0:/MUSIC");
while (res == FR_OK) /* 打开目录 */
{
atk_dir_sdi(&wavdir, wavoffsettbl[curindex]); /* 改变当前目录索引 */
res = f_readdir(&wavdir, wavfileinfo); /* 读取文件 */
if ((res != FR_OK) || (wavfileinfo->fname[0] == 0))
{
break;
}
strcpy((char *)pname, "0:/MUSIC/");
strcat((char *)pname, (const char *)wavfileinfo->fname);
lcd_fill(30, 190, mipidev.pwidth - 1, 190 + 16, WHITE);
audio_index_show(curindex + 1, totwavnum);
text_show_string(30, 190, mipidev.pwidth - 60, 16,
(char *)wavfileinfo->fname, 16, 0, BLUE);
key = audio_play_song(pname);
if (key == KEY1_PRES) /* 上一首 */
{
if (curindex)
{
curindex--;
}
else
{
curindex = totwavnum - 1;
}
}
else if (key == KEY0_PRES) /* 下一首 */
{
curindex++;
if (curindex >= totwavnum)
{
curindex = 0;
}
}
else
{
break;
}
}
free(wavfileinfo);
free(pname);
free(wavoffsettbl);
}
/**
* @brief 播放某个音频文件
* @param fname : 文件名
* @retval 按键值
* @arg KEY0_PRES , 下一曲.
* @arg KEY1_PRES , 上一曲.
* @arg 其他 , 错误
*/
uint8_t audio_play_song(uint8_t *fname)
{
uint8_t res;
res = exfuns_file_type((char *)fname);
switch (res)
{
case T_WAV:
res = wav_play_song(fname);
break;
case T_MP3:
/* 自行实现 */
break;
default: /* 其他文件,自动跳转到下一曲 */
printf("can't play:%s\r\n", fname);
res = KEY0_PRES;
break;
}
return res;
}
这里,audio_play函数在main函数里面被调用,该函数首先设置ES8388相关配置,然后查找SD卡里面的MUSIC文件夹,并统计该文件夹里面总共有多少音频文件(统计包括:WAV/MP3/APE/FLAC等),然后,该函数调用audio_play_song函数,按顺序播放这些音频文件。
在audio_play_song函数里面,通过判断文件类型,调用不同的解码函数,本实验,只支持WAV文件,通过wav_play_song函数实现WAV解码。
5,CMakeLists.txt文件
本例程的功能实现主要依靠MYI2S驱动。要在main函数中,成功调用MYI2S文件中的内容,就得需要修改BSP文件夹下的CMakeLists.txt文件,修改如下:
set(src_dirs
LED
KEY
MYIIC
XL9555
LCD
ES8388
MYI2S
SDMMC)
set(include_dirs
LED
KEY
MYIIC
XL9555
LCD
ES8388
MYI2S
SDMMC)
set(requires
driver
esp_lcd
esp_common
fatfs)
idf_component_register(SRC_DIRS ${src_dirs} INCLUDE_DIRS ${include_dirs}
REQUIRES ${requires})
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
6,main.c驱动代码
在main.c里面编写如下代码。
void app_main(void)
{
esp_err_t ret;
uint8_t key = 0;
ret = nvs_flash_init(); /* 初始化NVS */
if(ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ESP_ERROR_CHECK(nvs_flash_init());
}
key_init(); /* 初始化KEY */
myiic_init(); /* IIC0初始化 */
xl9555_init(); /* XL9555初始化 */
lcd_init(); /* LCD屏初始化 */
es8388_init(); /* ES8388初始化 */
xl9555_pin_write(SPK_EN_IO, 1); /* 打开喇叭 */
while (sdmmc_init()) /* 检测不到SD卡 */
{
lcd_show_string(30, 110, 200, 16, 16, "SD Card Error!", RED);
vTaskDelay(pdMS_TO_TICKS(200));
lcd_fill(30, 110, 239, 126, WHITE);
vTaskDelay(pdMS_TO_TICKS(200));
}
ret = exfuns_init(); /* 为fatfs相关变量申请内存 */
while (fonts_init()) /* 检查字库 */
{
lcd_clear(WHITE);
lcd_show_string(30, 30, 200, 16, 16, "ESP32-P4", RED);
key = fonts_update_font(30, 50, 16, (uint8_t *)"0:", RED); /* 更新字库 */
while (key) /* 更新失败 */
{
lcd_show_string(30, 50, 200, 16, 16, "Font Update Failed!", RED);
vTaskDelay(pdMS_TO_TICKS(200));
lcd_fill(20, 50, 200 + 20, 90 + 16, WHITE);
vTaskDelay(pdMS_TO_TICKS(200));
}
lcd_show_string(30, 50, 200, 16, 16, "Font Update Success! ", RED);
vTaskDelay(pdMS_TO_TICKS(1000));
lcd_clear(WHITE);
}
text_show_string(30, 50, 200, 16, "正点原子ESP32-P4开发板",16, 0, RED);
text_show_string(30, 70, 200, 16, "音乐播放器 实验", 16, 0, RED);
text_show_string(30, 90, 200, 16, "正点原子@ALIENTEK", 16, 0, RED);
text_show_string(30, 110, 200, 16, "KEY0:NEXT KEY1:PREV", 16, 0, RED);
text_show_string(30, 130, 200, 16, "KEY2:PAUSE/PLAY", 16, 0, RED);
vTaskDelay(pdMS_TO_TICKS(1000));
while (1)
{
audio_play(); /* 播放音乐 */
}
}
该函数就相对简单了,在初始化各个外设后,通过 audio_play 函数,开始音频播放,到这里本实验的代码基本就编写完成了。
43.4 下载验证
在代码编译成功之后,我们下载代码到开发板上,程序先执行字库检测,然后当检测到SD卡根目录的MUSIC文件夹有音频文件(WAV格式音频)的时候,就开始自动播放歌曲了,如下图所示:
图43.1.1 音乐播放中
从上图可以看出,总共10首歌曲,当前正在播放第2首歌曲,歌曲名、播放时间、总时长、码率等信息等也都有显示。LED0闪烁表现程序正在运行。
此时我们便可以听到开发板板载喇叭播放出来的音乐了,也可以在开发板的PHONE端子插入耳机来听歌。同时,我们可以通过按KEY0和KEY1来切换下一曲和上一曲,通过KEY2暂停和继续播放。
至此,我们就完成了一个简单的音乐播放器了,虽然只支持WAV文件,但是大家可以在此基础上,增加其他音频格式解码器,便可实现其他音频格式解码了。