2、显示驱动基础:LCD/OLED工作原理、像素与分辨率、色彩深度、帧缓冲概念
做嵌入式GUI,说白了就是跟屏幕打交道。你写的每一行代码,最终都要变成屏幕上的一个像素点。这一章我们就把显示驱动的基础打牢,后面讲架构时才不会发虚。
2.1 LCD与OLED:两种主流屏幕的工作原理
先聊聊屏幕本身。现在嵌入式设备里,LCD和OLED是绝对的主流。我刚开始做项目时,也分不清它们到底差在哪,直到有一次在户外项目上选错了屏,大太阳底下啥都看不清……嗯,从那以后我就认真研究了一回。
LCD(液晶显示器)
LCD自己不发光,它靠背后的背光源照亮。液晶分子像一个个小百叶窗,通过电压控制扭转角度,决定光线能不能透过去。再加上RGB滤色片,就形成了彩色像素。
- 背光常亮:即使显示全黑画面,背光也开着,所以黑色不够纯,有点发灰。
- 功耗较高:背光一直亮着,耗电相对大。
- 成本低:技术成熟,大尺寸便宜,适合工控、家电。
OLED(有机发光二极管)
OLED每个像素自己发光,不需要背光。电压一加,有机材料就亮了。黑色就是直接关掉像素,所以黑色是纯黑,对比度极高。
- 自发光:每个像素独立控制,黑色区域完全不耗电。
- 响应快:刷新速度比LCD快很多,适合动态画面。
- 寿命问题:蓝色像素衰减快,长时间显示固定界面容易烧屏。我在做智能手表项目时就踩过这个坑,后来加了像素偏移算法才解决。
2.2 像素与分辨率:屏幕的“最小颗粒”
像素是屏幕的最小显示单元。每个像素由R、G、B三个子像素组成。分辨率就是横向和纵向的像素数量,比如320×240,就是横向320个像素,纵向240个像素。
分辨率越高,画面越细腻。但代价是什么?显存占用和刷新压力都变大。我见过有人在小MCU上硬跑800×480的屏,结果帧率只有个位数,UI卡得像幻灯片。
| 分辨率 | 常见场景 | 像素总数 | RGB565显存占用 |
|---|---|---|---|
| 128×64 | 小OLED、手环 | 8,192 | 16 KB |
| 320×240 | 工控屏、MP3 | 76,800 | 150 KB |
| 800×480 | 智能家居、导航 | 384,000 | 750 KB |
| 1024×600 | 平板、HMI | 614,400 | 1.2 MB |
2.3 色彩深度:RGB565与RGB888
色彩深度决定了每个像素用多少bit来表示颜色。说白了,就是颜色够不够“真”。
RGB565
每个像素用16位:R占5位,G占6位,B占5位。总共能显示2^16=65536种颜色。
- 优点:省显存,传输快。一个像素只占2字节。
- 缺点:颜色过渡有断层,尤其是渐变和肤色,能看出条纹。
- 适用:低端MCU、小尺寸屏、对颜色要求不高的场景。
RGB888
每个像素用24位:R、G、B各占8位。总共2^24=1677万色,也就是真彩色。
- 优点:颜色细腻,过渡平滑,显示照片和图标效果很好。
- 缺点:一个像素占3字节,显存和带宽需求大。
- 适用:高性能MCU、带LCD控制器的芯片、高端HMI。
我的经验:在资源紧张的MCU上,RGB565是主流选择。但如果你的芯片有硬件DMA和足够的RAM,RGB888带来的视觉提升非常明显。我做过一个对比测试,同样的UI界面,RGB888的观感比RGB565好一个档次,尤其是圆角和阴影部分。
2.4 帧缓冲(FrameBuffer)概念
帧缓冲,说白了就是显存里的一块区域,专门用来存放一帧画面的像素数据。CPU或GPU把要显示的内容画到这块缓冲区里,然后显示控制器定期从缓冲区读取数据,刷新到屏幕上。
为什么需要帧缓冲?因为屏幕刷新和CPU绘图是异步的。如果没有缓冲区,CPU画到一半时屏幕刷新了,就会出现撕裂——上半屏是旧画面,下半屏是新画面。
单缓冲 vs 双缓冲
| 模式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 单缓冲 | CPU直接往显示缓冲区画 | 省内存 | 容易撕裂,闪烁 |
| 双缓冲 | CPU画到后台缓冲区,完成后交换到前台 | 无撕裂,画面流畅 | 多占一倍显存 |
双缓冲是嵌入式GUI的标配。我习惯的做法是:在初始化时分配两个缓冲区,一个作为显示缓冲区(由LCD控制器直接读取),另一个作为绘图缓冲区。绘图完成后,通过DMA或指针交换,把新画面推送到屏幕上。
2.5 核心知识体系图
下面这张图把本章的核心概念串起来了。你可以看到,从屏幕硬件到帧缓冲,再到上层GUI,每一层都有明确的职责。
2.6 代码示例:初始化一个RGB565帧缓冲
下面是一个简单的帧缓冲初始化代码,我用的是STM32平台,但思路是通用的。你可以在任何MCU上套用。
/* 定义帧缓冲结构 */
typedef struct {
uint16_t width;
uint16_t height;
uint16_t *buffer; /* RGB565像素数据 */
} framebuffer_t;
/* 分配双缓冲 */
static uint16_t fb_front[320 * 240]; /* 前台缓冲区 */
static uint16_t fb_back[320 * 240]; /* 后台缓冲区 */
static framebuffer_t fb;
void framebuffer_init(uint16_t w, uint16_t h) {
fb.width = w;
fb.height = h;
fb.buffer = fb_front; /* 默认前台显示 */
/* 清空缓冲区 */
memset(fb_front, 0, w * h * sizeof(uint16_t));
memset(fb_back, 0, w * h * sizeof(uint16_t));
printf("FrameBuffer initialized: %dx%d, RGB565\n", w, h);
}
/* 交换缓冲区(带VSync等待) */
void framebuffer_swap(void) {
while(!(LCD->SR & LCD_SR_VSYNC)); /* 等待垂直同步 */
/* 交换指针 */
uint16_t *tmp = fb.buffer;
fb.buffer = (fb.buffer == fb_front) ? fb_back : fb_front;
/* 更新LCD控制器起始地址 */
LCD->LR = (uint32_t)fb.buffer;
}
关键点:framebuffer_swap函数里的VSync等待不能省。我见过有人为了省那几微秒,结果画面撕裂被客户投诉。宁可慢一点,也要保证画面完整。
2.7 本章小结
这一章我们聊了LCD和OLED的区别,像素和分辨率怎么算,RGB565和RGB888怎么选,以及帧缓冲的核心概念。这些都是嵌入式GUI的底层基础,你理解得越深,后面做架构设计时就越有底气。
我个人觉得,帧缓冲是这一章最重要的概念。它就像一块画布,你的GUI框架在上面作画,显示控制器把它呈现给用户。搞懂了它,你就掌握了嵌入式GUI的“数据流”核心。