Framebuffer 优化策略:单缓冲、双缓冲、局部刷新与 DMA2D 加速
说到 TouchGFX 的内存占用,Framebuffer(帧缓冲)绝对是头号大户。我见过不少项目,UI 逻辑写得挺漂亮,结果一算内存,光 Framebuffer 就吃掉大半块 SRAM,剩下的连个像样的控件都放不下。这一章咱们就专门啃这块硬骨头。
说白了,Framebuffer 就是一块显存,MCU 把要显示的像素数据往里面写,LCD 驱动从里面读,配合好了画面就出来了。但怎么配、配多大、用几个,这里面的门道可不少。
单缓冲 vs 双缓冲:一个够用还是两个更稳?
先看最基础的两种模式。单缓冲,顾名思义,整个系统只有一块 Framebuffer。MCU 往里面画图的时候,LCD 控制器同时从里面读数据往外刷。你想想看,这就像一条单车道,画图的车和刷屏的车共用一条路,稍不留神就撞上了——画面撕裂(Tearing Effect)就是这么来的。
双缓冲就好办多了。一块 Front Buffer 专门给 LCD 读,一块 Back Buffer 给 MCU 写。写完了,通过 VSync 信号瞬间切换指针,LCD 读到的是完整的新画面。撕裂?不存在的。
但代价也很直接——内存翻倍。一个 480×272 的 16 位色屏,单缓冲只要 480×272×2 ≈ 261 KB,双缓冲直接干到 522 KB。对于内部 SRAM 只有 512 KB 甚至 256 KB 的 MCU,这简直是灾难。
核心结论:双缓冲解决撕裂,但内存翻倍。如果你的应用对画面撕裂不敏感(比如静态仪表盘、慢速菜单),单缓冲完全够用。我做过一个工业 HMI 项目,用的就是单缓冲 + 局部刷新,效果很稳。
| 模式 | 内存占用 | 画面撕裂 | 典型场景 |
|---|---|---|---|
| 单缓冲 | 1× Framebuffer | 可能发生 | 静态界面、慢速刷新 |
| 双缓冲 | 2× Framebuffer | 完全消除 | 视频播放、动画过渡 |
| 局部刷新 | 1× Framebuffer + 小块缓存 | 可控 | 局部变化、数字更新 |
局部刷新:省内存的利器
双缓冲太费内存,单缓冲又怕撕裂。有没有折中方案?有,局部刷新(Partial Refresh)。
这个思路其实很朴素:整个屏幕大部分区域是不动的,只有一小块区域在变化。比如一个数字时钟,只有中间那几位数字在跳,周围背景纹丝不动。那我何必把整块 Framebuffer 都重画一遍?只更新变化的那一小块矩形区域就行了。
TouchGFX 的局部刷新机制,说白了就是维护一个 Dirty Rectangle(脏矩形)列表。每次绘制时,只把变化区域的数据写入 Framebuffer,LCD 控制器也只从对应位置读取。这样一来,内存占用还是单缓冲的量,但撕裂只发生在脏矩形内部,视觉上几乎不可察觉。
我的经验:局部刷新最适合「大面积静态 + 小面积动态」的界面。我曾经在一个 800×480 的屏上做温控面板,整个界面只有温度数值和进度条在变,用局部刷新后 Framebuffer 还是单缓冲的大小,但画面流畅度跟双缓冲几乎没区别。不过要注意,如果全屏都在频繁变化(比如视频播放),局部刷新的优势就没了,反而会因为频繁计算脏矩形增加 CPU 开销。
DMA2D 加速:让硬件帮你干活
前面说的都是内存策略,但光省内存还不够,你还得考虑性能。尤其是局部刷新,每次要计算脏矩形、搬运像素数据,CPU 忙得团团转。这时候就该 DMA2D 出场了。
DMA2D 是 STM32 系列 MCU 里一个专门做 2D 图形加速的外设。它能在后台完成像素搬运、颜色格式转换、Alpha 混合这些操作,完全不占 CPU 核心。你想想看,CPU 只需要告诉 DMA2D「从地址 A 搬 N 个像素到地址 B,格式从 RGB565 转 ARGB8888」,然后就可以去干别的了。等 DMA2D 干完,触发一个中断通知你,完美。
在 TouchGFX 里启用 DMA2D 很简单,但前提是你的 MCU 得支持。我记得有一次帮客户调一个 STM32F429 的项目,他们死活觉得 UI 卡顿,我一看配置,DMA2D 根本没开,所有像素操作都是 CPU 硬扛的。打开之后,帧率直接翻倍。
注意:DMA2D 虽然快,但它操作的是物理地址。如果你的 Framebuffer 放在外部 SDRAM 里,DMA2D 访问 SDRAM 的速度取决于总线时钟和 SDRAM 时序。我踩过这个坑——外部 SDRAM 配得太慢,DMA2D 反而成了瓶颈。后来把 SDRAM 时钟从 90 MHz 提到 120 MHz,问题才解决。
四种策略怎么选?一张图说清楚
下面这张图是我自己总结的决策流程,帮你快速定位该用哪种策略。
实际配置怎么做?
在 TouchGFX Designer 里,这些策略的配置集中在项目设置面板。我习惯先确定 Framebuffer 数量,再选传输方式。
// TouchGFX HAL 配置示例(局部刷新 + DMA2D)
static TouchGFXHAL hal;
void touchgfx_init()
{
// 设置 Framebuffer 策略
hal.setFrameBufferStrategy(FRAME_BUFFER_STRATEGY_PARTIAL_BUFFER);
// 设置局部缓冲区大小(建议 1/4 屏幕高度)
hal.setPartialBufferSize(480 * 136 * 2); // 480×136×2 bytes
// 启用 DMA2D
hal.setDMATransfer(&dma2d_transfer);
// 注册帧缓冲
hal.setFrameBufferBeginAddress((uint16_t*)0xC0000000); // 外部 SDRAM 地址
}
这里有个细节:局部缓冲区的大小不是随便设的。设太大,省内存的效果就没了;设太小,频繁切换缓冲区反而增加开销。我个人建议取屏幕高度的 1/4 到 1/2,具体看你界面的变化区域大小。
避坑指南:我曾经在一个项目里把局部缓冲区设成整屏大小,结果跟单缓冲没区别,白忙活一场。后来改成 1/4 屏高,内存从 522 KB 降到 130 KB,效果立竿见影。另外,DMA2D 的中断优先级别忘了配,我见过有人配得太低,导致 DMA2D 传输被其他中断频繁打断,画面出现奇怪的条纹。
总结一下这四种策略
- 单缓冲:最省内存,适合静态或慢速界面。如果对撕裂不敏感,这是首选。
- 双缓冲:消除撕裂,但内存翻倍。适合动画、视频等需要流畅更新的场景。
- 局部刷新:单缓冲的内存 + 接近双缓冲的效果。适合「大面积静态 + 小面积动态」的界面。
- DMA2D 加速:不是独立策略,而是给上面三种策略加 Buff。只要硬件支持,建议无条件开启。
选哪种,说到底就是内存和性能的权衡。我的建议是:先评估你的界面变化频率和撕裂容忍度,再量一下剩余内存,最后看硬件支不支持 DMA2D。三步走完,答案自然就出来了。