3、绘制引擎调优:脏矩形刷新、局部更新与硬件加速
绘制引擎,说白了就是 LVGL 把你在屏幕上看到的一切画出来的那套机制。很多开发者觉得卡顿是 CPU 不够快,其实大部分时候,是绘制策略没用好。我见过太多项目,明明硬件不错,界面却像幻灯片一样——问题就出在「画了不该画的东西」。
这一章,我们聚焦三个核心手段:脏矩形刷新、局部更新、硬件加速。掌握了它们,你的界面就能从「勉强能动」变成「丝滑跟手」。
核心观点:LVGL 的绘制引擎默认是「按需绘制」的。它不会每帧重绘整个屏幕,而是只重绘那些「变脏」的区域。理解并控制这个「脏区域」,就是性能调优的起点。
3.1 脏矩形刷新:LVGL 的「偷懒」哲学
LVGL 不会傻乎乎地每帧重绘整个屏幕。它维护了一个「脏矩形列表」——只有被标记为「脏」的区域,才会在下一帧被重绘。这个机制,我称之为「偷懒的艺术」。
举个例子:你点击了一个按钮,按钮颜色变了。LVGL 只重绘按钮所在的那个小矩形,而不是整个 480x320 的屏幕。你想想看,如果每次点击都重绘全屏,那得浪费多少 CPU 周期?
我的习惯:在调试阶段,我会开启 LV_USE_PERF_MONITOR 和 LV_USE_DRAW_MONITOR。这两个宏能让你看到每帧的绘制区域和耗时。我曾经在一个项目里发现,一个隐藏的弹窗居然每帧都在触发全屏重绘——就是因为它的 hidden 属性没设对。
脏矩形刷新的核心逻辑,藏在 lv_refr.c 里。它的工作流程大致是:
- 标记脏区域:当对象属性变化(位置、大小、样式等),LVGL 调用
lv_obj_invalidate()将对象所在区域加入脏矩形列表。 - 合并脏矩形:多个脏矩形如果相邻或重叠,会被合并成一个更大的矩形。这是为了减少绘制调用的次数。
- 裁剪绘制:在
lv_refr_area()中,LVGL 只重绘合并后的脏矩形区域,其他部分保持不变。
注意:脏矩形合并虽然减少了绘制调用次数,但可能会扩大重绘区域。如果你的界面有大量分散的小控件频繁变化,合并后的脏矩形可能覆盖半个屏幕——那就得不偿失了。我建议在这种场景下,考虑使用局部更新策略。
3.2 局部更新:把「脏」控制在最小范围
局部更新,说白了就是手动控制脏矩形的范围,让它尽可能小。LVGL 提供了几个关键 API,用好了能大幅提升性能。
| API / 宏 | 作用 | 使用场景 |
|---|---|---|
lv_obj_invalidate(obj) |
标记对象所在区域为脏 | 对象属性变化后自动调用 |
lv_area_t + lv_inv_area() |
手动标记任意矩形区域为脏 | 自定义绘制、外部数据更新 |
LV_DISP_DEF_REFR_PERIOD |
设置刷新周期(默认 30ms) | 调整刷新频率,平衡性能与流畅度 |
lv_refr_set_disp_refr_period() |
运行时动态调整刷新周期 | 根据场景切换刷新率(如省电模式) |
我在一个智能家居面板项目里遇到过这样的问题:一个实时曲线图,每秒更新 30 次数据点。如果每次更新都让 LVGL 自动标记脏矩形,它会标记整个曲线控件区域——差不多 300x200 像素。实际上,每次只变了最新的一小段曲线,宽度可能只有 10 像素。
解决方案很简单:
// 手动计算只变化的那一小块区域
lv_area_t dirty_area;
dirty_area.x1 = chart->cur_x - 5; // 最新数据点附近
dirty_area.x2 = chart->cur_x + 5;
dirty_area.y1 = 0;
dirty_area.y2 = chart_height;
// 只标记这个窄条为脏
lv_inv_area(disp, &dirty_area);
就这么一个小改动,CPU 占用从 45% 降到了 12%。你想想看,这就是局部更新的威力。
关键原则:脏矩形越小,绘制开销越小。能用 lv_inv_area() 手动控制,就别依赖自动标记。尤其是那些高频更新的自定义控件,一定要自己管理脏区域。
3.3 硬件加速:让 GPU 帮你干活
软件绘制,就是 CPU 逐像素地把颜色填到显存里。对于简单的界面,这没问题。但如果你有大量透明叠加、圆角、阴影、抗锯齿——CPU 会累死。
硬件加速,就是把像素填充、颜色混合、旋转缩放这些脏活累活,交给 GPU 或 DMA2D 控制器。LVGL 支持多种硬件加速后端:
- STM32 DMA2D:STM32 系列自带的 2D 图形加速器,支持颜色填充、拷贝、混合。
- NXP PXP / GPU2D:i.MX RT 系列上的硬件加速器。
- ESP32 的 JPEG 解码 / 颜色转换:虽然不完全是 GPU,但也能分担部分工作。
- Linux DRM / EGL:在 Linux 平台上,可以通过 EGL 调用 GPU。
启用硬件加速,通常只需要在 lv_conf.h 里打开对应的宏:
/* 以 STM32 DMA2D 为例 */
#define LV_USE_GPU_STM32_DMA2D 1
/* 配置 DMA2D 缓冲区 */
#define LV_GPU_DMA2D_CBUF_SIZE (128 * 1024) // 128KB 缓冲区
然后,在显示驱动初始化时,注册硬件加速的回调函数:
static void disp_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p)
{
/* 使用 DMA2D 进行颜色填充 */
DMA2D_Fill(area->x1, area->y1, area->x2, area->y2, color_p);
lv_disp_flush_ready(drv); // 通知 LVGL 刷新完成
}
void my_disp_init(void)
{
lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.flush_cb = disp_flush_cb;
/* ... 其他配置 ... */
lv_disp_drv_register(&disp_drv);
}
避坑指南:我曾经在一个项目里,开了 DMA2D 加速后,界面反而更卡了。排查了半天,发现是 DMA2D 的中断优先级设得太低,被其他高优先级中断频繁打断。硬件加速不是银弹——你得确保它的优先级、时钟、缓冲区都配置正确。否则,加速器反而会成为瓶颈。
3.4 绘制引擎调优实战清单
说了这么多,我总结一份实战清单。你在调优时,可以按这个顺序排查:
- 检查脏矩形范围:开启
LV_USE_DRAW_MONITOR,观察每帧的重绘区域。如果经常是全屏重绘,说明有对象在「捣乱」。 - 手动控制脏区域:对于高频更新的自定义控件,用
lv_inv_area()替代自动标记。 - 减少透明叠加层数:每层透明叠加,都会增加绘制时的颜色混合计算。能不用透明就别用。
- 启用硬件加速:根据你的硬件平台,打开对应的
LV_USE_GPU_*宏。 - 调整刷新周期:如果界面变化不频繁,可以把
LV_DISP_DEF_REFR_PERIOD从 30ms 调到 50ms 甚至 100ms。 - 使用双缓冲:双缓冲能避免屏幕撕裂,但会增加内存占用。在资源允许的情况下,推荐开启。
最后提醒一句:绘制引擎调优,不要一上来就开硬件加速。先确认脏矩形策略是否正确,再考虑硬件加速。我见过太多人,硬件加速开了,但脏矩形是全屏的——结果加速了个寂寞。