4、局部刷新策略:脏矩形算法原理、LVGL局部刷新配置、减少刷新区域的实战技巧
做嵌入式UI优化,有个道理我越做越觉得对——能不刷的地方,坚决不刷。
你想想看,一个320×240的屏幕,全屏刷新一次要传输307200个像素数据。如果只是按了个按钮,按钮区域才40×30像素,只有1200个像素需要更新。全刷和局部刷,差了256倍。这个账算明白了,性能瓶颈就找到了。
LVGL的局部刷新机制,说白了就是靠「脏矩形」算法来干活。这一章我带你把它拆开看看,再聊聊怎么用好它。
4.1 脏矩形算法:到底在干什么?
脏矩形(Dirty Rectangle)这个名字挺形象——「脏」就是需要重绘的区域,「矩形」就是用一个最小的矩形框住所有变化点。
算法核心逻辑就三步:
- 标记脏区:当某个控件状态改变(比如按钮按下、滑块拖动),LVGL把这个控件所在的区域标记为「脏」。
- 合并脏区:如果多个控件同时变脏,或者同一控件连续变化,LVGL会把所有脏区合并成一个或几个不重叠的矩形。
- 只刷脏区:驱动层只把脏矩形内的像素数据刷到屏幕上,脏区之外纹丝不动。
核心思想:每次刷新只传输「变化量」,不传输「全量」。
我在一个智能手表项目里遇到过这种情况——表盘秒针每秒走一格,如果全刷,帧率直接掉到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%。
优化后:
- 温度显示区域独立,用
lv_label直接挂屏幕。 - 温度更新用
lv_obj_set_pos移动数字。 - 手动标记脏区,只刷温度数值那40×20的区域。
- 其他控件(时间、灯光)各自独立,互不干扰。
结果:帧率稳定60fps,CPU占用率降到32%。
核心原则:让每个控件的脏区尽可能小,让脏区之间尽可能不重叠,让脏区合并后尽可能接近实际变化区域。
嗯,局部刷新这块,说白了就是「抠门」——能省一个像素就省一个像素。但正是这种抠门,才能在资源有限的嵌入式平台上跑出流畅的动画效果。