2. LVGL 缓存架构总览:L1 / L2 / L3 三级缓存设计思想

大家好,我是你们的老朋友。上一章我们聊了为什么要给 LVGL 做图像缓存,说白了就是「别让屏幕等你」。这一章我们深入骨架——LVGL 的缓存架构到底长什么样?

我个人习惯把 LVGL 的缓存体系想象成一个「三级弹药库」。最靠近 CPU 的缓存最快但最小,离 CPU 越远的缓存越大但越慢。你想想看,这和计算机体系结构里的 L1/L2/L3 cache 是不是一个道理?LVGL 的设计者显然借鉴了这个思想。

2.1 整体设计思想:分层解耦,按需流动

LVGL 的图像缓存不是一个大池子,而是分了三层。每一层都有自己的职责和生命周期。数据从底层存储(比如 Flash 或 SD 卡)一路向上,最终到达屏幕像素。

为什么要分层?我在项目中遇到过一个问题:如果只用一个大缓存,一旦缓存被刷掉,所有图像都得重新解码。那画面卡顿得让人崩溃。分层之后,每一层只负责自己那一亩三分地,上层缓存失效了,下层还能顶一顶。

核心设计原则:

  • 就近访问:越靠近渲染管线的缓存,访问延迟越低
  • 容量递增:L1 最小,L3 最大,平衡速度和内存占用
  • 逐级回退:上层 miss 了,自动向下层查找,直到原始数据源
  • 透明替换:开发者不需要手动管理缓存替换,LVGL 内部自动处理

下面这张图是我手绘的三级缓存架构,你可以把它当作本章的「地图」。

LVGL 三级缓存架构总览 L1 缓存 解码后的像素数据 (ARGB8888/RGB565) 大小:几 KB ~ 几十 KB L2 缓存 解码后的图像数据 (完整图像帧) 大小:几百 KB ~ 几 MB L3 缓存 原始编码图像数据 (PNG/JPEG/BMP 等) 大小:几 MB ~ 几十 MB miss miss 原始数据源 Flash / SD / 文件系统 miss → 从文件加载 LVGL 渲染管线 draw_ctx → 刷入帧缓冲 hit → 直接使用 数据流动方向:原始数据源 → L3 → L2 → L1 → 渲染管线,逐级命中,逐级回退

2.2 L1 缓存:最前线的像素快递员

L1 缓存是离渲染管线最近的一层。它缓存的是已经解码好的像素数据,也就是 ARGB8888 或 RGB565 格式的原始像素。

你想想看,如果每次画一个图标都要重新解码 PNG,那 CPU 得累死。L1 缓存就是为了解决这个问题——它把最近用过的图像小块(比如 32x32 的图标)直接以像素形式存着,渲染时直接 memcpy 就行。

我的经验:L1 缓存的大小通常由 LV_IMG_CACHE_DEF_SIZE 宏控制,默认是 0(不启用)。我建议至少设成 8~16 个条目,对图标密集型的 UI 效果立竿见影。

L1 缓存的替换策略是 LRU(最近最少使用)。当一个图像块被渲染后,它会被放到 L1 的头部;如果 L1 满了,尾部的条目就会被踢出去。嗯,这里要注意:L1 缓存只缓存图像的一部分,不是整张图。比如一张 800x480 的背景图,L1 可能只缓存当前屏幕可见的那一块。

2.3 L2 缓存:完整的图像帧仓库

L2 缓存比 L1 大一个量级。它缓存的是完整解码后的图像帧。也就是说,一张 PNG 图片被解码成位图后,整个位图数据会放在 L2 里。

我曾经在一个智能家居面板项目上吃过亏:界面有 20 多个图标,每个图标 64x64,PNG 格式。一开始没开 L2 缓存,每次切换页面都要重新解码,那个卡顿啊……后来把 L2 缓存开到 512KB,所有图标解码一次后直接命中,切换页面丝般顺滑。

特性 L1 缓存 L2 缓存
缓存粒度 图像块(tile) 完整图像帧
数据格式 解码后像素 解码后位图
典型大小 几 KB ~ 几十 KB 几百 KB ~ 几 MB
替换策略 LRU LRU + 引用计数
适用场景 频繁变化的小图标 重复使用的大背景/图片

L2 缓存的替换策略除了 LRU,还加了一个引用计数。如果一个图像被多个对象引用(比如多个按钮共用同一个图标),它的引用计数会增加,被踢出的优先级会降低。这个设计很聪明,避免了「刚解码完就被踢出去」的尴尬。

2.4 L3 缓存:原始编码数据的压缩仓库

L3 缓存是最底层的一级缓存。它缓存的是原始编码数据——也就是 PNG、JPEG、BMP 这些还没解码的文件内容。

你可能会问:为什么不直接解码?因为解码很慢啊!L3 缓存的存在意义是:避免重复从文件系统读取。文件 I/O 的速度比内存访问慢几个数量级,尤其是从 Flash 或 SD 卡读取时。

避坑指南:我曾经在一个项目里把 L3 缓存开得太大,结果内存不够,系统频繁 OOM。L3 缓存的大小要根据你的硬件来定——如果 Flash 读取速度还行(比如 40MB/s),L3 可以小一点;如果用的是低速 SPI Flash,L3 就得大一些。

L3 缓存的数据是压缩的,所以占用的内存比 L2 小得多。一张 100KB 的 PNG 图片,解码后可能变成 600KB 的位图。在 L3 里它只占 100KB,在 L2 里却要占 600KB。这就是为什么 L3 可以做得比 L2 大很多。

2.5 三级缓存的协作流程

当 LVGL 需要渲染一张图片时,它会按照下面的顺序查找:

  1. 查 L1 缓存:看当前要渲染的图像块是否已经在 L1 里。如果命中,直接 memcpy 到帧缓冲,完事。
  2. 查 L2 缓存:L1 miss,去 L2 找完整的解码后位图。如果命中,从位图中裁剪出需要的块,同时把这块数据放到 L1 里。
  3. 查 L3 缓存:L2 也 miss,去 L3 找原始编码数据。如果命中,解码成位图,放入 L2,同时把需要的块放入 L1。
  4. 从文件系统加载:L3 也 miss,只能从 Flash/SD 卡读取原始文件。读取后放入 L3,然后解码、放入 L2、裁剪块放入 L1。

这个流程说白了就是「逐级回退」。每一级 miss 了,就往下一级找,直到找到原始数据源。数据一旦被加载,会同时填充所有经过的缓存层级——这叫缓存填充策略。

关键点:LVGL 的缓存架构是「写穿透」的。也就是说,当图像数据被修改时(比如动态生成的图像),它会直接写入 L2 和 L3,同时使 L1 中对应的缓存行失效。这样可以保证数据一致性。

2.6 缓存配置的实用建议

根据我的项目经验,不同场景下的缓存配置可以参考下表:

场景 L1 条目数 L2 大小 L3 大小 备注
低端 MCU(< 256KB RAM) 4~8 32~64 KB 不启用 内存紧张,优先保证 L2
中端 MCU(256KB~1MB RAM) 8~16 128~256 KB 64~128 KB 均衡配置,图标类 UI 首选
高端 MCU / MPU(> 1MB RAM) 16~32 512 KB ~ 2 MB 256 KB ~ 1 MB 大背景图多的场景,L2 优先

嗯,这里要注意:L3 缓存不是必须的。如果你的文件系统读取速度很快(比如用内存映射的 Flash),或者你的图片都是小尺寸的,完全可以关掉 L3,把内存留给 L2。

我个人习惯在项目初期先把 L2 缓存打开,设一个中等大小,然后跑一下 UI 看看命中率。如果命中率低于 80%,就加大 L2;如果内存还够,再考虑开 L3。说白了,缓存配置是个「试出来」的过程,没有银弹。