ESP-IDF+vscode开发ESP32第十讲——I2S工程2

0 阅读21分钟

目录

前言

前提工程

一、准备工作

二、代码编写

2.1 sdcard.c

2.2 es8311.c

2.3 es8311.h

2.4 代码解释

opendir函数:

readdir函数:

closedir函数:

stat函数:

fseek函数:

ftell函数:

fread函数:

feof函数:

ferror函数:

三、结果展示

3.1 输出日志


前言

上一讲的工程是吧音视频文件存入内部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);

  • 参数:dirpopendir 返回的目录流指针。

  • 返回值:

    • 返回一个指向 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 会被设置为相应的错误码。

fseek函数:

  • 功能:一个标准 C 函数,用于移动文件指针到文件中的指定位置,通常用于随机访问文件。

  • 原型:int fseek(FILE *stream, long offset, int whence);

  • 参数:

    • stream:指向 FILE 类型的指针,表示打开的文件流。

    • offset:文件指针移动的偏移量,单位是字节。

    • whence:指定偏移量的起始位置。它的值可以是以下常量之一:

      • SEEK_SET:从文件开头开始偏移。
      • SEEK_CUR:从当前文件指针的位置开始偏移。
      • SEEK_END:从文件末尾开始偏移。
  • 返回值:

    • 成功:返回 0
    • 失败:返回 -1,并设置 errno,可以通过 perrorferror 获取错误信息。

ftell函数:

  • 功能:一个标准 C 函数,用于返回当前文件指针的位置。它返回的是文件指针相对于文件开头的偏移量(字节数)。

  • 原型:long ftell(FILE *stream);

  • 参数:stream:指向 FILE 类型的指针,表示打开的文件流。

  • 返回值:

    • 成功:返回文件指针相对于文件开头的字节偏移量(即文件指针当前位置)。
    • 失败:返回 -1L,并设置 errno,可以通过 perrorferror 获取错误信息。

fread函数:

  • 功能:一个标准 C 函数,用于从文件中读取数据到内存中。它通常用于批量读取文件内容。

  • 原型:size_t fread(void *ptr, size_t size, size_t count, FILE *stream);

  • 参数:

    • ptr:指向内存的指针,fread 会将读取的数据存储到这个内存区域中。
    • size:每个元素的字节大小。
    • count:要读取的元素个数。
    • stream:指向 FILE 类型的指针,表示要从中读取数据的文件流。
  • 返回值:

    • 成功:返回实际读取的元素个数(即 count 的值),它可能小于请求的 count,尤其是在文件末尾或发生读取错误时。
    • 失败:返回 0,可以通过 ferrorfeof 来检查错误类型。
  • 特点:无论读取成功还是失败,都对把指向 FILE 类型的指针向后移动**count个字节。**

feof函数:

  • 功能:一个标准 C 函数,用于检查文件流是否已经到达文件的末尾。它通常与文件读取函数(如 freadfgetc 等)结合使用,以确定是否已经读取完文件。

  • 原型: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即可播放音乐。

我这个只是一个测试工程,有很多不完善的地方,比如结束音乐、重复播放等功能都没有加。