366 lines
18 KiB
Markdown
366 lines
18 KiB
Markdown
# 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)、播放器状态机与按键交互为本工程重新实现。
|