4、局部刷新策略:脏矩形算法原理、LVGL局部刷新配置、减少刷新区域的实战技巧

做嵌入式UI优化,有个道理我越做越觉得对——能不刷的地方,坚决不刷。

你想想看,一个320×240的屏幕,全屏刷新一次要传输307200个像素数据。如果只是按了个按钮,按钮区域才40×30像素,只有1200个像素需要更新。全刷和局部刷,差了256倍。这个账算明白了,性能瓶颈就找到了。

LVGL的局部刷新机制,说白了就是靠「脏矩形」算法来干活。这一章我带你把它拆开看看,再聊聊怎么用好它。

4.1 脏矩形算法:到底在干什么?

脏矩形(Dirty Rectangle)这个名字挺形象——「脏」就是需要重绘的区域,「矩形」就是用一个最小的矩形框住所有变化点。

算法核心逻辑就三步:

  1. 标记脏区:当某个控件状态改变(比如按钮按下、滑块拖动),LVGL把这个控件所在的区域标记为「脏」。
  2. 合并脏区:如果多个控件同时变脏,或者同一控件连续变化,LVGL会把所有脏区合并成一个或几个不重叠的矩形。
  3. 只刷脏区:驱动层只把脏矩形内的像素数据刷到屏幕上,脏区之外纹丝不动。

核心思想:每次刷新只传输「变化量」,不传输「全量」。

我在一个智能手表项目里遇到过这种情况——表盘秒针每秒走一格,如果全刷,帧率直接掉到15fps。打开脏矩形后,每次只刷秒针那几十个像素,帧率瞬间回到60fps。嗯,差别就这么大。

脏矩形算法核心流程 控件状态变化 标记脏区 合并脏区 只刷脏区 关键优化点 • 脏区合并减少刷新次数 • 脏区裁剪避免无效传输 • 只刷变化区域节省带宽

4.2 LVGL局部刷新配置:怎么打开?

LVGL默认就支持局部刷新,但需要你在lv_conf.h里正确配置。我见过不少开发者直接拿默认配置用,结果局部刷新根本没生效。

关键配置项就这几个:

配置宏 默认值 推荐值 说明
LV_DISP_DEF_REFR_PERIOD 30 15~30 刷新周期(ms),越小越流畅但越耗CPU
LV_USE_PERF_MONITOR 0 1 开启性能监视器,调试时必开
LV_INV_BUF_SIZE 32 64~128 脏矩形缓冲区大小,越大合并效果越好
LV_USE_GPU 0 视硬件而定 开启GPU加速后脏矩形策略会变化

我的习惯:调试阶段把LV_USE_PERF_MONITOR打开,在屏幕上能看到当前帧率和刷新区域大小。这样一眼就能看出局部刷新有没有生效。

配置示例:

/* lv_conf.h 局部刷新相关配置 */
#define LV_DISP_DEF_REFR_PERIOD    20      /* 20ms刷新周期,约50fps */
#define LV_USE_PERF_MONITOR        1       /* 开启性能监视器 */
#define LV_INV_BUF_SIZE            64      /* 脏矩形缓冲区64个 */
#define LV_USE_GPU                 0       /* 无GPU,纯CPU渲染 */

配置完别忘了检查显示驱动里的flush_cb函数。如果这个函数实现不对,局部刷新也白搭。

4.3 减少刷新区域的实战技巧

配置打开了,脏矩形算法也在跑了,但刷新区域还是偏大?别急,下面这几个技巧是我从项目里一点点磨出来的。

4.3.1 控件层级扁平化

LVGL的脏矩形合并有个特点——父子控件重叠时,脏区会合并成更大的矩形。

举个例子:一个容器里有10个按钮,每个按钮变脏时,如果容器本身也标记了LV_OBJ_FLAG_HIDDEN或LV_OBJ_FLAG_CLICKABLE之类的标志,脏区可能会扩大到整个容器。

我建议的做法:

  • 能不嵌套就不嵌套,尽量用lv_scr_act()直接挂子控件。
  • 必须用容器时,把LV_OBJ_FLAG_SCROLLABLE关掉,减少脏区扩散。
  • 静态背景用lv_img或lv_canvas,不要用容器+子控件的方式。

我曾经踩过的坑:在一个仪表盘项目里,我把所有控件都塞进了一个lv_cont容器里。结果每次更新一个数字,整个仪表盘区域都重绘了。后来把容器拆掉,刷新区域从400×300降到了60×40,帧率从22fps直接跳到58fps。

4.3.2 使用lv_obj_set_pos代替lv_obj_align

lv_obj_align每次调用都会重新计算对齐关系,导致父容器也被标记为脏。而lv_obj_set_pos只移动控件本身,脏区只包含控件的新旧位置。

动画场景下尤其明显:

/* ❌ 不推荐:动画中频繁调用align */
static void anim_cb(void *obj, int32_t v) {
    lv_obj_align(obj, LV_ALIGN_CENTER, v, 0);
}

/* ✅ 推荐:直接用set_pos */
static void anim_cb(void *obj, int32_t v) {
    lv_obj_set_pos(obj, v, 100);
}

这个改动看起来小,但在60fps的动画里,每帧少标记一个父容器为脏,累积效果非常可观。

4.3.3 手动控制脏区标记

有些场景下,LVGL自动标记的脏区不够精确。你可以手动干预:

  • 延迟标记:连续变化时,用lv_task或定时器合并多次变化,只标记一次脏区。
  • 精确标记:用lv_inv_area手动指定脏区范围,比自动标记更精准。
  • 禁止标记:临时用lv_obj_set_style_local_opa(obj, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_TRANSP)让控件透明,变化时不触发重绘。
/* 手动标记脏区示例 */
static void update_value(int32_t new_val) {
    /* 先更新数值,但不立即刷新 */
    label_value = new_val;
    
    /* 手动指定脏区:只刷新数值显示区域 */
    lv_area_t area;
    area.x1 = 100;
    area.y1 = 50;
    area.x2 = 200;
    area.y2 = 80;
    lv_inv_area(lv_scr_act(), &area);
}

小技巧:在flush_cb里加个打印,输出每次刷新的区域坐标。调试阶段开着,能直观看到脏区大小和位置是否合理。

4.3.4 双缓冲与脏矩形的配合

LVGL支持双缓冲模式。双缓冲下,脏矩形算法效果更好——因为后台缓冲区可以独立绘制脏区,不影响前台显示。

配置方式:

/* 双缓冲配置 */
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf1[MY_DISP_HOR_RES * 100];
static lv_color_t buf2[MY_DISP_HOR_RES * 100];

lv_disp_draw_buf_init(&draw_buf, buf1, buf2, MY_DISP_HOR_RES * 100);

注意缓冲区大小:不需要全屏那么大,够用就行。我一般取屏幕高度的1/4到1/2,配合脏矩形效果最好。

4.4 实战:一个脏矩形优化案例

拿一个智能家居控制面板来说。面板上有温度、湿度、灯光状态、时间显示,还有几个控制按钮。

优化前:每次温度变化,全屏刷新。帧率25fps,CPU占用率78%。

优化后:

  1. 温度显示区域独立,用lv_label直接挂屏幕。
  2. 温度更新用lv_obj_set_pos移动数字。
  3. 手动标记脏区,只刷温度数值那40×20的区域。
  4. 其他控件(时间、灯光)各自独立,互不干扰。

结果:帧率稳定60fps,CPU占用率降到32%。

核心原则:让每个控件的脏区尽可能小,让脏区之间尽可能不重叠,让脏区合并后尽可能接近实际变化区域。

嗯,局部刷新这块,说白了就是「抠门」——能省一个像素就省一个像素。但正是这种抠门,才能在资源有限的嵌入式平台上跑出流畅的动画效果。