# 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 │ ├──────────────────────────┤ │ 📁 .. │ │ 📁 Album │ ← 选中行带强调色描边 │ ♪ 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 + 原生位深)、文件管理器、播放器状态机与按键交互为本工程重新实现。