在一块 STM32H7S78-DK 开发板上,我们用 Zephyr 从零搭了一套完整的掌上影音系统: 开机进启动器,左右滑动翻页,图标点进应用——网络收音机、SD 卡 MP3 播放、 AVI 视频有声播放、逐小时天气预报,四个应用共用一套音频管道和一套 UI 框架。 本文介绍这套系统的设计思路、Zephyr 带来的开发体验,以及一路沉淀下来的编程心得。

一、开发板简介:小身材,全是硬货
STM32H7S78-DK 是 ST 新一代 H7RS 系列的官方开发板,几个关键资源决定了这套 系统的形态:
| MCU | Cortex-M7 @ 600MHz | 全部软件解码(MP3/JPEG)、UI 渲染 |
| 内部存储 | 64KB BootFlash + 456KB SRAM | MCUboot 引导、内核/栈/热点缓冲 |
| 外部 Flash | 128Mbit Octo-SPI(XiP) | 应用程序直接在上面执行(XiP) |
| PSRAM | 256Mbit Hexadeca-SPI(32MB) | 视频帧缓冲、音频块、JPEG 队列 |
| 显示 | 800×480 RGB 屏 + LTDC | LVGL 主界面 |
| 触摸 | GT911 电容触摸(I2C) | 全部交互 |
| 音频 | WM8904 codec + I2S6 + GPDMA | 48kHz 立体声输出,耳机/喇叭接口 |
| 网络 | 10/100M 以太网 | 收音机流、天气 API、NTP 对时 |
| SD 卡槽 | SDMMC1 4bit | MP3/视频片源 |
| GPU | NeoChrom(2.5D)+ DMA2D | 位图搬运/旋转/缩放加速(GPU 对比演示) |
设计上的两个关键认识:
"小内部 + 大外部"的存储结构是 H7RS 的特点。64KB 片内 flash 只够放 MCUboot,应用必须在外部 Octo-SPI Flash 上 XiP 运行;456KB SRAM 放内核、 栈和性能热点,32MB PSRAM 是所有大缓冲的家(视频双缓冲 2×240KB、 JPEG 队列 2×100KB、音频块 8×9KB 全在里面)。
外设组合天然就是一台播放器:屏+触摸做 UI,I2S+codec 出声,以太网 拿流,SD 卡存片源。软件要做的是把它们用一条干净的数据流串起来。
二、为什么是 Zephyr:这套系统里它实际帮了什么
选 Zephyr 不是因为它流行,而是这个项目里每一层都直接受益:
1. 板级支持开箱即用。 这块板在 Zephyr 官方板列表里,LTDC 显示、GT911 触摸、以太网、SDMMC、PSRAM 的设备树描述都是现成的。我们要做的只是在自己的 overlay 里补外设细节(比如给 sdmmc1 补 pinctrl 和时钟选择),不写一行 寄存器代码。
2. 统一的驱动模型。 display / input / audio codec / sdhc / phy / ethernet 全是同一套 device 抽象。应用层面对的不是某家芯片的 HAL,而是稳定 API—— 这也是后面"一条音频管道喂三种音源"能成立的前提。
3. 原生网络协议栈。 DHCP、DNS、TCP socket、SNTP 全是 Kconfig 打开就有。 网络收音机就是一个普通 socket 流;天气预报是 socket 上的 HTTP GET; 状态栏时间是 SNTP 对时。没有移植任何第三方协议栈。
4. LVGL 是官方模块。 CONFIG_LVGL=y 一行,GUI 就进来了,显示驱动和 触摸输入自动接到 LVGL 的 indev/display 框架。中文字库用 lv_font_conv 子集化生成(ASCII + 几十个汉字,24px 只有几十 KB)。
5. 内核对象让并发架构很干净。 这套系统有 7 个线程(UI、收音机解码、 MP3 解码、视频 reader、视频 decode、音频 TX、网络),全靠消息队列、 内存 slab、事件、优先级组合起来,没有一把大锁,没有共享变量乱飞。 线程模型本身就是设计文档。
6. west / sysbuild / MCUboot 生态。 --sysbuild 一条命令同时编出 MCUboot 和应用,west flash 烧外部 flash 还带 ExternalLoader。 引导 + XiP 这套在别的 RTOS 上要自己攒半天的东西,这里是默认路径。
7. Kconfig 裁剪直面资源约束。 456KB SRAM 用到了 91%,每一 KB 都要 交代:LVGL 堆 128KB、网络缓冲、字体、栈……Kconfig 让"这块内存从哪来、 给谁用"全部是显式声明。
三、系统设计思路
3.1 启动与运行形态
上电 → MCUboot(片内 64KB)→ 校验/跳转 → 应用在外部 Octo-SPI Flash XiP 运行
应用镜像约 820KB(含字体、minimp3、tjpgd),放在 128Mbit 外部 flash 的开头, 后续还可以做 OTA 双槽升级——MCUboot 的 slot 机制是现成的。
3.2 UI 框架:启动器 + 应用页,进入载入 / 离开释放
┌──────────────────────────────────────────┐ │ 悦聆 11-12 14:30 ● IP:192.168.3.27 │ ← 每页统一的顶栏(时钟/网络状态) ├──────────────────────────────────────────┤ │ Zephyr │ │ 影音系统 │ │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ │ │MP3 │ │视频 │ │收音│ │天气 │ ◀ 滑动 │ │ └────┘ └────┘ └────┘ └────┘ │ └──────────────────────────────────────────┘
启动器是 tileview,左右滑动翻页;每个应用是独立 screen,lv_screen_load_anim切页动画。
进入载入、离开释放:主循环 100ms 跟踪页面切换,离开收音机页就停台断流 (释放音频管道和网络连接,后台不留任何活动任务),进入时自动续播上次的台。 这条规则让每个应用的资源生命周期一目了然——你在哪个页面,就只有哪个 应用在消耗 CPU/带宽。
3.3 一条音频管道,三种音源
网络收音机(HTTP 流) ┐ 本地 MP3(SD 卡) ├→ minimp3 解码 → 线性插值重采样到 48.118kHz AVI 音轨(MP3/PCM) ┘ │ 频谱分析(Goertzel 8 频带)│ ▼ 块队列(8 块 × 48ms,PSRAM) ▼ TX 线程 → GPDMA 循环 → I2S6 → WM8904
重采样统一格式:任何音源、任何采样率/声道数,先重采样到固定的 48.118kHz 立体声,下游完全不用关心来源。
反压即真实时间:队列满时 audio_write_pcm 阻塞,生产者自然贴着 播放速率走——不需要任何定时器去"算"音频节奏。视频音画同步也建立在 这个性质上(音频做主时钟,视频帧按 PTS 追它)。
频谱是免费副产品:PCM 流经时顺手做 Goertzel 8 频带能量分析, 所有页面的频谱柱共用这份数据。
3.4 线程模型与优先级
| 音频 TX | 5(最高) | 块队列 → DMA,欠载时填静音 |
| 收音机解码 / MP3 解码 | 6 | 流解帧 + minimp3 + 重采样 |
| 视频 reader | 7 | 读 AVI、喂音频(主时钟路径) |
| 视频 decode | 9 | JPEG 解码、PTS 对时送显 |
| UI 主循环 | 10(最低) | LVGL 渲染、页面管理 |
原则就一条:音频链条上的线程永远比 UI 优先级高。800×480 软件渲染 动辄几十毫秒,如果 UI 能抢占解码线程,声音必断。
3.5 内存布局
| SRAM(91% 已用) | 456KB | 内核/栈、LVGL 堆 128KB、ETH DMA 区(nocache)、tjpgd 工作区 16KB |
| PSRAM | 32MB | 视频帧双缓冲、JPEG 帧队列、音频块、LTDC 显存 |
| 外部 Flash(XiP) | 16MB | MCUboot slot + 应用镜像 ~820KB |
四、主要功能
4.1 启动器:Zephyr 影音系统
深蓝灰主题,白色 "Zephyr" 48 号大字 + 青色"影音系统",四个应用图标 均布一行,tileview 左右滑动翻页,第二页留给以后的新应用。 顶栏常驻:品牌名、NTP 对时的实时时钟、网络状态圆点和 IP。 (题图即启动器页面)
4.2 网络收音机
10+ 个国内台(中国之声、经济之声、各地交通/音乐台),中文台名, 点台即播,换台自动重连;掉线 3 秒自动重连。
播放/暂停、停止、音量滑杆、1kHz 自测音、8 频带实时频谱柱。
离开页面自动释放(停台断流),回来自动续播——后台绝不偷跑流量和 CPU。

4.3 本地 MP3 播放
SD 卡(FATFS)根目录扫描建库,长文件名原样显示,最多 32 首。
播放/暂停(断点续播)、上一首/下一首、播完自动回卷、进度条。
热插拔:拔卡自动停播卸载,插卡自动挂载重扫,列表实时重建。
4.4 AVI 视频播放
支持 .vid 裸 RGB565 帧流和 .avi(MJPEG 视频 + PCM/MP3 音轨), ffmpeg 一行命令转码,拷卡即播。
纯软件解码:tjpgd 解 JPEG(约 15ms/帧),minimp3 解音轨, 400×300@15fps 流畅有声。
音画同步:音频主时钟 + 视频 PTS 追帧,拥塞时丢帧保音频, 声音全程连续。
自研 RIFF 容器解析约 300 行,无需任何外部库。

4.5 天气预报
Open-Meteo API(免 key)按坐标取逐小时预报(广西资源县), 48 小时逐小时列表:时间、WMO 天气码手绘图标(晴/多云/阴/雨/雪/雷暴)、 温度(0.1°C)、中文天气描述。
NTP 对时后从"当前小时"开始显示并高亮标记,07:00 行单独配色。
时间、联网状态、图标全部自绘,无外部图片资源。

4.6 附赠:GPU 渲染对比演示(独立工程)
另外做了一个渲染器对比 demo:同一 LVGL 场景分别用 CPU 软渲染 / DMA2D / NeoChrom GPU 绘制,按键循环切换,日志实时输出 FPS / 单帧渲染耗时 / CPU 占用率,并支持大画布放 PSRAM 做压力测试。 直观回答了"这颗 2.5D GPU 到底快多少"。

五、编程心得
1. 先把子系统用足,再自己造轮子。这套系统里我们自己写的"底层"只有两处:AVI 容器解析(300 行,因为没有 现成嵌入式库)和一个 GPDMA 循环 I2S 发送层(因为通用逐块重载驱动不合适)。 其余全部站在 Zephyr 子系统上。自己写的越少,出问题的面越小。
2. 并发设计让"反压"和"节奏"替你工作。音频不卡顿靠的不是调参,而是结构:队列满则生产者阻塞(节奏自动对齐)、 时间关键的线程优先级高于 UI、视频拥塞时丢帧而不是阻塞。把时序正确性 建立在结构上,比建立在"每次都来得及"的侥幸上可靠得多。
3. 内存规划要先行。H7RS 这种"小 SRAM + 大 PSRAM"的结构,动手之前先画内存地图:谁是性能 热点(解码工作区、LVGL 堆)留 SRAM,谁是大块头(帧缓冲、音频块、显存) 去 PSRAM。到 91% 占用才发现没规划,返工成本很高。
4. 用数据调试,不用想象调试。几乎每个关键决策都来自计数器:SD 读速基准(2MB/s)、每帧解码耗时、 音频输入/输出样本比(1.0909 = 48118/44100,证明播放速率分毫不差)、 欠载计数(silence 增量)。嵌入式调试器(GDB + openocd)不只能看崩溃, 更是最好的"示波器"——XiP 系统上记住只用变量读写和寄存器查看, 不做函数调用。
5. UI 只是众多任务之一。LVGL 很能吃 CPU(800×480 软渲染),把它当成最低优先级线程、给实时 任务让路,触摸输入队列按事件速率配深度,界面就既流畅又不影响音视频。
6. 状态机胜过标志位。每个播放器(收音机/MP3/视频)都是"NO_CARD / EMPTY / IDLE / PLAYING / PAUSED / ERROR"一张小状态表,UI 刷新只读状态字符串。页面进入/离开 时的载入释放,也只是在状态机外面包了一层。状态清晰,bug 就少一半。
六、源码结构
app/net_radio/ # "悦聆"影音系统主工程 ├── prj.conf # Kconfig:网络/LVGL/音频/SD/输入,全部显式声明 ├── boards/…ext_flash_app.overlay # sdmmc1/ETH nocache/PLL2 等板级补丁 ├── src/ │ ├── main.c # 启动器 + 各应用页 + 页面生命周期管理 │ ├── net_radio.c/net_stream.c # 网络收音机(socket 流 → minimp3) │ ├── mp3_player.c # SD 卡 MP3(挂载/扫描/热插拔/解码线程) │ ├── video_player.c # 视频扫描 + PSRAM 双缓冲 + reader 线程 │ ├── avi_player.c # AVI 容器解析 + 音画同步 + decode 线程 │ ├── audio_sai.c # 统一音频管道(块队列 → GPDMA → I2S6/WM8904) │ ├── pcm_util.c # 重采样 + Goertzel 频谱 │ ├── weather.c/time_sync.c # Open-Meteo 天气 + SNTP 对时 │ ├── tjpgd/ # JPEG 解码器(改 RGB565 直出) │ └── fonts/lv_font_cjk_24.c # 中文字库(子集化生成) └── sd_files/ # 示例片源(demo.vid / *.avi)
整套系统从零到四个应用可用,核心代码约 5000 行,其中八成在描述 "数据怎么流",两成在描述"界面长什么样"——这大概就是用 Zephyr 做 多媒体产品最舒服的比例。
七、开源
我已经将代码开源到了gitee:lugl/stm32h7s78-zephyr

我要赚赏金
