文档版本: 1.0
更新日期: 2025年11月
适用对象: LVGL9.4 显示驱动/移植工程师

1. 概述

1.1 文档目的

本篇旨在帮助读者理解 LVGL9.4 显示框架中的“局部刷新”和“脏区(dirty area)计算”机制:弄清 _lv_display_t 中与刷新状态、渲染模式、无效区域缓存相关的字段含义,理解从对象无效化到最终调用 flush_cb 之间的关键步骤,以及在双缓冲/直接渲染等场景下,如何通过合理配置减少无效刷新的开销,为后续的性能调优与问题排查提供参考。

1.2 代码版本与范围

  • 仓库路径:https://github.com/lvgl/lvgl.git
  • 版本:v9.4.0
  • commit: c016f72d4c125098287be5e83c0f1abed4706ee5
  • 重点文件:
    • src/display/lv_display_private.hstruct _lv_display_t 中的刷新与脏区相关字段)
    • src/core/lv_refr.c(局部刷新主流程与脏区处理逻辑)

2. 字段含义与版本差异

2.1 _lv_display_t 中的关键字段含义

围绕“局部刷新和脏区计算”的字段,主要集中在 _lv_display_t 的 Buffering 小节之后:

  • 刷新状态相关

    • volatile int flushing
      • 含义:当前是否有一次正在进行的显示刷新(flush);
      • 典型用途:配合 lv_display_flush_ready() 控制刷新完成时机,避免在刷新未结束时再次提交 buffer。
    • volatile int flushing_last
      • 含义:当前这次 flush 是否是本轮刷新中的“最后一块区域/最后一帧”;
      • 在双缓冲/三缓冲中经常用于判断是否需要切换 buf_act 或进行额外同步。
    • volatile uint32_t last_area : 1last_part : 1
      • 含义:标记“当前处理的是最后一个 area / 当前 area 的最后一个 part”;
      • 作用:配合 flushing_last 一起决定在何时进行资源释放、缓冲切换或同步。
  • 渲染模式与抗锯齿

    • lv_display_render_mode_t render_mode
      • 取值:LV_DISPLAY_RENDER_MODE_PARTIAL / FULL / DIRECT 等;
      • 影响:
        • 是否以“分块/脏区”为单位进行局部刷新;
        • 是否要求 draw buffer 至少覆盖整屏;
        • DIRECT 模式下,还会影响双缓冲同步逻辑(sync_areas)。
    • uint32_t antialiasing : 1
      • 含义:该 display 是否开启抗锯齿;
      • 会影响绘制管线的复杂度与性能,但不直接改变脏区算法本身。
  • Tile 与 stride 设置

    • uint32_t tile_cnt : 8
      • 含义:将显示缓冲划分为多少个 tile(小块);
      • 作用:在局部刷新和脏区处理时,可以按 tile 粒度分割刷新区域,有助于减少每次刷新处理的区域大小。
    • uint32_t stride_is_auto : 1
      • 含义:当前缓冲区的 stride 是否由 LVGL 自动计算;
      • 该标志主要与缓冲创建 API(如 lv_display_set_buffers*)相关,用于后续 reshape 或重建时决定是否重新计算 stride。
  • 脏区缓存与无效计数

    • lv_area_t inv_areas[LV_INV_BUF_SIZE]
      • 含义:本轮刷新中累计的“无效区域”列表;
      • 每个元素是一块需要重绘的矩形区域。
    • uint8_t inv_area_joined[LV_INV_BUF_SIZE]
      • 含义:与 inv_areas 数组一一对应的标记位,指示某个区域是否已经被合并到其它区域;
      • 作用:在进行“脏区合并”时,避免重复刷新被包含/合并的子区域。
    • uint32_t inv_p
      • 含义:当前已经记录了多少个有效的无效区域(即 inv_areas 中的有效条目数);
      • 在刷新循环中常用来遍历所有待处理的脏区。
    • int32_t inv_en_cnt
      • 含义:无效化(invalidate)是否被临时禁用的计数器,常见用法是通过成对的“禁用/启用”调用避免在某些批量操作中产生过多、过碎的脏区。
  • 双缓冲同步区域

    • lv_ll_t sync_areas
      • 含义:在“双缓冲 + DIRECT 渲染模式”下,记录上一次刷新中更新的区域链表;
      • 用途:下一帧刷新时,可利用这些区域在两个缓冲之间做同步,避免显示残影或信息不一致。

小结:flushing*last_* 决定了一次刷新生命周期的“起止与收尾时机”,render_mode/tile_cnt 决定“以什么粒度刷新”,而 inv_*sync_areas 则负责“记账和复用”这些局部刷新的信息。

2.2 与 LVGL 8.4.0 的差异简述

  • 结构组织层面
    • 8.4.0 中,部分刷新相关状态分散在 lv_disp_tlv_refr.c 的静态变量及驱动配置中;
    • 9.4.0 将更多“刷新状态 + 脏区缓存”字段集中到了 _lv_display_t 内部,更清晰地表达出 display 作为刷新单元的角色。
  • 脏区管理粒度
    • 8.4.0 主要依赖 lv_inv_area() 等接口,在内部做简单的脏区合并与裁剪;
    • 9.4.0 在保留这些接口的同时,引入了 tile_cnt、更丰富的 render_mode,为“按 tile 分块、按不同模式优化刷新路径”提供了基础。
  • 双缓冲同步机制
    • 8.4.0 的双缓冲更多是“简单的 buf1/buf2 轮换”,同步区域通常需要驱动端自行处理;
    • 9.4.0 在 DIRECT + double buffer 场景下,增加了 sync_areas 列表,用于记录和复用“需要在两块缓冲间同步的区域”。

从设计意图看,9.4 在保持向下兼容的基础上,强化了“display 是一个完整刷新单元”的概念,为多后端、多缓冲策略的统一实现打下了基础。

3. 局部刷新整体流程

3.1 从对象无效化到刷新触发

在应用层调用 lv_obj_invalidate(obj) 或修改对象属性时,LVGL 会将对应区域标记为“无效”,大致流程可以概括为:

UI 改动(属性/布局/动画等)
        │
        ▼
  lv_obj_invalidate(...)
        │
        ▼
  将对象区域写入 display 的 inv_areas[]
        │
        ▼
  刷新定时器触发(refr_timer)
        │
        ▼
  lv_refr_timer() → lv_refr_invalid_areas()
        │
        ▼
  扫描 inv_areas[],合并/裁剪脏区,并按 render_mode/tile_cnt 调度绘制与 flush_cb

关键点:

  • 所有无效区域最终都会进入某个 display 实例的 inv_areas[]
  • 刷新并不是每次 invalidate 立刻执行,而是通过刷新定时器节流(避免频繁小更新导致过多刷新);
  • 脏区的合并、裁剪与遍历,主要发生在 lv_refr.c 的一系列 refr_* 函数中。

3.2 lv_refr_timer() 主流程概览

在 9.4 中,lv_refr_timer() 是局部刷新的主入口之一(省略非关键细节后):

  • 1)获取当前 display 与 active draw buffer(disp_refr->buf_act),若无则直接返回;
  • 2)发送 LV_EVENT_REFR_START 事件,通知上层即将刷新;
  • 3)根据需要对 active screen、prev screen 以及 top/bottom/sys layer 做一次布局更新;
  • 4)若没有 active screen,清空 inv_areas[]inv_p 并结束;
  • 5)调用:
    • lv_refr_join_area():尝试合并重叠或相互包含的脏区;
    • refr_sync_areas():在 DIRECT 双缓冲模式下,将需要同步的区域记录到 sync_areas
    • refr_invalid_areas():对每个有效的 inv_areas[i] 进行实际绘制与 flush;
  • 6)刷新完成后,清空 inv_areas[] / inv_area_joined[],重置 inv_p,并发送 LV_EVENT_REFR_READY

用一个简化的 ASCII 流程图表示:

lv_refr_timer()
  ├─ 检查 disp_refr / buf_act 是否可用
  ├─ 发送 LV_EVENT_REFR_START
  ├─ 更新各层布局
  ├─ 无 active screen? → 清空 inv_* 并结束
  ├─ lv_refr_join_area()
  ├─ refr_sync_areas()
  ├─ refr_invalid_areas()
  ├─ 清空 inv_areas[] / inv_area_joined[] / inv_p
  └─ 发送 LV_EVENT_REFR_READY

4. 脏区计算与 tile 分块规则

4.1 脏区合并与裁剪(inv_areas / inv_area_joined / inv_p

在一帧内,应用层可能多次调用 lv_obj_invalidate(),如果所有小区域都原样刷新,会造成大量重复绘制。为此,LVGL 对脏区进行合并与裁剪:

  • 每次无效化:
    • 新区域尝试与 inv_areas[] 中已有区域进行合并(如 A 完全覆盖 B,或两者相交后合并为更大矩形);
    • 若某个已有区域被合并到新区域中,则在 inv_area_joined[] 中标记该区域已被“覆盖”,避免重复刷新;
    • 若不能合并,且 inv_p < LV_INV_BUF_SIZE,则将新区域追加到 inv_areas[inv_p],并递增 inv_p

简化理解可以用下面的示意图:

inv_areas[]:
  [0]  ── area A (有效)
  [1]  ── area B (已被 A 合并,inv_area_joined[1] = 1)
  [2]  ── area C (有效)
  ...

遍历时只处理 inv_area_joined[i] == 0 的条目,
这样可以避免对被完全包含/合并的旧区域再次刷新。

最终,refr_invalid_areas() 只会对那些“未被标记为 joined”的脏区执行绘制与 flush。

4.2 tile_cnt 对刷新粒度的影响与调优思路

tile_cnt 用于将显示缓冲划分为若干个 tile(例如上下拆成若干水平条),配合 partial 刷新模式,可以进一步减少单次刷新的区域大小。

4.2.1 基本直觉

  • tile_cnt > 1 时,可以理解为:
整屏区域:
  +------------------------+
  |   tile 0               |
  +------------------------+
  |   tile 1               |
  +------------------------+
  |   tile 2               |
  +------------------------+
  |   ...                  |
  +------------------------+
  • 刷新时,LVGL 会尝试按 tile 维度拆分脏区,使每次绘制/刷新的区域更小;
  • 好处:在带宽或渲染能力有限的场景中,可以降低单次操作的峰值压力;
  • 代价:管理逻辑更复杂,且如果 tile_cnt 过大,调度/遍历的开销也会增加,需要根据实际屏幕分辨率和缓冲大小酌情设置。

4.2.2 推荐的调参步骤

下面给出一套在实际项目中常用的调参思路,供参考:

  1. 从 1 开始,确认基线性能

    • tile_cnt 设为 1,保证逻辑最简单;
    • 在典型 UI 场景(列表滚动、页面切换、动画等)下观察:
      • FPS / 帧时间;
      • 刷新过程中 CPU 占用和总线带宽(如能监控的话)。
  2. 按“屏高的 1/2、1/3、1/4”尝试

    • 以垂直分辨率为例(如 480 行或 800 行),尝试:
      • tile_cnt = 2(每 tile 约半屏高);
      • tile_cnt = 3 ~ 4(每 tile 约 1/3 ~ 1/4 屏高);
    • 每次只改一个参数,再重复第 1 步的测试,记录帧时间和主观流畅度。
  3. 观察“典型脏区”的大小

    • 对滚动列表/局部更新场景,统计一帧内典型 inv_areas[] 的高度范围;
    • 经验上:
      • 若典型脏区高度远小于单个 tile 高度,tile_cnt 提升的收益有限;
      • 若典型脏区高度接近或略大于单 tile 高度,适当增加 tile_cnt 通常能降低单次刷新的面积。
  4. 平衡“刷新次数 vs 每次面积”

    • tile_cnt 增大:
      • 每次刷新的区域变小 → 每次带宽占用下降;
      • 但 tile 数增加 → 每帧可能需要多次绘制/flush;
    • 实践中建议通过简单表格记录:
      • 每帧平均刷新调用次数;
      • 单次刷新平均/最大区域高度;
      • 对应的 FPS / CPU 占用;
    • 选取在三者间平衡较好的组合,而不是一味追求“越多 tile 越好”。
  5. 结合硬件特性做微调

    • 若底层 LCD 控制器/总线在一条较宽的连续区域刷新的效率明显更高,可以适当降低 tile_cnt,让每次刷新区域更“长条”;
    • 若 DMA / GPU 启动一次开销较大,则不宜把 tile_cnt 调得太高,以免频繁启动绘制引擎。

总体来说:tile_cnt 是一个与分辨率、缓冲大小、硬件特性强相关的参数,需要在“测一测、记录数据”的基础上,选择一个对项目最合适的折中值。

4.3 DIRECT 双缓冲下的 sync_areas

render_mode == LV_DISPLAY_RENDER_MODE_DIRECT 且双缓冲启用的情况下,sync_areas 用于记录上一次刷新中更新的区域:

  • lv_refr_timer() 中,当满足条件时:
    • 对每个未被 inv_area_joined 标记的 inv_areas[i],插入到 sync_areas 链表中;
  • 下一帧开始前,可以利用这些区域在两块 buffer 之间进行同步,只更新真正发生变化的部分;
  • 这样可以避免“整屏拷贝”带来的巨大带宽开销,尤其是在高分辨率或显存总线较窄的场景下。

5. 关键代码片段简析

下面节选 lv_refr.c 中的一段核心流程,帮助理解 inv_areas / inv_area_joined / inv_psync_areas 的协同关系,以及它们对整个绘制/刷新流程的影响(略去错误处理与非核心分支):

lv_refr_timer(lv_timer_t * tmr)
{
    ...
    lv_draw_buf_t * buf_act = disp_refr->buf_act;
    if(!(buf_act && buf_act->data && buf_act->data_size)) { ... }
    ...
    if(disp_refr->act_scr == NULL) {
        disp_refr->inv_p = 0;
        goto refr_finish;
    }

    lv_refr_join_area();       /* 合并/裁剪 inv_areas[] 中的脏区 */
    refr_sync_areas();         /* 在 DIRECT 双缓冲模式下记录 sync_areas */
    refr_invalid_areas();      /* 对每个有效脏区执行绘制和 flush */

    if(disp_refr->inv_p == 0) goto refr_finish;

    if(lv_display_is_double_buffered(disp_refr) &&
       disp_refr->render_mode == LV_DISPLAY_RENDER_MODE_DIRECT) {
        uint32_t i;
        for(i = 0; i < disp_refr->inv_p; i++) {
            if(disp_refr->inv_area_joined[i]) continue;
            lv_area_t * sync_area = lv_ll_ins_tail(&disp_refr->sync_areas);
            *sync_area = disp_refr->inv_areas[i];
        }
    }

    lv_memzero(disp_refr->inv_areas, sizeof(disp_refr->inv_areas));
    lv_memzero(disp_refr->inv_area_joined, sizeof(disp_refr->inv_area_joined));
    disp_refr->inv_p = 0;
refr_finish:
    ...
}

这段代码串联起了几个关键点:

  • 先合并/裁剪脏区,再进行实际刷新;
  • 在 DIRECT 双缓冲下,将未合并的脏区记录到 sync_areas,用于后续同步;
  • 刷新完成后清理 inv_areas / inv_area_joined,为下一帧做好准备。

5.1 实战中需要特别关注的几点

  • 刷新生命周期与 flushing/flushing_last

    • flushing 未及时通过 lv_display_flush_ready() 置回 0,会导致后续帧无法顺利提交,表现为“画面卡在某一帧”;
    • flushing_last/last_area/last_part 决定了何时做缓冲切换或同步,驱动在判断“最后一块区域”时要与这些状态保持一致。
  • 脏区数量与合并策略

    • 如果 inv_areas[] 中条目很多、且 inv_area_joined[] 合并不充分,即使每块区域不大,也会放大 CPU 绘制与 flush 次数;
    • 在 UI 设计和控件实现上,尽量避免频繁、大面积的无效化(例如每帧重置整个容器),让脏区尽可能集中。
  • tile_cnt 的取值与刷新粒度

    • tile_cnt 过小,局部刷新的粒度太粗,会导致“微小改动也刷新很大一块”;
    • tile_cnt 过大,则每帧需要遍历和调度的 tile 数量爆炸,可能得不偿失;
    • 建议结合分辨率与缓冲大小,选取一个既能减小单次刷新的物理面积、又不过度增加调度开销的折中值。
  • DIRECT 双缓冲下 sync_areas 的正确性

    • 若驱动在 DIRECT 模式下自行操作 buffer,而忽略 sync_areas 的语义,可能导致前一帧残留内容被误复制到下一帧,出现“局部花屏”或“影子”;
    • 推荐在调试阶段打印/可视化 sync_areas,确认在双缓冲同步时只更新真正变动的区域。
  • inv_en_cnt 对批量操作的影响

    • 在做批量 UI 更新(如大规模创建/销毁对象)时,适当使用“禁止/启用 invalidate”的成对封装,可以避免产生大量零散脏区;
    • 但如果忘记恢复(计数不归零),会让后续正常的 invalidate 失效,表现为“UI 改了但不刷新”。
  • 调试建议

    • 可以在调试构建中加上对 inv_areas[]/inv_p/sync_areas 的日志打印或可视化 overlay,观察每一帧真正刷新的区域;
    • 结合 FPS/CPU 占用等指标,评估当前脏区策略是否合适,必要时调整控件实现或 render_mode/tile_cnt 配置。

6. 附录

A. 参考文档(外部)

B. 相关资源(外部)

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐