2. LVGL 图像架构:缓存、解码器与文件系统
好,咱们进入正题。LVGL 显示一张图片,背后其实有一套完整的流水线。我刚开始接触 LVGL 时,以为就是调个 lv_img_set_src() 完事。后来发现,图片加载慢、内存爆、花屏……各种问题全来了。
说白了,你得理解 LVGL 是怎么“吃”进一张图片的。它分三层:图像缓存机制、解码器框架、文件系统接口。这三层配合不好,你的 UI 就卡成 PPT。
2.1 图像缓存机制:LVGL 怎么存图?
LVGL 的图片缓存,我习惯叫它“三级缓存”。不是 CPU 那种 L1/L2/L3,而是针对不同场景设计的三种存储方式。
| 缓存类型 | 存储位置 | 特点 | 典型场景 |
|---|---|---|---|
| 内部 RAM 缓存 | MCU 内部 SRAM | 速度快,容量小(几十 KB ~ 几百 KB) | 小图标、按钮背景 |
| 外部 RAM 缓存 | SDRAM / PSRAM | 速度中等,容量大(几 MB ~ 几十 MB) | 全屏背景图、大尺寸图片 |
| Flash 缓存 | 内部 Flash / 外部 SPI Flash | 速度慢,容量大,掉电不丢失 | 开机 Logo、固定图标 |
嗯,这里要注意:LVGL 默认会把解码后的图片数据放在 内部 RAM。如果你加载一张 800×480 的 32 位图,光像素数据就 1.5MB。很多 MCU 内部 RAM 总共才 512KB,直接爆掉。
LVGL 的缓存分配策略,核心是 lv_img_cache.h 里的几个宏:
// 内部 RAM 最大缓存图片数量
#define LV_IMG_CACHE_DEF_SIZE 1
// 是否启用外部 RAM 缓存
#define LV_IMG_CACHE_DEF_EXT_RAM 0
// 单张图片最大缓存大小(字节)
#define LV_IMG_CACHE_MAX_SIZE (32 * 1024)
我个人习惯把 LV_IMG_CACHE_DEF_SIZE 设成 2~3,这样 UI 切换时能缓存最近几张图,避免反复解码。但别设太大,内部 RAM 扛不住。
2.2 解码器框架:LVGL 怎么“看懂”图片?
LVGL 支持多种图片格式:BMP、PNG、JPEG、GIF,还有它自己的 LVGL 原生格式(.bin 或 .c 数组)。每种格式对应一个解码器。
解码器的注册机制,说白了就是一个链表。每个解码器是一个 lv_img_decoder_t 结构体:
typedef struct _lv_img_decoder_t {
lv_img_decoder_info_cb_t info_cb;
lv_img_decoder_open_cb_t open_cb;
lv_img_decoder_read_cb_t read_cb;
lv_img_decoder_close_cb_t close_cb;
struct _lv_img_decoder_t * next;
} lv_img_decoder_t;
你想想看,LVGL 加载图片时,会遍历这个链表。先调用 info_cb 判断文件头是否匹配,匹配了就调用 open_cb 开始解码。解码过程中,read_cb 负责逐行读取像素数据。
我遇到过一个问题:同时注册了 PNG 和 JPEG 解码器,但 PNG 解码器总是先被调用,导致 JPEG 图片也被当成 PNG 去解析,结果花屏。后来发现是解码器注册顺序的问题。LVGL 按注册顺序遍历,先注册的先匹配。
解码器的核心流程,我画了张图:
2.3 文件系统接口:LVGL 怎么“读”文件?
LVGL 本身不直接操作文件系统。它通过一个抽象层——lv_fs 模块——来对接各种文件系统。你想想看,MCU 上常见的文件系统有 FatFS、LittleFS、SPIFFS,甚至 Linux 上的 VFS。LVGL 不能为每种都写一套代码,所以它定义了一套回调接口。
核心结构体是 lv_fs_drv_t:
typedef struct _lv_fs_drv_t {
char letter; // 驱动器盘符,如 'C', 'D', 'S'
uint16_t cache_size; // 文件缓存大小
lv_fs_open_cb_t open_cb;
lv_fs_close_cb_t close_cb;
lv_fs_read_cb_t read_cb;
lv_fs_write_cb_t write_cb;
lv_fs_seek_cb_t seek_cb;
lv_fs_tell_cb_t tell_cb;
lv_fs_dir_open_cb_t dir_open_cb;
lv_fs_dir_read_cb_t dir_read_cb;
lv_fs_dir_close_cb_t dir_close_cb;
} lv_fs_drv_t;
每个文件系统驱动注册时,需要指定一个 盘符。比如 FatFS 注册为 'F',那么 LVGL 中访问文件路径就是 "F:/images/logo.bmp"。这个设计很巧妙,不同存储介质可以共用一套文件访问代码。
实际项目中的配置:
- 内部 Flash:注册为
'I',只读,用于存放固定图标 - SD 卡(FatFS):注册为
'S',读写,用于存放用户图片 - SPI Flash(LittleFS):注册为
'L',读写,用于存放缓存文件
我做过一个项目,需要从 SD 卡加载图片,同时从内部 Flash 加载字体。当时注册了两个驱动:
// 注册 SD 卡驱动
lv_fs_drv_t sd_drv;
lv_fs_drv_init(&sd_drv);
sd_drv.letter = 'S';
sd_drv.open_cb = sd_open_cb;
sd_drv.close_cb = sd_close_cb;
sd_drv.read_cb = sd_read_cb;
sd_drv.seek_cb = sd_seek_cb;
sd_drv.tell_cb = sd_tell_cb;
lv_fs_drv_register(&sd_drv);
// 注册 Flash 驱动
lv_fs_drv_t flash_drv;
lv_fs_drv_init(&flash_drv);
flash_drv.letter = 'I';
flash_drv.open_cb = flash_open_cb;
flash_drv.close_cb = flash_close_cb;
flash_drv.read_cb = flash_read_cb;
flash_drv.seek_cb = flash_seek_cb;
flash_drv.tell_cb = flash_tell_cb;
lv_fs_drv_register(&flash_drv);
然后 LVGL 加载图片时,路径写 "S:/images/bg.bmp" 就会自动走 SD 卡驱动,写 "I:/logo.bin" 就走 Flash 驱动。嗯,这个设计真的很方便。
cache_size 字段容易被忽略。它决定了 LVGL 在读取文件时,一次缓存多少字节。设太小(比如 128 字节),读取大图片时会频繁调用 read_cb,性能很差。我一般设成 4096 字节,跟 SD 卡的扇区大小对齐。
2.4 三者如何配合?
咱们把这三层串起来看:
- 应用层 调用
lv_img_set_src(img, "S:/bg.bmp") - 文件系统接口 根据盘符
'S'找到对应驱动,打开文件 - 解码器框架 遍历解码器链表,找到能处理 BMP 格式的解码器
- 解码器 通过文件系统接口读取文件头,确认格式,开始解码
- 缓存机制 分配内存,存放解码后的像素数据
- 显示驱动 从缓存中读取像素,刷新到屏幕
这个流程中,任何一个环节出问题,图片都显示不出来。我遇到过最典型的问题:文件系统接口的 read_cb 返回数据太慢,解码器等超时,直接报错。后来在 lv_conf.h 里把 LV_IMG_DECODER_TIMEOUT 从默认的 100ms 改成了 500ms,问题解决。
lv_fs_open() 和 lv_fs_read() 读取文件前几个字节,看返回值。文件系统通了,再查解码器。