3、帧缓冲基础:帧缓冲(FrameBuffer)概念、显存映射、像素格式(RGB565/RGB888)

好,咱们今天聊点实在的。帧缓冲,英文叫 FrameBuffer,说白了就是一块专门用来存图像数据的内存区域。你想想看,游戏机要显示画面,总得有个地方先把画面数据放好,然后让显示控制器去读,对吧?这个“放数据的地方”,就是帧缓冲。

核心理解:帧缓冲 = 显存 + 映射机制 + 像素格式。三者缺一不可。

3.1 帧缓冲到底是什么?

我个人的理解很简单:帧缓冲就是一块内存,里面每个字节都对应屏幕上某个像素的颜色。CPU 往这块内存里写数据,显示控制器(比如 LCD 控制器、HDMI 发送器)就自动把数据刷到屏幕上。

嗯,这里要注意:帧缓冲不是显卡的专利。在嵌入式系统里,我们经常用 MCU 内部的 SRAM 或者外挂的 SDRAM 来充当帧缓冲。说白了,只要你的 CPU 能读写,显示控制器能读取,这块内存就能当帧缓冲用。

我在项目中遇到过一件事:有个同事把帧缓冲定义在内部 SRAM 里,结果分辨率一高,内存不够用了。后来改成外挂 SDRAM,但时序没调好,画面出现撕裂。嗯,这就是典型的帧缓冲设计没考虑周全。

3.2 显存映射:CPU 怎么找到像素?

显存映射,其实就是把帧缓冲的物理地址告诉 CPU 和显示控制器。CPU 通过地址总线访问这块内存,显示控制器通过 DMA 方式读取。

举个例子,假设你的屏幕分辨率是 320x240,像素格式是 RGB565(每个像素 2 字节)。那么帧缓冲的大小就是:

帧缓冲大小 = 320 × 240 × 2 = 153,600 字节 ≈ 150 KB

你需要在内存中开辟一块连续的 150KB 空间。然后告诉显示控制器:起始地址是 0x20000000,长度是 150KB。这样,CPU 往 0x20000000 写数据,屏幕左上角第一个像素就会变化。

我的经验:显存映射最好用 4KB 对齐的地址。很多显示控制器对非对齐地址支持不好,我曾经在这上面浪费了两天时间。

为什么会这样?因为很多嵌入式显示控制器内部用的是行缓冲(line buffer)机制,地址不对齐会导致行数据错位。你想想看,一个像素偏移,整行颜色都乱了。

3.3 像素格式:RGB565 vs RGB888

像素格式决定了每个像素用多少位来表示颜色。嵌入式系统里最常见的两种:RGB565 和 RGB888。

格式 位数 颜色深度 内存占用 典型场景
RGB565 16 位 65,536 色 2 字节/像素 低成本 MCU、小屏幕
RGB888 24 位 16,777,216 色 3 字节/像素 高性能 SoC、大屏幕

3.3.1 RGB565 详解

RGB565 用 16 位表示一个像素:红色 5 位、绿色 6 位、蓝色 5 位。为什么绿色多 1 位?因为人眼对绿色最敏感,多给 1 位能让颜色过渡更自然。

数据排列方式(小端模式):

位:  15 14 13 12 11 | 10 9 8 7 6 | 5 4 3 2 1 0
     R4 R3 R2 R1 R0 | G5 G4 G3 G2 G1 G0 | B4 B3 B2 B1 B0

在 C 语言里,我习惯这样构造 RGB565 颜色值:

// 将 R、G、B(0-255)转换为 RGB565
uint16_t rgb565(uint8_t r, uint8_t g, uint8_t b) {
    return ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3);
}

注意看,我把 8 位的 R 右移 3 位,变成 5 位;G 右移 2 位,变成 6 位;B 右移 3 位,变成 5 位。这样就把 24 位颜色压缩到了 16 位。

避坑指南:我曾经在 RGB565 的字节序上栽过跟头。有些显示控制器要求高字节在前(大端),有些要求低字节在前(小端)。如果你的画面颜色完全不对,先检查字节序!

3.3.2 RGB888 详解

RGB888 就简单多了,每个颜色分量占 8 位,一共 24 位。数据排列通常是:

字节偏移: +0    +1    +2
          R     G     B

在内存中,我习惯用结构体来表示:

typedef struct {
    uint8_t r;  // 红色 0-255
    uint8_t g;  // 绿色 0-255
    uint8_t b;  // 蓝色 0-255
} rgb888_t;

RGB888 的好处是颜色精度高,做渐变、抗锯齿效果更好。但代价是内存占用多了 50%。对于 1024x768 的屏幕,RGB565 只需要 1.5MB,RGB888 需要 2.25MB。

3.4 实际项目中的选择建议

我个人建议这样选:

  • MCU 驱动小屏幕(320x240 以下):用 RGB565。内存省,速度也快。
  • Linux 或 RTOS 驱动大屏幕(800x480 以上):用 RGB888。颜色表现好,而且现代 SoC 的内存带宽足够。
  • 特殊场景(如电子墨水屏):可能只需要 1 位色深,连 RGB565 都用不上。

你想想看,如果你的游戏机只有 2MB 内存,用 RGB888 的话,一个 800x480 的帧缓冲就占掉 1.15MB,剩下的内存还要跑游戏逻辑、音频、输入处理,够呛。这时候 RGB565 就香多了。

3.5 帧缓冲的初始化流程

最后,我分享一下帧缓冲初始化的标准步骤。这是我多年总结出来的,照着做基本不会出问题:

  1. 分配内存:在 SDRAM 或 SRAM 中分配连续空间,地址对齐到 4KB。
  2. 清零:把整个帧缓冲填 0(黑色),防止上电时出现花屏。
  3. 配置显示控制器:告诉控制器帧缓冲的起始地址、宽度、高度、像素格式。
  4. 使能显示:打开 LCD 或 HDMI 输出,此时屏幕应该是全黑。
  5. 写入测试图案:比如画一个红色矩形,验证颜色和位置是否正确。
// 帧缓冲初始化示例(伪代码)
void fb_init(void) {
    // 1. 分配内存
    uint32_t fb_addr = sdram_alloc(320 * 240 * 2);  // RGB565
    
    // 2. 清零
    memset((void*)fb_addr, 0, 320 * 240 * 2);
    
    // 3. 配置显示控制器
    lcd_set_framebuffer(fb_addr);
    lcd_set_resolution(320, 240);
    lcd_set_pixel_format(PIXEL_FORMAT_RGB565);
    
    // 4. 使能
    lcd_enable(1);
    
    // 5. 画测试图案
    draw_rect(10, 10, 100, 100, rgb565(255, 0, 0));  // 红色矩形
}

嗯,到这里帧缓冲的基础就讲完了。下一节我们会深入双缓冲和撕裂问题的处理,那才是真正考验功力的地方。