4. LVGL 刷新机制详解:lv_refr_set_rounding、full_refresh 与 direct_mode
刷新机制,说白了就是 LVGL 怎么把你在屏幕上画的东西,真正送到显示缓冲区,再推到 LCD 面板上。很多朋友做动画卡顿,第一反应是优化绘图函数,其实问题往往出在刷新策略上。
我早期做一个智能手表项目,动画帧率死活上不去。折腾了两周,最后发现是刷新配置没调对。嗯,从那以后,我每次建工程都会先检查这三个参数。
4.1 刷新流程的“最后一公里”
LVGL 的刷新不是全屏重绘,它只更新“脏区域”(invalidated area)。你调用 lv_obj_invalidate() 后,LVGL 会把这些区域收集起来,在 lv_task_handler() 里统一刷新。
整个流程分三步:
- 标记脏区:对象变化时,LVGL 自动或手动标记需要重绘的区域。
- 合并脏区:多个小脏区合并成一个大矩形,减少刷新次数。
- 调用 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 刷新的完整路径,以及三个配置项在流程中的位置:
4.7 实战建议:如何配置你的项目
说了这么多理论,最后给点实际建议。我一般按这个步骤来配置刷新:
- 先跑默认配置:不设置 rounding,full_refresh=0,direct_mode=0。看看帧率能不能接受。
- 检查内存占用:如果缓冲区太大(超过 RAM 的 30%),考虑开 direct_mode。
- 检查显示控制器:如果不支持局部刷新,开 full_refresh。
- 优化 flush_cb:不管哪种模式,flush_cb 都要用 DMA 传输,不要用阻塞式。
- 微调 rounding:如果 DMA 有对齐要求,设置合适的 rounding 值。
一个小技巧:在调试阶段,可以用 lv_disp_get_buf() 查看当前缓冲区状态,确认脏区合并是否合理。如果发现脏区过大,可能是 rounding 值设得太大了。
嗯,刷新机制这部分就讲到这里。记住一句话:没有最好的配置,只有最适合你硬件的配置。多试几次,找到平衡点,你的动画自然就流畅了。