Files
esp32s3_player/README.md
T

366 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HiFi 播放器(ESP32-S3 + LVGL + ES8388 + SD 卡)
基于正点原子 **DNESP32S3** 开发板标准例程(IDF v5.3.x)移植改造的 HiFi 音乐播放器。
使用 **SPI LCD** 作为显示界面、**SD 卡** 存储 WAV 音频、**4 个板载按键** 交互,界面底部实时标注每个按键的功能。
---
## 1. 功能特性
| 功能 | 说明 |
| :--- | :--- |
| 应用启动器 | 开机进入主界面,可选择并进入「音乐播放器」(预留 SETTINGS / ABOUT) |
| 图形界面 | LVGL 8.4,320×240 横屏,深色 HiFi 风格 |
| 音频播放 | **WAV**(PCM 16/24/32bit)与 **FLAC**(原生 FLAC / Ogg FLAC),I2S + ES8388,支持 8k~96k 采样率 |
| 存储 | SD 卡(SPI 模式),FATFS,音乐目录 `/MUSIC` |
| 播放控制 | 播放/暂停 / 下一曲 / 音量切换(无上一曲,K0 用于返回) |
| 状态显示 | 曲目序号、歌名、格式、播放进度、音量、运行状态 |
| 按键提示 | 底部 4 个按键卡片,含义随界面变化,按下时高亮 |
| 后台挂起 | 从播放器返回主界面自动暂停,再进入时从原位置继续 |
| 音量记忆 | 音量保存在 NVS,掉电不丢失 |
| 自动恢复 | SD 卡拔出/插入、目录为空时自动重新挂载与扫描 |
界面布局(两个界面):
```
[主界面 HOME · iPhone 风格图标网格] [播放器 PLAYER]
┌──────────────────────────┐ ┌──────────────────────────┐
│ ☰ APP LAUNCHER 1/4 │ │ ♪ HiFi PLAYER 01/12 │
├──────────────────────────┤ ├──────────────────────────┤
│ ╭──╮ ╭──╮ ╭──╮ ╭──╮ │ │ ┌────┐ 歌名 │
│ │♪ │ │📁│ │⚙ │ │☰ │ │ K3 打开 │ │ ♪ │ WAV 44100Hz 16bit│
│ ╰──╯ ╰──╯ ╰──╯ ╰──╯ │ ───────────▶ │ └────┘ ( PLAYING ) │
│ MUSIC FILES SETTINGS … │ ◀─────────── │ 00:42 ────●────── 03:35 │
│ WAV files in SD /MUSIC │ K0 返回 │ 🔊 ──────●───── 60% │
│ ● │ │ [←K0][⏯K1][⏭K2][🔊K3] │
│ [◀K1][▶K2][✔K3][→K0] │ │ BACK PLAY NEXT VOL │
└──────────────────────────┘ └──────────────────────────┘
```
> 界面文本使用 LVGL 内置 Montserrat 字体(含图标),因此为 ASCII。
> 如需中文界面,见第 9 节“扩展建议”。
---
## 2. 硬件资源与引脚
| 外设 | 引脚 / 总线 | 说明 |
| :--- | :--- | :--- |
| SPI LCD(ST7789 320×240) | SPI2:SCLK=12, MOSI=11, CS=21, DC=40 | 与 SD 卡共用 SPI2 |
| SD 卡 | SPI2:CS=2 | FATFS 挂载点 `/0:` |
| ES8388 | I2S0:MCLK=3, BCLK=46, WS=9, DOUT=10, DIN=14 | 16bit 输出 |
| XL9555 | I2C0:SDA=41, SCL=42 | 按键、LCD 电源/复位/背光、喇叭使能 |
| 按键 | XL9555 的 KEY0~KEY3 | 低电平有效 |
| LED0 | GPIO1 | 播放时点亮 |
> LCD 电源(SLCD_PWR)、复位(SLCD_RST)、背光(LCD_BL)、喇叭使能(SPK_EN)
> 均由 XL9555 扩展 IO 控制,初始化顺序必须是:`my_spi_init → myiic_init → xl9555_init → spilcd_init`。
---
## 3. 按键说明(界面底部同步显示)
**全局约定**:`K0` 固定为「返回 / 切换」键,任何 App 都一致;`K1/K2/K3` 由各个 App 自行定义。
| 按键 | 全局含义 |
| :--- | :--- |
| K0 短按 | 在任意 App 内 → **返回主界面**;在主界面 → **切换到下一个应用** |
| K0 长按(0.7s) | 任意界面 → **直接切换到下一个应用** |
### 主界面(应用启动器)
| 按键 | 功能 | 界面提示 |
| :--- | :--- | :--- |
| KEY0 | 切换到下一个应用 | `→ K0 SWITCH` |
| KEY1 | 选择上一个图标 | `◀ K1 PREV` |
| KEY2 | 选择下一个图标 | `▶ K2 NEXT` |
| KEY3 | 打开选中的图标 | `✔ K3 OPEN` |
### 播放器界面
| 按键 | 功能 | 界面提示 |
| :--- | :--- | :--- |
| KEY0 | **返回主界面**(长按 = 切换应用) | `← K0 BACK` |
| KEY1 | 播放 / 暂停 | `K1 PLAY`(播放中图标变为暂停) |
| KEY2 | **下一曲** | `K2 NEXT` |
| KEY3 | 音量切换(20/40/60/80/100%) | `K3 VOL` |
> 已按要求取消「上一首」功能,K0 释放出来作为独立的返回键。
按键由 `app.c` 的按键服务统一扫描(10ms 周期、30ms 去抖、短按/长按区分),
`ui.c` 是事件的唯一消费者并负责分发,因此**不会重复扫描按键,也不会因为去抖延时而卡住音频**。
---
## 4. 目录结构
```
hifi_player/
├── CMakeLists.txt # 工程入口
├── partitions-16MiB.csv # 16MB Flash 分区表(app 3MB)
├── sdkconfig.defaults # 默认配置:PSRAM/FATFS/LVGL 等
├── components/
│ └── BSP/ # 从标准例程移植的板级驱动
│ ├── CMakeLists.txt
│ ├── LED/ # 指示灯
│ ├── MYIIC/ # I2C0(新 I2C 主机驱动)
│ ├── XL9555/ # IO 扩展:按键 / LCD 控制 / 功放使能
│ ├── MYSPI/ # SPI2 总线 + SD 卡设备
│ ├── SPILCD/ # ST7789 SPI LCD(含 flush 同步)
│ ├── SPI_SD/ # SD 卡 FATFS 挂载
│ ├── ES8388/ # 音频 DAC 配置
│ └── MYI2S/ # I2S 标准模式收发
└── main/
├── CMakeLists.txt
├── idf_component.yml # 依赖:LVGL 8.4 + micro-flac
├── main.c # 初始化流程
└── APP/
├── app.c/.h # 应用管理器 + 按键服务(模式 / 事件队列 / 切换)
├── ui.c/.h # 界面路由:换屏 + 按键分发(唯一按键消费者)
├── ui_common.c/.h # 界面公共组件:配色 / 卡片 / 按键提示条
├── lvgl_port.c/.h # LVGL 移植层(显示 + 1ms 时基)
├── home/ # 主界面(应用启动器)
│ ├── ui_home.c
│ └── ui_home.h
└── player/ # 播放器 App
├── ui_player.c/.h # 播放器界面
├── player.c/.h # 播放核心(扫描 / I2S / 请求处理)
├── audio_source.c/.h # 统一音源:WAV 直读 + FLAC 解码转发
├── flac_dec.cpp # FLAC 解码的 C 封装(C++ 实现)
└── flac_dec.h # 纯 C 接口
```
### 分层关系
```
按键: XL9555 → app.c 的 key_task → 事件队列
│
界面: ui.c(LVGL 任务,50ms) ────────┘ 唯一消费者
├─ K0 → 返回主界面 / 切换应用(全局)
└─ K1~K3 → 交给当前 App(ui_home / ui_player)
│
播放: ui_player → player_request() → player_task 消费并执行
(只有 APP_MODE_PLAYER 时才输出 I2S)
```
---
## 5. 编译与烧录
### 5.1 环境
* ESP-IDF **v6.1**(本工程已在此版本上编译验证通过,生成固件约 **893 KB**)
* 也兼容 **v5.3.x / v5.4**(BSP 已按 IDF 6.x 的驱动组件拆分方式声明依赖)
* 目标芯片:`esp32s3`
* 首次编译需要联网下载 `lvgl/lvgl` 8.4.0 与 `esphome/micro-flac` 0.3.0
### 5.2 命令
```bash
# 1. 设置目标(只需一次)
idf.py set-target esp32s3
# 2. 编译
idf.py build
# 3. 烧录并查看日志(把 COMx 换成实际串口)
idf.py -p COMx flash monitor
```
或直接在 VS Code 的 ESP-IDF 插件里点击 **Build / Flash / Monitor**。
> **注意**:`sdkconfig.defaults` 已配置 16MB Flash、八线 PSRAM(80MHz)、自定义分区表。
> 首次编译前请确认板载 Flash 为 16MB(DNESP32S3 默认如此)。
---
## 6. SD 卡准备
1. SD 卡格式化为 **FAT32**(建议 ≤ 32GB)。
2. 在根目录新建文件夹 **`MUSIC`**(大写),把音频文件放进去:
```
SD 卡根目录/
└── MUSIC/
├── canon.wav
├── song.flac
└── ...
```
3. 音频要求:
| 容器 | 编码 | 位深 | 声道 | 采样率 |
| :--- | :--- | :--- | :--- | :--- |
| **WAV**(RIFF/WAVE) | PCM(`WAVE_FORMAT_PCM` 或 `EXTENSIBLE`+PCM) | 16 / 24 / 32bit | 单声道 / 立体声 | 8k ~ 96kHz |
| **FLAC**(原生 `fLaC` / `Ogg FLAC`) | FLAC | 任意(4~32bit) | 1 ~ 8 声道 | 8k ~ 96kHz |
4. 说明:
* 两种格式都统一**转换为 16bit 立体声**后送给 ES8388:
24/32bit 取高位,单声道复制到左右,多声道(FLAC)只取前两声道。
* 界面上的 `WAV/FLAC xxxHz xxbit` 显示的是**源文件**参数。
* 文件类型按**文件头**自动识别(`RIFF` / `fLaC` / `OggS`),扩展名只用于扫描列表。
---
## 7. 软件架构
```
┌──────────────────────┐ ┌───────────────────────┐
│ player_task │ │ lvgl_task │
│ (优先级 6) │ │ (优先级 4, 8KB 栈) │
│ │ │ │
│ 扫描 /MUSIC │ │ lv_timer_handler() │
│ audio_source_read() │ │ ui_tick_cb(50ms) │
│ → I2S 写 │ │ │ │
│ 消费按键请求队列 │ │ ▼ │
│ │ │ │ player_get_state() │
│ ▼ │ │ (互斥锁保护的状态快照)│
│ s_state (互斥锁) │───────▶│ │
└──────────────────────┘ └───────────┬───────────┘
│ flush_cb
▼
spilcd_flush() → SPI DMA → ST7789
```
### 音频解码链路
```
┌─ WAV ── f_read 原始 PCM ── 取高位 / 单声道复制 ─┐
audio_source_read┤ ├─▶ 16bit 立体声 ──▶ I2S
└─ FLAC ── micro_flac 解码 ── 取高位 / 声道归一化 ─┘
```
`audio_source.c` 只对外暴露「给我一块 16bit 立体声 PCM」,因此 `player.c` 的播放循环
与容器格式完全无关;新增格式(如 MP3)只需在 `audio_source_open()` 里加一个分支。
### 关键实现点
1. **单任务音频流**
一个任务完成“读文件 + 解码 + 写 I2S + 处理按键请求”,每块音频约 46ms,
避免多任务同时访问 FATFS/I2S 的竞争。
2. **非阻塞按键去抖**
`key_scan()` 每次读取 XL9555 输入寄存器(一次 I2C 读 2 字节),
连续 3 次采样一致才确认状态变化,不使用 `vTaskDelay` 阻塞去抖。
3. **LVGL 与 SPI LCD**
* 绘图缓冲区 320×40 像素,放在**内部 DMA RAM**(SPI DMA 不能直接读 PSRAM)。
* `spilcd_flush()` 通过 esp_lcd 注册的 `on_color_trans_done` 回调 + 二值信号量
等待传输完成,保证 LVGL 缓冲区不会被提前复用。
* LVGL 的 RGB565 是小端,ST7789 要求高字节在前,因此在 `disp_flush_cb()` 中做原地字节交换。
4. **I2S 通道管理**
`i2s_trx_start()/i2s_trx_stop()` 增加了状态判断,可以重复调用而不会报错;
切换采样率时会先关闭通道,重新配置 `data_bit_width / ws_width / sample_rate` 后再打开。
5. **FLAC 解码**
* 使用 `esphome/micro-flac`(ESP32-S3 带 Xtensa 汇编优化,16bit 约 30 倍实时)。
* 该库是 C++,`flac_dec.cpp` 提供一层纯 C 封装(`flac_dec_open/read/close`),
播放器侧代码仍是 C。
* 码流缓冲 8KB,采用「未消费数据前移 + 追加读取」,避免解码器返回
`NEED_MORE_DATA` 时丢数据;同时有“无进展”保护,坏文件不会死循环。
* 解码输出缓冲按 STREAMINFO 的 `max_block_size` 动态分配,优先放 PSRAM。
6. **状态同步**
播放器只写 `s_state`(互斥锁保护),UI 每 50ms 读取一次快照,
LVGL 的所有控件操作都发生在 LVGL 任务内,天然线程安全。
---
## 8. 常见问题
| 现象 | 排查方向 |
| :--- | :--- |
| 屏幕全白 / 全黑 | 检查 XL9555 是否初始化成功、背光 `LCD_BL` 是否置 1;确认 `sdkconfig.defaults` 的 PSRAM 配置与板子一致 |
| 屏幕花屏 | 确认 SPI 时钟 60MHz 未超频,排线接触良好;检查 `LCD_RGB_DATA_ENDIAN_BIG` 与字节交换逻辑 |
| 显示 `NO SD CARD` | SD 卡需 FAT32;`my_spi_init()` 必须在 `spilcd_init()` 之前;检查 CS=GPIO2 |
| 显示 `PUT WAV IN /MUSIC` | SD 卡根目录需有 `MUSIC` 文件夹且包含 `.wav` / `.flac` 文件 |
| 显示 `UNSUPPORTED WAV` / `UNSUPPORTED FILE` | WAV 需为 PCM(16/24/32bit);FLAC 需为标准 `fLaC` 或 Ogg FLAC;APE/MP3 需要额外解码库 |
| FLAC 无法播放但 WAV 正常 | 检查串口日志 `flac_dec` 前缀的报错;确认组件已下载(`managed_components/esphome__micro-flac`) |
| FLAC 播放有断续 | 提高 `CONFIG_SPIRAM_SPEED_80M`、确认 PSRAM 已启用;或降低 SD 卡 SPI 时钟 |
---
## 9. 扩展建议
### 9.1 新增一个应用(推荐流程)
工程已经把「界面」「按键」和「应用」解耦,新增应用只需要 4 步,并且**独立文件夹**:
1. **建立文件夹**:例如 `main/APP/clock/`,放入 `ui_clock.c` / `ui_clock.h`,
参考 `player/ui_player.c` 实现 4 个函数:
```c
void ui_clock_create(void); /* 创建屏幕(lv_obj_create(NULL)) */
void ui_clock_show(void); /* 刷新一次并 lv_scr_load() */
void ui_clock_tick(void); /* 周期刷新显示 */
void ui_clock_handle_key(uint8_t key); /* 只处理 K1/K2/K3 */
```
2. **加模式**:在 `main/APP/app.h` 的 `app_mode_t` 中,于 `APP_MODE_NUM` 之前加一项,
例如 `APP_MODE_CLOCK`。
3. **加表项**:在 `main/APP/app.c` 的 `s_apps[]` 中加一条:
```c
{
.icon = LV_SYMBOL_FILE, /* 图标字形 */
.title = "CLOCK", /* 图标下方名称(建议 ≤ 8 字符) */
.subtitle = "Digital clock", /* 选中时下方显示的说明 */
.mode = APP_MODE_CLOCK, /* 进入后切到的模式 */
.enabled = true, /* 允许进入(false 时半透明显示) */
.color_from = 0x4FC3F7, /* 图标渐变起始色 */
.color_to = 0x2E86DE, /* 图标渐变结束色 */
},
```
4. **挂路由**:在 `main/APP/ui.c` 的 `ui_load_mode()` 与 `ui_tick_cb()` 里各加一个
`case APP_MODE_CLOCK:` 分支;同时把 `main/CMakeLists.txt` 的 `SRC_DIRS` /
`INCLUDE_DIRS` 加上 `"APP/clock"`。
返回主界面、切换应用是**全局自动生效**的(`ui.c` 里统一处理 K0),新 App 不需要关心。
### 9.2 其他扩展
> 注意:主界面按 iPhone 风格 **一排 4 个图标**,超过 4 个应用会自动分页,
> 屏幕下方出现页码圆点;K1/K2 可在页间连续移动。
* **中文界面**:在 `menuconfig → Component config → LVGL → Font usage` 中启用
`LV_FONT_SIMSUN_16_CJK`(内置 1000 常用汉字),或使用
[lv_font_conv](https://github.com/lvgl/lv_font_conv) 生成子集字体,然后把
`ui_home.c` / `ui_player.c` 中的英文文案替换为中文。
* **图标换成图片**:当前图标是「圆角渐变 + 内置字形」,如需真实图标,
可用 `LV_IMG_DECLARE()` + `lv_img_create()` 替代 `s_glyph[]` 处的标签
(图片需要用 LVGL 图片转换工具转成 C 数组,或开启 `LV_USE_PNG` + 文件系统驱动)。
* **再增加一种格式(如 MP3 / APE)**:引入对应解码库后,只需在
`audio_source.c` 的 `audio_source_open()` 里加一个分支、在 `audio_source_read()`
里加一个转发即可,`player.c` 与界面代码都不需要改:
```c
if (is_mp3) {
src->codec = AUDIO_CODEC_MP3;
src->mp3 = mp3_dec_open(path);
...
}
```
* **专辑封面**:LVGL 支持从文件系统读取 PNG/JPG(需开启 `LV_USE_PNG` / `LV_USE_SJPG`
+ 文件系统驱动),可在播放器左侧封面卡片显示 `/MUSIC` 中的同名图片。
* **歌词显示**:解析同名 `.lrc` 文件,在进度条下方增加滚动歌词控件。
---
## 10. 参考例程
* `10_spilcd` —— SPI LCD 初始化、引脚定义
* `30_music` —— ES8388 / I2S / WAV 播放 / XL9555 按键
* `25_sd` —— SD 卡(SPI)挂载
* `4,扩展例程-IDF版/4,LVGL例程/01_lvgl_transplant` —— LVGL 8.4 移植结构
第三方组件:
* [esphome/micro-flac](https://github.com/esphome-libs/micro-flac) —— FLAC 解码(Apache-2.0)
驱动代码从上述例程移植;LVGL 移植层、应用管理器、界面路由、图标式主界面、
统一音源层(WAV/FLAC)、播放器状态机与按键交互为本工程重新实现。