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,问题才解决。

四种策略怎么选?一张图说清楚

下面这张图是我自己总结的决策流程,帮你快速定位该用哪种策略。

Framebuffer 策略决策流程 开始选择策略 画面撕裂敏感? 否 单缓冲 是 双缓冲 内存够吗? 够 双缓冲 + DMA2D 不够 局部刷新 + DMA2D 提示:DMA2D 在单缓冲和局部刷新模式下同样可以加速像素搬运,建议只要硬件支持就开启

实际配置怎么做?

在 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。三步走完,答案自然就出来了。