2. 帧缓冲区原理:Framebuffer 的工作机制与像素格式详解

帧缓冲区,英文叫 Framebuffer。说白了,它就是显存里的一块区域,专门用来存放一帧画面的像素数据。MCU 内部的 LTDC(LCD 控制器)或者 DMA2D 会不停地从这块区域读取数据,然后刷新到屏幕上。

我刚开始接触 TouchGFX 时,总觉得花屏是代码逻辑写错了。后来排查了三天,才发现是帧缓冲区的配置出了问题。嗯,从那以后,我对这块就特别上心。

2.1 帧缓冲区的工作机制

你可以把帧缓冲区想象成一个“画板”。CPU 或者 GPU 在上面画画,而屏幕控制器则不停地复制这个画板的内容到显示屏上。整个过程是周期性的,大概像这样:

核心流程:

  1. 写入阶段: TouchGFX 的渲染引擎将控件、文本、图片绘制到帧缓冲区。
  2. 同步阶段: LTDC 控制器通过 DMA 方式,从帧缓冲区读取像素数据。
  3. 刷新阶段: 数据通过 RGB 接口或 LTDC 接口传输到屏幕驱动 IC。
  4. 显示阶段: 屏幕逐行扫描,将电信号转换为可见光。

这里有个关键点:如果 CPU 正在往帧缓冲区写数据,而 LTDC 同时也在读,就会出现“撕裂”现象——画面一半是新的,一半是旧的。这就是花屏的常见原因之一。

帧缓冲区工作机制流程图 TouchGFX 渲染引擎 绘制控件/文本/图片 帧缓冲区 SDRAM / SRAM LTDC 控制器 DMA 读取 写入像素 读取像素 显示驱动 IC RGB / MIPI 接口 LCD 屏幕 逐行扫描显示 LTDC 输出 信号传输 ⚠ 注意:读写冲突 CPU写 + LTDC读 = 撕裂/花屏

我的经验: 在 STM32F429 上做项目时,我习惯开启 LTDC 的 FIFO 阈值中断,配合双缓冲机制来避免撕裂。单缓冲情况下,帧率稍微一高就容易出问题。

2.2 RGB565 与 ARGB8888 格式详解

帧缓冲区里存的数据不是“颜色”,而是“像素值”。不同的像素格式决定了每个像素占用多少 bit,以及颜色精度。TouchGFX 最常用的就是 RGB565 和 ARGB8888。

2.2.1 RGB565 格式

每个像素占用 16 bit(2 字节)。红色占 5 bit,绿色占 6 bit,蓝色占 5 bit。为什么绿色多 1 bit?因为人眼对绿色最敏感,多一位能减少色阶断层。

颜色分量 位数 取值范围 内存布局(高→低)
红色 (R) 5 bit 0 ~ 31 bit[15:11]
绿色 (G) 6 bit 0 ~ 63 bit[10:5]
蓝色 (B) 5 bit 0 ~ 31 bit[4:0]

举个例子,纯红色(R=31, G=0, B=0)在内存里就是 0xF800。你想想看,一个 16 位的值就能表示 65536 种颜色,对于大多数嵌入式 UI 来说完全够用。

// RGB565 像素操作示例
uint16_t rgb565_pixel(uint8_t r, uint8_t g, uint8_t b) {
    return ((r & 0x1F) << 11) | ((g & 0x3F) << 5) | (b & 0x1F);
}

注意: RGB565 不支持透明度(Alpha)。如果你需要做透明叠加效果,比如按钮悬停、图标半透明,RGB565 会直接忽略 Alpha 通道,导致显示异常。我曾经在做一个菜单动画时踩过这个坑,按钮边缘全是锯齿。

2.2.2 ARGB8888 格式

每个像素占用 32 bit(4 字节)。Alpha(透明度)占 8 bit,红、绿、蓝各占 8 bit。颜色精度更高,支持 256 级透明度。

颜色分量 位数 取值范围 内存布局(高→低)
Alpha (A) 8 bit 0 ~ 255 bit[31:24]
红色 (R) 8 bit 0 ~ 255 bit[23:16]
绿色 (G) 8 bit 0 ~ 255 bit[15:8]
蓝色 (B) 8 bit 0 ~ 255 bit[7:0]
// ARGB8888 像素操作示例
uint32_t argb8888_pixel(uint8_t a, uint8_t r, uint8_t g, uint8_t b) {
    return ((uint32_t)a << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b;
}

什么时候用 ARGB8888?

  • 需要 Alpha 混合(如阴影、渐变、半透明控件)
  • 颜色精度要求高(如显示照片、医疗图像)
  • MCU 有足够 RAM 或外挂 SDRAM

2.3 缓冲区大小计算

这是最容易算错的地方。我见过不少工程师把分辨率乘以 2 就完事了,结果花屏了还不知道为什么。

公式很简单:

缓冲区大小(字节) = 宽度 × 高度 × 每像素字节数

每像素字节数:

  • RGB565: 2 字节
  • ARGB8888: 4 字节

举个例子,一块 480×272 的屏幕:

格式 计算公式 大小
RGB565 480 × 272 × 2 261,120 字节 ≈ 255 KB
ARGB8888 480 × 272 × 4 522,240 字节 ≈ 510 KB

如果是双缓冲,大小直接翻倍。三缓冲的话,再翻 1.5 倍。我建议你一开始就按最大可能来预留内存,否则后期加功能时发现缓冲区不够,改起来很痛苦。

避坑指南: 我曾经在 STM32F746 上做 800×480 的 ARGB8888 双缓冲,算出来需要 800×480×4×2 = 3,072,000 字节 ≈ 3 MB。当时没注意 SDRAM 的地址对齐,结果 LTDC 读出来的数据全是错位的,画面像万花筒一样。后来发现帧缓冲区的起始地址必须 32 字节对齐。

2.4 格式选择建议

我个人习惯这样选:

  • RAM 紧张、不需要透明效果: RGB565,省一半内存
  • 需要透明、动画流畅: ARGB8888,配合 DMA2D 硬件加速
  • 混合使用: 主缓冲区用 RGB565,小区域用 ARGB8888 做 overlay

你想想看,如果屏幕分辨率不高,比如 320×240,RGB565 单缓冲才 150 KB,随便一个 MCU 都能跑。但要是上了 1024×600 的 ARGB8888 双缓冲,那就是 4.8 MB,没有 SDRAM 基本没戏。

最后提醒一句: 不管选哪种格式,一定要确认 LTDC 的像素时钟(Pixel Clock)和帧缓冲区的带宽是否匹配。否则即使缓冲区大小算对了,刷新率上不去,画面还是会闪。