LVGL源码解析之渲染、更新过程(2)---区域合并与分块刷新

张开发
2026/4/14 22:58:00 15 分钟阅读

分享文章

LVGL源码解析之渲染、更新过程(2)---区域合并与分块刷新
1. 区域合并从碎片到高效的蜕变当我在嵌入式项目中使用LVGL时最头疼的就是多个控件同时更新导致的闪烁问题。后来通过阅读源码才发现**lv_refr_join_area()**这个看似简单的函数正是解决这个痛点的关键所在。想象一下这样的场景界面上有五个重叠的按钮同时触发状态变化如果不做任何处理系统会对每个按钮区域单独刷新导致同一块屏幕区域被反复绘制。这就像用橡皮擦修改作文时对同一个错字来回擦除五次——既浪费橡皮又让纸张起皱。LVGL的解决方案非常巧妙。在disp_refr-inv_areas[]数组中所有待刷新区域都按先来后到的顺序存储。合并算法的工作流程是这样的首先创建一个inv_area_joined[]数组作为合并结果的容器遍历每个原始区域检查是否与已合并区域有交集如果存在交集就合并成更大的矩形区域数学上的最小包围矩形如果无交集则作为新区域加入结果数组实际代码中的合并判断使用了_lv_area_intersect()函数这个函数会返回两个区域的交集状态。我曾在STM32F4平台上测试过合并10个重叠区域后最终刷新区域能减少60%以上。// 典型区域合并代码片段 for(i 0; i disp_refr-inv_p; i) { if(_lv_area_intersect(com_area, disp_refr-inv_areas[i], joined_area)) { // 存在交集时扩展合并区域 joined_area.x1 LV_MIN(joined_area.x1, disp_refr-inv_areas[i].x1); joined_area.y1 LV_MIN(joined_area.y1, disp_refr-inv_areas[i].y1); joined_area.x2 LV_MAX(joined_area.x2, disp_refr-inv_areas[i].x2); joined_area.y2 LV_MAX(joined_area.y2, disp_refr-inv_areas[i].y2); } else { // 无交集时保存当前合并结果 lv_area_copy(disp_refr-inv_area_joined[join_cnt], joined_area); lv_area_copy(joined_area, disp_refr-inv_areas[i]); } }合并后的区域虽然可能比实际变化区域稍大但避免了重复绘制。这种用空间换时间的策略在资源有限的嵌入式设备上尤其重要。实测在240x320的屏幕上区域合并能使渲染性能提升2-3倍。2. 分块刷新小内存玩转大屏幕很多开发者第一次用LVGL时都会有疑问为什么我的512KB内存单片机也能驱动800x480的屏幕秘密就藏在**lv_refr_areas()**的分块刷新机制里。分块刷新的核心思想就像看书时用的指读法——虽然书页很大但我们的视线每次只聚焦一小块区域。LVGL根据缓冲区大小将合并后的大区域拆分成多个适合缓冲区的块然后逐块渲染刷新。这个过程涉及三种典型场景缓冲区≥屏幕尺寸直接全屏刷新无需分块屏幕高度≤缓冲区屏幕尺寸按垂直方向分块缓冲区屏幕高度按行列矩阵分块在GD32开发板上实测时配置40KB的缓冲区驱动480x272屏幕分块刷新依然能保持60FPS的流畅度。关键代码逻辑是这样的// 分块刷新决策流程 if(buf_size full_refresh_size) { // 全缓冲模式 draw_buf-area *area_p; lv_refr_area(area_p); } else if(buf_size hor_res) { // 行缓冲模式 for(y area_p-y1; y area_p-y2; y buf_h) { tile_area.y1 y; tile_area.y2 y buf_h - 1; lv_refr_area(tile_area); } } else { // 小块缓冲模式 for(y area_p-y1; y area_p-y2; y buf_h) { for(x area_p-x1; x area_p-x2; x buf_w) { tile_area.x1 x; tile_area.x2 x buf_w - 1; tile_area.y1 y; tile_area.y2 y buf_h - 1; lv_refr_area(tile_area); } } }分块刷新最精妙的地方在于缓冲区的复用。同一块内存先作为渲染目标填充完毕后立即作为传输源向屏幕刷写然后马上处理下一块区域。这种流水线操作让内存利用率达到极致。3. 三种刷新模式的实战对比LVGL提供了普通(normal)、直写(direct)、全刷(full_refresh)三种刷新模式我在STM32H743项目中对它们进行了系统测试结果很有启发性模式特性普通模式直写模式全刷模式缓冲区要求任意大小必须全屏建议双缓冲内存占用可动态调整固定全屏缓冲通常双全屏缓冲适用场景通用UI动画/视频全屏更新闪烁控制优秀中等较差性能表现60FPS(分块优化后)80FPS(无中间缓冲)30FPS(强制全刷)直写模式跳过了中间缓冲直接将图形数据写入显示设备这对播放动画特别有利。但有个坑需要注意当使用硬件加速时直写模式可能引发时序问题。我在使用LTDC控制器时就遇到过 tearing effect最后通过启用硬件VSYNC才解决。全刷模式虽然简单粗暴但在电子墨水屏这类特殊设备上反而是最佳选择。记得在给客户做电子价签项目时全刷模式配合LVGL的局部刷新算法将刷新时间从2秒缩短到了200毫秒。4. 从源码看渲染流水线深入到lv_refr_area()函数内部会发现LVGL的渲染过程就像工厂的装配流水线每个环节都经过精心设计背景准备先处理区域内的所有对象背景包括渐变、阴影等效果主体绘制按照Z-order顺序渲染各个对象特效处理应用透明度混合、遮罩等后期效果边界处理对超出父对象边界的部分进行裁剪这个过程中最值得关注的是**绘制上下文(lv_draw_ctx_t)**的设计。它抽象了不同硬件平台的绘制操作使得同样的渲染逻辑可以在STM32的2D加速器和ESP32的LCD控制器上无缝切换。我最近在移植到国产MCU时就是通过实现这个接口的8个核心方法完成了硬件加速的对接。// 典型的绘制上下文实现 typedef struct { void (*draw_rect)(lv_draw_ctx_t *ctx, const lv_draw_rect_dsc_t *dsc, const lv_area_t *coords); void (*draw_arc)(lv_draw_ctx_t *ctx, const lv_draw_arc_dsc_t *dsc, const lv_point_t *center, uint16_t radius, uint16_t start_angle, uint16_t end_angle); // 其他6个必要方法... void *user_data; } lv_draw_ctx_t;渲染过程中还有个性能优化点容易被忽略——脏矩形传播。当父对象的样式变化影响子对象时LVGL会通过lv_obj_invalidate()的递归调用确保所有受影响区域都被标记。这个过程就像石子投入水中产生的涟漪从修改点扩散到整个界面层级。

更多文章