ESP-IDF+vscode开发ESP32第九讲——I2S工程1

目录

前言

[一、 准备工作](#一、 准备工作)

二、代码编写

[2.1 es8311.h](#2.1 es8311.h)

[2.2 es8311.c](#2.2 es8311.c)

[2.3 mian.c](#2.3 mian.c)

[2.4 工程日志](#2.4 工程日志)


前言

经过第八章的学习,相信大家已经音频信号的传输的各种方式有了体系的了解。那么本章来实现基于外部音频解码器播放flash中存储的音乐的工程。

开发板是微雪的**ESP32-P4-Module-DEV-KIT。**ESP-IDF版本是6.0。基于第一章的模板工程。


一、 准备工作

音频解码器是ES8311,上一章也简单介绍了这款芯片的构成。

首先我们要先制作一个.wav格式的音乐文件。其他格式当然也可以,不过先以该格式测试。因为WAV 格式本质是PCM 原始数据 + 一个 44 字节的文件头(最接近原生 PCM)。所以解码器可以直接使用。

我这使用软件 "Audacity" 截取了歌曲《错位时空》前一小段得到了《cwsk.wav》,转换时采样频率为48K,数据位为32位,得到大小大约9MB。注意查看在自己flash的大小,我的flash为16MB。

另外,注意去设置中把Flash size大小改为自己的实际大小。

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

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

bash 复制代码
# ESP-IDF Partition Table
# Name                   , Type, SubType, Offset , Size  , Flags
nvs                      , data, nvs    , 0x9000 , 0x6000, 
phy_init                 , data, phy    , 0xf000 , 0x1000, 
factory                  , app , factory, 0x10000, 10M    , , 
分区类型 对齐要求 原因
data(如 nvs、phy_init) 4KB(0x1000 存储配置数据,Flash 擦除最小单位是 4KB
app(如 factory、ota_0/ota_1) 64KB(0x10000 ESP32 的 Flash MMU(内存管理单元)映射粒度是 64KB,代码加载时必须按 64KB 建立地址映射

二、代码编写

首先添加官方音频解码器驱动,这里面包含很多音频解码芯片的底层驱动。其中就有我使用的ES8311。

打开终端,输入idf.py add-dependency "espressif/esp_codec_dev^1.5.8",重新编译组件就添加成功了。

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

2.1 es8311.h

cpp 复制代码
#ifndef __ES8311_H__
#define __ES8311_H__

#include "esp_log.h"             // ESP32日志函数
#include "FreeRTOS/FreeRTOS.h"   // FreeRTOS函数
#include "FreeRTOS/task.h"       // FreeRTOS任务管理函数
#include "FreeRTOS/semphr.h"     // FreeRTOS信号量管理函数

#define     I2S_num     0                   // I2S端口号
#define     role        I2S_ROLE_MASTER     // I2S工作模式,设置为主机模式
#define     I2S_MCLK    GPIO_NUM_13         // I2S主时钟引脚,连接到ES8311的MCLK引脚
#define     I2S_BCLK    GPIO_NUM_12         // I2S位时钟引脚,连接到ES8311的BCLK引脚
#define     I2S_DOUT    GPIO_NUM_9          // I2S数据输出引脚,连接到ES8311的SDOUT引脚
#define     I2S_WS      GPIO_NUM_10         // I2S帧同步引脚,连接到ES8311的LRCK引脚
#define     I2S_DIN     GPIO_NUM_11         // I2S数据输入引脚,连接到ES8311的SDIN引脚
#define     sample_hz   48000               // 采样率,设置为48000Hz    
#define     I2S_bits    32                  // 每个采样的位数,设置为32位以兼容ES8311的24位数据格式 
#define     MCLK_multiple   256             // MCLK倍频,设置为256倍以满足ES8311的时钟要求

#define     I2C_SDA     GPIO_NUM_7          // I2C数据引脚,连接到ES8311的SDA引脚
#define     I2C_SCL     GPIO_NUM_8          // I2C时钟引脚,连接到ES8311的SCL引脚
#define     NS4150_PA   GPIO_NUM_53         // NS4150功放使能引脚,连接到NS4150的EN引脚

#define     CONFIG_MODE_MUSIC    1          // 1表示启用音乐模式,0表示关闭
#define     CONFIG_MODE_ECHO    0           // 1表示启用回声模式,0表示关闭

void audio_start(void);
#endif

2.2 es8311.c

cpp 复制代码
#include <stdio.h>
#include "es8311.h"
#include "driver/i2s_std.h"
#include "driver/i2c_master.h"
#include "driver/gpio.h"
#include "esp_codec_dev_defaults.h"
#include "esp_codec_dev.h"

static char *TAG = "es8311";

static i2s_chan_handle_t tx_handle = NULL;
static i2s_chan_handle_t rx_handle = NULL;


void I2S_driver_init(void)
{
    i2s_chan_config_t i2s_chan_config = I2S_CHANNEL_DEFAULT_CONFIG(I2S_num, role);
    ESP_ERROR_CHECK(i2s_new_channel(&i2s_chan_config, &tx_handle, &rx_handle));              // 创建I2S通道
 
    i2s_std_config_t std_cfg = {
        .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(sample_hz),
        .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_bits, I2S_SLOT_MODE_STEREO),
        .gpio_cfg = {
            .mclk = I2S_MCLK,
            .bclk = I2S_BCLK,
            .ws = I2S_WS,
            .dout = I2S_DOUT,
            .din = I2S_DIN,
            .invert_flags = {
                .mclk_inv = false,
                .bclk_inv = false,
                .ws_inv = false,
            },
        },
    };
    std_cfg.clk_cfg.clk_src = I2S_CLK_SRC_APLL;
    std_cfg.clk_cfg.mclk_multiple = MCLK_multiple;
    ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle, &std_cfg));
    ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle, &std_cfg));
    ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));
    ESP_ERROR_CHECK(i2s_channel_enable(rx_handle));
}
i2c_master_bus_handle_t I2C_driver_init(void)
{
    // I2C主机初始化代码
    i2c_master_bus_handle_t i2c_bus_handle = NULL;
    i2c_master_bus_config_t bus_config = {
        .i2c_port = I2C_NUM_0,
        .sda_io_num = I2C_SDA,
        .scl_io_num = I2C_SCL,
        .clk_source = I2C_CLK_SRC_DEFAULT,
        .glitch_ignore_cnt = 7,
        .flags.enable_internal_pullup = true,
    };
    ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, &i2c_bus_handle));
    return i2c_bus_handle;
}

首先进行I2S设备初始化,各API可以看我整理的《ESP32实用API指南3》上面代码实现了I2S通道的创建和I2S标准模式的创建。

查看ES8311,发现支持所有标准模式下所有格式的传输,所以我这用了最广泛使用的Philips 格式。

接着初始化ES8311控制接口I2C设备的初始化。

完成后会有以下日志(我在设置中开启了I2S调试日志),

bash 复制代码
D (439) i2s_common: tx channel is registered on I2S0 successfully
Desp> ) i2s_common: rx channel is registered on I2S0 successfully
D (441) i2s_common: DMA malloc info: dma_desc_num = 6, dma_desc_buf_size = dma_frame_num * slot_num * data_bit_width = 1920
D (443) i2s_common: APLL expected frequency is 24576000 Hz, real frequency is 24575996 Hz
D (444) i2s_std: Clock division info: [sclk] 24575996 Hz [mdiv] 2 0/2 [mclk] 12287998 Hz [bdiv] 4 [bclk] 3072000 Hz
D (445) i2s_common: MCLK is pinned to GPIO13 on I2S0
D (446) i2s_std: The tx channel on I2S0 has been initialized to STD mode successfully
D (447) i2s_common: DMA malloc info: dma_desc_num = 6, dma_desc_buf_size = dma_frame_num * slot_num * data_bit_width = 1920
W (448) i2s_common: APLL is occupied already, it is working at 24575996 Hz while the expected frequency is 24576000 Hz
W (449) i2s_common: Trying to work at 24575996 Hz...
D (450) i2s_common: APLL expected frequency is 24576000 Hz, real frequency is 24575996 Hz
D (451) i2s_std: Clock division info: [sclk] 24575996 Hz [mdiv] 2 0/2 [mclk] 12287998 Hz [bdiv] 4 [bclk] 3072000 Hz
D (452) i2s_common: MCLK is pinned to GPIO13 on I2S0
D (452) i2s_std: The rx channel on I2S0 has been initialized to STD mode successfully
D (453) i2s_common: i2s tx channel enabled
D (454) i2s_common: i2s rx channel enabled

从日志中可以看出:

I2S0 的发送 (TX)接收 (RX) 通道已成功注册到硬件控制器

开启了6 个 DMA 描述符,每个DMA缓冲区大小 1920 字节

音频时钟APLL期望 24.576MHz,由采样率自动计算得到,实际 24575996Hz(仅4Hz 误差 )ESP32 APLL 是小数分频硬件,微小误差是**正常硬件特性,**完全不影响音频功能。

MCLK期望值:48000*256 = 12288000,实际值:12287998

BCLK期望值:48000*32*2 = 3072000,实际值:3072000

cpp 复制代码
void ES8311_coder_init(void)
{
    i2c_master_bus_handle_t i2c_bus_handle = I2C_driver_init();
    // es8311 控制接口设置
    audio_codec_i2c_cfg_t i2c_cfg = {
        .port = I2C_NUM_0,
        .addr = ES8311_CODEC_DEFAULT_ADDR,
        .bus_handle = i2c_bus_handle,
    };
    const audio_codec_ctrl_if_t *ctrl_if = audio_codec_new_i2c_ctrl(&i2c_cfg);
    assert(ctrl_if); // 判断控制接口是否创建成功
    // es8311 数据接口设置
    audio_codec_i2s_cfg_t i2s_cfg = {
        .port = I2S_num,
        .tx_handle = tx_handle,
        .rx_handle = rx_handle,
        .clk_src = I2S_CLK_SRC_APLL,
    };
    const audio_codec_data_if_t *data_if = audio_codec_new_i2s_data(&i2s_cfg);
    assert(data_if);
    // es8311 GPIO接口设置
    const audio_codec_gpio_if_t *gpio_if = audio_codec_new_gpio();
    assert(gpio_if);
    // es8311 编码器配置
    es8311_codec_cfg_t codec_cfg = {
        .codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH,
        .ctrl_if = ctrl_if,
        .gpio_if = gpio_if,
        .pa_pin = NS4150_PA,
        .pa_reverted = false,
        .master_mode = false,
        .digital_mic = false,
        .use_mclk = true,
        // 查看实际电路获得
        .hw_gain = { 
            .pa_voltage = 5.0,
            .codec_dac_voltage = 3.3,
            .pa_gain = 1.6, 
        },
        .mclk_div = MCLK_multiple,
    };
    const audio_codec_if_t *es8311_if = es8311_codec_new(&codec_cfg);
    assert(es8311_if);
    // es8311 配置
    esp_codec_dev_cfg_t codec_dev_cfg = {
        .codec_if = es8311_if,
        .data_if = data_if,
        .dev_type = ESP_CODEC_DEV_TYPE_IN_OUT,
    };
    esp_codec_dev_handle_t codec_handle = esp_codec_dev_new(&codec_dev_cfg);
    assert(codec_handle);
    // 启动es8311
    esp_codec_dev_sample_info_t sample_info = {
        .bits_per_sample = I2S_bits,
        .channel = I2S_SLOT_MODE_STEREO,
        .channel_mask =I2S_STD_SLOT_BOTH,
        .sample_rate = sample_hz,
        .mclk_multiple = MCLK_multiple,
    };
    esp_codec_dev_open(codec_handle, &sample_info);
    // 设置输出音量
    esp_codec_dev_set_out_vol(codec_handle, 30);
    // 设置输入音量
    //esp_codec_dev_set_in_gain(codec_handle, 30);
}

assert(_e); 断言,判断_e是不是非空指针

该组件将硬件行为抽象如下:

通讯通道抽象为两种接口:

  • audio_codec_ctrl_if_t 控制接口: 主要提供 read_regwrite_reg API 来配置编解码器设备 常用控制通道包括 I2C, SPI 等
  • audio_codec_data_if_t 数据接口: 主要提供 readwrite API 用来交换音频数据 常用数据通道包括 I2S, SPI 等

esp_codec_dev 为用户提供便捷的上层 API 来实现播放和录音功能。我们这使用了esp_codec_dev_new和esp_codec_dev_open。

音量统一通过 API **esp_codec_dev_set_out_vol**进行设定。 默认的音量调节区间是 0 - 100,音量 100 对应为 0 dB,每个刻度对应 0.5 dB,音量 0 被特殊映射为 -96 dB。

cpp 复制代码
#if CONFIG_MODE_MUSIC
extern const uint8_t* music_start asm("_binary_cwsk_wav_start");
extern const uint8_t* music_end   asm("_binary_cwsk_wav_end");
#endif

void music_task(void* args);
void audio_start(void)
{
    I2S_driver_init();
    ES8311_coder_init();
#if CONFIG_MODE_MUSIC
    xTaskCreate(music_task, "music_task", 4096, NULL, 5, NULL);
#elif CONFIG_MODE_ECHO
    xTaskCreate(echo_task, "echo_task", 4096, NULL, 5, NULL);
#endif
}
void music_task(void* args)
{
    size_t bytes_write = 0;
    const uint8_t* data_ptr = music_start+44;

    ESP_ERROR_CHECK(i2s_channel_disable(tx_handle));
    ESP_ERROR_CHECK(i2s_channel_preload_data(tx_handle, data_ptr, music_end - data_ptr, &bytes_write));
    data_ptr += bytes_write;
    ESP_LOGI(TAG, "music len: %d", music_end - data_ptr);
    ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));
    while(1)
    {
        i2s_channel_write(tx_handle, data_ptr, music_end - data_ptr, &bytes_write, portMAX_DELAY);
        data_ptr = (uint8_t *)music_start+44;
        ESP_LOGI(TAG, "i2s music played, %d bytes are written", bytes_write);
    }
}

添加依赖访问如下:

cpp 复制代码
idf_component_register(SRCS "es8311.c"
                    INCLUDE_DIRS "include"
                    PRIV_REQUIRES esp_driver_i2s esp_driver_i2c esp_driver_gpio
                    EMBED_FILES "cwsk.wav")

EMBED_FILES "cwsk.wav"是 ESP-IDF 工程中专门用于嵌入二进制文件到固件的 CMake 配置。表示将 cwsk.wav 音频文件以原始二进制数据的形式烧录到芯片 Flash 中。并自动生成链接器符号:

_binary_cwsk_wav_start(文件起始地址)

_binary_cwsk_wav_end(文件结束地址)

_binary_cwsk_wav_size(文件总大小)

注意: 链接器生成的这些不是一个 "存储地址的指针变量" ,而是一个直接代表 Flash 地址的标签(常量地址)。

在开头也有两个很特殊的用法

bash 复制代码
extern const uint8_t music_start[] asm("_binary_canon_pcm_start");
extern const uint8_t music_end[]   asm("_binary_canon_pcm_end");

**extern表示:**这两个符号不在该 C 文件中定义,由外部提供。

**const:**强制只读,避免修改

**asm(x):**GCC 特殊扩展语法,修改变量x在链接层的符号名。

这是固定搭配操作,相当于给_binary_cwsk_wav_start重命名为music_start ,给_binary_cwsk_wav_end重命名为music_end,所以必须加extern,因为不是新建指针。

**注意:**只有数组名能直接映射这个地址标签,如果换成指针变量会因为「间接寻址」出错。

因为I2S只传输PCM 原始数据,所以首地址data_ptr为music_start+44,避开文件头。

在music任务内,首先先关闭通道,将通道状态变为就绪态,方便调用函数i2s_channel_preload_data,该函数会提前储存一段音频信息在DMA缓冲区,接着启动通道,通道状态变为运行态。

**注意:**此时首地址data_ptr要下移,覆盖提前储存的数据

**作用:**启动播放时无静音、无爆音,无缝开始。可以删除

在while循环中,会不断地重复播放音乐,注意此时预存储的音频数据,所以首地址要恢复过来。另外函数i2s_channel_write是DMA阻塞,阻塞过程可以运行其他任务,对CPU负担小。

2.3 mian.c

cpp 复制代码
#include <stdio.h>
#include "user.h"
#include "es8311.h"

void app_main(void)
{
    CONSOLE_REPL_INIT(); // 初始化控制台REPL环境
    //ESP_LDOV4_SET(3300);
    audio_start();
    while(1)
    {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

2.4 工程日志

bash 复制代码
D (909) i2s_common: tx channel is registered on I2S0 successfully
D (909) i2s_common: rx channel is registered on I2S0 successfully
D (910) i2s_common: DMA malloc info: dma_desc_num = 6, dma_desc_buf_size = dma_frame_num * slot_num * data_bit_width = 960
D (912) i2s_common: APLL expected frequency is 24576000 Hz, real frequency is 24575996 Hz
D (913) i2s_std: Clock division info: [sclk] 24575996 Hz [mdiv] 2 0/2 [mclk] 12287998 Hz [bdiv] 8 [bclk] 1536000 Hz
                                                                                                             D (914) i2s_common: MCLK is pinned to GPIO13 on I2S0
D (915) i2s_std: The tx channel on I2S0 has been initialized to STD mode successfully
D (916) i2s_common: DMA malloc info: dma_desc_num = 6, dma_desc_buf_size = dma_frame_num * slot_num * data_bit_width = 960
W (917) i2s_common: APLL is occupied already, it is working at 24575996 Hz while the expected frequency is 24576000 Hz
W (919) i2s_common: Trying to work at 24575996 Hz...
D (919) i2s_common: APLL expected frequency is 24576000 Hz, real frequency is 24575996 Hz
D (920) i2s_std: Clock division info: [sclk] 24575996 Hz [mdiv] 2 0/2 [mclk] 12287998 Hz [bdiv] 8 [bclk] 1536000 Hz
D (921) i2s_common: MCLK is pinned to GPIO13 on I2S0
D (922) i2s_std: The rx channel on I2S0 has been initialized to STD mode successfully
esp> D (923) i2s_common: i2s tx channel enabled
D (924) i2s_common: i2s rx channel enabled
I (931) ES8311: Work in Slave mode
D (934) i2s_common: i2s tx channel disabled
D (935) i2s_common: i2s rx channel disabled
W (935) i2s_common: APLL is occupied already, it is working at 24575996 Hz while the expected frequency is 24576000 Hz
W (936) i2s_common: Trying to work at 24575996 Hz...
D (937) i2s_common: APLL expected frequency is 24576000 Hz, real frequency is 24575996 Hz
D (938) i2s_std: Clock division info: [sclk] 24575996 Hz [mdiv] 2 0/2 [mclk] 12287998 Hz [bdiv] 8 [bclk] 1536000 Hz
I (939) I2S_IF: STD: TX, data_bit: 16, slot_bit: 16, ws_width: 16, slot_mode: STEREO, slot_mask: 0x3
I (940) I2S_IF: STD: TX, sample_rate_hz: 48000, mclk_multiple: 256, clk_src: 20
W (941) i2s_common: APLL is occupied already, it is working at 24575996 Hz while the expected frequency is 24576000 Hz
W (942) i2s_common: Trying to work at 24575996 Hz...
D (943) i2s_common: APLL expected frequency is 24576000 Hz, real frequency is 24575996 Hz
D (944) i2s_std: Clock division info: [sclk] 24575996 Hz [mdiv] 2 0/2 [mclk] 12287998 Hz [bdiv] 8 [bclk] 1536000 Hz
I (945) I2S_IF: STD: RX, data_bit: 16, slot_bit: 16, ws_width: 16, slot_mode: STEREO, slot_mask: 0x3
I (946) I2S_IF: STD: RX, sample_rate_hz: 48000, mclk_multiple: 256, clk_src: 20
D (947) i2s_common: i2s tx channel enabled
D (947) i2s_common: i2s rx channel enabled
I (963) Adev_Codec: Open codec device OK
D (964) i2s_common: i2s tx channel disabled
I (965) es8311: music len: 3731254
D (965) i2s_common: i2s tx channel enabled
I (20401) es8311: i2s music played, 3731254 bytes are written
I (39865) es8311: i2s music played, 3737014 bytes are written
I (59330) es8311: i2s music played, 3737014 bytes are written
I (78791) es8311: i2s music played, 3737014 bytes are written
I (98255) es8311: i2s music played, 3737014 bytes are written
I (117720) es8311: i2s music played, 3737014 bytes are written
相关推荐
magrich2 小时前
VSCode-reinstall-remote-extension备份重装vscodeextension
ide·vscode·编辑器
L-影17 小时前
vscode安装SQLAlchemy步骤
ide·vscode·编辑器
无限进步_20 小时前
C++ 继承机制完全解析:从基础原理到菱形继承问题
java·开发语言·数据结构·c++·vscode·后端·算法
Hy行者勇哥1 天前
国内外与vscode类似的软件有哪些?国内那些软件可以平替?
ide·vscode·编辑器
π同学1 天前
ESP-IDF+vscode开发ESP32第八讲——音频信号全解
vscode·esp32·i2s·音频信号
上弦月-编程1 天前
IDE调试快捷键全攻略
vscode
freewlt2 天前
VS Code 扩展开发:集成 GitHub Copilot 的完整指南
vscode·node.js
此生只爱蛋2 天前
【vscode环境配置心得】C++版
c++·ide·vscode
xinhuanjieyi2 天前
windows安装vscode服务端
ide·windows·vscode