嵌入式 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) 发生变化,对应网格被标脏:

320×240 屏幕 · 16×16 分块 · 20×15 网格 按钮 0 1 2 3 4 5 6 7 19 0 1 2 3 4 5 6 14 ← col 1~6 (x: 16~112px) → ← row 2~5 →
脏网格 24/300 = 8% 干净网格 按钮实际区域

标脏计算:

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 屏幕):

SPI 传输量对比(240×240 RGB565) 全量刷新 115,200 bytes (240×240×2) 局部刷新 6,400 bytes (80×40×2) 局部刷新仅需 5.6% 的数据量,差 18 倍 SPI 40MHz 下:全量 ~23ms vs 局部 ~1.3ms(不含绘制开销)

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);
}
LVGL PARTIAL 模式:脏区域 → row band 切分 脏区域 inv_a (x1,y1)~(x2,y2) 整个屏幕 切分 Row Band 切分结果 Band 0: row 0~max_row-1 → refr + flush Band 1: row max_row~2*max_row-1 → refr + flush Band 2: row 2*max_row~3*max_row-1 → refr + flush ··· Band N: row N*max_row~y2 (剩余) → refr + flush 每个 band 独立渲染 + flush 到 LCD max_row = draw_buf_size / stride

三种渲染模式

模式 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 辅助生成,可能存在错误或遗漏,请以实际资料和官方文档为准。