Files

544 lines
27 KiB
Markdown
Raw Permalink 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** 开发板标准例程移植改造的 HiFi 音乐播放器。
使用 **SPI LCD** 作为显示界面、**SD 卡** 存储音频、**4 个板载按键** 交互,
界面底部实时标注每个按键的功能。
当前包含 **3 个应用**:主界面(应用启动器)、音乐播放器、文件管理器。
音频通路为 **原生位深透传**:源文件是 16/24/32bit,就按对应位宽重新配置 I2S 与 ES8388,
不做位深压缩,配合 FLAC 无损解码构成完整的无损链路。
---
## 版本记录
| 版本 | 主要变更 |
| :--- | :--- |
| V1.0 | 移植 BSP、LVGL 8.4、ES8388 + I2S,WAV 播放 |
| V1.2 | 应用启动器框架、全局 K0 返回键、按应用分文件夹 |
| V1.3 | iPhone 风格图标网格主界面 |
| V1.4 | 增加 FLAC 解码(`esphome/micro-flac`),WAV/FLAC 统一音源层 |
| **V1.5** | **原生位深透传 + I2S 位宽/采样率动态重配**;新增**文件管理器**应用;修复 `.flac` 文件无法被扫描到的问题 |
---
## 1. 功能特性
| 功能 | 说明 |
| :--- | :--- |
| 应用启动器 | 开机进入主界面,图标网格选择应用(MUSIC / FILES 可用,SETTINGS / ABOUT 占位) |
| 音乐播放器 | **WAV**(PCM 16/24/32bit)与 **FLAC**(原生 FLAC / Ogg FLAC),支持 8k~96k 采样率 |
| 文件管理器 | 浏览 SD 卡全部文件,可进出任意层级子文件夹,显示文件大小与类型图标 |
| 原生位深输出 | 16/24/32bit 按源文件位深直接输出,**不做位深压缩**;I2S 位宽与采样率随曲目动态重配 |
| 图形界面 | LVGL 8.4,320×240 横屏,深色 HiFi 风格 |
| 存储 | SD 卡(SPI 模式),FATFS,音乐目录 `/MUSIC` |
| 播放控制 | 播放/暂停 / 下一曲 / 音量切换(无上一曲,K0 用于返回) |
| 状态显示 | 曲目序号、歌名、格式、播放进度、音量、运行状态 |
| 按键提示 | 底部 4 个按键卡片,含义随界面变化,按下时高亮 |
| 后台挂起 | 从播放器返回主界面自动暂停,再进入时从原位置继续 |
| 音量记忆 | 音量保存在 NVS,掉电不丢失 |
| 自动恢复 | SD 卡拔出/插入、目录为空时自动重新挂载与扫描 |
### 界面布局
```
[主界面 HOME · iPhone 风格图标网格]
┌──────────────────────────┐
│ ☰ APP LAUNCHER 1/4 │
├──────────────────────────┤
│ ╭──╮ ╭──╮ ╭──╮ ╭──╮ │
│ │♪ │ │📁│ │⚙ │ │☰ │ │ K3 打开选中的应用
│ ╰──╯ ╰──╯ ╰──╯ ╰──╯ │
│ MUSIC FILES SETTING ABOUT│
│ WAV/FLAC files in SD … │
│ ● │
├──────────────────────────┤
│ [◀K1] [▶K2] [✔K3] [→K0]│
│ PREV NEXT OPEN SWITCH│
└──────────────────────────┘
[播放器 PLAYER]
┌──────────────────────────┐
│ ♪ HiFi PLAYER 01/12 │
├──────────────────────────┤
│ ┌────┐ 歌名 │
│ │ ♪ │ FLAC 44100Hz │
│ └────┘ 24bit STEREO │
│ ( PLAYING ) │
│ 00:42 ────●────── 03:35 │
│ 🔊 ──────●───── 60% │
├──────────────────────────┤
│ [←K0] [⏯K1] [⏭K2] [🔊K3] │
│ BACK PLAY NEXT VOL │
└──────────────────────────┘
[文件管理器 FILES]
┌──────────────────────────┐
│ 📁 FILES 12 SD │
├──────────────────────────┤
│ 0:/MUSIC/Album │
├──────────────────────────┤
│ 📁 .. <DIR> │
│ 📁 Album <DIR> │ ← 选中行带强调色描边
│ ♪ song01.flac 24.1M │
│ 📄 notes.txt 128B │
│ 📄 readme.md 2K │
├──────────────────────────┤
│ [←K0] [▲K1] [▼K2] [✔K3] │
│ PARENT UP DOWN OPEN │
└──────────────────────────┘
```
> 界面文本使用 LVGL 内置 Montserrat 字体(含图标),因此为 ASCII。
> 如需中文界面,见第 10 节“扩展建议”。
---
## 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 | 位宽随曲目动态配置(16/24/32bit) |
| 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) | 任意界面 → **直接切换到下一个应用** |
> **例外**:实现了“就地返回”的 App 可以先消费掉 K0 短按。目前**文件管理器**就是这样——
> 在子目录里按 K0 是返回上一级目录,只有在 `0:` 根目录时才回主界面。
> 该行为由 `ui.c` 中调用 `ui_files_back()` 钩子实现,其它 App 不受影响。
### 主界面(应用启动器)
| 按键 | 功能 | 界面提示 |
| :--- | :--- | :--- |
| 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 释放出来作为独立的返回键。
### 文件管理器界面
| 按键 | 功能 | 界面提示 |
| :--- | :--- | :--- |
| KEY0 | **返回上一级目录**;已在根目录时回主界面(长按 = 切换应用) | `← K0 UP` / `← K0 HOME` |
| KEY1 | 选择上一项(到顶后跳到底部) | `▲ K1 UP` |
| KEY2 | 选择下一项 | `▼ K2 DOWN` |
| KEY3 | 进入文件夹 / 查看文件详情 | `✔ K3 OPEN` |
> 退出子文件夹有两条路:按 **K0**,或选中列表最上方的 **`..`** 行按 **K3**。
> 按 K3 打开普通文件时,屏幕下方会弹出 2 秒浮层,显示文件名、大小和类型
> (`AUDIO` / `IMAGE` / `FILE`)。
按键由 `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
├── files/ # 文件管理器 App
│ ├── ui_files.c # 浏览 SD 卡目录(滑动窗口列表)
│ └── ui_files.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 → 返回主界面 / 切换应用(全局)
│ └─ 例外:FILES 在子目录时先返回上一级
└─ K1~K3 → 交给当前 App(ui_home / ui_files / ui_player)
│
播放: ui_player → player_request() → player_task 消费并执行
(只有 APP_MODE_PLAYER 时才输出 I2S)
```
---
## 5. 编译与烧录
### 5.1 环境
* ESP-IDF **v6.1**(本工程已在此版本上编译验证通过,固件 `0xdb3f0` = **898,032 字节 ≈ 877 KB**,分区剩余 71%)
* 也兼容 **v5.3.x / v5.4**(BSP 已按 IDF 6.x 的驱动组件拆分方式声明依赖)
* 目标芯片:`esp32s3`
* 首次编译需要联网下载 `lvgl/lvgl` 8.4.0 与 `esphome/micro-flac` 0.3.0
> **IDF 6.x 注意**:`driver` 这个“伞形”组件不再转发 `esp_driver_*` 子组件,
> 因此在 `components/BSP/CMakeLists.txt` 里必须逐个显式列出:
> `esp_driver_gpio esp_driver_i2c esp_driver_i2s esp_driver_spi esp_driver_sdspi esp_driver_sdmmc sdmmc esp_lcd fatfs`。
> 同时 `driver/i2s.h`、`driver/i2c.h`、`I2S_BITS_PER_SAMPLE_*` 已被移除,
> 代码里改用 `esp_driver_i2s` 头文件与纯数字位宽。
### 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)。**不支持 exFAT**。
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. 说明:
* 文件名建议**只用 ASCII**(英文 / 数字 / 下划线)。中文文件名能正常播放,
但界面上会显示为 `?`(见下方第 5 条)。
* 单曲数量上限 **200 首**(`MAX_TRACKS`),扩展名支持 `.wav` / `.flac`(大小写不敏感)。
* 文件类型按**文件头**自动识别(`RIFF` / `fLaC` / `OggS`),扩展名只用于筛选列表。
* **位深不再统一转 16bit**:源文件是什么位深就按什么位宽输出,映射关系见下表。
* 中文文件名是 GBK 编码,LVGL 内置字体无法渲染,`make_display_name()` 会把
连续的非 ASCII 字节折叠成一个 `?`,但**底层路径使用原始字节**,不影响播放。
### 6.1 位深与输出映射(V1.5)
| 源位深 | 每采样字节 | 每帧字节(立体声) | ES8388 R23 字长 | I2S `data_bit_width` / `ws_width` | `mclk_multiple` |
| :--- | :--- | :--- | :--- | :--- | :--- |
| ≤ 16bit(8 / 16) | 2 | 4 | 16bit(码 3) | `I2S_DATA_BIT_WIDTH_16BIT` | 256 |
| 17 ~ 24bit(20 / 24) | 3 | 6 | 24bit(码 0) | `I2S_DATA_BIT_WIDTH_24BIT` | **384** |
| 25 ~ 32bit(32) | 4 | 8 | 32bit(码 4) | `I2S_DATA_BIT_WIDTH_32BIT` | 256 |
> **为什么 24bit 要用 384 倍 MCLK**:ESP-IDF 的 `i2s_std.c` 有一条硬性校验——
> slot 位宽为 24bit 时 `mclk_multiple` 必须是 3 的倍数,否则
> `i2s_channel_reconfig_std_clock()` 直接返回 `ESP_ERR_INVALID_ARG`。
> `myi2s.c` 的 `i2s_set_samplerate_bits_sample()` 会自动切换:24bit → 384,其余 → 256。
>
> 例:20bit 的 FLAC 会输出成 24bit 槽位(`out_bytes = 3`),
> 界面上显示为 `FLAC 44100Hz 20->24bit`,提示“源 20bit,实际按 24bit 输出”。
---
## 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 │
└─ FLAC ── micro_flac 解码 ── 声道归一化 ────────┘
│
┌────────────────────────────────┘
▼
立体声 + 原生位深(16/24/32bit,左对齐)
│
audio_set_format(rate, out_bits)
├─ es8388_i2s_cfg(0, len_code)
└─ i2s_set_samplerate_bits_sample(rate, bits)
│
▼
i2s_tx_write(frames * 2 * out_bytes)
```
`audio_source.c` 对外只暴露「给我一块**立体声 + 原生位深** PCM,并告诉我 `out_bytes`」,
因此 `player.c` 的播放循环与容器格式完全无关;新增格式(如 MP3)只需在
`audio_source_open()` 里加一个分支。
### 关键实现点
1. **单任务音频流**
一个任务完成“读文件 + 解码 + 写 I2S + 处理按键请求”,每块音频约 23ms
(`AUDIO_CHUNK_FRAMES = 1024` @44.1kHz),避免多任务同时访问 FATFS/I2S 的竞争。
发送缓冲按最坏情况分配 8 字节/帧(32bit 立体声):`AUDIO_BUF_SIZE = 1024 × 8 = 8KB`。
2. **非阻塞按键去抖**
`key_task()` 每 10ms 读一次 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 / mclk_multiple /
sample_rate` 后再由播放循环按需打开。
5. **FLAC 解码**
* 使用 `esphome/micro-flac`(ESP32-S3 带 Xtensa 汇编优化,16bit 约 30 倍实时)。
* 该库是 C++,`flac_dec.cpp` 提供一层纯 C 封装(`flac_dec_open/read/close`),
播放器侧代码仍是 C。
* 用的是 `uint8_t*` 输出重载(**原生字节打包**),而不是 `int32_t*` 的 32bit 归一化重载,
这样才能保留源位深。
* 码流缓冲 8KB,采用「未消费数据前移 + 追加读取」,避免解码器返回
`NEED_MORE_DATA` 时丢数据;同时有“无进展”保护,坏文件不会死循环。
* 解码输出缓冲按 STREAMINFO 的 `max_block_size` 动态分配,优先放 PSRAM。
6. **状态同步**
播放器只写 `s_state`(互斥锁保护),UI 每 50ms 读取一次快照,
LVGL 的所有控件操作都发生在 LVGL 任务内,天然线程安全。
---
## 8. 文件管理器实现要点
`main/APP/files/ui_files.c` 是一个可直接复用的「列表型 App」模板:
1. **滑动窗口列表**
界面创建时只建 **5 个固定行控件**(`FILES_ROWS`),刷新时把
`[s_top, s_top + 5)` 区间映射到这些行上。目录里有几百个文件也不会撑爆
LVGL 的 48KB 内存池;也完全避免“清空重建”带来的闪烁。
2. **排序规则**
`qsort` + 自定义比较:`..` 永远第一 → 目录 → 文件,同类按名称
`strcasecmp`(不区分大小写)。
3. **过滤**
隐藏(`AM_HID`)、系统(`AM_SYS`)、卷标(`0x08`)属性一律不显示。
单目录最多显示 `FILES_MAX_ENTRIES = 96` 条,超出部分截断。
4. **图标着色**
目录 → 黄色 `LV_SYMBOL_DIRECTORY`;音频(`.wav`/`.flac`)→ 青色 `LV_SYMBOL_AUDIO`;
图片(`.bmp/.png/.jpg/.jpeg/.gif`)→ 紫色 `LV_SYMBOL_IMAGE`;其它 → 灰色 `LV_SYMBOL_FILE`。
5. **大小格式化**
`files_format_size()` 用纯整数运算输出 `128B` / `2K` / `24.1M`,
不依赖浮点 `printf`(省 flash、避免 `%f` 在 newlib nano 下失效)。
6. **别名安全的路径切换**
`files_open_dir()` 先把入参复制到 `target[]` 再写入 `s_path`,
因此可以安全地用 `s_path` 自身作为实参调用(`files_go_up()` 就是这么做的)。
打开失败会自动回退到原目录并恢复原来的选中项。
7. **SD 卡挂载策略**
先 `f_opendir("0:")` **探测**;只有探测失败且从未尝试过时才调用 `sd_spi_init()`。
正常开机时播放器任务已经挂好卡,这里只读探测,绝不和播放器抢挂载
(否则正在播放的文件句柄会失效)。
> FatFs 配置为 `FF_FS_REENTRANT = 1`,因此播放器任务与 LVGL 任务同时访问 SD 卡是安全的。
---
## 9. 常见问题
| 现象 | 排查方向 |
| :--- | :--- |
| 屏幕全白 / 全黑 | 检查 XL9555 是否初始化成功、背光 `LCD_BL` 是否置 1;确认 PSRAM 配置与板子一致 |
| 屏幕花屏 | 确认 SPI 时钟 60MHz 未超频,排线接触良好;检查字节交换逻辑 |
| 显示 `NO SD CARD` | SD 卡需 **FAT32**(不是 exFAT);`my_spi_init()` 必须在 `spilcd_init()` 之前;检查 CS=GPIO2 |
| 显示 `PUT WAV/FLAC IN /MUSIC` | SD 卡根目录需有 `MUSIC` 文件夹且**直接**包含 `.wav` / `.flac`(不递归子目录) |
| **FLAC 文件不显示 / 不能播放** | V1.5 已修复 `name_is_audio()` 提前 `return false` 导致 `.flac` 全被过滤的 bug。若仍异常,确认扩展名确实是 `.flac`、文件是标准 `fLaC` 或 Ogg FLAC,并查看串口 `flac_dec` 前缀日志 |
| 显示 `UNSUPPORTED FILE` | WAV 必须是 PCM(16/24/32bit);APE / MP3 / M4A 需要额外解码库 |
| 界面显示 `24->32bit` 之类 | 这是正常提示:源位深与实际 I2S 输出位宽不同(如 20bit 源按 24bit 输出) |
| 24bit 曲目无声 / 报错 | 检查 `mclk_multiple` 是否为 3 的倍数;ESP-IDF 要求 24bit slot 必须满足该条件,`myi2s.c` 已自动设 384 |
| FLAC 播放有断续 | 提高 `CONFIG_SPIRAM_SPEED_80M`、确认 PSRAM 已启用;或降低 SD 卡 SPI 时钟 |
| 文件管理器看不到某些文件 | 隐藏 / 系统属性的文件被有意过滤;单目录超过 96 条会被截断 |
| 文件管理器中文名显示 `?` | 预期行为:GBK 文件名无法用内置字体渲染,功能不受影响 |
| 编译报 `'LIST_H' redefined` | 自定义宏名与 FreeRTOS `list.h` 的 include guard 冲突(`-Werror`),改名即可 |
| 编译报 `'AM_VOL' undeclared` | 本版 FatFs 未定义该宏,用字面量 `0x08` 代替 |
| 编译报 `-Werror=format-truncation` | `FILINFO::fname` 是 `char[256]`,拷入更小缓冲时改用 `strlcpy` 或显式长度校验 |
---
## 10. 扩展建议
### 10.1 新增一个应用(推荐流程)
工程已经把「界面」「按键」和「应用」解耦,新增应用只需要 4 步,并且**独立文件夹**:
1. **建立文件夹**:例如 `main/APP/clock/`,放入 `ui_clock.c` / `ui_clock.h`,
参考 `player/ui_player.c` 或 `files/ui_files.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 */
```
如需 K0 短按在 App 内“就地返回”(像文件管理器那样逐级退出),再额外实现:
```c
bool ui_clock_back(void); /* true = 已就地处理,ui.c 不再回主界面 */
```
并在 `main/APP/ui.c` 的 `ui_dispatch()` 里加一个 `else if` 分支。
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_dispatch()`、
`ui_tick_cb()` 里各加一个 `case APP_MODE_CLOCK:` 分支;
同时把 `main/CMakeLists.txt` 的 `SRCS` 加上 `"APP/clock/ui_clock.c"`、
`INCLUDE_DIRS` 加上 `"APP/clock"`(本工程用的是显式文件列表,不是 `SRC_DIRS`)。
返回主界面、切换应用是**全局自动生效**的(`ui.c` 里统一处理 K0),新 App 不需要关心。
### 10.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) 生成子集字体,然后把
各界面里的英文文案替换为中文。文件名同理——启用中文字体后,
`make_display_name()` / `files_display_name()` 里的 `?` 折叠逻辑就可以去掉。
* **图标换成图片**:当前图标是「圆角渐变 + 内置字形」,如需真实图标,
可用 `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);
...
}
```
注意 MP3 等有损格式位深固定,`out_bytes` 填 2(16bit)即可,
`audio_set_format()` 会自动把 ES8388 / I2S 配成 16bit。
* **专辑封面**:LVGL 支持从文件系统读取 PNG/JPG(需开启 `LV_USE_PNG` / `LV_USE_SJPG`
+ 文件系统驱动),可在播放器左侧封面卡片显示 `/MUSIC` 中的同名图片。
* **歌词显示**:解析同名 `.lrc` 文件,在进度条下方增加滚动歌词控件。
* **文件管理器直接播放**:在 `files_enter()` 里判断选中项是音频文件时,
调用 `player_load_path()`(需在 `player.h` 新增该接口)+ `app_set_mode(APP_MODE_PLAYER)`,
即可实现「浏览目录 → 点开即播」。
---
## 11. 参考例程
* `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 + 原生位深)、文件管理器、播放器状态机与按键交互为本工程重新实现。