LVGL 存储架构:从内部到外部
做嵌入式 UI,说白了就是跟存储资源较劲。LVGL 本身不生产像素,它只是像素的搬运工。但搬运工也得有仓库,这个仓库就是存储系统。今天咱们就聊聊 LVGL 的存储架构,看看它内部怎么折腾数据,又为什么非得把外部存储请进来。
LVGL 内部存储机制
LVGL 内部维护了一套自己的内存管理。它不像有些框架直接 malloc/free 裸奔,而是搞了个内存池。你初始化时给它划一块地,它自己在这块地里分配、回收。
核心要点:LVGL 内部存储主要分三块——动态内存、显示缓冲区、以及字体/图片的缓存区。
我刚开始用 LVGL 时犯过一个低级错误:没算好内存池大小,结果界面一跑起来就卡死。后来养成习惯,先估算再配置。
// 典型的 LVGL 内存配置
#define MY_DISP_HOR_RES 480
#define MY_DISP_VER_RES 320
#define MY_DISP_BUF_SIZE (MY_DISP_HOR_RES * MY_DISP_VER_RES / 10)
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf[MY_DISP_BUF_SIZE];
lv_disp_draw_buf_init(&draw_buf, buf, NULL, MY_DISP_BUF_SIZE);
这里有个坑:显示缓冲区大小不是随便填的。太小了刷新跟不上,太大了内存扛不住。我个人习惯是取屏幕分辨率的 1/10 到 1/6,具体看你的刷新率要求。
RAM 与 Flash 的局限
咱们做嵌入式的都清楚,MCU 上的 RAM 和 Flash 金贵得很。一个 480×320 的 16 位色图片,裸数据就要 300KB。你想想看,很多 MCU 总共才 128KB RAM,一张图就撑爆了。
| 存储介质 | 典型容量 | 访问速度 | 适合存什么 |
|---|---|---|---|
| MCU 内部 RAM | 32KB - 512KB | 纳秒级 | 运行时数据、缓冲区 |
| MCU 内部 Flash | 128KB - 2MB | 几十纳秒 | 代码、小图标、字体 |
| 外部 SPI Flash | 2MB - 64MB | 微秒级 | 大图片、字库、音频 |
| 外部 SDRAM | 8MB - 64MB | 几十纳秒 | 帧缓冲、大缓存 |
我在一个项目里遇到过这种情况:产品要显示 50 张 320×240 的 JPEG 图片,内部 Flash 总共才 1MB,代码和系统一占,剩下不到 300KB。硬塞进去?不可能。这就是典型的 Flash 局限——容量不够,而且写一次还麻烦。
避坑指南:我曾经把图片直接编译进固件,结果每次改图都要重新烧录整个固件。生产线上的人差点没把我骂死。后来学乖了,图片全放外部存储,固件只负责加载。
RAM 的局限更直接。LVGL 的显示缓冲区、控件对象、动画帧,全都要吃 RAM。你开两个全屏缓冲区试试?128KB 的 RAM 直接见底。所以很多项目不得不做单缓冲,牺牲流畅度换内存。
外部存储的引入
既然内部存不下,那就往外搬。外部存储说白了就是给 MCU 接个「外挂硬盘」——SPI Flash、SD 卡、甚至 NAND Flash。LVGL 从 7.0 开始就支持文件系统接口,你只需要实现几个回调函数。
小技巧:LVGL 的文件系统接口是抽象层,底层可以是 FatFS、LittleFS 或者你自己写的驱动。我个人偏爱 LittleFS,掉电安全做得好,适合嵌入式场景。
引入外部存储后,图片加载流程就变了:
- LVGL 需要显示图片时,通过文件系统接口去外部存储读数据
- 数据先读到 RAM 缓冲区
- 解码器(如 PNG、JPEG 解码库)把数据解成像素
- 像素送入显示缓冲区
这个流程看着简单,实际坑不少。最典型的就是速度问题——从 SPI Flash 读数据,速度再快也就几十兆比特每秒,跟内部 Flash 差了一个数量级。
// LVGL 文件系统回调示例
static void * fs_open(lv_fs_drv_t * drv, const char * path, lv_fs_mode_t mode) {
// 打开外部存储上的文件
return my_fs_open(path, mode);
}
static lv_fs_res_t fs_read(lv_fs_drv_t * drv, void * file_p,
void * buf, uint32_t btr, uint32_t * br) {
// 从外部存储读取数据
return my_fs_read(file_p, buf, btr, br);
}
// 注册文件系统驱动
lv_fs_drv_t fs_drv;
lv_fs_drv_init(&fs_drv);
fs_drv.letter = 'S'; // 挂载点为 "S:"
fs_drv.open_cb = fs_open;
fs_drv.read_cb = fs_read;
// ... 其他回调
lv_fs_drv_register(&fs_drv);
注册完之后,LVGL 里引用图片就可以写成 "S:/images/logo.bin"。嗯,这里要注意:路径格式和底层文件系统要匹配,别搞混了大小写。
外部存储带来的新问题
外部存储解决了容量问题,但引入了延迟问题。我测过一组数据:
- 从内部 Flash 读一张 100KB 的图片:约 0.5ms
- 从 SPI Flash 读同样图片:约 5-10ms
- 从 SD 卡读(FAT32 文件系统):约 15-30ms
这还只是读数据的时间。加上解码、DMA 传输、缓冲区切换,一张图从请求到显示,轻松上百毫秒。用户滑动界面时,图片加载跟不上,就会出现「白块」或者「撕裂」。
核心矛盾:外部存储容量大但速度慢,内部存储速度快但容量小。LVGL 的图片加载优化,本质上就是在两者之间做平衡。
怎么平衡?我总结了几条经验:
- 小图标、常用图标:编译进内部 Flash,零延迟加载
- 大图片、背景图:放外部存储,用双缓冲 + DMA 异步加载
- 中间尺寸的图片:可以预加载到 RAM 缓存,用空间换时间
你想想看,如果能把「从外部读数据」和「CPU 干别的活」重叠起来,延迟不就藏住了吗?这就是后面章节要讲的双缓冲和 DMA2D 优化的核心思路。
这张图把 LVGL 的图片加载路径画清楚了。你可以看到,无论内部还是外部,最终都要汇入显示缓冲区。区别在于:内部路径短、延迟低;外部路径长、延迟高。后面的课程,咱们就专门解决这个「延迟高」的问题。