本文记录在 STM32H7S78-DK(STM32H7S7L8,Cortex-M7 @600MHz,800×480 屏)上用 Zephyr 4.5 + LVGL 9.6 实现视频播放的方案:把一段 MP4 截取转码成裸帧序列,链入应用镜像存进外部 Octo-SPI Flash,运行时逐帧送 LVGL 显示。环境沿用系列第一篇《STM32H7S78-DK 上跑通 Zephyr:从 Blinky 到外部 Flash 启动》。
1. 路线选择:为什么是 RAW 裸帧
H7S7 带 JPEG 硬件编解码器,理论上最优方案是 MJPEG 硬解。但查证发现 Zephyr 的 video_stm32_jpeg 驱动当前只支持编码(源码注释明确标注 decode 待支持),硬解路线需要自己用 STM32 HAL 写 JPEG 解码 + DMA2D 色彩转换,工作量不小。
因此选择 RAW RGB565 裸帧路线:无解码负担、行为完全可控,视频数据放外部 Flash 内存映射区(0x70000000),不占片内 SRAM,CPU 直接读。这也是许多 MCU 端视频 demo 的通用做法。
2. 视频转码
源视频 1920×1280 H.264 30fps,截取前 4 秒,缩放到 400×300、12fps,转 RGB565 裸帧:
ffmpeg -y -i input.mp4 -t 4 -vf "scale=400:300,fps=12" -pix_fmt rgb565le -f rawvideo frames.bin
产物 11.5MB = 400×300×2 字节 × 48 帧。容量估算:60MB 的 MCUboot slot 能放约 20 秒 400×300@12fps,降分辨率/帧率可到分钟级。
3. 工程解析(app/video_player)
CMakeLists.txt —— 关键点是用 -I 给 .incbin 指定二进制文件的搜索路径:
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(video_player)
set_source_files_properties(src/video.S PROPERTIES
COMPILE_OPTIONS "-I${CMAKE_CURRENT_SOURCE_DIR}/video")
target_sources(app PRIVATE src/main.c src/video.S)src/video.S —— 用 .incbin 把二进制帧数据直接链入 rodata,避免生成 11MB 的 C 数组拖慢编译:
.section .rodata .align 8 .global video_frames_start .global video_frames_end video_frames_start: .incbin "frames.bin" video_frames_end:
由于应用是 ext_flash_app 变体,这段 rodata 落在外部 Octo-SPI Flash 的内存映射区,运行时通过 video_frames_start 指针直接访问。
src/main.c —— LVGL 图像控件逐帧换数据指针。注意一个细节:dsc 指针没变,LVGL 不会自动重绘,需要手动 lv_obj_invalidate:
#include <zephyr/kernel.h>
#include <zephyr/drivers/display.h>
#include <zephyr/logging/log.h>
#include <lvgl.h>
#include <lvgl_zephyr.h>
LOG_MODULE_REGISTER(app, LOG_LEVEL_INF);
/* 视频参数:与 ffmpeg 转码命令对应(400x300 RGB565 @12fps,4 秒 48 帧) */
#define VIDEO_W 400
#define VIDEO_H 300
#define VIDEO_FPS 12
#define FRAME_SIZE (VIDEO_W * VIDEO_H * 2)
#define FRAME_COUNT 48
/* 帧数据由 video.S 通过 .incbin 链入 rodata,位于外部 Octo-SPI Flash(内存映射) */
extern const uint8_t video_frames_start[];
extern const uint8_t video_frames_end[];
static lv_image_dsc_t video_dsc;
int main(void)
{
const struct device *display_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_display));
if (!device_is_ready(display_dev)) {
LOG_ERR("display device not ready");
return 0;
}
lvgl_lock();
lv_obj_set_style_bg_color(lv_screen_active(), lv_color_black(), 0);
lv_obj_t *title = lv_label_create(lv_screen_active());
lv_label_set_text(title, "Video 400x300@12fps from OSPI flash");
lv_obj_set_style_text_font(title, &lv_font_montserrat_24, 0);
lv_obj_set_style_text_color(title, lv_color_white(), 0);
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 30);
lv_obj_t *img = lv_image_create(lv_screen_active());
lv_obj_align(img, LV_ALIGN_CENTER, 0, 30);
video_dsc.header.cf = LV_COLOR_FORMAT_RGB565;
video_dsc.header.w = VIDEO_W;
video_dsc.header.h = VIDEO_H;
video_dsc.header.stride = VIDEO_W * 2;
video_dsc.data_size = FRAME_SIZE;
video_dsc.data = video_frames_start;
lv_image_set_src(img, &video_dsc);
lv_timer_handler();
lvgl_unlock();
display_blanking_off(display_dev);
LOG_INF("video player started, frames=%u size=%u", FRAME_COUNT, FRAME_SIZE);
uint32_t frame = 0;
int64_t next_tick = k_uptime_get();
while (1) {
next_tick += 1000 / VIDEO_FPS;
int64_t now = k_uptime_get();
int32_t wait_ms = (int32_t)(next_tick - now);
if (wait_ms > 0) {
k_msleep(wait_ms);
}
video_dsc.data = video_frames_start + frame * FRAME_SIZE;
lvgl_lock();
/* dsc 指针未变,需要手动失效触发重绘 */
lv_obj_invalidate(img);
lv_timer_handler();
lvgl_unlock();
frame = (frame + 1) % FRAME_COUNT;
}
return 0;
}prj.conf 只需基础显示配置(帧源 RGB565,屏 RGB888,LVGL 渲染时自动转格式):
CONFIG_MAIN_STACK_SIZE=8192 CONFIG_LOG=y CONFIG_DISPLAY=y CONFIG_LVGL=y CONFIG_LV_Z_MEM_POOL_SIZE=98304 CONFIG_LV_COLOR_DEPTH_24=y CONFIG_LV_FONT_MONTSERRAT_24=y
4. 编译与烧录
cd /d D:\luglZephyrproject set ZEPHYR_SDK_INSTALL_DIR=D:\luglZephyrproject\zephyr-sdk-1.0.1\zephyr-sdk-1.0.1 set ZEPHYR_TOOLCHAIN_VARIANT=zephyr west build -b stm32h7s78_dk/stm32h7s7xx/ext_flash_app --sysbuild -s D:\STM32\stm32H7S78-dk\app\video_player -d D:\STM32\stm32H7S78-dk\app\video_player\build west flash -d D:\STM32\stm32H7S78-dk\app\video_player\build --domain video_player
实测结果:
应用镜像 11.8MB(代码 0.3MB + 视频 11.5MB),占 60MB slot 的 18.8%
烧录 11.8MB 约 68 秒(外部 Flash 加载器写入速度 ~170KB/s)
屏幕循环播放 400×300@12fps 视频,串口输出 video player started, frames=48
5. 延伸方向
更长视频:降分辨率/帧率换时长,或把视频放 SD 卡走文件系统读取(板上 SDMMC 已在 DTS 使能)
真解码:绕过 Zephyr 驱动,直接用 STM32 HAL 的 JPEG 硬解 + DMA2D 做 YCbCr→RGB 转换,可到 25~30fps 全屏
性能叠加:结合第三篇的 NeoChrom 加速,RGB565→RGB888 的转换和缩放都可以卸载给 GPU2D
我要赚赏金
