3. 嵌入式图形系统基础
说到图形加速,帧缓冲是个绕不开的基础概念。我刚开始接触嵌入式图形时,总觉得帧缓冲就是个“存像素的地方”,后来踩过几次坑才明白——它远不止这么简单。
说白了,帧缓冲就是一块专门的内存区域,用来存放一帧完整的图像数据。MCU 的显示控制器会以固定频率(比如 60Hz)从这块内存里读取数据,然后通过 LTDC 或 RGB 接口推送到屏幕。你看到的每一帧画面,本质上都是帧缓冲里那堆像素点的“快照”。
3.1 单缓冲的问题
最简单的方案就是单缓冲——整个系统只有一块帧缓冲。MCU 往里面画图,显示控制器同时从里面读数据往外送。
听起来挺合理?但实际跑起来问题就来了。
我在一个车载仪表盘项目里就吃过这个亏。当时用单缓冲跑一个转速表指针动画,指针每转一圈,屏幕中间就闪一道横线。客户看了直接摇头。嗯,从那以后我再也不敢在动画场景里用单缓冲了。
3.2 双缓冲机制
解决撕裂的标准方案就是双缓冲。系统维护两块帧缓冲:
- 前端缓冲(Front Buffer):显示控制器正在读取并送往屏幕的那一帧。只读,不写。
- 后端缓冲(Back Buffer):MCU 正在绘制新内容的那一帧。只写,不读。
当后端缓冲绘制完成后,通过一次“交换”操作,让前端缓冲变成后端,后端变成前端。这样显示控制器永远只读完整的一帧,撕裂自然就消失了。
3.3 缓冲切换策略
双缓冲有了,怎么切换?这里就有讲究了。我见过不少新手直接改指针地址,结果画面闪得更厉害。
3.3.1 指针交换法
这是 TouchGFX 默认的策略。系统维护两个指针:pFront 和 pBack。切换时只需要交换指针值,不需要拷贝任何像素数据。
// 伪代码示意
void swap_buffers() {
uint16_t* temp = pFront;
pFront = pBack;
pBack = temp;
// 告诉显示控制器新的前端缓冲地址
LTDC->CFBAR = (uint32_t)pFront;
}
这个操作是 O(1) 的,几乎不花时间。但有个前提——显示控制器必须在 VBlank(垂直消隐) 期间更新地址,否则还是会出现撕裂。
3.3.2 拷贝切换法
有些硬件不支持动态修改帧缓冲地址,那就只能用拷贝法:后端绘制完成后,把整帧数据拷贝到前端缓冲。
你想想看,如果分辨率是 480x272,颜色深度 16bit,一帧就是 480×272×2 ≈ 261KB。用 DMA 拷贝大概要花几毫秒。对于 60fps 来说,16ms 一帧,这几毫秒还能接受。但如果是 1024x600 甚至更高分辨率,拷贝开销就会明显拖累帧率。
3.3.3 等待 VBlank
无论用哪种切换策略,都必须和 VBlank 同步。VBlank 是显示控制器扫完一帧后、开始扫下一帧前的短暂间隙。在这个间隙里切换缓冲,用户是感知不到的。
TouchGFX 的 OSWrappers::waitForVSync() 就是干这个的。它会阻塞当前任务,直到 VBlank 信号到来。
3.4 三缓冲机制
双缓冲解决了撕裂,但引入了一个新问题:帧率受限。如果绘制一帧的时间刚好超过 16ms(60fps 的周期),那系统就不得不等到下一个 VBlank 才能开始绘制新帧,帧率直接腰斩到 30fps。
三缓冲就是为这个场景准备的。系统维护三块帧缓冲:
| 缓冲 | 角色 | 谁在访问 |
|---|---|---|
| Buffer 0 | 前端缓冲(正在显示) | 显示控制器(只读) |
| Buffer 1 | 就绪缓冲(已完成绘制) | 等待切换(无人访问) |
| Buffer 2 | 后端缓冲(正在绘制) | MCU(只写) |
三缓冲的好处是:即使绘制时间偶尔超过 16ms,系统仍然可以在下一个 VBlank 切换到“就绪缓冲”,保持 60fps 输出。绘制任务可以“超前”绘制,把空闲帧存起来。
代价也很明显——多占一块帧缓冲的内存。对于资源紧张的 MCU 来说,这可能是奢侈的。我个人习惯是:只有 UI 中有复杂动画或频繁重绘的场景才用三缓冲,静态页面双缓冲完全够用。
3.5 TouchGFX 中的缓冲配置
在 TouchGFX 中,缓冲配置是在 TouchGFXConfiguration.cpp 里完成的。关键参数就两个:
frame_buffer_count:缓冲数量(1、2 或 3)frame_buffer_stride:每行像素的字节数(通常等于 宽度 × 每像素字节数)
// TouchGFX 4.x 典型配置
static TouchGFXHAL hal(display, dma, touch, 480, 272,
reinterpret_cast<uint16_t*>(0xC0000000), // 帧缓冲基地址
frame_buffer_stride, // 步长
frame_buffer_count); // 缓冲数量
这里有个细节:frame_buffer_stride 不一定等于 width * bytes_per_pixel。有些硬件要求每行对齐到 4 字节或 8 字节,如果宽度不对齐,就需要填充。我遇到过一块 STM32F769 的板子,LTDC 要求每行 32 字节对齐,480×2=960 刚好不对齐,必须补到 992 字节。当时查了好几天才找到原因。
3.6 缓冲策略的选择建议
说了这么多,到底怎么选?我整理了一个简单的对照表:
| 场景 | 推荐策略 | 理由 |
|---|---|---|
| 静态页面、设置菜单 | 单缓冲 | 没有动画,不会撕裂,省内存 |
| 简单动画、列表滚动 | 双缓冲 + 指针交换 | 消除撕裂,开销极小 |
| 复杂动画、视频播放 | 三缓冲 | 防止绘制超时导致掉帧 |
| 硬件不支持指针交换 | 双缓冲 + DMA 拷贝 | 兼容性优先,性能次之 |
这张图展示了帧缓冲的核心工作流。注意看箭头方向——显示控制器只读前端缓冲,MCU 只写后端缓冲,两者互不干扰。三缓冲时多了一个“就绪缓冲”作为缓冲池,让绘制任务可以提前准备下一帧。