嵌入式 GUI 脏区域局部刷新机制:LVGL 与 ARM-2D 源码级实现对比
屏幕上只有一个小按钮变了,为什么要整屏重画?脏区域(Dirty Region)机制记录哪些区域内容失效,刷新时只重绘这些区域,减少 CPU 绘制和 LCD 传输开销。
两种策略:脏矩形 vs 脏矩阵
脏矩形:记录每个变化区域的精确矩形坐标。区域精确,但合并算法复杂。LVGL 采用此方案。
typedef struct {
int x, y, w, h;
} dirty_rect_t;脏矩阵:把屏幕切成网格,用二维数组记录哪些块脏了。逻辑简单、内存可控,适合低端 MCU。
#define TILE_W 16
#define TILE_H 16
uint8_t dirty_matrix[ROWS][COLS]; // 0=干净, 1=脏下面是一个 320×240 屏幕按 16×16 分块的脏矩阵可视化。按钮区域 (x=30, y=40, w=80, h=48) 发生变化,对应网格被标脏:
标脏计算:
col_start = 30 / 16 = 1 col_end = (30+80-1) / 16 = 6
row_start = 40 / 16 = 2 row_end = (40+48-1) / 16 = 5
→ 标记 row 2~5, col 1~6 共 4×6 = 24 个网格块| 特性 | 脏矩形 | 脏矩阵 |
|---|---|---|
| 精度 | 像素级 | 块级 |
| 内存 | 取决于矩形数量 | 固定 |
| 合并 | 复杂 | 天然连续 |
| 代表 | LVGL、Qt | 部分低端嵌入式 GUI |
全量刷新 vs 局部刷新的 SPI 传输量对比(240×240 RGB565 屏幕):
LVGL 的脏区域实现
核心数据结构
脏区域管理在 lv_display_private.h 中定义:
#define LV_INV_BUF_SIZE 32 // 最多记录 32 个脏区域
struct _lv_display_t {
lv_area_t inv_areas[LV_INV_BUF_SIZE]; // 32 个矩形区域
uint8_t inv_area_joined[LV_INV_BUF_SIZE]; // 合并标记
uint32_t inv_p; // 当前脏区域计数
lv_display_render_mode_t render_mode; // PARTIAL / DIRECT / FULL
uint32_t tile_cnt : 8; // 并行渲染 tile 数量
lv_draw_buf_t *buf_1; // 缓冲区 1
lv_draw_buf_t *buf_2; // 缓冲区 2(双缓冲)
lv_draw_buf_t *buf_act; // 当前活动缓冲区
lv_display_flush_cb_t flush_cb; // 写入 LCD 的回调
};lv_area_t 用四个坐标定义矩形:
typedef struct {
int32_t x1, y1; // 左上角
int32_t x2, y2; // 右下角
} lv_area_t;标脏流程
控件状态变化时,调用链为:
lv_obj_invalidate(obj)
→ obj_invalidate_area_internal(disp, obj, &obj_coords)
→ invalidate_area_core(obj, &area_tmp)
→ lv_inv_area(disp, area_p)lv_inv_area() 核心逻辑(src/core/lv_refr.c):
lv_result_t lv_inv_area(lv_display_t * disp, const lv_area_t * area_p)
{
// 1. 裁剪到屏幕范围
lv_area_t com_area;
if(!lv_area_intersect(&com_area, area_p, &scr_area))
return LV_RESULT_INVALID;
// 2. 全屏模式:直接标记整个屏幕
if(disp->render_mode == LV_DISPLAY_RENDER_MODE_FULL) {
disp->inv_areas[0] = scr_area;
disp->inv_p = 1;
return LV_RESULT_OK;
}
// 3. 去重:如果新区域已被包含,跳过
for(i = 0; i < disp->inv_p; i++) {
if(lv_area_is_in(&com_area, &disp->inv_areas[i], 0))
return LV_RESULT_OK;
}
// 4. 缓冲区满则回退到整屏刷新
if(disp->inv_p >= LV_INV_BUF_SIZE) {
disp->inv_p = 0;
tmp_area_p = &scr_area;
}
lv_area_copy(&disp->inv_areas[disp->inv_p], tmp_area_p);
disp->inv_p++;
}区域合并
刷新前,lv_refr_join_area() 合并重叠或相邻的脏区域:
static void lv_refr_join_area(void)
{
for(join_in = 0; join_in < disp_refr->inv_p; join_in++) {
for(join_from = 0; join_from < disp_refr->inv_p; join_from++) {
if(!lv_area_is_on(&inv_areas[join_in], &inv_areas[join_from]))
continue;
lv_area_join(&joined_area, &inv_areas[join_in], &inv_areas[join_from]);
// 只有合并后面积更小时才合并
if(lv_area_get_size(&joined_area) <
lv_area_get_size(&inv_areas[join_in]) +
lv_area_get_size(&inv_areas[join_from])) {
lv_area_copy(&inv_areas[join_in], &joined_area);
inv_area_joined[join_from] = 1;
}
}
}
}刷新流程
定时器驱动 lv_display_refr_timer():
lv_obj_update_layout() → 更新布局
lv_refr_join_area() → 合并脏区域
refr_sync_areas() → 同步双缓冲区
refr_invalid_areas() → 逐区域渲染
清空 inv_areas[] → 完成在 PARTIAL 模式下,refr_invalid_areas() 将脏区域切分为多个 row band:
int32_t max_row = get_max_row(disp, w, h);
for(row = inv_a.y1; row <= inv_a.y2; row += max_row) {
sub_area = {inv_a.x1, row, inv_a.x2, row+max_row-1};
refr_area(&sub_area, y_off);
draw_buf_flush(disp);
}三种渲染模式
| 模式 | Buffer | 脏区域处理 | RAM | 适用平台 |
|---|---|---|---|---|
| PARTIAL | 小 buffer | 切分为 row band 逐块渲染 | 极低 | STM32/ESP32 |
| DIRECT | 完整 framebuffer | 直接渲染脏区域 | 高 | Linux/高配嵌入式 |
| FULL | 完整 framebuffer | 忽略脏区域,全屏刷新 | 最高 | 电子墨水屏 |
ARM-2D 的脏区域实现
ARM-2D(ARM-software/arm-2d)是 ARM 官方的 2D 图形加速库,专为 Cortex-M 优化。脏区域管理放在 Helper 层,每个 Scene 独立管理。
核心数据结构
typedef struct arm_2d_region_t {
arm_2d_location_t tLocation; // {int16_t iX, iY}
arm_2d_size_t tSize; // {int16_t iWidth, iHeight}
} arm_2d_region_t;arm_2d_tile_t 同时描述区域和像素缓冲区:
struct arm_2d_tile_t {
struct {
uint8_t bIsRoot : 1;
uint8_t bVirtualScreen : 1; // 用于脏区域计算
uint8_t u3ExtensionID : 3;
} tInfo;
arm_2d_region_t tRegion;
union {
arm_2d_tile_t *ptParent; // 父 tile
uint16_t *phwBuffer; // 像素缓冲区
};
};配置参数
在 arm_2d_disp_adapter_0.h 中配置:
#define __DISP0_CFG_OPTIMIZE_DIRTY_REGIONS__ 1 // 启用优化
#define __DISP0_CFG_DIRTY_REGION_POOL_SIZE__ 8 // 脏区域池大小
#define __DISP0_CFG_PFB_BLOCK_WIDTH__ 320 // PFB 块宽度
#define __DISP0_CFG_PFB_BLOCK_HEIGHT__ 24 // PFB 块高度
#define __DISP0_CFG_PFB_HEAP_SIZE__ 1 // PFB 块数量PFB(Partial Frame Buffer)大小 = 320 × 24 × 2 = 15 KB,相比全屏 320 × 240 × 2 = 153 KB,节省 90% 内存。
脏区域池
ARM-2D 使用对象池模式管理脏区域,避免动态内存分配:
arm_2d_region_list_item_t *
__arm_2d_helper_dirty_region_pool_new(arm_2d_helper_pfb_t *ptThis);
static void __arm_2d_helper_dirty_region_pool_free(
arm_2d_helper_pfb_t *ptThis,
arm_2d_region_list_item_t *ptRegion);更新流程
场景级更新,每个控件调用:
arm_2d_helper_dirty_region_update_dirty_regions(
&ptScene->tDirtyRegionHelper,
ptTargetTile,
ptVisibleArea, // 裁剪区域
ptNewRegion); // 新脏区域内部实现(arm_2d_helper_pfb.c):
void __arm_2d_helper_dirty_region_update_dirty_regions2(
arm_2d_helper_dirty_region_t *ptThis,
const arm_2d_tile_t *ptTargetTile,
const arm_2d_region_t *ptVisibleArea,
const arm_2d_region_t *ptNewRegion)
{
__arm_2d_helper_dirty_region_item_update(
&this.tDefaultItem, ptTargetTile, ptVisibleArea, ptNewRegion);
__arm_2d_helper_dirty_region_update_dirty_regions(ptThis, ptTargetTile);
}加权列表优化
ARM-2D 用加权列表决定脏区域处理优先级:
void __arm_2d_helper_add_item_to_weighted_dirty_region_list(
arm_2d_helper_pfb_t *ptThis,
arm_2d_region_list_item_t *ptItem);
arm_2d_region_list_item_t *
__arm_2d_helper_remove_item_from_weighted_dirty_region_list(
arm_2d_helper_pfb_t *ptThis);合并策略使用包围盒优化:当两个区域的包围盒面积小于两者面积之和时,合并为一个刷新区域。
刷新流程
arm_2d_helper_dirty_region_on_frame_start() → 帧开始
控件更新脏区域 → 池分配 + 加权排序
disp_adapter0_task() → 驱动 PFB 刷新
PFB 块绘制(Helium/MVE SIMD 加速) → 像素生成
flush_cb() → LCD(支持异步 DMA) → 传输
脏区域池释放 → 完成LVGL vs ARM-2D 对比
| 特性 | LVGL | ARM-2D |
|---|---|---|
| 脏区域存储 | 固定数组 inv_areas[32] | 对象池 + 链表 |
| 合并策略 | O(n²) 遍历合并 | 加权列表 + 包围盒 |
| 管理粒度 | 全局(per display) | 场景级(per scene) |
| 内存分配 | 静态数组 | 对象池(避免 malloc) |
| 硬件加速 | 软件渲染为主 | Helium/MVE SIMD |
| 适用平台 | 通用(MCU 到 Linux) | Cortex-M 专用 |
有无 Framebuffer 的架构差异
有完整 framebuffer:GUI 绘制到 framebuffer,脏矩阵决定哪些区域 flush 到 LCD。适合 Linux、RTOS + 外部 SRAM。
小 buffer + 脏区域:只有小块 draw buffer,脏区域决定哪些 tile 需要现画。每次刷新重新遍历控件树绘制。适合 STM32/ESP32、SPI LCD。
无 framebuffer 时,GUI 框架保存的是界面描述 + 当前状态(控件树),不是整张像素图。刷新时需要从背景开始,按层级重画该区域内所有相关控件。
工程实践要点
旧位置也要标脏:控件移动时,旧位置和新位置都需要标脏。很多残影问题就是因为忘记标脏旧位置。
脏区域要适当扩大:阴影、边框、抗锯齿的实际影响区域可能比控件本身大。LVGL 中 invalidate_area_core() 会自动扩大 5 像素。
不要每次标脏就立即刷新:多次标脏后统一刷新,效率更高。
缓冲区满时回退整屏刷新:LVGL 的 LV_INV_BUF_SIZE = 32,超过时 inv_p 重置为 0 并标脏整个屏幕。
常用 API 速查
LVGL:
| 函数 | 作用 |
|---|---|
lv_obj_invalidate(obj) |
标脏整个控件 |
lv_obj_invalidate_area(obj, area) |
标脏指定区域 |
lv_inv_area(disp, area) |
底层标脏 |
lv_refr_join_area() |
合并重叠区域 |
lv_display_refr_timer() |
定时刷新入口 |
ARM-2D:
| 函数 | 作用 |
|---|---|
arm_2d_helper_dirty_region_update_dirty_regions() |
更新脏区域 |
arm_2d_helper_dirty_region_on_frame_start() |
帧开始初始化 |
arm_2d_helper_pfb_enable_dirty_region_optimization() |
启用优化 |
disp_adapter0_task() |
显示适配器任务 |
本文由 AI 辅助生成,可能存在错误或遗漏,请以实际资料和官方文档为准。