2. 帧缓冲区原理:显存映射、像素格式、颜色深度、行对齐

帧缓冲区,说白了就是一块专门给显示用的内存区域。CPU 往里面写数据,显示控制器从里面读数据,然后刷到屏幕上。我刚开始接触嵌入式图形时,总觉得这玩意儿不就是个数组嘛,有啥好研究的?后来被坑过几次才明白——里面的门道真不少。

2.1 显存映射:CPU 和显示控制器怎么“握手”

帧缓冲区在物理上就是一段连续的 RAM。它可能位于系统内存中,也可能在专用的显存里。关键问题是:CPU 和显示控制器怎么共享这块区域?

我习惯把显存映射分成两种模式:

  • 直接映射:帧缓冲区地址直接落在 CPU 的地址空间里。CPU 像访问普通数组一样读写,显示控制器也同时从同一地址读取。简单,但容易产生撕裂。
  • DMA 搬运:CPU 写到一个后台缓冲区,然后通过 DMA 引擎一次性拷贝到显示控制器的读取区域。开销大一点,但能避免撕裂。

在大多数嵌入式 MCU 上,我们用的是第一种。比如 STM32 的 LTDC 外设,你给它一个起始地址,它就开始疯狂刷屏。CPU 改这个地址里的数据,屏幕就跟着变。

核心要点:帧缓冲区的物理地址必须对齐到显示控制器的要求。有些控制器要求 32 字节对齐,有些要求 64 字节。不对齐?画面直接花掉。我踩过这个坑。

2.2 像素格式:一个像素到底占几个字节?

像素格式决定了每个像素在内存里怎么存。常见的格式有这些:

格式 颜色深度 每像素字节数 典型场景
RGB565 16 位 2 字节 低成本 TFT、低端 MCU
RGB888 24 位 3 字节 中高端屏幕、色彩要求高
ARGB8888 32 位 4 字节 带透明通道、UI 叠加
L8(索引色) 8 位 1 字节 极简 UI、低内存设备

我个人最常用的是 RGB565。为什么?因为它占内存少,一个像素才 2 字节。对于 320x240 的屏幕,整帧才 150KB 左右。这在很多 MCU 上是可以接受的。RGB888 虽然色彩更细腻,但一帧直接翻 1.5 倍,很多芯片扛不住。

小技巧:如果你的 UI 只有少量固定颜色,考虑用索引色格式。我曾经在一个资源极度紧张的项目里,用 L8 格式把帧缓冲区从 300KB 压到了 75KB,代价只是颜色数限制在 256 色以内。

2.3 颜色深度:位越多,色越真

颜色深度就是每个颜色通道用多少位来表示。RGB565 的 R 用 5 位、G 用 6 位、B 用 5 位。为什么 G 多 1 位?因为人眼对绿色最敏感。这是硬件设计的老传统了。

颜色深度直接影响显示效果:

  • 16 位(RGB565):总共 65536 种颜色。渐变会有肉眼可见的色带,尤其在天蓝色、肤色这些区域。
  • 24 位(RGB888):1670 万色。渐变平滑,但内存占用大。
  • 32 位(ARGB8888):比 24 位多了 8 位 Alpha 通道,用于透明度混合。做弹窗、菜单叠加时必备。

嗯,这里要注意:不是屏幕支持 24 位,你的 MCU 就能流畅跑。我记得有一次给客户做方案,选了 RGB888 的屏,结果 MCU 的 DMA 带宽不够,刷一帧要 50ms,掉帧掉到没法看。最后老老实实换回 RGB565。

避坑指南:我曾经在一个项目里直接用了 32 位格式,想着反正内存够。结果没注意显示控制器只支持 24 位,Alpha 通道被忽略,透明效果全没了。选格式前,一定先看显示控制器的数据手册。

2.4 行对齐:为什么每行要“补位”?

行对齐是个容易被忽略的细节。很多显示控制器要求每行像素的字节数必须是某个值的整数倍——通常是 4 字节、8 字节或 16 字节。

举个例子:一个 320x240 的 RGB565 屏幕,每行 320 个像素,每个像素 2 字节,总共 640 字节。640 除以 4 等于 160,刚好对齐。但如果屏幕宽度是 322 像素呢?每行 644 字节,644 除以 4 等于 161 余 0?不对,644 除以 4 等于 161,没有余数?等等,我算一下——644 ÷ 4 = 161,确实能整除。但如果是 321 像素呢?每行 642 字节,642 ÷ 4 = 160.5,不对齐。

不对齐会怎样?显示控制器可能会读错数据,画面出现斜条纹或者偏移。解决办法是在每行末尾填充几个无效字节,让行字节数对齐到要求的值。

行对齐的计算公式很简单:

// 计算对齐后的行字节数
// bytes_per_line 是原始行字节数
// alignment 是对齐要求(通常是 4、8、16)
uint32_t aligned_bytes_per_line = 
    (bytes_per_line + alignment - 1) & ~(alignment - 1);

// 填充字节数
uint32_t padding = aligned_bytes_per_line - bytes_per_line;

我习惯在分配帧缓冲区时直接按对齐后的尺寸来。这样 CPU 写数据时,每行末尾留几个字节不写就行。显示控制器读到这些填充字节会直接忽略。

实战经验:我曾经在一个项目里忘了做行对齐,画面在屏幕右侧出现了一条细细的彩色条纹。排查了两天才发现是行字节数没对齐到 8 字节。从那以后,我写任何帧缓冲区分配代码,第一件事就是算对齐。

2.5 知识体系总览

下面这张图把帧缓冲区的核心概念串起来了。你可以看到显存映射、像素格式、颜色深度和行对齐之间的关系。

帧缓冲区核心知识体系 帧缓冲区 显存映射 像素格式 颜色深度 行对齐 直接映射 DMA 搬运 RGB565 / RGB888 16/24/32 位 4/8/16 字节对齐 四个概念环环相扣:映射方式决定访问效率,格式和深度决定内存占用, 行对齐保证硬件正确读取。任何一个出问题,画面都别想正常显示。

你想想看,这四个概念其实是一体的。映射方式决定了 CPU 怎么访问内存;像素格式和颜色深度决定了每个像素占多少字节;行对齐则保证了硬件能正确读取每一行数据。任何一个环节出问题,画面都别想正常显示。

我个人习惯在项目初期就把这些参数定死。先确定屏幕分辨率,再选像素格式,然后算对齐,最后分配缓冲区。顺序不能乱,否则后面改起来很痛苦。