3、显示原理基础:像素与分辨率、色彩深度(RGB565/RGB888)、帧缓冲(FrameBuffer)概念、刷新率与同步

做多屏交互系统,说白了就是跟「眼睛」打交道。你想想看,屏幕上的每一帧画面,背后都藏着一整套硬件与软件协同工作的逻辑。今天咱们就把显示原理这块地基打牢。

3.1 像素与分辨率

像素是显示的最小单元。每个像素就是一个发光点,成千上万个像素排列成矩阵,就构成了我们看到的画面。

分辨率就是像素矩阵的尺寸。比如 1024×768,意思是有 1024 列、768 行像素点。

我个人习惯把分辨率理解为「画布的精细度」。分辨率越高,画面越细腻。但代价是什么?显存占用更大,刷新压力也更大。

常见分辨率速查表

名称分辨率宽高比典型应用
QVGA320×2404:3老式功能机、低端屏
VGA640×4804:3早期显示器、投影
WVGA800×4805:37寸平板、车载屏
HD1280×72016:9入门高清屏
Full HD1920×108016:9主流显示器、电视

我在项目中遇到过一个问题:客户要求用 320×240 的屏显示地图,结果字根本看不清。后来才发现,分辨率太低,像素点太大,细节全丢了。所以选屏时,分辨率一定要跟内容精细度匹配。

3.2 色彩深度:RGB565 与 RGB888

每个像素显示什么颜色,由色彩深度决定。说白了,就是每个像素用多少 bit 来表示颜色。

RGB888:红、绿、蓝各占 8 bit,共 24 bit。能显示 2^24 ≈ 1677 万色。这是真彩色,效果最好。

RGB565:红占 5 bit,绿占 6 bit,蓝占 5 bit,共 16 bit。能显示 2^16 = 65536 色。绿色多给 1 bit,因为人眼对绿色最敏感。

我的经验:在嵌入式系统里,我一般优先用 RGB565。为什么?显存减半,传输带宽减半,刷新速度更快。人眼其实很难分辨 65536 色和 1677 万色的区别,除非是渐变天空或肤色这类场景。

来看一个实际的数据结构对比:

// RGB888 像素结构(24位)
typedef struct {
    uint8_t red;   // 0-255
    uint8_t green; // 0-255
    uint8_t blue;  // 0-255
} rgb888_t;

// RGB565 像素结构(16位)
typedef struct {
    uint16_t value; // 位域: R[15:11] G[10:5] B[4:0]
} rgb565_t;

// 颜色转换函数
uint16_t rgb888_to_rgb565(uint8_t r, uint8_t g, uint8_t b) {
    return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
}

嗯,这里要注意:RGB565 转 RGB888 时,低位会丢失。比如红色从 8 bit 降到 5 bit,精度损失了 3 bit。但实际显示效果,绝大多数场景下完全够用。

3.3 帧缓冲(FrameBuffer)概念

帧缓冲,英文叫 FrameBuffer。说白了就是显存里的一块区域,专门用来存放一帧画面的像素数据。

显示控制器会不停地从帧缓冲里读取数据,然后送给屏幕。你往帧缓冲里写什么,屏幕就显示什么。

我刚开始做嵌入式 GUI 时,总觉得帧缓冲很神秘。后来发现,它其实就是一块连续的内存:

// 定义一个 320x240 的 RGB565 帧缓冲
#define SCREEN_WIDTH  320
#define SCREEN_HEIGHT 240
#define PIXEL_SIZE    2  // RGB565 每个像素占 2 字节

uint8_t framebuffer[SCREEN_WIDTH * SCREEN_HEIGHT * PIXEL_SIZE];

// 在坐标 (x, y) 处画一个红色像素
void draw_pixel(int x, int y, uint16_t color) {
    int offset = (y * SCREEN_WIDTH + x) * PIXEL_SIZE;
    framebuffer[offset]     = (color >> 8) & 0xFF;   // 高字节
    framebuffer[offset + 1] = color & 0xFF;          // 低字节
}

避坑指南:我曾经在项目里犯过一个低级错误——帧缓冲大小算错了。RGB565 每个像素 2 字节,320×240 需要 320×240×2 = 153600 字节。我一开始写成了 320×240,结果屏幕下半部分全是花屏。排查了半天才发现是显存越界了。

帧缓冲有几个关键参数:

  • 大小:分辨率 × 每像素字节数
  • 对齐:很多 LCD 控制器要求帧缓冲地址 4 字节或 8 字节对齐
  • 双缓冲:为了避免画面撕裂,常用两个帧缓冲交替使用

3.4 刷新率与同步

刷新率,就是屏幕每秒更新多少次画面,单位是 Hz。60Hz 就是每秒刷新 60 次,每帧约 16.67ms。

为什么会需要刷新率?因为人眼有视觉暂留效应。如果刷新率太低,你会感觉到闪烁。我记得早期 CRT 显示器 60Hz 时,很多人会觉得眼睛累。现在 LCD 屏 60Hz 基本够用,但游戏玩家追求 120Hz、144Hz。

同步问题:这是个大坑。

假设你在帧缓冲里画一帧画面,画到一半时,显示控制器突然开始读取帧缓冲。结果就是:屏幕上半部分是旧画面,下半部分是新画面——这就是「画面撕裂」。

解决方案:双缓冲 + 垂直同步

  1. 准备两个帧缓冲:front buffer 和 back buffer
  2. 在 back buffer 里绘制新画面
  3. 等垂直消隐期(VBlank)到来时,交换两个缓冲区的指针
  4. 显示控制器开始读取新的 front buffer

垂直消隐期是什么?就是屏幕扫描完最后一行像素,回到第一行开始位置的那段空闲时间。这段时间里,屏幕不读取数据,你可以安全地切换缓冲区。

// 伪代码:双缓冲刷新流程
void vsync_handler() {
    // 垂直同步中断触发
    current_buffer = (current_buffer == 0) ? 1 : 0;
    // 设置 LCD 控制器的帧缓冲地址寄存器
    LCD->FB_ADDR = framebuffers[current_buffer];
}

void draw_scene() {
    int back_buffer = (current_buffer == 0) ? 1 : 0;
    // 在 back buffer 里绘制
    render_to(framebuffers[back_buffer]);
    // 等待下一次 VBlank 自动切换
}

我的建议:在嵌入式系统里,如果 CPU 性能有限,可以适当降低刷新率。比如从 60Hz 降到 50Hz,人眼几乎感觉不到差异,但 CPU 负载能降低 15% 以上。我在一个低功耗项目里就这么干过,效果很好。

最后总结一下:像素和分辨率决定了画面精细度,色彩深度决定了颜色丰富度,帧缓冲是显存的核心结构,刷新率和同步则保证了流畅无撕裂的显示体验。这四个概念,是理解多屏交互系统的基础中的基础。

下一章,咱们聊聊 LCD 接口协议——怎么把数据从芯片送到屏幕上。