4. LVGL 刷新机制详解:lv_refr_set_rounding、full_refresh 与 direct_mode

刷新机制,说白了就是 LVGL 怎么把你在屏幕上画的东西,真正送到显示缓冲区,再推到 LCD 面板上。很多朋友做动画卡顿,第一反应是优化绘图函数,其实问题往往出在刷新策略上。

我早期做一个智能手表项目,动画帧率死活上不去。折腾了两周,最后发现是刷新配置没调对。嗯,从那以后,我每次建工程都会先检查这三个参数。

4.1 刷新流程的“最后一公里”

LVGL 的刷新不是全屏重绘,它只更新“脏区域”(invalidated area)。你调用 lv_obj_invalidate() 后,LVGL 会把这些区域收集起来,在 lv_task_handler() 里统一刷新。

整个流程分三步:

  1. 标记脏区:对象变化时,LVGL 自动或手动标记需要重绘的区域。
  2. 合并脏区:多个小脏区合并成一个大矩形,减少刷新次数。
  3. 调用 flush_cb:把合并后的缓冲区内容刷到显示器。

这里有个关键点——合并策略。默认情况下,LVGL 会把相邻的脏区合并成一个更大的矩形。这样做的好处是减少 flush_cb 调用次数,但坏处是可能多刷了一些没变化的像素。

核心原则:脏区合并是性能与精度的权衡。合并太多,无效刷新增加;合并太少,flush_cb 调用频繁,CPU 开销大。

4.2 lv_refr_set_rounding:对齐像素边界

lv_refr_set_rounding 这个函数,很多人不知道它的存在。它的作用是让脏区的坐标和尺寸对齐到某个整数倍。

为什么需要对齐?

我遇到过一种情况:脏区宽度是 17 像素,而显示控制器的 DMA 传输要求 16 字节对齐。如果不做 rounding,每次刷新都要做非对齐内存访问,性能直接打五折。

// 设置 rounding 为 8 像素对齐
lv_refr_set_rounding(8);

对齐后的脏区会向外扩展,保证覆盖原始区域。代价是多刷了几个像素,但换来了内存访问效率的提升。

对齐值 适用场景 性能影响
1(默认) 无特殊对齐要求 无额外开销
2 16 位色深,2 字节对齐 轻微
4 32 位色深,4 字节对齐 中等
8 DMA 传输要求 8 字节对齐 较高,但 DMA 效率提升明显

我的习惯:先查显示控制器的数据手册,看它的显存访问对齐要求。如果是 RGB 接口的屏,通常 4 字节对齐就够了;如果是 MIPI DSI,可能需要 8 字节。

4.3 full_refresh:全屏刷新的取舍

full_refresh 是 lv_disp_drv_t 里的一个标志位。设置为 1 时,LVGL 每次刷新都会把整个屏幕的脏区标记为全屏。

你可能会问:这不是很浪费吗?

确实,全屏刷新会刷很多没变化的像素。但有些场景下,它反而是最优解:

  • 显示控制器不支持局部刷新:比如某些单色 OLED 屏,只能整屏更新。
  • 帧缓存是双缓冲:全屏刷新配合双缓冲,可以避免撕裂。
  • 动画复杂度高:全屏刷新能简化脏区管理,减少 CPU 计算脏区合并的开销。
lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);

// 启用全屏刷新
disp_drv.full_refresh = 1;

我曾经踩过的坑:在一个 320x240 的屏上开了 full_refresh,结果帧率从 30fps 掉到 12fps。后来发现是 flush_cb 里用了阻塞式 SPI 传输。换成 DMA 传输后,全屏刷新也能跑到 25fps。所以 full_refresh 不是不能用,但要配合高效的 flush_cb。

4.4 direct_mode:直接操作帧缓存

direct_mode 是 LVGL 8.x 引入的一个高级特性。启用后,LVGL 不再维护内部缓冲区,而是直接操作你提供的帧缓存。

这意味着什么?

你想想看,LVGL 默认会分配一个或多个缓冲区,绘图时先画到缓冲区,再通过 flush_cb 刷到显示器。而 direct_mode 跳过了“画到缓冲区”这一步,直接画到显示器的显存里。

disp_drv.direct_mode = 1;

这样做的好处很明显:

  • 省内存:不需要额外的缓冲区。
  • 省时间:少了一次内存拷贝。
  • 适合高分辨率屏:比如 1024x600 的屏,双缓冲要 2.4MB(32 位色深),很多 MCU 扛不住。

但代价也很直接:

  • 必须保证 flush_cb 是原子的:如果绘图过程中被中断,屏幕会出现撕裂。
  • 不能做脏区合并:每次刷新都是全屏或指定区域直接写入。
  • 调试困难:出问题时很难区分是绘图错误还是刷新错误。

适用场景判断:如果你的 MCU 内存小于 512KB,且分辨率超过 480x320,direct_mode 几乎是唯一选择。反之,如果内存充裕,建议用双缓冲 + 局部刷新,开发更省心。

4.5 三种模式的对比与选择

我把这三种配置放在一起对比,方便你快速决策:

配置项 内存占用 刷新效率 适用场景
默认模式 中(1~2 个缓冲区) 高(局部刷新) 大多数 MCU 项目
full_refresh 中 低(全屏刷新) 不支持局部刷新的屏
direct_mode 低(无额外缓冲区) 中(直接写显存) 内存受限的高分辨率屏

我个人建议:默认先用局部刷新,如果遇到性能瓶颈或内存不足,再考虑 full_refresh 或 direct_mode。不要一上来就开 direct_mode,调试起来真的很痛苦。

4.6 刷新机制核心流程图

下面这张图展示了 LVGL 刷新的完整路径,以及三个配置项在流程中的位置:

LVGL 刷新流程与配置项作用位置 对象变化 标记脏区 脏区合并 lv_refr_set_rounding 在此生效 判断 full_refresh 是 → 全屏刷新;否 → 局部刷新 判断 direct_mode 是 → 直接写显存;否 → 写缓冲区 调用 flush_cb 配置项作用说明 • rounding:对齐脏区边界 • full_refresh:强制全屏刷新 • direct_mode:跳过缓冲区

4.7 实战建议:如何配置你的项目

说了这么多理论,最后给点实际建议。我一般按这个步骤来配置刷新:

  1. 先跑默认配置:不设置 rounding,full_refresh=0,direct_mode=0。看看帧率能不能接受。
  2. 检查内存占用:如果缓冲区太大(超过 RAM 的 30%),考虑开 direct_mode。
  3. 检查显示控制器:如果不支持局部刷新,开 full_refresh。
  4. 优化 flush_cb:不管哪种模式,flush_cb 都要用 DMA 传输,不要用阻塞式。
  5. 微调 rounding:如果 DMA 有对齐要求,设置合适的 rounding 值。

一个小技巧:在调试阶段,可以用 lv_disp_get_buf() 查看当前缓冲区状态,确认脏区合并是否合理。如果发现脏区过大,可能是 rounding 值设得太大了。

嗯,刷新机制这部分就讲到这里。记住一句话:没有最好的配置,只有最适合你硬件的配置。多试几次,找到平衡点,你的动画自然就流畅了。