第4章:像素与颜色格式——RGB565、RGB888与颜色空间转换
好,咱们今天聊点实在的。像素和颜色格式,说白了就是屏幕上一个点怎么用数字表示。我刚开始做驱动时,觉得这东西太简单了,不就是RGB三个分量嘛。结果第一次调屏,颜色全偏了,折腾了两天才发现是格式搞错了。嗯,从那以后我再也不敢小看这个环节了。
4.1 像素的本质
一个像素,就是一个发光点。它由红、绿、蓝三个子像素组成。你想想看,我们人眼对这三种颜色最敏感,所以显示器就用它们来混合出所有颜色。
但问题来了——每个颜色分量用多少位来表示?这就引出了不同的颜色格式。
4.2 RGB565格式
RGB565,名字就告诉你了:红色5位,绿色6位,蓝色5位。加起来16位,也就是2个字节。
为什么绿色多1位?
因为人眼对绿色最敏感。多给绿色1位,能让颜色过渡更平滑。这是硬件设计上的一个经典权衡。
我在项目中遇到过一块低成本的MCU屏,只支持RGB565。当时觉得16位色够用了,结果显示渐变色时出现了明显的色带。后来我用了抖动算法(dithering)才勉强解决。
RGB565的存储格式是这样的:
// 一个像素占2个字节
// 高字节: R[4:0] | G[5:3]
// 低字节: G[2:0] | B[4:0]
// 举个例子:纯红色 (R=31, G=0, B=0)
uint16_t red_pixel = 0xF800; // 二进制: 11111 000000 00000
// 从RGB888转换到RGB565
uint16_t rgb565_from_rgb888(uint8_t r, uint8_t g, uint8_t b) {
return ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3);
}
我的习惯:在嵌入式开发中,我通常会预先把图片转成RGB565的数组,而不是在运行时转换。省CPU,省内存,跑起来也快。
4.3 RGB888格式
RGB888,每个分量8位,总共24位,3个字节。这是目前最主流的格式,电脑显示器、手机屏幕基本都是这个。
为什么?因为8位能表示256级亮度,三个通道组合起来就是1600多万色。人眼基本分辨不出更细的色阶了。
// RGB888像素结构体
typedef struct {
uint8_t red; // 0-255
uint8_t green; // 0-255
uint8_t blue; // 0-255
} rgb888_pixel_t;
// 注意:有些硬件用BGR顺序,不是RGB
// 我踩过这个坑——在某个LCD控制器上,颜色顺序是BGR
// 结果红色和蓝色全反了
typedef struct {
uint8_t blue;
uint8_t green;
uint8_t red;
} bgr888_pixel_t;
注意:RGB888虽然颜色丰富,但占用的显存也大。一个1024x768的屏幕,RGB888需要2.25MB显存。而RGB565只要1.5MB。在资源紧张的嵌入式系统里,这个差距很要命。
4.4 RGB565与RGB888的对比
| 特性 | RGB565 | RGB888 |
|---|---|---|
| 每像素位数 | 16位 | 24位 |
| 颜色总数 | 65,536色 | 16,777,216色 |
| 显存占用 | 低 | 高(多50%) |
| 颜色过渡 | 可能有色带 | 平滑 |
| 典型应用 | 低端MCU、小屏 | 手机、电脑、大屏 |
我个人习惯是:如果屏幕分辨率低于320x240,用RGB565就够了。超过这个分辨率,尤其是要显示照片或渐变色,必须上RGB888。
4.5 颜色空间转换
颜色空间转换,说白了就是把一种颜色表示方式换成另一种。最常见的场景是:摄像头输出的是YUV,但屏幕要的是RGB。
为什么会这样?因为YUV格式把亮度和颜色分开,压缩效率高。摄像头传感器天生就喜欢YUV。但屏幕显示必须用RGB。所以驱动里必须做转换。
4.5.1 YUV到RGB的转换
YUV转RGB的公式有好几个版本,我用的最多的是BT.601标准:
// YUV转RGB (BT.601标准)
// Y: 亮度, U/V: 色度
void yuv_to_rgb(uint8_t y, uint8_t u, uint8_t v,
uint8_t *r, uint8_t *g, uint8_t *b) {
int c = y - 16;
int d = u - 128;
int e = v - 128;
*r = clamp((298 * c + 409 * e + 128) >> 8, 0, 255);
*g = clamp((298 * c - 100 * d - 208 * e + 128) >> 8, 0, 255);
*b = clamp((298 * c + 516 * d + 128) >> 8, 0, 255);
}
// 辅助函数:限制值在0-255之间
static inline uint8_t clamp(int val, int min, int max) {
return (val < min) ? min : ((val > max) ? max : val);
}
性能优化建议:这个转换如果用浮点运算,慢得没法看。我一般用查表法——把Y、U、V的所有组合提前算好存成数组。虽然占点内存,但速度快了10倍不止。
4.5.2 RGB到灰度图的转换
有时候我们需要把彩色图转成灰度。比如做墨水屏驱动时,只能显示黑白。公式很简单:
// RGB转灰度 (人眼感知亮度)
// 注意:不是简单的平均,而是加权平均
uint8_t rgb_to_gray(uint8_t r, uint8_t g, uint8_t b) {
// 人眼对绿色最敏感,红色次之,蓝色最不敏感
return (r * 77 + g * 150 + b * 29) >> 8;
}
我曾经犯过一个错:直接用 (r+g+b)/3 取平均。结果灰度图看起来特别奇怪,亮的地方不够亮,暗的地方不够暗。后来查资料才知道,人眼对RGB的敏感度是不一样的。
4.6 实际项目中的坑
嗯,这里我要多说几句。颜色格式转换看着简单,但实际项目中坑特别多:
- 字节序问题:有些CPU是大端,有些是小端。RGB565的两个字节顺序可能不一样。我遇到过在STM32上正常,换到NXP上就颜色错乱的情况。
- 颜色空间差异:同样的RGB值,在不同的屏幕上显示效果可能完全不同。因为每个屏幕的色域、伽马曲线都不一样。
- 性能瓶颈:如果每帧都要做颜色空间转换,CPU可能扛不住。我建议用DMA+查表法,或者干脆用硬件加速。
避坑指南:我曾经在一个项目里,摄像头输出YUV422,屏幕要求RGB565。我直接在中断里做转换,结果帧率掉到10fps。后来改成DMA乒乓缓冲+双缓冲查表,才跑到30fps。记住:颜色转换一定要放在DMA或者硬件加速里做,别在CPU里死磕。
4.7 小结
像素和颜色格式,是显示驱动的基础。RGB565省空间但颜色少,RGB888颜色丰富但占内存。颜色空间转换是连接摄像头和屏幕的桥梁,性能优化是关键。
下一章我们会讲帧缓冲(Framebuffer)的设计与实现。到时候你会看到,这些颜色格式的知识会怎么用到实际驱动里。