最近用 QuecPython(MicroPython)在移远 EC600M-CNLE 上做了个便携音乐播放器,纯 Python 写的,没碰 C。功能比较普通但做完挺有成就感:本地 Flash 曲库播放 + 从 Gitee 公开仓库边下边播不落盘,支持暂停 / 断点续播 / 进度条 / 自动切歌。
先发个开篇介绍一下硬件和整体设计,后面两篇会分别讲核心架构和踩过的坑。
硬件清单(极简单)
| 部件 | 型号 / 接法 | 说明 |
|---|---|---|
| 主控 | Quectel EC600M-CNLE | 4G Cat.1 模组,自带 QuecPython 固件 |
| 屏 | ST7789 240×240,LCM 接口 | 13MHz,4 线 SPI,LVGL 渲染 |
| 功放 | NS4160 | 单端输入,PA_GPIO=9 使能 |
| 按键 | 2 颗,ExtInt 下降沿 | 选择键 GPIO13 / 确认键 GPIO12,50ms 硬件消抖 |
| 音频 | 板载 DAC + 外接喇叭 | audio.Audio 驱动 |
| 存储 | 模组自带 Flash,曲库放 /usr/music | 在线歌不落盘 |
整个交互就靠两颗按键:SELECT 切焦点,CONFIRM 执行。主界面 7 个焦点位(上一首 / 播放暂停 / 下一首 / 曲库 / 音量 -/ 音量 +/ 模式),曲库页 4 个焦点位。圆屏 240×240 配合 LVGL,做出来挺有小玩具那味儿。
最终效果
-
本地歌直接扫 /usr/music,扩展名 + 文件头魔数双重校验(防止把改名的假文件放进去)
-
在线歌不用手动填 URL,开机自动扫 Gitee 公开仓库目录,mp3/amr/wav 自动拼成直链
-
4G 联网失败自动降级纯本地,开不了机这种事不会发生
-
进度条、时长、暂停恢复、自动切下一首都有
-
上传新歌到 Gitee 后,打开曲库 1 秒内原地刷新出来,不用重启设备
整体架构
一句话总结:UI 只渲染,状态机是唯一真状态;所有会阻塞的网络请求全部丢后台线程,后台线程绝不碰 LVGL,只写缓存字典,主循环 50ms tick 回来消费。
┌─────────────────────────────────────────────────┐
│ 主循环 (50ms tick) │
│ ├─ player.tick() ← 唯一真状态改变者 │
│ ├─ poll_online_refresh()← 消费后台线程的产物 │
│ ├─ inp.consume_events() ← 按键事件 │
│ └─ ui.refresh(state) ← 唯一碰 LVGL 的地方 │
└──────┬───────────────┬───────────────┬──────────┘
│ │ │
┌───▼────┐ ┌───────▼──────┐ ┌───▼────────┐
│播放worker│ │online子线程 │ │Gitee扫描线程│
│(切歌) │ │(HTTP下载+喂流)│ │(列目录API) │
└───┬────┘ └───────┬──────┘ └───┬────────┘
│ │ │
└──── 只写缓存字典 / 事件队列 ───┘
绝不碰 LVGL
几个关键约定:
-
LVGL 只在主线程碰。任何后台线程改 UI,轻则花屏重则硬 fault。
-
MicroPython 是单 GIL,纯 CPU 密集循环会饿死主循环 —— 所以所有 “扫描 / 喂流” 都必须做 “只扫一次 / 分块 / 水位停手”。网络 request.get 在等待时会让出 GIL,所以挪后台真的有效。
-
每个后台任务带防重标志,避免重复发请求。
下一篇讲这个状态机具体怎么设计的,特别是 QuecPython playStream 那个反直觉的坑 —— 它每喂 4KB 数据就吐一对假 START/FINISH 事件,把我进度条搞疯了好久。