ESP32S3播放音频文件

硬件基于立创实战派esp32s3

软件代码基于立创实战派教程修改,分析

播放PCM格式音频

原理图分析

音频芯片ES8311

ES8311_I2C_ADD:0x18

音频功放芯片NS4150B

由于esp引脚数量不够,音频功放芯片使能脚由IO拓展芯片PCA9557控制,要使喇叭输出还需开启

PCA9557,IO拓展芯片

该芯片可以拓展为8位IO输出或输入

IIC高位地址0011 低位地址由A2,A1,A0和控制位控制

PCA9557通过IIC总线写入数据控制8位IO口输出,A0,A1,A2可通过接上拉电阻或接地,改变PCA9557的IIC的器件地址,避免冲突

软件分析:

流程:

初始化:

初始化IO拓展芯片PCA9557 ——》IIC

初始化音频芯片ES8311 —— 》IIS(数据),IIC(初始化配置)

通过IO拓展芯片使能音频功放芯片NS4150B

播放音频

创建播放音乐任务

将音频通过IIS通道传入音频芯片

代码

初始化IIS

#include "BSP_I2S.h"

#include "driver/i2s.h"
#include "sdkconfig.h"
#include "driver/i2s_std.h"
#include "esp_system.h"
#include "esp_check.h" i2s_chan_handle_t tx_handle = NULL; /* I2S port and GPIOs */
#define I2S_NUM (0)
#define I2S_MCK_IO (GPIO_NUM_38)
#define I2S_BCK_IO (GPIO_NUM_14)
#define I2S_WS_IO (GPIO_NUM_13)
#define I2S_DO_IO (GPIO_NUM_45)
#define I2S_DI_IO (-1) // 初始化I2S外设
esp_err_t i2s_driver_init(void)
{
/* 配置i2s发送通道 */
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM, I2S_ROLE_MASTER);
chan_cfg.auto_clear = true; // Auto clear the legacy data in the DMA buffer
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle, NULL));
/* 初始化i2s为std模式 并打开i2s发送通道 */
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(16000),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
.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,
},
},
};
std_cfg.clk_cfg.mclk_multiple = 384;// If not using 24-bit data width, 256 should be enough ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle, &std_cfg));
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));
return ESP_OK;
}

初始化IIC

#include "BSP_IIC.h"
// 包含ESP-IDF错误代码定义的头文件
#include "esp_err.h"
// 包含ESP-IDF日志记录功能的头文件
#include "esp_log.h"
// 包含I2C驱动程序的头文件,用于I2C通信
#include "driver/i2c.h" // 再次包含ESP-IDF日志记录功能的头文件,可能是为了确保在其他地方也能使用
#include "esp_log.h"
// 包含FreeRTOS实时操作系统的头文件,用于任务管理和调度
#include "freertos/FreeRTOS.h"
// 包含FreeRTOS任务相关的头文件,用于创建和管理任务
#include "freertos/task.h"
// 包含数学函数的头文件,用于数学计算
#include "math.h" #define BSP_I2C_SDA 1 //IO1
#define BSP_I2C_SCL 2 #define BSP_I2C_NUM 0 // I2C外设,选择IIC0
#define BSP_IIC_FREQ_HZ 100000 // 100kHz #define I2C_MASTER_TX_BUF_DISABLE 0 /*!< I2C master doesn't need buffer */
#define I2C_MASTER_RX_BUF_DISABLE 0 esp_err_t bsp_i2c_init(void)//初始化i2c
{
i2c_config_t conf = {
// 设置I2C模式为主机模式
.mode = I2C_MODE_MASTER,
.sda_io_num = BSP_I2C_SDA,
.scl_io_num = BSP_I2C_SCL,
// 启用I2C数据线(SDA)的内部上拉电阻
.sda_pullup_en = GPIO_PULLUP_ENABLE,
// 启用I2C时钟线(SCL)的内部上拉电阻
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = BSP_IIC_FREQ_HZ,
}; i2c_param_config(BSP_I2C_NUM, &conf);//选择IIC0初始化
//通常用来检查驱动程序是否成功安装。确保在调用 i2c_driver_install 之前,已经正确配置了 conf.mode 等参数。
return i2c_driver_install(BSP_I2C_NUM, conf.mode, I2C_MASTER_RX_BUF_DISABLE, I2C_MASTER_TX_BUF_DISABLE, 0);
}

初始化ES8311音频芯片(IIS,IIC)

添加ES8311组件和依赖

乐鑫组件管理器官网:https://components.espressif.com/

搜索ES8311,找到命令下载并复制

esp-idf终端通过命令添加组件

idf.py add-dependency "espressif/es8311^1.0.0~1"

注意:添加组件后需重新选择芯片,才能使组件成功添加,然后再检查flash配置是否还是16M

参考es8311.c编写音频驱动文件user_es8311.c

#include "es8311.h"
#include "BSP_I2S.h"
#include "BSP_I2C.h"
#include "user_es8311.h"
#include "esp_check.h"
/* Example configurations */
// 定义接收缓冲区的大小为2400字节
#define EXAMPLE_RECV_BUF_SIZE (2400)
// 定义采样率为16000赫兹
#define EXAMPLE_SAMPLE_RATE (16000)
// 定义MCLK(主时钟)的倍数为384,如果不使用24位数据宽度,256就足够了
#define EXAMPLE_MCLK_MULTIPLE (384) // If not using 24-bit data width, 256 should be enough
// 计算MCLK的频率,MCLK频率 = 采样率 * MCLK倍数
#define EXAMPLE_MCLK_FREQ_HZ (EXAMPLE_SAMPLE_RATE * EXAMPLE_MCLK_MULTIPLE)
// 定义语音音量为70
#define EXAMPLE_VOICE_VOLUME (70) static const char *TAG = "es8311"; // 初始化es8311芯片(前提初始化I2C接口)
esp_err_t es8311_codec_init(void)
{
/* 初始化es8311芯片 */
es8311_handle_t es_handle = es8311_create(BSP_I2C_NUM, ES8311_ADDRRES_0);
ESP_RETURN_ON_FALSE(es_handle, ESP_FAIL, TAG, "es8311 create failed");
const es8311_clock_config_t es_clk = {
.mclk_inverted = false,
.sclk_inverted = false,
.mclk_from_mclk_pin = true,
.mclk_frequency = EXAMPLE_MCLK_FREQ_HZ,
.sample_frequency = EXAMPLE_SAMPLE_RATE
}; // 检查ES8311初始化函数的返回值,确保初始化成功
ESP_ERROR_CHECK(es8311_init(es_handle, &es_clk, ES8311_RESOLUTION_16, ES8311_RESOLUTION_16));
// 设置ES8311的采样频率
ESP_RETURN_ON_ERROR(es8311_sample_frequency_config(es_handle, EXAMPLE_SAMPLE_RATE * EXAMPLE_MCLK_MULTIPLE, EXAMPLE_SAMPLE_RATE), TAG, "set es8311 sample frequency failed");
// 设置ES8311的音量
ESP_RETURN_ON_ERROR(es8311_voice_volume_set(es_handle, EXAMPLE_VOICE_VOLUME, NULL), TAG, "set es8311 volume failed");
// 配置ES8311的麦克风
ESP_RETURN_ON_ERROR(es8311_microphone_config(es_handle, false), TAG, "set es8311 microphone failed");
return ESP_OK;
}

初始化PCA9557,IO拓展芯片

#include "pca9557.h"
#include "BSP_I2C.h" #define PCA9557_INPUT_PORT 0x00
#define PCA9557_OUTPUT_PORT 0x01
#define PCA9557_POLARITY_INVERSION_PORT 0x02
#define PCA9557_CONFIGURATION_PORT 0x03 #define LCD_CS_GPIO BIT(0) // PCA9557_GPIO_NUM_1
#define PA_EN_GPIO BIT(1) // PCA9557_GPIO_NUM_2
#define DVP_PWDN_GPIO BIT(2) // PCA9557_GPIO_NUM_3 #define PCA9557_SENSOR_ADDR 0x19 /*!< Slave address of the MPU9250 sensor */ #define SET_BITS(_m, _s, _v) ((_v) ? (_m)|((_s)) : (_m)&~((_s))) void pca9557_init(void);
void lcd_cs(uint8_t level);
void pa_en(uint8_t level);
void dvp_pwdn(uint8_t level); // 读取PCA9557寄存器的值
esp_err_t pca9557_register_read(uint8_t reg_addr, uint8_t *data, size_t len)
{
return i2c_master_write_read_device(BSP_I2C_NUM, PCA9557_SENSOR_ADDR, &reg_addr, 1, data, len, 1000 / portTICK_PERIOD_MS);
} // 给PCA9557的寄存器写值
esp_err_t pca9557_register_write_byte(uint8_t reg_addr, uint8_t data)
{
uint8_t write_buf[2] = {reg_addr, data}; return i2c_master_write_to_device(BSP_I2C_NUM, PCA9557_SENSOR_ADDR, write_buf, sizeof(write_buf), 1000 / portTICK_PERIOD_MS);
} // 初始化PCA9557 IO扩展芯片
void pca9557_init(void)
{
pca9557_register_write_byte(PCA9557_OUTPUT_PORT, 0x05);
pca9557_register_write_byte(PCA9557_CONFIGURATION_PORT, 0xf8);
} // 设置PCA9557芯片的某个IO引脚输出高低电平
esp_err_t pca9557_set_output_state(uint8_t gpio_bit, uint8_t level)
{
uint8_t data;
esp_err_t res = ESP_FAIL; pca9557_register_read(PCA9557_OUTPUT_PORT, &data, 1);
res = pca9557_register_write_byte(PCA9557_OUTPUT_PORT, SET_BITS(data, gpio_bit, level)); return res;
}
// 控制 PCA9557_PA_EN 引脚输出高低电平 参数0输出低电平 参数1输出高电平
void pa_en(uint8_t level)
{
pca9557_set_output_state(PA_EN_GPIO, level);
}

多个文件同时使用一个句柄,不能声明在被多方引用的头文件

否则报错:

解决办法,只在一个.c文件中声明句柄(不能在.h中声明),在其他文件通过extern引用

主程序:

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "driver/gpio.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "esp_log.h" #include "BSP_I2S.h"
#include "BSP_I2C.h"
#include "user_es8311.h"
#include "pca9557.h"
static const char *TAG = "main";
extern i2s_chan_handle_t tx_handle;
void Init(void)
{
printf("i2s Init\n-----------------------------\n");
/* 初始化I2S总线设备 */
if (i2s_driver_init() != ESP_OK) {
ESP_LOGE(TAG, "i2s driver init failed");
abort();//用于立即终止当前程序的执行,
} else {
ESP_LOGI(TAG, "i2s driver init success");
} /* 初始化I2C总线设备 */
if (bsp_i2c_init() != ESP_OK) {
ESP_LOGE(TAG, "i2c driver init failed");
abort();
} else {
ESP_LOGI(TAG, "i2c driver init success");
}
/* 初始化es8311音频芯片*/
if (es8311_codec_init() != ESP_OK) {
ESP_LOGE(TAG, "es8311 driver init failed");
abort();
} else {
ESP_LOGI(TAG, "es8311 driver init success");
}
pca9557_init(); //初始化IO扩展芯片
pa_en(1); // 打开音频
}
static const char err_reason[][30] = {"input param is invalid",
"operation timeout"
};
/* Import music file as buffer */
extern const uint8_t music_pcm_start[] asm("_binary_canon_pcm_start");
extern const uint8_t music_pcm_end[] asm("_binary_canon_pcm_end"); // 定义一个静态函数i2s_music,用于播放音乐,参数args为传入的参数
static void i2s_music(void *args)
{
esp_err_t ret = ESP_OK; // 定义一个esp_err_t类型的变量ret,用于存储函数返回的错误码,初始值为ESP_OK
size_t bytes_write = 0; // 定义一个size_t类型的变量bytes_write,用于存储每次写入的字节数
uint8_t *data_ptr = (uint8_t *)music_pcm_start; // 定义一个uint8_t指针data_ptr,指向音乐数据的起始地址 /* (Optional) Disable TX channel and preload the data before enabling the TX channel,
* so that the valid data can be transmitted immediately */
// 可选操作:禁用TX通道并在启用TX通道之前预加载数据,以便立即传输有效数据
//预加载数据(禁用TX通道,写入数据,启用TX通道),以便立即传输有效数据
ESP_ERROR_CHECK(i2s_channel_disable(tx_handle)); // 禁用TX通道
ESP_ERROR_CHECK(i2s_channel_preload_data(tx_handle, data_ptr, music_pcm_end - data_ptr, &bytes_write));//IIS预加载数据
//用于向 I2S 通道的 DMA 缓冲区预加载音频数据,通常用于实现连续音频流的播放
// esp_err_t i2s_channel_preload_data(
// i2s_chan_handle_t handle, // I2S 通道句柄
// const void *data, // 音频数据指针
// size_t size, // 数据长度(字节)
// size_t *bytes_preloaded // 实际预加载的字节数(输出参数)
// );
/* Import music file as buffer */
// extern const uint8_t music_pcm_start[] asm("_binary_canon_pcm_start");
// extern const uint8_t music_pcm_end[] asm("_binary_canon_pcm_end");
data_ptr += bytes_write; // Move forward the data pointer
/* Enable the TX channel */
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));// 使能 TX通道 while (1) {
/* Write music to earphone */
ret = i2s_channel_write(tx_handle, data_ptr, music_pcm_end - data_ptr, &bytes_write, portMAX_DELAY);
if (ret != ESP_OK) {
/* Since we set timeout to 'portMAX_DELAY' in 'i2s_channel_write'
so you won't reach here unless you set other timeout value,
if timeout detected, it means write operation failed. */
ESP_LOGE(TAG, "[music] i2s write failed, %s", err_reason[ret == ESP_ERR_TIMEOUT]);
abort();
}
if (bytes_write > 0) {
ESP_LOGI(TAG, "[music] i2s music played, %d bytes are written.", bytes_write);
} else {
ESP_LOGE(TAG, "[music] i2s music play failed.");
abort();
}
data_ptr = (uint8_t *)music_pcm_start;
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}
void app_main(void)
{
Init();
/* 创建播放音乐任务 */
xTaskCreate(i2s_music, "i2s_music", 4096, NULL, 5, NULL);
}

音频文件添加进编译链的原理细节

将音频文件添加进编译

文件目录 CMakeLists文件

代码实现:

extern const uint8_t music_pcm_start[] asm("_binary_music3_pcm_start");//访问音频起始地址
extern const uint8_t music_pcm_end[] asm("_binary_music3_pcm_end");//访问音频结束地址

这是用于在嵌入式系统(如基于ESP32的乐鑫开发板)中嵌入二进制音频数据(PCM格式)的常见方法。这段代码通过链接器将二进制文件直接嵌入到固件中,并通过符号(Symbol)访问数据。以下是详细解释

  1. 作用

    • 声明两个外部常量数组 music_pcm_startmusic_pcm_end,分别指向二进制音频文件(music3.pcm)在内存中的起始地址结束地址

    • 通过这两个指针可以访问完整的音频数据,例如:

      size_t music_pcm_length = music_pcm_end - music_pcm_start; // 计算音频数据长度
  2. 关键语法

    • extern const uint8_t: 声明外部常量数组,表示数据存储在只读区域(如Flash)。
    • asm("符号名"): 强制指定数组对应的汇编符号名,确保链接器能正确关联二进制文件。

实现原理

  1. 二进制文件嵌入

    • 在编译时,工具链(如ESP-IDF)会将二进制文件(music3.pcm)转换为目标文件(.o),并自动生成符号 _binary_music3_pcm_start_binary_music3_pcm_end
    • 文件名 music3.pcm 会被转换为符号前缀 _binary_music3_pcm_,附加 startend 表示首尾地址。
  2. 链接器处理
    • 链接器将二进制数据分配到Flash的只读段(如 .rodata),开发者无需手动管理地址。

播放MP3格式音频

参考乐鑫官方例程esp-box(乐鑫ESP-BOX是乐鑫信息科技推出的AIoT(人工智能物联网)开发平台系列)

https://github.com/espressif/esp-box

拉取代码

git clone https://github.com/espressif/esp-box.git

需要参考examples下的mp3demo

移植文件系统:SPIFFS(在esp-bsp中)

路径:

D:\Esp_ALL_Project\ESP官方组件例程\esp-bsp-master\esp-bsp-master\bsp\esp-box-3\esp-box-3.c

添加分区表partitions.csv

# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 24k
phy_init, data, phy, 0xf000, 4k
factory, app, factory, , 3M
storage, data, spiffs, , 3M,

IIS和IIC驱动文件无需修改,但音频芯片驱动不使用组件es8311,更改为

espressif/esp_codec_dev组件,里面包含es8311驱动

路径:\managed_components\espressif__esp_codec_dev\device\es8311\es8311.c

  chmorgan/esp-audio-player: "~1.0.7" # 音频播放
chmorgan/esp-file-iterator: "1.0.0" # 获取文件
espressif/esp_codec_dev: "~1.3.0" # 音频驱动

组件chmorgan/esp-file-iterator 提供文件系统的操作函数(文件操作)

组件espressif/esp_codec_dev 音频驱动有驱动层的函数供调用(硬件对接)

组件chmorgan/esp-audio-player 封装了音频播放器函数(播放器软件函数)

[程序启动]

[初始化 I2C 总线设备]

[初始化 IO 扩展芯片]

[挂载 SPIFFS 文件系统]

[初始化音频芯片]

[程序进入主循环或任务调度]

原例程程序通过lvgl的按键控件实现用户操作,在此简化为按键的长短按和双击触发

新建10ms定时器,用作实现

按键状态机(长短按+双击)

按键gpio和定时器初始化

void key_gpio_time_init(void)
{
// 配置GPIO结构体
gpio_config_t io_conf = {
.intr_type = GPIO_INTR_DISABLE, // 禁用中断
.mode = GPIO_MODE_INPUT, // 输入模式
.pin_bit_mask = 1ULL << GPIO_NUM_0,// GPIO0
.pull_down_en = 0, // 禁用下拉
.pull_up_en = 1 // 启用上拉(默认高电平)
};
// 应用GPIO配置(上拉)
gpio_config(&io_conf); // 创建定时器
x10msTimer = xTimerCreate(
"10msTimer", // 定时器名称
x10msPeriod, // 定时周期(已转换为ticks)
pdTRUE, // 自动重载(周期模式)
(void *)0, // 定时器ID(可用于传递参数)
vTimerCallback // 回调函数
); // 检查定时器是否创建成功
if (x10msTimer == NULL) {
printf("Timer creation failed!\n");
return;
} // 启动定时器(必须在调度器启动后调用)
if (xTimerStart(x10msTimer, 0) != pdPASS) {
printf("Timer start failed!\n");
return;
}
}

定时器回调函数

int keytime,s;
void vTimerCallback(TimerHandle_t xTimer) {
// 在此处执行周期性操作(注意不要阻塞!)
keytime++;
key_T_process();//案按键状态机
key_w_process();//按键事件处理
}//10ms

按键状态机

struct key
{
uint8_t key_val;//当前电平
uint8_t key_state;//按脚循环状态
uint8_t key_long_flag;//长按标志位
uint8_t key_double_flag;//双击前置标志位
uint8_t key_one_flag;//单击标志位
uint8_t long_time;//长按计时
uint8_t double_time;//双击计时
uint8_t key_double;//双击触发
}; struct key keys;
void key_T_process(void)//放在10ms定时器回调函数中
{
keys.key_val=gpio_get_level(GPIO_NUM_0);
switch(keys.key_state)
{
case 0://状态0:初始检测
if(keys.key_val==0)keys.key_state=1;
break; case 1://状态1:消抖确认
if(keys.key_val==0)keys.key_state=2;
else keys.key_state=0;
break; case 2://状态2:长按检测
if(keys.key_val==1)
{
if(keys.long_time>100)
{
keys.key_long_flag=1;//长按
keys.key_state=0;
}
else keys.key_state=3;//检测跳转短按或双击检测
keys.long_time=0;
}
else keys.long_time++;
break; case 3://状态3:双击(短按)检测
if(keys.key_val==0)
{
if(keys.double_time<=35)
keys.key_double_flag=1;//
}
else
{
keys.double_time++;
if(keys.double_time>35)
{
keys.key_one_flag=1;//短按
keys.double_time=0;
keys.key_state=0;
}
}
if(keys.key_double_flag==1&&keys.key_val==1)
{
keys.key_double=1;//双击
keys.double_time=0;
keys.key_state=0;
keys.key_double_flag=0;
}
break;
}
}

按键处理

uint8_t keynum;
// 定义一个函数用于处理按键W的事件
void key_w_process(void)
{
// 检查按键1的标志位是否为1,表示按键1被按下
if(keys.key_one_flag==1)
{
keynum++;
keys.key_one_flag=0;
}
if(keys.key_long_flag==1)
{
keynum--;
keys.key_long_flag=0;
}
if(keys.key_double==1)
{
keynum+=10;
keys.key_double=0;
}
}

初始化SPIFFS文件系统

头文件:

#include "esp_spiffs.h"// 包含ESP-IDF的SPIFFS文件系统库
#include "esp_vfs_fat.h"// 包含ESP-IDF的FAT文件系统库
#include "sdmmc_cmd.h"// 包含SDMMC命令库
#include "driver/sdmmc_host.h"// 包含SDMMC主机驱动库
#include "esp_log.h"// 包含ESP-IDF的日志库

挂载点(宏定义)

#define SPIFFS_BASE             "/spiffs"     //文件系统的基路径

SPIFFS文件系统初始化

// 定义一个函数用于挂载SPIFFS文件系统
esp_err_t bsp_spiffs_mount(void)
{
// 定义并初始化SPIFFS配置结构体
esp_vfs_spiffs_conf_t conf = {
.base_path ="/spiffs", // 设置SPIFFS的挂载路径#define SPIFFS_BASE "/spiffs"
.partition_label = "storage", // 设置SPIFFS的分区标签(分区表中的标签)
.max_files = 5, // 设置最大同时打开的文件数
.format_if_mount_failed = false, // 设置在挂载失败时是否格式化SPIFFS
}; // 注册SPIFFS文件系统
esp_err_t ret_val = esp_vfs_spiffs_register(&conf); // 检查注册SPIFFS文件系统的返回值,如果出错则打印错误信息并终止程序
ESP_ERROR_CHECK(ret_val); // 定义变量用于存储SPIFFS的总大小和已使用大小
size_t total = 0, used = 0;
// 获取SPIFFS分区的信息
ret_val = esp_spiffs_info(conf.partition_label, &total, &used);
// 如果获取信息失败,则打印错误信息
if (ret_val != ESP_OK) {
ESP_LOGE(TAG, "Failed to get SPIFFS partition information (%s)", esp_err_to_name(ret_val));
} else {
// 如果获取信息成功,则打印SPIFFS分区的总大小和已使用大小
ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used);
} // 返回操作结果
return ret_val;
}

分区表体现

挂载 SPIFFS 文件系统的作用:


1. 挂载 SPIFFS 文件系统的作用

SPIFFS(SPI Flash File System)是为嵌入式设备设计的轻量级文件系统,主要用于管理 SPI Flash 存储器中的文件。挂载 SPIFFS 的作用包括:

  • 持久化存储:允许在 Flash 中存储配置文件、网页资源、日志等数据,即使设备断电后数据仍能保留。
  • 文件操作接口:提供类似 POSIX 的文件读写接口(fopenfwritefread 等),简化数据管理。
  • 资源管理:适合存储静态资源(如 HTML/CSS/JS 文件),常用于 Web 服务器或 IoT 设备。
  • 分区隔离:通过分区表将 Flash 划分为不同区域,实现代码、文件系统的物理隔离。

2. 关键参数解析

在 ESP-IDF 中挂载 SPIFFS 时,需配置 esp_vfs_spiffs_conf_t 结构体参数:

(1) .base_path = "/spiffs"

  • 作用:定义文件系统的逻辑挂载点(虚拟目录路径)。
  • 示例:若设置 base_path/spiffs,则所有文件操作需基于此路径(如 /spiffs/config.json)。
  • 意义
    • 提供统一的访问入口,类似 Linux 的挂载点(如 /mnt)。
    • 允许多个文件系统挂载到不同路径(如同时挂载 SPIFFS 和 SD 卡)。

(2) .partition_label = "storage"

  • 作用:指定 SPIFFS 对应的物理分区标签(需与分区表中的标签一致)。

  • 意义

    • 分区表(partitions.csv)中需存在名为 storagedata 类型分区,例如:
      # Name, Type, SubType, Offset, Size, Flags
      storage, data, spiffs, 0x200000, 1M,
    • ESP32 根据此标签找到对应的 Flash 物理地址和大小,确保文件系统操作在正确区域进行。

(3) #define SPIFFS_BASE "/spiffs"

  • 作用:通过宏定义简化路径引用,避免硬编码。
  • 示例
    FILE *fp = fopen(SPIFFS_BASE "/data.txt", "r");

3. 参数间的关系

  • 物理层partition_label 关联 Flash 中的实际存储分区(物理地址和大小)。
  • 逻辑层base_path 定义文件系统的访问路径(虚拟目录)。
  • 协作流程
    1. 根据 partition_label 找到对应的 Flash 物理分区。
    2. 将该分区挂载到 base_path 指定的路径,建立物理存储与逻辑路径的映射。

4. 典型问题与注意事项

  • 挂载失败

    • 检查分区表中是否存在 partition_label 指定的分区。
    • 确认分区类型为 spiffs,且大小足够。
  • 路径冲突:避免多个文件系统挂载到同一 base_path
  • 格式化:首次使用或分区损坏时需调用 spiffs_format()

初始化I2C总线设备

与前文一致

初始化IO扩展芯片

与前文一致

初始化音频芯片(总)

// 音频芯片初始化
// 定义一个函数 bsp_codec_init 用于初始化音频编解码器
esp_err_t bsp_codec_init(void)
{
// 调用 bsp_audio_codec_speaker_init 函数初始化扬声器,并将返回的设备句柄赋值给 play_dev_handle
play_dev_handle = bsp_audio_codec_speaker_init();
// 使用 assert 断言确保 play_dev_handle 已成功初始化,否则输出错误信息 "play_dev_handle not initialized"
assert((play_dev_handle) && "play_dev_handle not initialized"); // 调用 bsp_codec_set_fs 函数设置音频编解码器的采样率、位宽和声道数
bsp_codec_set_fs(CODEC_DEFAULT_SAMPLE_RATE, CODEC_DEFAULT_BIT_WIDTH, CODEC_DEFAULT_CHANNEL);
return ESP_OK;
}

初始化音频芯片(ES8311)

bsp_audio_codec_speaker_init()

// 初始化音频输出芯片
// 定义函数 bsp_audio_codec_speaker_init,用于初始化音频编码器并配置扬声器
esp_codec_dev_handle_t bsp_audio_codec_speaker_init(void)
{
// 检查 i2s_data_if 是否为 NULL,如果是,则初始化音频接口和功放
if (i2s_data_if == NULL) {
/* Configure I2S peripheral and Power Amplifier */
ESP_ERROR_CHECK(bsp_audio_init());
}
// 断言 i2s_data_if 不为 NULL,确保音频接口已初始化
assert(i2s_data_if); // 创建一个新的 GPIO 接口用于音频编解码器
const audio_codec_gpio_if_t *gpio_if = audio_codec_new_gpio(); // 配置 I2C 接口
audio_codec_i2c_cfg_t i2c_cfg = {
.port = BSP_I2C_NUM, // I2C 端口号
.addr = ES8311_CODEC_DEFAULT_ADDR, // ES8311 编解码器的默认 I2C 地址
};
// 创建一个新的 I2C 控制接口
const audio_codec_ctrl_if_t *i2c_ctrl_if = audio_codec_new_i2c_ctrl(&i2c_cfg);
// 断言 i2c_ctrl_if 不为 NULL,确保 I2C 控制接口已创建
assert(i2c_ctrl_if); // 配置硬件增益
esp_codec_dev_hw_gain_t gain = {
.pa_voltage = 5.0, // 功放电压为 5.0V
.codec_dac_voltage = 3.3, // 编解码器 DAC 电压为 3.3V
}; // 配置 ES8311 编解码器
es8311_codec_cfg_t es8311_cfg = {
.ctrl_if = i2c_ctrl_if, // 控制接口
.gpio_if = gpio_if, // GPIO 接口
.codec_mode = ESP_CODEC_DEV_WORK_MODE_DAC, // 工作模式为 DAC
.pa_pin = GPIO_PWR_CTRL, // 功放控制引脚
.pa_reverted = false, // 功放控制引脚未反转
.master_mode = false, // 从模式
.use_mclk = true, // 使用主时钟
.digital_mic = false, // 不使用数字麦克风
.invert_mclk = false, // 主时钟不反转
.invert_sclk = false, // 串行时钟不反转
.hw_gain = gain, // 硬件增益配置
};
// 创建一个新的 ES8311 编解码器设备
const audio_codec_if_t *es8311_dev = es8311_codec_new(&es8311_cfg);
// 断言 es8311_dev 不为 NULL,确保编解码器设备已创建
assert(es8311_dev); // 配置编解码器设备
esp_codec_dev_cfg_t codec_dev_cfg = {
.dev_type = ESP_CODEC_DEV_TYPE_OUT, // 设备类型为输出
.codec_if = es8311_dev, // 编解码器接口
.data_if = i2s_data_if, // 数据接口
};
// 创建并返回一个新的编解码器设备句柄
return esp_codec_dev_new(&codec_dev_cfg);
}

设置采样率

bsp_codec_set_fs():

// 函数声明:设置编解码器的采样率、采样位宽和声道模式
esp_err_t bsp_codec_set_fs(uint32_t rate, uint32_t bits_cfg, i2s_slot_mode_t ch)
{
// 初始化返回值为ESP_OK,表示操作成功
esp_err_t ret = ESP_OK; // 创建一个采样信息结构体,并初始化采样率、声道模式和采样位宽
esp_codec_dev_sample_info_t fs = {
.sample_rate = rate, // 采样率
.channel = ch, // 声道模式
.bits_per_sample = bits_cfg, // 采样位宽
}; // 如果播放设备句柄有效,则关闭播放设备
if (play_dev_handle) {
ret = esp_codec_dev_close(play_dev_handle);
}
// 注释掉的代码:如果录音设备句柄有效,则关闭录音设备,并设置默认的ADC音量
// if (record_dev_handle) {
// ret |= esp_codec_dev_close(record_dev_handle);
// ret |= esp_codec_dev_set_in_gain(record_dev_handle, CODEC_DEFAULT_ADC_VOLUME);
// } // 如果播放设备句柄有效,则重新打开播放设备,并设置新的采样信息
if (play_dev_handle) {
ret |= esp_codec_dev_open(play_dev_handle, &fs);
}
// 注释掉的代码:如果录音设备句柄有效,则重新打开录音设备,并设置新的采样信息
// if (record_dev_handle) {
// ret |= esp_codec_dev_open(record_dev_handle, &fs);
// }
// 返回操作结果
return ret;
}

初始化MP3播放器

mp3_player_init():


// mp3播放器初始化
void mp3_player_init(void)
{
// 获取文件信息
file_iterator = file_iterator_new(SPIFFS_BASE);
assert(file_iterator != NULL); // 初始化音频播放
// 将音频播放器的静音功能函数设置为 _audio_player_mute_fn
player_config.mute_fn = _audio_player_mute_fn;
// 将音频播放器的写入数据功能函数设置为 _audio_player_write_fn
player_config.write_fn = _audio_player_write_fn;
// 将音频播放器的时钟设置功能函数设置为 _audio_player_std_clock
player_config.clk_set_fn = _audio_player_std_clock;
// 设置音频播放器的优先级为 1
player_config.priority = 1; // 检查 audio_player_new 函数的返回值,确保音频播放器创建成功
ESP_ERROR_CHECK(audio_player_new(player_config));
ESP_ERROR_CHECK(audio_player_callback_register(_audio_player_callback, NULL)); pa_en(1); // 打开音频功放
for(int i = 0; i < 3; i++) {
play_index(0);
vTaskDelay(5000 / portTICK_PERIOD_MS); // 延时5秒
play_index(1);
vTaskDelay(5000 / portTICK_PERIOD_MS); // 延时5秒
play_index(2);
}
}

ESP32S3播放音频文件的更多相关文章

  1. 关于Window Server2008 服务器上无法播放音频文件的解决方案

    在偌大的百度当中查找我所需要的资源信息,但网络上所描述的都不能解决,发生此类问题的人很多,但是都没有得到准确的解决方法!经个人各方面的尝试,其实非常简单的解决了无法播放音频文件的问题,如果各位今后也遇 ...

  2. ArcGIS API for Silverlight 当DataGrid选中项时,地图聚焦弹出窗口,并可以播放音频文件

    原文:ArcGIS API for Silverlight 当DataGrid选中项时,地图聚焦弹出窗口,并可以播放音频文件 先看效果图,然后上代码: <UserControl x:Class= ...

  3. .NET winform播放音频文件

    前提:最近要求做一个在winform端做一个音频文件播放的功能,至此,总结最近搜寻的相关资料. 一.微软提供了三种方式来播放音频文件 1.通过System.Media.SoundPlayer来播放 2 ...

  4. C#调用mciSendString播放音频文件

    mciSendString函数是一个WinAPI,主要用来向MCI(Media Control Interface)设备发送字符串命令. 一.函数的声明如下: private static exter ...

  5. Unity 播放音频文件

    Unity 播放音频文件参考代码: public void Play(string strSoundName, float autoDestroyTime = 0f, bool bLoop = fal ...

  6. Qt 播放音频文件

    Qt播放音频文件的方法有好多中,简单介绍几种 不过一下几种方式都需要在Qt工程文件中添加 QT       += multimedia 第一 QMediaPlayer类 可以播放MP3文件,同时使用也 ...

  7. h5 audio播放音频文件

    h5 audio播放音频文件 注:下面html中样式及不相关的内容去掉了 第一个例子 播放没有防盗链的外网音频文件是可以的 <!doctype html> <html> < ...

  8. jquery自动播放音频文件

    使用jquery自动播放音频文件 <!DOCTYPE html> <html lang="en"> <head> <meta charse ...

  9. 使用audio标签播放音频文件

    HTML5定义了一个新的元素用来指定标准的方式来插入音频文件到web页面中:<audio>标签.使用audio标签可以控制音频的播放与停止,循环播放与播放次数设置,以及播放位置等等. 例如 ...

  10. linux dsp 播放音频文件

    #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/ioc ...

随机推荐

  1. C#/.NET/.NET Core技术前沿周刊 | 第 22 期(2025年1.13-1.19)

    前言 C#/.NET/.NET Core技术前沿周刊,你的每周技术指南针!记录.追踪C#/.NET/.NET Core领域.生态的每周最新.最实用.最有价值的技术文章.社区动态.优质项目和学习资源等. ...

  2. 记一次LLVM平行宇宙修包实战

      最近加入了LLVM平行宇宙计划小组,在小组内提交了一定数量的PR.这个计划究竟是做什么的呢?LLVM平行宇宙计划是基于LLVM技术栈构建openEuler软件包,大白话讲就是原本一个软件包是用gc ...

  3. 用python做时间序列预测三:时间序列分解

    在初始概念篇中,我们简单提到了时间序列由趋势.周期性.季节性.误差构成,本文将介绍如何将时间序列的这些成分分解出来.分解的使用场景有很多,比如当我们需要计算该时间序列是否具有季节性,或者我们要去除该时 ...

  4. [CF696B] Puzzles 题解

    首先很好想到要用树形 \(dp\). 然后设 \(dp_i\) 为遍历到第 \(i\) 个点的期望时间,\(sz_i\) 代表 \(i\) 的子树大小. 发现有转移方程: \[dp_i=dp_{fa_ ...

  5. JavaGUI - [01] 常见API

    题记部分 一.Component 作为基类,提供了如下常用的方法来设置组件的大小.位置.可见性等. setLocation(int x,int y) 设置组件的位置 setSize(int width ...

  6. 【渗透测试】Vulnhub GROTESQUE 1.0.1

    渗透环境 攻击机:   IP: 192.168.10.18(Kali) 靶机:     IP:192.168.10.9 靶机下载地址:https://www.vulnhub.com/entry/gro ...

  7. luogu-P3343题解

    简要题意 给定一张 \(n\) 个点 \(m\) 条边的图,边的边权是 \([0, 1]\) 之间均匀分布的随机实数,且相互独立.求最小生成树的最大边权的期望值. 思路 首先有一个比较神秘的跟概率有关 ...

  8. Appflowy cloud 部署测试避坑指南

    在进行 Appflowy cloud 部署测试时,我可谓是踩坑无数.下面,我想从几个关键方面来分享一下我的经验. 先给大家讲讲我的基础情况.Appflowy cloud 的部署是在 docker 环境 ...

  9. es6 形参的陷阱

    先看代码: var x = 1; function s (a,y = function (){ x = 2 }){     var x = 1;     y();     console.log(x) ...

  10. 2个月搞定计算机二级C语言——真题(8)解析

    1. 前言 本篇我们讲解2个月搞定计算机二级C语言--真题8 2. 程序填空题 2.1 题目要求 2.2 提供的代码 #include <stdio.h> #define N 3 #def ...