找回密码
 立即注册
首页 业界区 业界 ESP32S3播放音频文件

ESP32S3播放音频文件

陆菊 2025-6-2 00:43:23
ESP32S3播放音频文件

硬件基于立创实战派esp32s3
软件代码基于立创实战派教程修改,分析
播放PCM格式音频

原理图分析

音频芯片ES8311

ES8311_I2C_ADD:0x18
1.png

音频功放芯片NS4150B

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

PCA9557,IO拓展芯片

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

IIC高位地址0011 低位地址由A2,A1,A0和控制位控制
5.png
PCA9557通过IIC总线写入数据控制8位IO口输出,A0,A1,A2可通过接上拉电阻或接地,改变PCA9557的IIC的器件地址,避免冲突
6.png
软件分析:

流程:

初始化:

初始化IO拓展芯片PCA9557 ——》IIC
初始化音频芯片ES8311 —— 》IIS(数据),IIC(初始化配置)
通过IO拓展芯片使能音频功放芯片NS4150B
播放音频

创建播放音乐任务
将音频通过IIS通道传入音频芯片
代码

初始化IIS
  1. #include "BSP_I2S.h"
  2. #include "driver/i2s.h"
  3. #include "sdkconfig.h"
  4. #include "driver/i2s_std.h"
  5. #include "esp_system.h"
  6. #include "esp_check.h"
  7. i2s_chan_handle_t tx_handle = NULL;
  8. /* I2S port and GPIOs */
  9. #define I2S_NUM         (0)
  10. #define I2S_MCK_IO      (GPIO_NUM_38)
  11. #define I2S_BCK_IO      (GPIO_NUM_14)
  12. #define I2S_WS_IO       (GPIO_NUM_13)
  13. #define I2S_DO_IO       (GPIO_NUM_45)
  14. #define I2S_DI_IO       (-1)
  15. // 初始化I2S外设
  16. esp_err_t i2s_driver_init(void)
  17. {
  18.     /* 配置i2s发送通道 */
  19.     i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM, I2S_ROLE_MASTER);
  20.     chan_cfg.auto_clear = true; // Auto clear the legacy data in the DMA buffer
  21.     ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle, NULL));
  22.     /* 初始化i2s为std模式 并打开i2s发送通道 */
  23.     i2s_std_config_t std_cfg = {
  24.         .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(16000),
  25.         .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
  26.         .gpio_cfg = {
  27.             .mclk = I2S_MCK_IO,
  28.             .bclk = I2S_BCK_IO,
  29.             .ws = I2S_WS_IO,
  30.             .dout = I2S_DO_IO,
  31.             .din = I2S_DI_IO,
  32.             .invert_flags = {
  33.                 .mclk_inv = false,
  34.                 .bclk_inv = false,
  35.                 .ws_inv = false,
  36.             },
  37.         },
  38.     };
  39.     std_cfg.clk_cfg.mclk_multiple = 384;// If not using 24-bit data width, 256 should be enough
  40.     ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle, &std_cfg));
  41.     ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));
  42.     return ESP_OK;
  43. }
复制代码
初始化IIC
  1. #include "BSP_IIC.h"
  2. // 包含ESP-IDF错误代码定义的头文件
  3. #include "esp_err.h"
  4. // 包含ESP-IDF日志记录功能的头文件
  5. #include "esp_log.h"
  6. // 包含I2C驱动程序的头文件,用于I2C通信
  7. #include "driver/i2c.h"
  8. // 再次包含ESP-IDF日志记录功能的头文件,可能是为了确保在其他地方也能使用
  9. #include "esp_log.h"
  10. // 包含FreeRTOS实时操作系统的头文件,用于任务管理和调度
  11. #include "freertos/FreeRTOS.h"
  12. // 包含FreeRTOS任务相关的头文件,用于创建和管理任务
  13. #include "freertos/task.h"
  14. // 包含数学函数的头文件,用于数学计算
  15. #include "math.h"
  16. #define BSP_I2C_SDA 1  //IO1
  17. #define BSP_I2C_SCL 2
  18. #define BSP_I2C_NUM 0 // I2C外设,选择IIC0
  19. #define BSP_IIC_FREQ_HZ  100000 // 100kHz
  20. #define I2C_MASTER_TX_BUF_DISABLE   0                          /*!< I2C master doesn't need buffer */
  21. #define I2C_MASTER_RX_BUF_DISABLE   0   
  22. esp_err_t bsp_i2c_init(void)//初始化i2c
  23. {
  24.     i2c_config_t conf = {
  25.         // 设置I2C模式为主机模式
  26.         .mode = I2C_MODE_MASTER,
  27.         .sda_io_num = BSP_I2C_SDA,
  28.         .scl_io_num = BSP_I2C_SCL,
  29.         // 启用I2C数据线(SDA)的内部上拉电阻
  30.         .sda_pullup_en = GPIO_PULLUP_ENABLE,
  31.         // 启用I2C时钟线(SCL)的内部上拉电阻
  32.         .scl_pullup_en = GPIO_PULLUP_ENABLE,
  33.         .master.clk_speed = BSP_IIC_FREQ_HZ,
  34.     };
  35.     i2c_param_config(BSP_I2C_NUM, &conf);//选择IIC0初始化
  36.     //通常用来检查驱动程序是否成功安装。确保在调用 i2c_driver_install 之前,已经正确配置了 conf.mode 等参数。
  37.     return i2c_driver_install(BSP_I2C_NUM, conf.mode, I2C_MASTER_RX_BUF_DISABLE, I2C_MASTER_TX_BUF_DISABLE, 0);
  38. }
复制代码
初始化ES8311音频芯片(IIS,IIC)

添加ES8311组件和依赖

乐鑫组件管理器官网:https://components.espressif.com/
搜索ES8311,找到命令下载并复制
7.png
esp-idf终端通过命令添加组件
  1. idf.py add-dependency "espressif/es8311^1.0.0~1"
复制代码
注意:添加组件后需重新选择芯片,才能使组件成功添加,然后再检查flash配置是否还是16M
参考es8311.c编写音频驱动文件user_es8311.c
  1. #include "es8311.h"
  2. #include "BSP_I2S.h"
  3. #include "BSP_I2C.h"
  4. #include "user_es8311.h"
  5. #include "esp_check.h"
  6. /* Example configurations */
  7. // 定义接收缓冲区的大小为2400字节
  8. #define EXAMPLE_RECV_BUF_SIZE   (2400)
  9. // 定义采样率为16000赫兹
  10. #define EXAMPLE_SAMPLE_RATE     (16000)
  11. // 定义MCLK(主时钟)的倍数为384,如果不使用24位数据宽度,256就足够了
  12. #define EXAMPLE_MCLK_MULTIPLE   (384) // If not using 24-bit data width, 256 should be enough
  13. // 计算MCLK的频率,MCLK频率 = 采样率 * MCLK倍数
  14. #define EXAMPLE_MCLK_FREQ_HZ    (EXAMPLE_SAMPLE_RATE * EXAMPLE_MCLK_MULTIPLE)
  15. // 定义语音音量为70
  16. #define EXAMPLE_VOICE_VOLUME    (70)
  17. static const char *TAG = "es8311";
  18. // 初始化es8311芯片(前提初始化I2C接口)
  19. esp_err_t es8311_codec_init(void)
  20. {
  21.     /* 初始化es8311芯片 */
  22.     es8311_handle_t es_handle = es8311_create(BSP_I2C_NUM, ES8311_ADDRRES_0);
  23.     ESP_RETURN_ON_FALSE(es_handle, ESP_FAIL, TAG, "es8311 create failed");
  24.     const es8311_clock_config_t es_clk = {
  25.         .mclk_inverted = false,
  26.         .sclk_inverted = false,
  27.         .mclk_from_mclk_pin = true,
  28.         .mclk_frequency = EXAMPLE_MCLK_FREQ_HZ,
  29.         .sample_frequency = EXAMPLE_SAMPLE_RATE
  30.     };
  31.     // 检查ES8311初始化函数的返回值,确保初始化成功
  32.     ESP_ERROR_CHECK(es8311_init(es_handle, &es_clk, ES8311_RESOLUTION_16, ES8311_RESOLUTION_16));
  33.     // 设置ES8311的采样频率
  34.     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");
  35.     // 设置ES8311的音量
  36.     ESP_RETURN_ON_ERROR(es8311_voice_volume_set(es_handle, EXAMPLE_VOICE_VOLUME, NULL), TAG, "set es8311 volume failed");
  37.     // 配置ES8311的麦克风
  38.     ESP_RETURN_ON_ERROR(es8311_microphone_config(es_handle, false), TAG, "set es8311 microphone failed");
  39.     return ESP_OK;
  40. }
复制代码
初始化PCA9557,IO拓展芯片
  1. #include "pca9557.h"
  2. #include "BSP_I2C.h"
  3. #define PCA9557_INPUT_PORT              0x00
  4. #define PCA9557_OUTPUT_PORT             0x01
  5. #define PCA9557_POLARITY_INVERSION_PORT 0x02
  6. #define PCA9557_CONFIGURATION_PORT      0x03
  7. #define LCD_CS_GPIO                 BIT(0)    // PCA9557_GPIO_NUM_1
  8. #define PA_EN_GPIO                  BIT(1)    // PCA9557_GPIO_NUM_2
  9. #define DVP_PWDN_GPIO               BIT(2)    // PCA9557_GPIO_NUM_3
  10. #define PCA9557_SENSOR_ADDR             0x19        /*!< Slave address of the MPU9250 sensor */
  11. #define SET_BITS(_m, _s, _v)  ((_v) ? (_m)|((_s)) : (_m)&~((_s)))
  12. void pca9557_init(void);
  13. void lcd_cs(uint8_t level);
  14. void pa_en(uint8_t level);
  15. void dvp_pwdn(uint8_t level);
  16. // 读取PCA9557寄存器的值
  17. esp_err_t pca9557_register_read(uint8_t reg_addr, uint8_t *data, size_t len)
  18. {
  19.     return i2c_master_write_read_device(BSP_I2C_NUM, PCA9557_SENSOR_ADDR,  &reg_addr, 1, data, len, 1000 / portTICK_PERIOD_MS);
  20. }
  21. // 给PCA9557的寄存器写值
  22. esp_err_t pca9557_register_write_byte(uint8_t reg_addr, uint8_t data)
  23. {
  24.     uint8_t write_buf[2] = {reg_addr, data};
  25.     return i2c_master_write_to_device(BSP_I2C_NUM, PCA9557_SENSOR_ADDR, write_buf, sizeof(write_buf), 1000 / portTICK_PERIOD_MS);
  26. }
  27. // 初始化PCA9557 IO扩展芯片
  28. void pca9557_init(void)
  29. {
  30.     pca9557_register_write_byte(PCA9557_OUTPUT_PORT, 0x05);  
  31.     pca9557_register_write_byte(PCA9557_CONFIGURATION_PORT, 0xf8);
  32. }
  33. // 设置PCA9557芯片的某个IO引脚输出高低电平
  34. esp_err_t pca9557_set_output_state(uint8_t gpio_bit, uint8_t level)
  35. {
  36.     uint8_t data;
  37.     esp_err_t res = ESP_FAIL;
  38.     pca9557_register_read(PCA9557_OUTPUT_PORT, &data, 1);
  39.     res = pca9557_register_write_byte(PCA9557_OUTPUT_PORT, SET_BITS(data, gpio_bit, level));
  40.     return res;
  41. }
  42. // 控制 PCA9557_PA_EN 引脚输出高低电平 参数0输出低电平 参数1输出高电平
  43. void pa_en(uint8_t level)
  44. {
  45.     pca9557_set_output_state(PA_EN_GPIO, level);
  46. }
复制代码
多个文件同时使用一个句柄,不能声明在被多方引用的头文件
否则报错:
8.png

解决办法,只在一个.c文件中声明句柄(不能在.h中声明),在其他文件通过extern引用
主程序:
  1. #include <stdio.h>
  2. #include "freertos/FreeRTOS.h"
  3. #include "driver/gpio.h"
  4. #include "freertos/task.h"
  5. #include "esp_wifi.h"
  6. #include "esp_log.h"
  7. #include "BSP_I2S.h"
  8. #include "BSP_I2C.h"
  9. #include "user_es8311.h"
  10. #include "pca9557.h"
  11. static const char *TAG = "main";
  12. extern i2s_chan_handle_t tx_handle;
  13. void Init(void)
  14. {
  15.     printf("i2s Init\n-----------------------------\n");
  16.     /* 初始化I2S总线设备 */
  17.     if (i2s_driver_init() != ESP_OK) {
  18.         ESP_LOGE(TAG, "i2s driver init failed");
  19.         abort();//用于立即终止当前程序的执行,
  20.     } else {
  21.         ESP_LOGI(TAG, "i2s driver init success");
  22.     }
  23.   
  24.     /* 初始化I2C总线设备 */
  25.     if (bsp_i2c_init() != ESP_OK) {
  26.         ESP_LOGE(TAG, "i2c driver init failed");
  27.         abort();
  28.     } else {   
  29.         ESP_LOGI(TAG, "i2c driver init success");
  30.     }
  31.     /* 初始化es8311音频芯片*/
  32.     if (es8311_codec_init() != ESP_OK) {
  33.         ESP_LOGE(TAG, "es8311 driver init failed");
  34.         abort();
  35.     } else {   
  36.         ESP_LOGI(TAG, "es8311 driver init success");
  37.     }
  38.     pca9557_init(); //初始化IO扩展芯片
  39.     pa_en(1); // 打开音频
  40. }
  41. static const char err_reason[][30] = {"input param is invalid",
  42.                                       "operation timeout"
  43.                                      };
  44. /* Import music file as buffer */
  45. extern const uint8_t music_pcm_start[] asm("_binary_canon_pcm_start");
  46. extern const uint8_t music_pcm_end[]   asm("_binary_canon_pcm_end");
  47. // 定义一个静态函数i2s_music,用于播放音乐,参数args为传入的参数
  48. static void i2s_music(void *args)
  49. {
  50.     esp_err_t ret = ESP_OK;  // 定义一个esp_err_t类型的变量ret,用于存储函数返回的错误码,初始值为ESP_OK
  51.     size_t bytes_write = 0;  // 定义一个size_t类型的变量bytes_write,用于存储每次写入的字节数
  52.     uint8_t *data_ptr = (uint8_t *)music_pcm_start;  // 定义一个uint8_t指针data_ptr,指向音乐数据的起始地址
  53.     /* (Optional) Disable TX channel and preload the data before enabling the TX channel,
  54.      * so that the valid data can be transmitted immediately */
  55.     // 可选操作:禁用TX通道并在启用TX通道之前预加载数据,以便立即传输有效数据
  56.     //预加载数据(禁用TX通道,写入数据,启用TX通道),以便立即传输有效数据
  57.     ESP_ERROR_CHECK(i2s_channel_disable(tx_handle));    // 禁用TX通道
  58.     ESP_ERROR_CHECK(i2s_channel_preload_data(tx_handle, data_ptr, music_pcm_end - data_ptr, &bytes_write));//IIS预加载数据
  59.     //用于向 I2S 通道的 DMA 缓冲区预加载音频数据,通常用于实现连续音频流的播放
  60.         // esp_err_t i2s_channel_preload_data(
  61.         //     i2s_chan_handle_t handle,  // I2S 通道句柄
  62.         //     const void *data,          // 音频数据指针
  63.         //     size_t size,               // 数据长度(字节)
  64.         //     size_t *bytes_preloaded    // 实际预加载的字节数(输出参数)
  65.         // );
  66.         /* Import music file as buffer */
  67.         // extern const uint8_t music_pcm_start[] asm("_binary_canon_pcm_start");
  68.         // extern const uint8_t music_pcm_end[]   asm("_binary_canon_pcm_end");
  69.     data_ptr += bytes_write;  // Move forward the data pointer
  70.     /* Enable the TX channel */
  71.     ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));// 使能  TX通道
  72.     while (1) {
  73.         /* Write music to earphone */
  74.         ret = i2s_channel_write(tx_handle, data_ptr, music_pcm_end - data_ptr, &bytes_write, portMAX_DELAY);
  75.         if (ret != ESP_OK) {
  76.             /* Since we set timeout to 'portMAX_DELAY' in 'i2s_channel_write'
  77.                so you won't reach here unless you set other timeout value,
  78.                if timeout detected, it means write operation failed. */
  79.             ESP_LOGE(TAG, "[music] i2s write failed, %s", err_reason[ret == ESP_ERR_TIMEOUT]);
  80.             abort();
  81.         }
  82.         if (bytes_write > 0) {
  83.             ESP_LOGI(TAG, "[music] i2s music played, %d bytes are written.", bytes_write);
  84.         } else {
  85.             ESP_LOGE(TAG, "[music] i2s music play failed.");
  86.             abort();
  87.         }
  88.         data_ptr = (uint8_t *)music_pcm_start;
  89.         vTaskDelay(1000 / portTICK_PERIOD_MS);
  90.     }
  91.     vTaskDelete(NULL);
  92. }
  93. void app_main(void)
  94. {
  95.     Init();
  96.     /* 创建播放音乐任务 */
  97.     xTaskCreate(i2s_music, "i2s_music", 4096, NULL, 5, NULL);     
  98. }
复制代码
音频文件添加进编译链的原理细节

将音频文件添加进编译

文件目录CMakeLists文件
9.png
10.png
代码实现:
  1. extern const uint8_t music_pcm_start[] asm("_binary_music3_pcm_start");//访问音频起始地址
  2. extern const uint8_t music_pcm_end[]   asm("_binary_music3_pcm_end");//访问音频结束地址
复制代码
这是用于在嵌入式系统(如基于ESP32的乐鑫开发板)中嵌入二进制音频数据(PCM格式)的常见方法。这段代码通过链接器将二进制文件直接嵌入到固件中,并通过符号(Symbol)访问数据。以下是详细解释

  • 作用

    • 声明两个外部常量数组 music_pcm_start 和 music_pcm_end,分别指向二进制音频文件(music3.pcm)在内存中的起始地址结束地址
    • 通过这两个指针可以访问完整的音频数据,例如:
      1. size_t music_pcm_length = music_pcm_end - music_pcm_start; // 计算音频数据长度
      复制代码

  • 关键语法

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

实现原理


  • 二进制文件嵌入

    • 在编译时,工具链(如ESP-IDF)会将二进制文件(music3.pcm)转换为目标文件(.o),并自动生成符号 _binary_music3_pcm_start 和 _binary_music3_pcm_end。
    • 文件名 music3.pcm 会被转换为符号前缀 _binary_music3_pcm_,附加 start 和 end 表示首尾地址。

  • 链接器处理

    • 链接器将二进制数据分配到Flash的只读段(如 .rodata),开发者无需手动管理地址。

播放MP3格式音频

参考乐鑫官方例程esp-box(乐鑫ESP-BOX是乐鑫信息科技推出的AIoT(人工智能物联网)开发平台系列)
  1. https://github.com/espressif/esp-box
复制代码
拉取代码
  1. 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
  1. # Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
  2. # Name,   Type, SubType, Offset,  Size, Flags
  3. nvs,      data, nvs,     0x9000,  24k
  4. phy_init, data, phy,     0xf000,  4k
  5. factory,  app,  factory, ,        3M
  6. storage,  data, spiffs,  ,        3M,
复制代码
IIS和IIC驱动文件无需修改,但音频芯片驱动不使用组件es8311,更改为
espressif/esp_codec_dev组件,里面包含es8311驱动
路径:\managed_components\espressif__esp_codec_dev\device\es8311\es8311.c
  1.   chmorgan/esp-audio-player: "~1.0.7" # 音频播放
  2.   chmorgan/esp-file-iterator: "1.0.0" # 获取文件
  3.   espressif/esp_codec_dev: "~1.3.0" # 音频驱动
复制代码
组件chmorgan/esp-file-iterator 提供文件系统的操作函数(文件操作)
组件espressif/esp_codec_dev 音频驱动有驱动层的函数供调用(硬件对接)
组件chmorgan/esp-audio-player 封装了音频播放器函数(播放器软件函数)
  1. [程序启动]
  2.      ↓
  3. [初始化 I2C 总线设备]
  4.      ↓
  5. [初始化 IO 扩展芯片]
  6.      ↓
  7. [挂载 SPIFFS 文件系统]
  8.      ↓
  9. [初始化音频芯片]
  10.      ↓
  11. [程序进入主循环或任务调度]
复制代码
原例程程序通过lvgl的按键控件实现用户操作,在此简化为按键的长短按和双击触发
新建10ms定时器,用作实现
按键状态机(长短按+双击)

按键gpio和定时器初始化
  1. void key_gpio_time_init(void)
  2. {
  3.      // 配置GPIO结构体
  4.     gpio_config_t io_conf = {
  5.         .intr_type = GPIO_INTR_DISABLE,    // 禁用中断
  6.         .mode = GPIO_MODE_INPUT,           // 输入模式
  7.         .pin_bit_mask = 1ULL << GPIO_NUM_0,// GPIO0
  8.         .pull_down_en = 0,                 // 禁用下拉
  9.         .pull_up_en = 1                    // 启用上拉(默认高电平)
  10.     };
  11.     // 应用GPIO配置(上拉)
  12.     gpio_config(&io_conf);
  13.     // 创建定时器
  14.     x10msTimer = xTimerCreate(
  15.         "10msTimer",        // 定时器名称
  16.         x10msPeriod,        // 定时周期(已转换为ticks)
  17.         pdTRUE,             // 自动重载(周期模式)
  18.         (void *)0,          // 定时器ID(可用于传递参数)
  19.         vTimerCallback      // 回调函数
  20.     );
  21.     // 检查定时器是否创建成功
  22.     if (x10msTimer == NULL) {
  23.         printf("Timer creation failed!\n");
  24.         return;
  25.     }
  26.     // 启动定时器(必须在调度器启动后调用)
  27.     if (xTimerStart(x10msTimer, 0) != pdPASS) {
  28.         printf("Timer start failed!\n");
  29.         return;
  30.     }
  31. }
复制代码
按键处理
  1. int keytime,s;
  2. void vTimerCallback(TimerHandle_t xTimer) {
  3.     // 在此处执行周期性操作(注意不要阻塞!)
  4.     keytime++;
  5.     key_T_process();//案按键状态机
  6.     key_w_process();//按键事件处理
  7. }//10ms
复制代码
初始化SPIFFS文件系统

头文件:
  1. struct key
  2. {
  3.         uint8_t key_val;//当前电平
  4.         uint8_t key_state;//按脚循环状态
  5.         uint8_t key_long_flag;//长按标志位
  6.     uint8_t key_double_flag;//双击前置标志位
  7.         uint8_t key_one_flag;//单击标志位
  8.     uint8_t long_time;//长按计时
  9.         uint8_t double_time;//双击计时
  10.         uint8_t key_double;//双击触发
  11. };
  12. struct key keys;
  13. void key_T_process(void)//放在10ms定时器回调函数中
  14. {
  15.     keys.key_val=gpio_get_level(GPIO_NUM_0);
  16.                 switch(keys.key_state)
  17.                 {
  18.                         case 0://状态0:初始检测
  19.                                 if(keys.key_val==0)keys.key_state=1;
  20.                         break;
  21.                        
  22.                         case 1://状态1:消抖确认
  23.                                 if(keys.key_val==0)keys.key_state=2;
  24.                                 else keys.key_state=0;
  25.                         break;
  26.                        
  27.                         case 2://状态2:长按检测
  28.                                 if(keys.key_val==1)
  29.                                 {
  30.                                         if(keys.long_time>100)
  31.                                         {
  32.                                                 keys.key_long_flag=1;//长按
  33.                                                 keys.key_state=0;
  34.                                         }
  35.                                         else keys.key_state=3;//检测跳转短按或双击检测
  36.                                         keys.long_time=0;                       
  37.                                 }
  38.                                 else keys.long_time++;
  39.                         break;
  40.                
  41.                         case 3://状态3:双击(短按)检测
  42.                                 if(keys.key_val==0)
  43.                                 {
  44.                                         if(keys.double_time<=35)
  45.                                                 keys.key_double_flag=1;//
  46.                                 }
  47.                                 else
  48.                                 {
  49.                                         keys.double_time++;
  50.                                         if(keys.double_time>35)
  51.                                         {
  52.                                                 keys.key_one_flag=1;//短按
  53.                                                 keys.double_time=0;
  54.                                                 keys.key_state=0;
  55.                                         }
  56.                                 }
  57.                                 if(keys.key_double_flag==1&&keys.key_val==1)
  58.                                 {
  59.                                                 keys.key_double=1;//双击
  60.                                                 keys.double_time=0;
  61.                                                 keys.key_state=0;
  62.                                                 keys.key_double_flag=0;
  63.                                 }
  64.                             break;
  65.         }       
  66. }
复制代码
挂载点(宏定义)
  1. uint8_t keynum;
  2. // 定义一个函数用于处理按键W的事件
  3. void key_w_process(void)
  4. {
  5.     // 检查按键1的标志位是否为1,表示按键1被按下
  6.         if(keys.key_one_flag==1)
  7.         {
  8.                 keynum++;
  9.                 keys.key_one_flag=0;
  10.         }
  11.         if(keys.key_long_flag==1)
  12.         {
  13.                 keynum--;
  14.                 keys.key_long_flag=0;
  15.         }         
  16.         if(keys.key_double==1)
  17.         {
  18.                 keynum+=10;
  19.                 keys.key_double=0;
  20.         }
  21. }
复制代码
11.png
SPIFFS文件系统初始化
  1. #include "esp_spiffs.h"// 包含ESP-IDF的SPIFFS文件系统库
  2. #include "esp_vfs_fat.h"// 包含ESP-IDF的FAT文件系统库
  3. #include "sdmmc_cmd.h"// 包含SDMMC命令库
  4. #include "driver/sdmmc_host.h"// 包含SDMMC主机驱动库
  5. #include "esp_log.h"// 包含ESP-IDF的日志库
复制代码
分区表体现

12.png

挂载 SPIFFS 文件系统的作用:

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

  • 持久化存储:允许在 Flash 中存储配置文件、网页资源、日志等数据,即使设备断电后数据仍能保留。
  • 文件操作接口:提供类似 POSIX 的文件读写接口(fopen、fwrite、fread 等),简化数据管理。
  • 资源管理:适合存储静态资源(如 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)中需存在名为 storage 的 data 类型分区,例如:
      1. #define SPIFFS_BASE             "/spiffs"     //文件系统的基路径
      复制代码
    • ESP32 根据此标签找到对应的 Flash 物理地址和大小,确保文件系统操作在正确区域进行。

(3) #define SPIFFS_BASE "/spiffs"


  • 作用:通过宏定义简化路径引用,避免硬编码。
  • 示例
    1. // 定义一个函数用于挂载SPIFFS文件系统
    2. esp_err_t bsp_spiffs_mount(void)
    3. {
    4.     // 定义并初始化SPIFFS配置结构体
    5.     esp_vfs_spiffs_conf_t conf = {
    6.         .base_path ="/spiffs", // 设置SPIFFS的挂载路径#define SPIFFS_BASE             "/spiffs"
    7.         .partition_label = "storage", // 设置SPIFFS的分区标签(分区表中的标签)
    8.         .max_files = 5, // 设置最大同时打开的文件数
    9.         .format_if_mount_failed = false, // 设置在挂载失败时是否格式化SPIFFS
    10.     };
    11.     // 注册SPIFFS文件系统
    12.     esp_err_t ret_val = esp_vfs_spiffs_register(&conf);
    13.     // 检查注册SPIFFS文件系统的返回值,如果出错则打印错误信息并终止程序
    14.     ESP_ERROR_CHECK(ret_val);
    15.     // 定义变量用于存储SPIFFS的总大小和已使用大小   
    16.     size_t total = 0, used = 0;
    17.     // 获取SPIFFS分区的信息
    18.     ret_val = esp_spiffs_info(conf.partition_label, &total, &used);
    19.     // 如果获取信息失败,则打印错误信息
    20.     if (ret_val != ESP_OK) {
    21.         ESP_LOGE(TAG, "Failed to get SPIFFS partition information (%s)", esp_err_to_name(ret_val));
    22.     } else {
    23.         // 如果获取信息成功,则打印SPIFFS分区的总大小和已使用大小
    24.         ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used);
    25.     }
    26.     // 返回操作结果
    27.     return ret_val;
    28. }
    复制代码
3. 参数间的关系

  • 物理层:partition_label 关联 Flash 中的实际存储分区(物理地址和大小)。
  • 逻辑层:base_path 定义文件系统的访问路径(虚拟目录)。
  • 协作流程

    • 根据 partition_label 找到对应的 Flash 物理分区。
    • 将该分区挂载到 base_path 指定的路径,建立物理存储与逻辑路径的映射。

4. 典型问题与注意事项

  • 挂载失败

    • 检查分区表中是否存在 partition_label 指定的分区。
    • 确认分区类型为 spiffs,且大小足够。

  • 路径冲突:避免多个文件系统挂载到同一 base_path。
  • 格式化:首次使用或分区损坏时需调用 spiffs_format()。
初始化I2C总线设备

与前文一致
初始化IO扩展芯片

与前文一致
初始化音频芯片(总)
  1. # Name, Type, SubType, Offset, Size, Flags
  2. storage, data, spiffs, 0x200000, 1M,
复制代码
初始化音频芯片(ES8311)

bsp_audio_codec_speaker_init()
  1. FILE *fp = fopen(SPIFFS_BASE "/data.txt", "r");
复制代码
设置采样率

bsp_codec_set_fs():
  1. // 音频芯片初始化
  2. // 定义一个函数 bsp_codec_init 用于初始化音频编解码器
  3. esp_err_t bsp_codec_init(void)
  4. {
  5.     // 调用 bsp_audio_codec_speaker_init 函数初始化扬声器,并将返回的设备句柄赋值给 play_dev_handle
  6.     play_dev_handle = bsp_audio_codec_speaker_init();
  7.     // 使用 assert 断言确保 play_dev_handle 已成功初始化,否则输出错误信息 "play_dev_handle not initialized"
  8.     assert((play_dev_handle) && "play_dev_handle not initialized");
  9.    
  10.     // 调用 bsp_codec_set_fs 函数设置音频编解码器的采样率、位宽和声道数
  11.     bsp_codec_set_fs(CODEC_DEFAULT_SAMPLE_RATE, CODEC_DEFAULT_BIT_WIDTH, CODEC_DEFAULT_CHANNEL);
  12.     return ESP_OK;
  13. }
复制代码
初始化MP3播放器

mp3_player_init():
  1. // 初始化音频输出芯片
  2. // 定义函数 bsp_audio_codec_speaker_init,用于初始化音频编码器并配置扬声器
  3. esp_codec_dev_handle_t bsp_audio_codec_speaker_init(void)
  4. {
  5.     // 检查 i2s_data_if 是否为 NULL,如果是,则初始化音频接口和功放
  6.     if (i2s_data_if == NULL) {
  7.         /* Configure I2S peripheral and Power Amplifier */
  8.         ESP_ERROR_CHECK(bsp_audio_init());
  9.     }
  10.     // 断言 i2s_data_if 不为 NULL,确保音频接口已初始化
  11.     assert(i2s_data_if);
  12.     // 创建一个新的 GPIO 接口用于音频编解码器
  13.     const audio_codec_gpio_if_t *gpio_if = audio_codec_new_gpio();
  14.     // 配置 I2C 接口
  15.     audio_codec_i2c_cfg_t i2c_cfg = {
  16.         .port = BSP_I2C_NUM, // I2C 端口号
  17.         .addr = ES8311_CODEC_DEFAULT_ADDR, // ES8311 编解码器的默认 I2C 地址
  18.     };
  19.     // 创建一个新的 I2C 控制接口
  20.     const audio_codec_ctrl_if_t *i2c_ctrl_if = audio_codec_new_i2c_ctrl(&i2c_cfg);
  21.     // 断言 i2c_ctrl_if 不为 NULL,确保 I2C 控制接口已创建
  22.     assert(i2c_ctrl_if);
  23.     // 配置硬件增益
  24.     esp_codec_dev_hw_gain_t gain = {
  25.         .pa_voltage = 5.0, // 功放电压为 5.0V
  26.         .codec_dac_voltage = 3.3, // 编解码器 DAC 电压为 3.3V
  27.     };
  28.     // 配置 ES8311 编解码器
  29.     es8311_codec_cfg_t es8311_cfg = {
  30.         .ctrl_if = i2c_ctrl_if, // 控制接口
  31.         .gpio_if = gpio_if, // GPIO 接口
  32.         .codec_mode = ESP_CODEC_DEV_WORK_MODE_DAC, // 工作模式为 DAC
  33.         .pa_pin = GPIO_PWR_CTRL, // 功放控制引脚
  34.         .pa_reverted = false, // 功放控制引脚未反转
  35.         .master_mode = false, // 从模式
  36.         .use_mclk = true, // 使用主时钟
  37.         .digital_mic = false, // 不使用数字麦克风
  38.         .invert_mclk = false, // 主时钟不反转
  39.         .invert_sclk = false, // 串行时钟不反转
  40.         .hw_gain = gain, // 硬件增益配置
  41.     };
  42.     // 创建一个新的 ES8311 编解码器设备
  43.     const audio_codec_if_t *es8311_dev = es8311_codec_new(&es8311_cfg);
  44.     // 断言 es8311_dev 不为 NULL,确保编解码器设备已创建
  45.     assert(es8311_dev);
  46.     // 配置编解码器设备
  47.     esp_codec_dev_cfg_t codec_dev_cfg = {
  48.         .dev_type = ESP_CODEC_DEV_TYPE_OUT, // 设备类型为输出
  49.         .codec_if = es8311_dev, // 编解码器接口
  50.         .data_if = i2s_data_if, // 数据接口
  51.     };
  52.     // 创建并返回一个新的编解码器设备句柄
  53.     return esp_codec_dev_new(&codec_dev_cfg);
  54. }
复制代码
如果文章对你有所帮助,可以帮我点一下左下角推荐该文,万分感谢
博主目前在广州,深圳找实习。
如有大佬(HR,BOSS)能推荐实习,恳请私信小弟,给个机会,帮帮小弟,万分感谢!!!

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册