第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)的设计与实现。到时候你会看到,这些颜色格式的知识会怎么用到实际驱动里。