2. 嵌入式GUI基础
做嵌入式GUI,说白了就是跟显示硬件打交道。你写的每一行绘图代码,最终都要变成显示控制器能理解的信号。这一章,我就带你看看显示控制器到底怎么工作的,像素是怎么变成画面的,以及那个绕不开的概念——帧缓冲。
显示控制器:画面的幕后指挥官
不管是TFT液晶屏还是OLED屏,背后都有一个显示控制器在干活。它的任务很简单:把内存里的图像数据,一行一行、一帧一帧地推送到面板上。
我刚开始做项目时,总觉得显示控制器就是个“黑盒子”,直到有一次调试屏幕闪烁,才逼着自己把它的时序图啃了一遍。嗯,这里有几个关键概念你得知道:
- 像素时钟(PCLK):每输出一个像素,时钟跳一次。频率越高,刷新越快。
- 行同步(HSYNC):扫完一行,发个信号告诉控制器“下一行开始”。
- 场同步(VSYNC):扫完一帧,发个信号告诉控制器“下一帧开始”。
- 数据使能(DE):告诉面板“当前数据是有效像素,不是消隐区”。
你想想看,如果没有这些同步信号,画面就会乱掉——行不对行,帧不对帧。我曾经在调试一块4.3寸屏时,就因为HSYNC的极性设反了,画面一直斜着撕裂,折腾了大半天才找到原因。
核心要点:显示控制器本质上是一个状态机,它按照固定的时序,从内存中搬运像素数据到面板。你写的GUI代码,最终就是往这块内存里填数据。
像素与颜色深度:一个点能有多少种颜色
每个像素就是一个点。但一个点能显示多少种颜色,取决于它的颜色深度(Color Depth)。
说白了,颜色深度就是每个像素用多少个bit来表示。常见的深度有:
| 颜色深度 | 每个像素位数 | 可显示颜色数 | 常见场景 |
|---|---|---|---|
| 1-bit | 1 | 2(黑/白) | 电子墨水屏、单色OLED |
| 8-bit | 8 | 256 | 低端彩屏、调色板模式 |
| 16-bit(RGB565) | 16 | 65,536 | 嵌入式GUI最常用 |
| 24-bit(RGB888) | 24 | 16,777,216 | 高端显示、手机屏 |
我个人习惯在资源受限的MCU上优先用RGB565。为什么?因为它占内存少,显示效果也够用。RGB565的意思是:红色5位、绿色6位、蓝色5位。绿色多一位,是因为人眼对绿色最敏感。
避坑指南:我曾经在一个项目里,为了追求色彩丰富,硬上了RGB888。结果帧缓冲内存翻了一倍,刷新速度掉了一半。后来老老实实换回RGB565,用户根本看不出区别。所以,选颜色深度时,别只看参数,要算算你的内存和带宽扛不扛得住。
帧缓冲(Framebuffer):画布就是一块内存
帧缓冲,英文叫Framebuffer。名字听着高大上,其实它就是一块内存区域,专门用来存放一帧画面的像素数据。
你可以把它想象成一块画布。你的GUI代码往画布上画东西,显示控制器则不停地读取这块画布,然后推送到屏幕上。画布有多大?很简单:
帧缓冲大小 = 屏幕宽度 × 屏幕高度 × 每个像素字节数
举例:
屏幕分辨率:320 × 240
颜色深度:RGB565(2字节/像素)
帧缓冲大小 = 320 × 240 × 2 = 153,600 字节 ≈ 150KB
150KB,对于现在的MCU来说不算大。但如果你用的是480×272的屏,同样RGB565,帧缓冲就变成了261KB。再往上,800×480的话,就是768KB。你想想看,很多MCU的RAM总共才512KB或1MB,一个帧缓冲就吃掉大半,剩下的还要跑系统、堆栈、变量……
注意:帧缓冲必须放在连续的内存区域中。显示控制器通过DMA(直接内存访问)来搬运数据,它要求源地址是连续的。如果你的内存是分散的,或者用了外部SRAM但地址不连续,画面就会出现撕裂或花屏。我踩过这个坑,后来老老实实用了内部连续RAM。
双缓冲:为什么需要两个画布?
单缓冲的问题很明显:你在画布上画图时,显示控制器也在读同一块画布。如果你画到一半,控制器正好读到这部分,画面就会出现“撕裂”——上半部分是旧画面,下半部分是新画面。
解决办法?用两个帧缓冲。一个给显示控制器读(称为“前台缓冲”),另一个给你画(称为“后台缓冲”)。画完后,通过一个指针交换操作,把后台缓冲切换成前台缓冲。这样,控制器永远读到的是完整的画面。
这就是双缓冲的核心思想。后面几章我会详细讲它的实现细节,这里先记住一句话:双缓冲用空间换时间,用内存换画面完整性。
一句话总结:显示控制器按固定时序从帧缓冲读数据;帧缓冲是一块连续内存,大小由分辨率×颜色深度决定;双缓冲解决画面撕裂,但需要两倍内存。
本章知识结构图
下面这张图,把显示驱动的工作原理串起来了。从GUI代码到最终像素点亮,每一步都离不开帧缓冲和显示控制器的配合。
从这张图可以看得很清楚:GUI代码画到后台缓冲,显示控制器从前台缓冲读数据,两者互不干扰。这就是双缓冲能解决撕裂问题的根本原因。