4. 内存与显存优化:内存池管理、显存带宽优化、纹理压缩格式、内存对齐策略

嵌入式GUI开发,说白了就是一场与有限资源的博弈。CPU主频不高,RAM就那么几百KB,显存更是挤牙膏。很多开发者把PC端的开发习惯带过来,结果一跑就崩。我早期做智能手表项目时就吃过这个亏——界面刚渲染完,内存直接爆了。

这一章,咱们就聊聊怎么把每一字节都用在刀刃上。

4.1 内存池管理:告别碎片化

嵌入式系统里,频繁的malloc/free是灾难。你想想看,GUI里控件创建、销毁、位图加载、释放,这些操作每天都在发生。用标准堆分配器,时间一长,内存碎片就像河道里的淤泥,越积越厚。

核心思路:预分配一大块连续内存,自己管理。按固定大小分块,需要时直接拿,用完还回去。没有碎片,速度还快。

我习惯用伙伴算法(Buddy System)做内存池。它把内存按2的幂次分成块,分配时找最小满足的块,释放时合并相邻空闲块。举个例子:

/* 简易内存池结构 */
typedef struct {
    uint8_t *pool;          /* 内存池基址 */
    uint32_t total_size;    /* 总大小 */
    uint32_t block_size;    /* 最小块大小 */
    uint32_t free_list;     /* 空闲链表头 */
} mem_pool_t;

/* 初始化:将整块内存切成2^n大小的块 */
void mem_pool_init(mem_pool_t *mp, void *buf, uint32_t size, uint32_t min_block) {
    mp->pool = (uint8_t *)buf;
    mp->total_size = size;
    mp->block_size = min_block;
    /* 建立空闲链表 */
    uint32_t blocks = size / min_block;
    for (uint32_t i = 0; i < blocks - 1; i++) {
        *(uint32_t *)(mp->pool + i * min_block) = (i + 1) * min_block;
    }
    *(uint32_t *)(mp->pool + (blocks - 1) * min_block) = 0xFFFFFFFF; /* 结束标记 */
    mp->free_list = 0;
}

/* 分配:从空闲链表头部取一块 */
void *mem_pool_alloc(mem_pool_t *mp) {
    if (mp->free_list == 0xFFFFFFFF) return NULL;
    uint32_t addr = mp->free_list;
    mp->free_list = *(uint32_t *)(mp->pool + addr);
    return mp->pool + addr;
}

/* 释放:把块放回空闲链表头部 */
void mem_pool_free(mem_pool_t *mp, void *ptr) {
    uint32_t offset = (uint8_t *)ptr - mp->pool;
    *(uint32_t *)ptr = mp->free_list;
    mp->free_list = offset;
}

我的经验:内存池的块大小要根据GUI对象尺寸来定。比如按钮、标签这类小控件用32字节块,位图缓存用256字节块。我曾经在一个项目里分了三个不同大小的池,碎片率从15%降到了0.3%。

4.2 显存带宽优化:别让像素堵在路上

显存带宽是GUI性能的咽喉。你想想看,一个800×480的屏幕,16位色,每秒刷新60帧,数据量是800×480×2×60 ≈ 46MB/s。如果MCU的LTDC接口只有50MB/s带宽,那基本跑满了。

怎么优化?我总结了三个方向:

  • 减少无效传输:只更新变化区域(Dirty Rect)。别整屏刷新,只画变化的部分。
  • 利用硬件加速:很多MCU有2D图形加速器(如DMA2D),可以自动做颜色格式转换、Alpha混合。别用CPU去逐像素操作。
  • 双缓冲 vs 单缓冲:双缓冲能避免撕裂,但多占一倍显存。如果显存紧张,可以用局部双缓冲——只对变化区域开一个小的后备缓冲区。

我曾经踩过的坑:在一个项目里,我用了双缓冲,但忘了把两个缓冲区的地址放在不同的SDRAM行中。结果每次切换缓冲区都触发行刷新,带宽直接腰斩。后来我把两个缓冲区放在不同的Bank里,性能立刻翻倍。

4.3 纹理压缩格式:RGB565与ARGB1555

嵌入式GUI里,位图是显存大户。一张800×480的全屏背景图,用ARGB8888格式要1.5MB。很多MCU的显存总共才2MB,一张图就快占满了。

这时候就得用压缩格式。我常用的两种:

格式 每像素位数 颜色数 透明度 适用场景
RGB565 16 65536 无 照片、渐变背景、图标
ARGB1555 16 32768 1位(透明/不透明) 按钮、光标、带透明区域的图标

RGB565用5位红、6位绿、5位蓝。人眼对绿色最敏感,所以绿色多给1位。实际效果和24位色差别不大,但内存省了三分之一。

ARGB1555用1位透明度,要么全透明要么全不透明。适合做不规则形状的控件——比如圆形按钮,边缘像素设成透明,中间设成不透明。

转换示例:把ARGB8888转成RGB565

/* ARGB8888 -> RGB565 */
uint16_t argb8888_to_rgb565(uint32_t pixel) {
    uint8_t r = (pixel >> 16) & 0xFF;
    uint8_t g = (pixel >> 8) & 0xFF;
    uint8_t b = pixel & 0xFF;
    return ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3);
}

/* ARGB8888 -> ARGB1555 */
uint16_t argb8888_to_argb1555(uint32_t pixel) {
    uint8_t a = (pixel >> 24) & 0xFF;
    uint8_t r = (pixel >> 16) & 0xFF;
    uint8_t g = (pixel >> 8) & 0xFF;
    uint8_t b = pixel & 0xFF;
    uint16_t alpha_bit = (a > 128) ? 0x8000 : 0x0000;
    return alpha_bit | ((r >> 3) << 10) | ((g >> 3) << 5) | (b >> 3);
}

我的建议:如果UI里图标多,可以做一个离线工具,在编译前把所有PNG转成RGB565数组。运行时直接加载,省去解码时间。我做过一个项目,这样优化后启动时间从3秒降到了0.8秒。

4.4 内存对齐策略:让CPU跑得更顺

很多嵌入式CPU访问未对齐的数据会触发异常,或者性能骤降。比如Cortex-M4,访问一个32位整数,如果地址不是4的倍数,会触发UsageFault。就算不触发,也要多花好几个时钟周期。

GUI里到处都是结构体——控件属性、坐标、颜色值。如果不对齐,性能损失很大。

我常用的对齐策略:

  • 结构体成员按大小降序排列:把大的成员(如指针、32位整数)放前面,小的放后面。这样编译器填充的padding最少。
  • 显存缓冲区地址对齐:DMA传输通常要求源地址和目标地址对齐到4字节或8字节。我习惯把显存缓冲区对齐到32字节(Cache Line大小)。
  • 使用__attribute__((aligned)):GCC和ARMCC都支持。
/* 对齐声明示例 */
uint8_t framebuffer[320 * 240 * 2] __attribute__((aligned(32)));

/* 结构体对齐优化 */
typedef struct {
    void   *callback;   /* 4字节 */
    uint32_t color;     /* 4字节 */
    uint16_t x, y;      /* 2+2=4字节 */
    uint8_t  alpha;     /* 1字节 */
    uint8_t  flags;     /* 1字节 */
    /* 这里编译器会自动填充2字节到4字节边界 */
} __attribute__((packed)) gui_widget_t;

注意:用了__attribute__((packed))后,结构体成员可能变成非对齐访问。如果成员里有指针或32位整数,访问时会变慢。我一般只在结构体需要序列化到文件或网络时用packed,平时尽量靠成员排序来减少padding。

4.5 本章知识体系

下面这张图总结了内存与显存优化的四个核心方向,以及它们之间的关系:

内存与显存优化知识体系 内存与显存优化 内存池管理 显存带宽优化 纹理压缩格式 内存对齐策略 伙伴算法 固定块分配 Dirty Rect DMA2D加速 RGB565 ARGB1555 结构体排序 Cache Line对齐 内存池减少碎片 分配时间O(1) 压缩格式省显存 带宽需求降50%

这四个方向不是孤立的。比如你用RGB565压缩纹理,显存带宽需求就降下来了;你做好内存对齐,DMA传输效率就上去了。在实际项目中,我一般先做内存池和纹理压缩,这两项收益最明显。然后再根据性能瓶颈,决定要不要优化对齐和带宽。

总结一下:内存和显存是嵌入式GUI的命脉。管好它们,你的UI才能跑得稳、跑得快。别等到卡顿了才去优化,设计阶段就把这些策略考虑进去。