当前位置: 首页 > news >正文

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

1. 区域合并:从碎片到高效的蜕变

当我在嵌入式项目中使用LVGL时,最头疼的就是多个控件同时更新导致的闪烁问题。后来通过阅读源码才发现,**lv_refr_join_area()**这个看似简单的函数,正是解决这个痛点的关键所在。

想象一下这样的场景:界面上有五个重叠的按钮同时触发状态变化,如果不做任何处理,系统会对每个按钮区域单独刷新,导致同一块屏幕区域被反复绘制。这就像用橡皮擦修改作文时,对同一个错字来回擦除五次——既浪费橡皮又让纸张起皱。

LVGL的解决方案非常巧妙。在disp_refr->inv_areas[]数组中,所有待刷新区域都按先来后到的顺序存储。合并算法的工作流程是这样的:

  1. 首先创建一个inv_area_joined[]数组作为合并结果的容器
  2. 遍历每个原始区域,检查是否与已合并区域有交集
  3. 如果存在交集,就合并成更大的矩形区域(数学上的最小包围矩形)
  4. 如果无交集,则作为新区域加入结果数组

实际代码中的合并判断使用了_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根据缓冲区大小,将合并后的大区域拆分成多个适合缓冲区的"块",然后逐块渲染刷新。这个过程涉及三种典型场景:

  1. 缓冲区≥屏幕尺寸:直接全屏刷新,无需分块
  2. 屏幕高度≤缓冲区<屏幕尺寸:按垂直方向分块
  3. 缓冲区<屏幕高度:按行列矩阵分块

在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的渲染过程就像工厂的装配流水线,每个环节都经过精心设计:

  1. 背景准备:先处理区域内的所有对象背景,包括渐变、阴影等效果
  2. 主体绘制:按照Z-order顺序渲染各个对象
  3. 特效处理:应用透明度混合、遮罩等后期效果
  4. 边界处理:对超出父对象边界的部分进行裁剪

这个过程中最值得关注的是**绘制上下文(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()的递归调用,确保所有受影响区域都被标记。这个过程就像石子投入水中产生的涟漪,从修改点扩散到整个界面层级。

http://www.cnnetsun.cn/news/1895563.html

相关文章:

  • Xilinx程序固化避坑指南:为什么你的FSBL编译总失败?从工程配置到Flash烧录全解析
  • 别再手动写FAQ了!用Spring AI + 阿里云百炼 + Elasticsearch,30分钟给你的业务系统装个“智能客服”
  • 阿里专有云的产品架构(多region)
  • 通义千问1.5-1.8B-Chat-GPTQ-Int4开发环境搭建:PyCharm专业版调试技巧大全
  • 迁移临时数据脚本
  • 实战指南:基于RGB活体检测的人脸识别系统开发
  • 【C++11 高性能并发】线程池从原理到实现:一篇吃透线程池核心设计
  • 【SCI一区复现】基于配电网韧性提升的应急移动电源预配置和动态调度(下)—MPS动态调附Matlab代码
  • 从CLIP到M3A再到Gemma-MoE:SITS2026圆桌绘制多模态架构演进图谱(含6代模型参数量/延迟/泛化率三维对比)
  • 跨考中山大学人工智能学院:从零基础到高分上岸的408备考全攻略
  • 如何抵御CC攻击?从原理到实战的全面防护手册
  • JSM8563T/TS低功耗I2C接口实时时钟/日历
  • 4月 YouTube 关键词:完播率,500粉带货
  • 处理报错:org.apache.tomcat.util.http.fileupload.impl.FileCountLimitExceededException
  • MySQL触发器中如何获取新插入值_MySQL触发器NEW关键字
  • IDEA如何使用教育网账号激活
  • 为什么你的Spring AI MCP Server总是断联?深入解析SSE连接超时问题
  • Spring Boot @Value 绑定 Set 失败?
  • 茉莉花插件完整教程:3步提升Zotero中文文献管理效率
  • Arthas + MCP:AI 终于把 Java 排查这件事变顺了
  • Chrome与Web标准演进:从“浏览器大战”到“兼容性基线”的深度解析
  • Go语言如何做WebSocket服务_Go语言WebSocket实时通信教程【对比】
  • 面试官问‘怎么测nn.Linear’?我现场写了个单元测试给他看(PyTorch版)
  • 基于ESP8266与ITR8307的智能车竞赛光电检测方案优化:抗干扰与远距离检测实践
  • 2026届必备的六大AI辅助论文工具推荐
  • OpenCV实战:用arcLength函数5分钟搞定轮廓周长计算(附完整C++代码)
  • Phi-4-Reasoning-Vision部署教程:解决显存溢出与流式解析混乱的3个关键步骤
  • 多类别语义分割中Loss函数的优化策略与实践
  • Android OTG有线网络终极指南:从硬件兼容到adb命令配置(附主流机型实测)
  • TypeScript数学算法大全:从斐波那契到质数筛法的完整实现