LVGL 图片系统概览:从对象到解码器
做嵌入式 UI 这么多年,我接触过不少图形框架。但 LVGL 的图片系统,说实话,设计得相当巧妙。它把「显示一张图」这件事,拆成了三个清晰的层次:图片对象、解码流程、解码器。你只要搞懂这三层,后面做外部存储加载,心里就有底了。
2.1 图片对象 (lv_img):不只是个容器
在 LVGL 里,lv_img 本质上是一个控件。它负责在屏幕上占个位置,然后把解码后的像素数据渲染出来。但很多人容易忽略一点——它本身不存像素数据。
核心概念:lv_img 对象只持有指向图片数据的「指针」或「描述符」。真正的像素数据,要么在内部 RAM 里,要么在外部 Flash/存储介质上。
我刚开始用 LVGL 时,犯过一个低级错误:以为创建了 lv_img 对象,图片就自动加载到内存了。其实不是。你只是搭了个「画框」,画的内容还得靠解码器去填。
创建一张图片显示,通常就这几步:
/* 1. 创建图片对象 */
lv_obj_t * img = lv_img_create(lv_scr_act());
/* 2. 设置图片源(可以是文件路径、数组、符号) */
lv_img_set_src(img, "S:/images/logo.bin");
/* 3. 让图片居中 */
lv_obj_center(img);
你看,代码很简单。但第二步 lv_img_set_src 背后,触发了一整套解码流程。这就是我们接下来要聊的。
2.2 图片解码流程:一张图是怎么上屏的?
一张图片从存储介质到屏幕,中间经历了什么?我画了张流程图,你可以对照着看。
这个流程里,最耗时的其实是第三步和第四步。特别是从外部存储(比如 SD 卡、SPI Flash)读取数据时,I/O 速度会成为瓶颈。我曾在某个项目里,用 SPI Flash 加载一张 800x480 的 JPEG 图,解码花了将近 2 秒。后来换成自定义解码器,才把时间压到 300ms 以内。
2.3 内置解码器:开箱即用
LVGL 自带了几种解码器,覆盖了最常见的图片格式。我用表格给你列一下:
| 解码器 | 支持格式 | 内存占用 | 解码速度 | 适用场景 |
|---|---|---|---|---|
| LV_IMG_CF_RAW | 原始像素数据(RGB565、ARGB8888) | 低(直接渲染) | 极快 | 小图标、启动画面 |
| LV_IMG_CF_INDEXED | 索引色图片(1/2/4/8 bit) | 极低 | 快 | 低端 MCU、小尺寸 UI |
| LV_IMG_CF_JPEG | JPEG(需要开启 LV_USE_JPEG) | 高(解压后全尺寸) | 慢 | 照片、复杂背景 |
| LV_IMG_CF_PNG | PNG(需要开启 LV_USE_PNG) | 高 | 中等 | 需要透明通道的图片 |
| LV_IMG_CF_BMP | BMP | 高 | 中等 | 兼容旧格式 |
我的建议:如果你的 MCU 资源紧张(比如 RAM 只有 256KB),尽量用 RAW 格式或索引色格式。JPEG 和 PNG 解码需要额外的内存池,很容易把堆撑爆。
内置解码器的好处是「零配置」。你只要在 lv_conf.h 里打开对应的宏,LVGL 就会自动注册这些解码器。比如:
/* lv_conf.h */
#define LV_USE_JPEG 1
#define LV_USE_PNG 1
#define LV_USE_BMP 1
然后你直接传文件路径,LVGL 会根据文件头(比如 JPEG 的 0xFF 0xD8)自动匹配解码器。方便是方便,但灵活性不够。比如你想从加密的 Flash 里读图片,或者想用硬件 JPEG 加速器,内置解码器就无能为力了。
2.4 自定义解码器:掌控一切
什么时候需要自己写解码器?我遇到过三种情况:
- 特殊存储介质:图片数据不是普通文件,而是通过自定义协议从云端或加密芯片读取。
- 硬件加速:MCU 自带 JPEG 硬件解码器,不用就浪费了。
- 性能优化:内置解码器太慢,你想用更高效的算法(比如只解码缩略图)。
自定义解码器的核心,是实现一个 lv_img_decoder_t 结构体。我直接给你看一个简化版的模板:
/* 自定义解码器结构体 */
lv_img_decoder_t my_decoder;
/* 1. 注册解码器 */
void my_decoder_init(void) {
lv_img_decoder_init();
lv_img_decoder_set_info_cb(&my_decoder, my_decoder_info);
lv_img_decoder_set_open_cb(&my_decoder, my_decoder_open);
lv_img_decoder_set_read_line_cb(&my_decoder, my_decoder_read_line);
lv_img_decoder_set_close_cb(&my_decoder, my_decoder_close);
lv_img_decoder_register(&my_decoder);
}
/* 2. 信息回调:告诉 LVGL 这张图的基本信息 */
lv_res_t my_decoder_info(lv_img_decoder_t *decoder, const void *src, lv_img_header_t *header) {
/* 解析文件头,填充 width、height、format */
header->w = 800;
header->h = 480;
header->cf = LV_IMG_CF_RAW; /* 输出 RAW 格式 */
return LV_RES_OK;
}
/* 3. 打开回调:准备解码资源 */
lv_res_t my_decoder_open(lv_img_decoder_t *decoder, lv_img_decoder_dsc_t *dsc) {
/* 分配解码缓冲区、初始化硬件等 */
return LV_RES_OK;
}
/* 4. 读取行回调:逐行输出像素 */
lv_res_t my_decoder_read_line(lv_img_decoder_t *decoder, lv_img_decoder_dsc_t *dsc,
lv_coord_t x, lv_coord_t y, lv_coord_t len, uint8_t *buf) {
/* 从你的存储介质读取一行像素,写入 buf */
return LV_RES_OK;
}
/* 5. 关闭回调:释放资源 */
void my_decoder_close(lv_img_decoder_t *decoder, lv_img_decoder_dsc_t *dsc) {
/* 释放缓冲区、关闭文件句柄等 */
}
注意:自定义解码器的 read_line 回调是 LVGL 渲染引擎逐行调用的。如果你的图片很大,不要一次性解码全部像素到内存,否则 RAM 会爆。逐行解码才是正道。
我记得有一次,客户要求从外部 SPI NOR Flash 加载图片,但 Flash 里存的是压缩过的自定义格式。内置解码器根本不认识。我写了个自定义解码器,在 open 回调里解压头部信息,在 read_line 里按需解压每一行。最终效果很好,RAM 占用只多了 2KB。
2.5 解码器优先级与匹配规则
LVGL 内部维护了一个解码器链表。当你调用 lv_img_set_src 时,它会遍历所有注册的解码器,调用每个解码器的 info_cb 来判断是否能处理这张图。谁先返回 LV_RES_OK,谁就负责解码。
优先级规则很简单:
- 后注册的解码器优先级更高(插在链表头部)。
- 如果所有解码器都返回
LV_RES_INV,LVGL 会报错。
所以,如果你想让自定义解码器优先于内置解码器,记得在 lv_init() 之后、lv_img_set_src 之前注册它。
实战经验:我习惯在自定义解码器的 info_cb 里先检查文件魔数(Magic Number)。比如我的自定义格式文件头是 0x4D 0x59("MY"),只有匹配了才返回 OK。这样就不会误抢其他格式的图片。
2.6 小结:图片系统的设计哲学
LVGL 的图片系统,说白了就是「分层解耦」。lv_img 只管显示,解码器只管解码,中间通过标准接口通信。这种设计让你可以轻松替换任意一层——想换存储介质?写个新解码器就行。想优化解码速度?改解码器内部实现就行。上层代码完全不用动。
嗯,这一章的内容就到这里。你只要记住:lv_img 是壳,解码流程是管道,解码器是引擎。后面讲双缓冲和刷新率调优时,你会反复用到这些概念。