目录
前言
上一讲的工程是吧音视频文件存入内部flash中然后再利用I2S传输到音频解码芯片来播放音乐。但是这有一些弊端,首先是完整音频文件较大,内部flash较小,一般无法储存。其次内部flash的大文件烧录速度较慢,不利于工作。
所以本章使用外置SD卡储存音频文件来实现播放音乐。很多知识前两章都有解释,本章不再重复
开发板是微雪的ESP32-P4-Module-DEV-KIT。 ESP-IDF版本是6.0。基于上一章的工程文件。
前提工程
ESP-IDF+vscode开发ESP32第七讲——存储设备读写
ESP-IDF+vscode开发ESP32第八讲——音频信号全解
ESP-IDF+vscode开发ESP32第九讲——I2S工程1
一、准备工作
首先注册SD卡的FAT32文件系统,然后电脑首先准备好音频文件《xxxx.wav》,使用读卡器将《xxxx.wav》存入SD卡中。此时注意查看SD卡的文件系统是否是FAT32。
二、代码编写
es8311组件中新建文件sdcard.c
2.1 sdcard.c
#include <stdio.h>
#include "es8311.h"
#include <dirent.h>
#include <sys/unistd.h>
#include <sys/stat.h>
#include "driver/sdmmc_host.h"
#include "esp_vfs_fat.h"
#include "sdmmc_cmd.h"
#include "sd_protocol_types.h"
#include "esp_console.h"
static const char *TAG = "ES8311";
uint8_t* SD_buf = NULL;
uint8_t* mic_buf = NULL;
#define base_path "/sdcard"
static esp_err_t audio_play(int argc, char **argv);
static esp_err_t sdmmc_list(int argc, char **argv);
/*--------------------------------------------------------------------------*/
/**
* @brief 获取注册在 VFS 中的 SD 卡上的 FAT 文件系统
* @param[in] void
* @note
* @return void
*/
/*--------------------------------------------------------------------------*/
void SDMMC_init(void)
{
sdmmc_host_t host = SDMMC_HOST_DEFAULT();
//host.max_freq_khz = 1000;
sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
slot_config.clk = sd_clk;
slot_config.cmd = sd_cmd;
slot_config.d0 = sd_d0;
slot_config.d1 = sd_d1;
slot_config.d2 = sd_d2;
slot_config.d3 = sd_d3;
slot_config.width = 4;
esp_vfs_fat_mount_config_t mount_config = {
.format_if_mount_failed = false,
.max_files = 5,
.allocation_unit_size = 0,
};
sdmmc_card_t *card = NULL;
ESP_ERROR_CHECK(esp_vfs_fat_sdmmc_mount(base_path, &host, &slot_config, &mount_config, &card));
sdmmc_card_print_info(stdout, card);
//esp_vfs_fat_sdcard_unmount(base_path, card);
esp_console_cmd_t cmd = {};
cmd.command = "CDlist";
cmd.help = "list files";
cmd.func = sdmmc_list;
esp_console_cmd_register(&cmd);
cmd.command = "music";
cmd.help = "play music file";
cmd.func = audio_play;
esp_console_cmd_register(&cmd);
}
/*--------------------------------------------------------------------------*/
/**
* @brief console sd卡目录文件查询操作
* @param[in] argc 命令行参数个数
* @param[in] argv 命令行参数数组
* @note
* @return void
*/
/*--------------------------------------------------------------------------*/
static esp_err_t sdmmc_list(int argc, char **argv)
{
struct dirent *entry;
char path[64];
if (argc > 1) {
sprintf(path,"%s/%s" ,base_path ,argv[1]);
} else {
sprintf(path,"%s" ,base_path);
}
ESP_LOGI(TAG, "Reading file %s", path);
DIR *dir = opendir(path); // 打开一个目录并返回一个 DIR 类型的指针
while ((entry = readdir(dir)) != NULL) { // 读取目录中的每个条目,直到没有更多条目为止
printf("file name: %s\n", entry->d_name);
}
closedir(dir); // 关闭目录流
return ESP_OK;
}
void sd_music_read(void* args);
bool file_read(void *__ptr, size_t __size, size_t __nmemb,FILE *__stream);
TaskHandle_t music_task_handle = NULL;
/*--------------------------------------------------------------------------*/
/**
* @brief console 音乐播放操作
* @param[in] argc 命令行参数个数
* @param[in] argv 命令行参数数组
* @note
* @return void
*/
/*--------------------------------------------------------------------------*/
static esp_err_t audio_play(int argc, char **argv)
{
struct stat info;
char path[64];
sprintf(path,"%s/%s" ,base_path ,argv[1]);
ESP_LOGI(TAG, "Opening file %s", path);
FILE *f = fopen(path, "r");
if (f == NULL) {
ESP_LOGE(TAG, "Failed to open file for writing");
return ESP_FAIL;
}
if(stat(path, &info) < 0){
ESP_LOGE(TAG, "Failed to read file stats");
return ESP_FAIL;
}
ESP_LOGI(TAG, "File size: %ld", info.st_size);
fseek(f, 44, SEEK_SET); // 跳过WAV文件头部的44字节
xTaskCreate(sd_music_read, "sd_music_read", 4096, f, 5, NULL);
return ESP_OK;
}
/*--------------------------------------------------------------------------*/
/**
* @brief sd卡音频文件读取操作
* @param[in] args 文件指针
* @note
* @return void
*/
/*--------------------------------------------------------------------------*/
void sd_music_read(void* args)
{
bool end_flag = false;
FILE *f = (FILE*)args;
mic_buf = malloc(read_buf_size); // 分配麦克风数据缓冲区
SD_buf = malloc(read_buf_size); // 分配麦克风数据缓冲区
file_read(mic_buf, 1, read_buf_size - 10, f);
file_read(SD_buf, 1, read_buf_size - 10, f);
xSemaphoreGive(music_start); // 发送信号量通知音乐任务有新数据可用
xTaskCreate(music_task, "music_task", 4096, NULL, 10, &music_task_handle);
while(!end_flag)
{
xSemaphoreTake(music_end, portMAX_DELAY); // 等待音乐任务处理完当前数据
end_flag = file_read(SD_buf, 1, read_buf_size - 10, f); // 从文件中读取数据到缓冲区,返回实际读取的字节数
xSemaphoreGive(music_start); // 发送信号量通知音乐任务有新数据可用
}
fclose(f); // 关闭文件
vTaskDelete(music_task_handle); // 删除音乐任务
vTaskDelete(NULL); // 删除当前任务
}
/*--------------------------------------------------------------------------*/
/**
* @brief sd卡文件读取操作,具有错误重试机制
* @param[in]
* @note
* @return void
*/
/*--------------------------------------------------------------------------*/
bool file_read(void *__ptr, size_t __size, size_t __nmemb,FILE *__stream)
{
bool flag = true;
long current_pos;
size_t bytes_read;
while(flag)
{
flag = false;
current_pos = ftell(__stream);
bytes_read = fread(__ptr, __size, __nmemb, __stream);
if(bytes_read != __nmemb)
{
if(feof(__stream))
{
ESP_LOGI(TAG, "End of file reached");
return 1;
}
else if(ferror(__stream))
{
ESP_LOGE(TAG, "Error reading file, retrying...");
fseek(__stream, current_pos, SEEK_SET);
flag = true; // 继续尝试读取
}
}
}
return 0;
}
在SDMMC_init中,没有启动FAT分区挂载失败格式化操作。避免删除了音频信号,可以先按第七讲的方式得到完好的挂载成功的SD卡,再利用读卡器烧录音频文件,再拿来使用。
sdmmc_list命令能帮助我们查看SD卡当前目录下的文件,方便选择音频文件
接着就是音频播放命令audio_play。
2.2 es8311.c
#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;
SemaphoreHandle_t music_start = NULL;
SemaphoreHandle_t music_end = NULL;
void I2S_driver_init(void)
{
i2s_chan_config_t i2s_chan_config = I2S_CHANNEL_DEFAULT_CONFIG(I2S_num, role);
i2s_chan_config.dma_frame_num = 4092 /(I2S_bits/8) / 2; // 设置每个DMA缓冲区的帧数,确保每个缓冲区可以容纳4092字节的数据
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;
}
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, 50);
// 设置输入音量
//esp_codec_dev_set_in_gain(codec_handle, 30);
}
void audio_start(void)
{
I2S_driver_init();
ES8311_coder_init();
#if CONFIG_MODE_MUSIC
music_start = xSemaphoreCreateBinary();
music_end = xSemaphoreCreateBinary();
#elif CONFIG_MODE_ECHO
xTaskCreate(echo_task, "echo_task", 4096, NULL, 5, NULL);
#endif
}
#if CONFIG_MODE_MUSIC
void music_task(void* args)
{
uint8_t *tmp;
size_t bytes_write = 0;
while(1)
{
i2s_channel_write(tx_handle, mic_buf, read_buf_size-10, &bytes_write, pdMS_TO_TICKS(1000));
ESP_LOGI(TAG, "i2s music played, %d bytes are written", bytes_write);
xSemaphoreTake(music_start, portMAX_DELAY); // 等待音乐数据准备好
tmp = mic_buf;
mic_buf = SD_buf;
SD_buf = tmp;
xSemaphoreGive(music_end); // 等待音乐数据准备好
}
}
#endif
2.3 es8311.h
#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 sd_clk 43
#define sd_cmd 44
#define sd_d0 39
#define sd_d1 40
#define sd_d2 41
#define sd_d3 42
#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 16 // 每个采样的位数,设置为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表示关闭
extern SemaphoreHandle_t music_start;
extern SemaphoreHandle_t music_end;
#define read_buf_size 4092 * 6 + 10 // 4092是DMA缓冲区大小,6是DMA数量,+10是为了防止越界
extern uint8_t* SD_buf; // 全局变量,用于存储麦克风数据
extern uint8_t* mic_buf; // 全局变量,用于存储麦克风数据
void SDMMC_init(void);
void audio_start(void);
void music_task(void* args);
#endif
2.4 mian.c
#include <stdio.h>
#include "user.h"
#include "es8311.h"
void app_main(void)
{
CONSOLE_REPL_INIT(); // 初始化控制台REPL环境
ESP_LDOV4_SET(3300);
SDMMC_init();
audio_start();
while(1)
{
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
2.5 组件依赖
idf_component_register(SRCS "es8311.c" "sdcard.c"
INCLUDE_DIRS "include"
PRIV_REQUIRES fatfs esp_driver_sdmmc sdmmc console
PRIV_REQUIRES esp_driver_i2s esp_driver_i2c esp_driver_gpio)
2.6 代码解释
es8311.c中的代码和上一章的内容几乎没有什么改变。增加了music_start和music_end信号量。然后将每个DMA缓冲区的帧数设置到最大值,(一个DMA 缓冲区大小不超过最大值 4092),提高使用效率。关于这里面的知识可以查看《I2S应用注意事项》。
在es8311.h中新增SD卡管脚定义、函数声明、外部变量。其中read_buf_size是需要创建的音频缓冲区的大小,比最大一次DMA传输的数据量大10字节,防止溢出。
在sdcard中的很多函数见第七讲。新增函数用法如下:
opendir函数:
-
功能:用于打开目录的标准 C 函数,它允许程序获取一个指向目录流的指针,之后可以通过其他函数访问该目录的内容。
-
原型:DIR *opendir(const char *name);
-
参数:
name:一个指向字符串的指针,表示要打开的目录的路径。路径可以是相对路径或绝对路径 -
返回值:
- 成功:返回一个
DIR类型的指针,表示打开的目录流。 - 失败:返回
NULL,并设置errno来指示错误原因。
- 成功:返回一个
readdir函数:
-
功能:是一个用于读取目录项的标准 C 函数。它从由
opendir打开的目录流中读取一个目录项,并返回一个指向struct dirent的指针,表示目录中的一个文件或子目录。 -
原型:struct dirent *readdir(DIR *dirp);
-
参数:
dirp:opendir返回的目录流指针。 -
返回值:
- 返回一个指向
struct dirent的指针,表示当前目录项(如文件或子目录)。 - 返回
NULL表示目录项读取完毕或发生错误。
- 返回一个指向
closedir函数:
-
功能:是一个用于关闭已打开目录流的函数,通常在通过
opendir打开目录并通过readdir遍历目录项之后调用。 -
原型:int closedir(DIR *dirp);
-
参数:
dirp:指向opendir返回的目录流指针。 -
返回值:
- 成功:返回
0。 - 失败:返回
-1,并且设置errno来指示错误。
- 成功:返回
stat函数:
-
功能:用于获取文件信息的标准 C 库函数。它的主要作用是获取指定路径文件的各种信息,如文件大小、文件类型、权限、修改时间等。
-
原型:int stat(const char *path, struct stat *buf);
-
参数:
path:一个指向字符串的指针,表示要获取信息的文件或目录的路径。buf:指向一个struct stat结构体的指针,用来存储获取到的文件信息。
-
返回值:
- 返回 0:表示成功获取文件信息,
buf中会填充文件的详细信息。 - 返回 -1:表示出错,文件信息无法获取,
errno会被设置为相应的错误码。
- 返回 0:表示成功获取文件信息,
fseek函数:
-
功能:一个标准 C 函数,用于移动文件指针到文件中的指定位置,通常用于随机访问文件。
-
原型:int fseek(FILE *stream, long offset, int whence);
-
参数:
-
stream:指向FILE类型的指针,表示打开的文件流。 -
offset:文件指针移动的偏移量,单位是字节。 -
whence:指定偏移量的起始位置。它的值可以是以下常量之一:SEEK_SET:从文件开头开始偏移。SEEK_CUR:从当前文件指针的位置开始偏移。SEEK_END:从文件末尾开始偏移。
-
-
返回值:
- 成功:返回
0。 - 失败:返回
-1,并设置errno,可以通过perror或ferror获取错误信息。
- 成功:返回
ftell函数:
-
功能:一个标准 C 函数,用于返回当前文件指针的位置。它返回的是文件指针相对于文件开头的偏移量(字节数)。
-
原型:long ftell(FILE *stream);
-
参数:
stream:指向FILE类型的指针,表示打开的文件流。 -
返回值:
- 成功:返回文件指针相对于文件开头的字节偏移量(即文件指针当前位置)。
- 失败:返回
-1L,并设置errno,可以通过perror或ferror获取错误信息。
fread函数:
-
功能:一个标准 C 函数,用于从文件中读取数据到内存中。它通常用于批量读取文件内容。
-
原型:size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
-
参数:
ptr:指向内存的指针,fread会将读取的数据存储到这个内存区域中。size:每个元素的字节大小。count:要读取的元素个数。stream:指向FILE类型的指针,表示要从中读取数据的文件流。
-
返回值:
- 成功:返回实际读取的元素个数(即
count的值),它可能小于请求的count,尤其是在文件末尾或发生读取错误时。 - 失败:返回
0,可以通过ferror或feof来检查错误类型。
- 成功:返回实际读取的元素个数(即
-
特点:无论读取成功还是失败,都对把指向
FILE类型的指针向后移动**count个字节。**
feof函数:
-
功能:一个标准 C 函数,用于检查文件流是否已经到达文件的末尾。它通常与文件读取函数(如
fread、fgetc等)结合使用,以确定是否已经读取完文件。 -
原型:int feof(FILE *stream);
-
参数:
stream:指向FILE类型的指针,表示打开的文件流。 -
返回值:
- 非零值(通常是 1) :表示文件指针已经到达文件末尾。
- 零值(0) :表示文件指针没有到达文件末尾。
ferror函数:
-
功能:用于检查文件流在读写操作过程中是否发生了错误。它常常用于在文件操作后,检查是否出现了 I/O 错误(例如读取失败、写入失败等)。
-
原型:int ferror(FILE *stream);
-
参数:
stream:指向FILE类型的指针,表示打开的文件流。 -
返回值:
- 非零值:表示文件流发生了错误。
- 0:表示文件流没有发生错误。
在命令audio_play中,新建了一个sd卡音频数据读取任务sd_music_read,在该任务中会创建两个音频数据缓冲区。
因为I2S总线没有办法直接去SD卡读取数据,所以需要先把SD的数据出入内存,I2S再次内存读取数据,这就是电脑的冯诺依曼架构。这种方式还能避免I2S总线和SD卡读取数据速度不同的问题。但是如果只创建一个音频数据缓冲区。那么运行逻辑如下:
SD卡读取数据完成——I2S总线读取数据完成——SD卡读取数据完成。。。。
这样速度快的需要等速度慢的,音乐播放最小延迟一个SD卡缓冲区读取周期,播放音乐会感觉到明显的卡顿。
创建两个音频数据缓冲区的运行逻辑如下:
SD卡先读满两个音频数据缓冲区——I2S总线读取一个缓冲区——I2S总线读取完成,继续读下一个缓冲区,此时SD卡读第一个缓冲区——。。。。
这种方式SD卡和I2S交替读取缓冲区,两种同步运行,如果SD卡读取速度快于I2S,则音乐播放不会有任何延迟。如果SD卡读取速度满于I2S,音乐播放延长为SD卡缓冲区读取周期减去I2S缓冲区读取周期,几乎也感受不到延迟。而事实上SD卡读取速度能到20MHz,远快于I2S的音频采样率。
三、结果展示
3.1 输出日志
Type 'help' to get the list of commands.
Use UP/DOWN arrows to navigate through command history.
Press TAB when typing command name to auto-complete.
ESP LDO Channel State:
Index ID ref_cnt voltage_mv adjustable
esp> 0 1 1 3300 no
1 -1 0 0 yes
2 -1 0 0 yes
3 4 1 3300 yes
Name: SDABC
Type: SDHC
Speed: 20.00 MHz (limit: 20.00 MHz)
Size: 60014MB
CSD: ver=2, sector_size=512, capacity=122909696 read_bl_len=9
SSR: bus_width=4
D (816) i2s_common: tx channel is registered on I2S0 successfully
D (817) i2s_common: rx channel is registered on I2S0 successfully
W (818) i2s_common: dma frame num is adjusted to 1008 to align the dma buffer with 64, bufsize = 4032
D (819) i2s_common: DMA malloc info: dma_desc_num = 6, dma_desc_buf_size = dma_frame_num * slot_num * data_bit_width = 4032
D (821) i2s_common: APLL expected frequency is 24576000 Hz, real frequency is 24575996 Hz
D (822) i2s_std: Clock division info: [sclk] 24575996 Hz [mdiv] 2 0/2 [mclk] 12287998 Hz [bdiv] 8 [bclk] 1536000 Hz
D (823) i2s_common: MCLK is pinned to GPIO13 on I2S0
D (824) i2s_std: The tx channel on I2S0 has been initialized to STD mode successfully
W (825) i2s_common: dma frame num is adjusted to 1008 to align the dma buffer with 64, bufsize = 4032
D (826) i2s_common: DMA malloc info: dma_desc_num = 6, dma_desc_buf_size = dma_frame_num * slot_num * data_bit_width = 4032
W (827) i2s_common: APLL is occupied already, it is working at 24575996 Hz while the expected frequency is 24576000 Hz
W (828) i2s_common: Trying to work at 24575996 Hz...
D (829) i2s_common: APLL expected frequency is 24576000 Hz, real frequency is 24575996 Hz
D (830) i2s_std: Clock division info: [sclk] 24575996 Hz [mdiv] 2 0/2 [mclk] 12287998 Hz [bdiv] 8 [bclk] 1536000 Hz
D (831) i2s_common: MCLK is pinned to GPIO13 on I2S0
D (831) i2s_std: The rx channel on I2S0 has been initialized to STD mode successfully
D (832) i2s_common: i2s tx channel enabled
D (833) i2s_common: i2s rx channel enabled
I (840) ES8311: Work in Slave mode
D (843) i2s_common: i2s tx channel disabled
D (843) i2s_common: i2s rx channel disabled
W (844) i2s_common: dma frame num is adjusted to 1008 to align the dma buffer with 64, bufsize = 4032
W (845) i2s_common: APLL is occupied already, it is working at 24575996 Hz while the expected frequency is 24576000 Hz
W (846) i2s_common: Trying to work at 24575996 Hz...
D (847) i2s_common: APLL expected frequency is 24576000 Hz, real frequency is 24575996 Hz
D (848) i2s_std: Clock division info: [sclk] 24575996 Hz [mdiv] 2 0/2 [mclk] 12287998 Hz [bdiv] 8 [bclk] 1536000 Hz
I (849) I2S_IF: STD: TX, data_bit: 16, slot_bit: 16, ws_width: 16, slot_mode: STEREO, slot_mask: 0x3
I (850) I2S_IF: STD: TX, sample_rate_hz: 48000, mclk_multiple: 256, clk_src: 20
W (851) i2s_common: dma frame num is adjusted to 1008 to align the dma buffer with 64, bufsize = 4032
W (852) i2s_common: APLL is occupied already, it is working at 24575996 Hz while the expected frequency is 24576000 Hz
W (853) i2s_common: Trying to work at 24575996 Hz...
D (854) i2s_common: APLL expected frequency is 24576000 Hz, real frequency is 24575996 Hz
D (854) i2s_std: Clock division info: [sclk] 24575996 Hz [mdiv] 2 0/2 [mclk] 12287998 Hz [bdiv] 8 [bclk] 1536000 Hz
I (856) I2S_IF: STD: RX, data_bit: 16, slot_bit: 16, ws_width: 16, slot_mode: STEREO, slot_mask: 0x3
I (857) I2S_IF: STD: RX, sample_rate_hz: 48000, mclk_multiple: 256, clk_src: 20
D (858) i2s_common: i2s tx channel enabled
D (858) i2s_common: i2s rx channel enabled
I (874) Adev_Codec: Open codec device OK
我这的SD卡读取速度为20M,如果允许发现不稳定,出现读取错误,可以适当降低SD卡读取速度。
编辑
输入CDlist查看SD卡中的文件列表,然后使用music xxx.wav即可播放音乐。
我这个只是一个测试工程,有很多不完善的地方,比如结束音乐、重复播放等功能都没有加。