3. 帧缓冲(FrameBuffer)管理:单缓冲与双缓冲机制、显存分配策略、DMA加速刷屏、局部刷新与脏矩形算法
帧缓冲管理,说白了就是解决「画布怎么铺、怎么画、怎么刷到屏幕上」的问题。我早年做第一个GUI移植时,觉得这玩意儿不就是个内存块嘛,结果被撕裂、闪烁、刷屏慢折磨得够呛。今天咱们把这几个核心机制掰开揉碎讲清楚。
3.1 单缓冲与双缓冲机制
先看最基础的概念——缓冲模式。你想想看,MCU往显存里写像素数据,LCD控制器同时从显存读数据刷新屏幕,这俩操作如果共用一块内存,冲突就来了。
3.1.1 单缓冲(Single Buffer)
系统只有一块显存。CPU写数据时,LCD控制器可能正在读同一块内存。结果就是:屏幕上出现「撕裂」——上半部分是新画面,下半部分还是旧的。
适用场景:低分辨率、静态界面、对显示质量要求不高的产品。
典型问题:画面撕裂、闪烁(尤其全屏更新时)。
我在一个温控器项目里用过单缓冲。屏幕只有128×64,刷新率低,撕裂不明显。但换到320×240的彩屏后,肉眼可见的撕裂线,客户直接退货。嗯,从那以后我再也不敢在彩屏上用单缓冲了。
3.1.2 双缓冲(Double Buffer)
分配两块显存:前端缓冲(Front Buffer)和后端缓冲(Back Buffer)。LCD控制器只读前端缓冲,CPU只写后端缓冲。画完后,通过一个「交换」操作(Flip/Swap)把前后端角色互换。
| 特性 | 单缓冲 | 双缓冲 |
|---|---|---|
| 内存占用 | 1×显存大小 | 2×显存大小 |
| 画面撕裂 | 有 | 无(垂直同步配合) |
| 实现复杂度 | 低 | 中 |
| 适用场景 | 低端、静态 | 动画、视频、交互频繁 |
我的习惯:只要RAM够用,一律上双缓冲。多花几十KB内存,换来的是用户肉眼可见的流畅体验,这笔账怎么算都划算。
3.2 显存分配策略
显存怎么分?不是简单malloc一块内存就完事。嵌入式系统里,显存位置、对齐方式、连续性问题,直接影响刷屏性能。
3.2.1 分配位置
- 内部SRAM:速度快,但容量小。适合小尺寸屏幕(< 320×240)。
- 外部SDRAM:容量大,但速度慢。适合大尺寸屏幕,但要注意DMA传输效率。
- LTDC/DSI专用内存:部分MCU有专用显存接口,直接连到LCD控制器,效率最高。
3.2.2 对齐要求
很多LCD控制器要求显存地址按4字节、8字节甚至16字节对齐。我踩过一个坑:用malloc分配显存,地址没对齐,结果刷屏时颜色错位,查了两天才发现是地址问题。
// 正确做法:使用对齐分配
// 以STM32为例,使用DMA2D时要求32字节对齐
#define FRAMEBUFFER_ALIGN 32
static uint8_t fb_back[FRAMEBUFFER_SIZE] __attribute__((aligned(FRAMEBUFFER_ALIGN)));
static uint8_t fb_front[FRAMEBUFFER_SIZE] __attribute__((aligned(FRAMEBUFFER_ALIGN)));
注意:不要用动态分配(malloc)管理显存。显存需要长期占用、地址固定、对齐可控。静态数组或内存池才是正解。
3.3 DMA加速刷屏
CPU逐像素拷贝显存?太慢了。DMA(直接存储器访问)可以解放CPU,让数据搬运在后台完成。
3.3.1 为什么需要DMA
假设屏幕分辨率800×480,颜色深度16位(RGB565)。一帧数据量:800×480×2 = 768,000字节。CPU主频200MHz,逐字节拷贝大约需要几毫秒。如果加上颜色转换、alpha混合,CPU占用率直接拉满。
DMA可以做到:CPU发个启动命令,然后去干别的,DMA自己把数据搬完再触发中断。
3.3.2 常用DMA引擎
| DMA类型 | 特点 | 典型MCU |
|---|---|---|
| 普通DMA | 内存→内存,逐字节/字拷贝 | STM32F4/F7 DMA2 |
| DMA2D(Chrom-ART) | 支持颜色格式转换、alpha混合、图像旋转 | STM32F7/H7 |
| PXP(Pixel Pipeline) | 类似DMA2D,支持缩放、旋转、颜色空间转换 | NXP i.MX RT系列 |
我的经验:DMA2D是STM32上刷屏的神器。一次DMA2D传输可以同时完成「从后端缓冲到前端缓冲的拷贝」+「RGB565转RGB888」+「alpha混合」。CPU只需要设置寄存器,然后等中断。
// DMA2D 刷屏示例(伪代码)
void dma2d_copy_fb(void *src, void *dst, uint32_t size) {
// 等待DMA2D空闲
while (DMA2D->CR & DMA2D_CR_START);
// 配置源地址、目标地址、传输大小
DMA2D->FGMAR = (uint32_t)src; // 源地址
DMA2D->OMAR = (uint32_t)dst; // 目标地址
DMA2D->NLR = (height << 16) | width; // 行列数
DMA2D->FGPFCCR = DMA2D_FGPFCCR_CM_RGB565; // 源格式
DMA2D->OPFCCR = DMA2D_OPFCCR_CM_RGB565; // 目标格式
// 启动传输
DMA2D->CR |= DMA2D_CR_START;
// 可以去做其他事了,传输完成中断会通知
}
3.4 局部刷新与脏矩形算法
全屏刷新?那是杀鸡用牛刀。大多数GUI更新只涉及屏幕的一小部分。比如按钮按下、滑块拖动、文本更新。局部刷新可以大幅降低刷屏带宽和CPU占用。
3.4.1 脏矩形(Dirty Rectangle)原理
核心思想:只更新「发生变化」的区域。GUI框架维护一个或多个矩形区域(脏矩形),每次绘制操作后,把受影响区域标记为「脏」。刷屏时,只把这些脏矩形对应的显存区域拷贝到前端缓冲。
3.4.2 脏矩形合并策略
多个脏矩形如果靠得太近,合并成一个大的矩形反而更高效。为什么?因为DMA传输有启动开销,传输一个大块比传输多个小块更快。
- 不合并:每个控件独立刷新,适合控件分散、更新频率低的情况。
- 合并:将相邻或重叠的脏矩形合并成最小外接矩形,适合密集更新场景。
- 全屏回退:如果脏矩形面积超过屏幕的70%,干脆全屏刷新,省去合并计算开销。
我建议:在低端MCU上,优先用「不合并+单脏矩形」策略。每次只更新一个矩形区域,实现简单,内存开销小。等性能有富余了再上合并算法。
3.4.3 脏矩形实现要点
// 脏矩形结构体
typedef struct {
int16_t x, y; // 左上角坐标
uint16_t w, h; // 宽度和高度
} dirty_rect_t;
// 脏矩形管理器
typedef struct {
dirty_rect_t rects[MAX_DIRTY_RECTS];
uint8_t count;
} dirty_manager_t;
// 标记脏区域
void mark_dirty(dirty_manager_t *mgr, int16_t x, int16_t y, uint16_t w, uint16_t h) {
if (mgr->count >= MAX_DIRTY_RECTS) {
// 溢出:标记全屏脏
mgr->rects[0] = (dirty_rect_t){0, 0, SCREEN_WIDTH, SCREEN_HEIGHT};
mgr->count = 1;
return;
}
mgr->rects[mgr->count++] = (dirty_rect_t){x, y, w, h};
}
// 执行局部刷新
void flush_dirty(dirty_manager_t *mgr, uint8_t *fb_back, uint8_t *fb_front) {
for (int i = 0; i < mgr->count; i++) {
dirty_rect_t *r = &mgr->rects[i];
// 计算显存偏移
uint32_t offset = (r->y * SCREEN_WIDTH + r->x) * BYTES_PER_PIXEL;
uint32_t line_bytes = r->w * BYTES_PER_PIXEL;
// 逐行拷贝(或用DMA2D)
for (int row = 0; row < r->h; row++) {
memcpy(fb_front + offset + row * SCREEN_WIDTH * BYTES_PER_PIXEL,
fb_back + offset + row * SCREEN_WIDTH * BYTES_PER_PIXEL,
line_bytes);
}
}
mgr->count = 0; // 清空脏矩形列表
}
我曾经踩过的坑:脏矩形坐标计算时,忘了考虑控件边框和阴影的扩展区域。结果按钮按下后,阴影部分没刷新,屏幕上留下一块「鬼影」。后来我统一在脏矩形周围加2-4像素的padding,问题解决。
3.5 综合策略建议
不同硬件配置,帧缓冲管理策略完全不同。我整理了一张决策表,供你参考:
| 硬件条件 | 缓冲模式 | 刷屏方式 | 刷新策略 |
|---|---|---|---|
| RAM < 64KB,屏幕 < 320×240 | 单缓冲 | CPU逐行拷贝 | 全屏刷新 |
| RAM 64KB~256KB,屏幕 320×240 | 双缓冲 | DMA内存拷贝 | 脏矩形(不合并) |
| RAM > 256KB,屏幕 800×480 | 双缓冲 | DMA2D/PXP硬件加速 | 脏矩形(合并优化) |
| 带专用显存控制器 | 三缓冲(可选) | 硬件自动刷新 | 脏矩形+垂直同步 |
帧缓冲管理没有银弹。你得根据硬件资源、屏幕尺寸、交互复杂度来权衡。我个人习惯是:先上双缓冲+脏矩形,这是性价比最高的组合。如果内存紧张,再考虑降级到单缓冲,但一定要做好撕裂补偿(比如只在垂直消隐期更新)。