2. 帧缓冲区原理:Framebuffer 的工作机制与像素格式详解
帧缓冲区,英文叫 Framebuffer。说白了,它就是显存里的一块区域,专门用来存放一帧画面的像素数据。MCU 内部的 LTDC(LCD 控制器)或者 DMA2D 会不停地从这块区域读取数据,然后刷新到屏幕上。
我刚开始接触 TouchGFX 时,总觉得花屏是代码逻辑写错了。后来排查了三天,才发现是帧缓冲区的配置出了问题。嗯,从那以后,我对这块就特别上心。
2.1 帧缓冲区的工作机制
你可以把帧缓冲区想象成一个“画板”。CPU 或者 GPU 在上面画画,而屏幕控制器则不停地复制这个画板的内容到显示屏上。整个过程是周期性的,大概像这样:
核心流程:
- 写入阶段: TouchGFX 的渲染引擎将控件、文本、图片绘制到帧缓冲区。
- 同步阶段: LTDC 控制器通过 DMA 方式,从帧缓冲区读取像素数据。
- 刷新阶段: 数据通过 RGB 接口或 LTDC 接口传输到屏幕驱动 IC。
- 显示阶段: 屏幕逐行扫描,将电信号转换为可见光。
这里有个关键点:如果 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)和帧缓冲区的带宽是否匹配。否则即使缓冲区大小算对了,刷新率上不去,画面还是会闪。