3、显示原理基础:像素与分辨率、色彩深度(RGB565/RGB888)、帧缓冲(FrameBuffer)概念、刷新率与同步
做多屏交互系统,说白了就是跟「眼睛」打交道。你想想看,屏幕上的每一帧画面,背后都藏着一整套硬件与软件协同工作的逻辑。今天咱们就把显示原理这块地基打牢。
3.1 像素与分辨率
像素是显示的最小单元。每个像素就是一个发光点,成千上万个像素排列成矩阵,就构成了我们看到的画面。
分辨率就是像素矩阵的尺寸。比如 1024×768,意思是有 1024 列、768 行像素点。
我个人习惯把分辨率理解为「画布的精细度」。分辨率越高,画面越细腻。但代价是什么?显存占用更大,刷新压力也更大。
常见分辨率速查表
| 名称 | 分辨率 | 宽高比 | 典型应用 |
|---|---|---|---|
| QVGA | 320×240 | 4:3 | 老式功能机、低端屏 |
| VGA | 640×480 | 4:3 | 早期显示器、投影 |
| WVGA | 800×480 | 5:3 | 7寸平板、车载屏 |
| HD | 1280×720 | 16:9 | 入门高清屏 |
| Full HD | 1920×1080 | 16: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。
同步问题:这是个大坑。
假设你在帧缓冲里画一帧画面,画到一半时,显示控制器突然开始读取帧缓冲。结果就是:屏幕上半部分是旧画面,下半部分是新画面——这就是「画面撕裂」。
解决方案:双缓冲 + 垂直同步
- 准备两个帧缓冲:front buffer 和 back buffer
- 在 back buffer 里绘制新画面
- 等垂直消隐期(VBlank)到来时,交换两个缓冲区的指针
- 显示控制器开始读取新的 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 接口协议——怎么把数据从芯片送到屏幕上。