《DNESP32P4开发指南_V1.0》第四十三章 音乐播放器实验(下)

0 阅读32分钟

第四十三章 音乐播放器实验

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);

函数形参:

QQ截图20260813101116

表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);

函数形参:

QQ截图20260813101133

表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);

函数形参:

QQ截图20260813101153

表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);

函数形参:

QQ截图20260813101205

表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);

函数形参:

QQ截图20260813101217

表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);

函数形参:

QQ截图20260813101226

表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);

函数形参:

QQ截图20260813101235

表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);

函数形参:

QQ截图20260813101243

表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);

函数形参:

QQ截图20260813101252

表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 程序流程图

image028

图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, &reg_addr, 1, &reg_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格式音频)的时候,就开始自动播放歌曲了,如下图所示:

image029

图43.1.1 音乐播放中

从上图可以看出,总共10首歌曲,当前正在播放第2首歌曲,歌曲名、播放时间、总时长、码率等信息等也都有显示。LED0闪烁表现程序正在运行。
此时我们便可以听到开发板板载喇叭播放出来的音乐了,也可以在开发板的PHONE端子插入耳机来听歌。同时,我们可以通过按KEY0和KEY1来切换下一曲和上一曲,通过KEY2暂停和继续播放。
至此,我们就完成了一个简单的音乐播放器了,虽然只支持WAV文件,但是大家可以在此基础上,增加其他音频格式解码器,便可实现其他音频格式解码了。