4. 像素操作基础:画点函数实现、颜色编码(RGB565)、缓冲区管理

好,咱们进入第四章节。前面几章我们把 SPI 通信和 LCD 初始化搞定了,屏幕能亮,能刷个纯色。但说真的,那离真正的显示还差得远。你想在屏幕上画个图标、显示个汉字,归根结底就一件事——操作像素。

这一章,我们就来啃下像素操作这块硬骨头。说白了,就是三个核心问题:怎么在任意位置画一个点?颜色数据在内存里怎么存?怎么管理显存缓冲区?搞懂这些,后面所有显示功能都是水到渠成的事。

4.1 画点函数——显示系统的基石

你想想看,不管是画线、画圆、显示字符还是图片,最底层的操作都是「在坐标 (x, y) 处设置一个颜色」。所以画点函数就是整个显示驱动的地基。地基不稳,上面盖什么都白搭。

我刚开始做 LCD 驱动时,犯过一个低级错误——画点函数里坐标没做边界检查。结果程序跑飞了,屏幕显示花成一团,查了半天才发现是坐标越界写到了显存外面。嗯,从那以后,我的画点函数第一行永远是边界判断。

画点函数的核心职责:
  • 接收目标坐标 (x, y) 和颜色值
  • 检查坐标是否在屏幕有效范围内
  • 通过 SPI 向 LCD 发送设置坐标和写入像素的命令
  • 将颜色数据写入显存缓冲区(如果有的话)

来看一个典型的画点函数实现。假设我们用的是 240x320 分辨率的 ILI9341 驱动芯片:

// 画点函数 - 直接写屏模式
void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color)
{
    // 第一步:边界检查,防止越界
    if (x >= LCD_WIDTH) return;
    if (y >= LCD_HEIGHT) return;

    // 第二步:设置列地址范围(x坐标)
    LCD_WriteCmd(0x2A);  // 列地址设置命令
    LCD_WriteData(x >> 8);
    LCD_WriteData(x & 0xFF);
    LCD_WriteData(x >> 8);
    LCD_WriteData(x & 0xFF);

    // 第三步:设置行地址范围(y坐标)
    LCD_WriteCmd(0x2B);  // 行地址设置命令
    LCD_WriteData(y >> 8);
    LCD_WriteData(y & 0xFF);
    LCD_WriteData(y >> 8);
    LCD_WriteData(y & 0xFF);

    // 第四步:写入像素数据
    LCD_WriteCmd(0x2C);  // 写内存命令
    LCD_WriteData(color >> 8);
    LCD_WriteData(color & 0xFF);
}
我的经验:很多初学者会忽略坐标的高低位顺序。ILI9341 要求先传高 8 位再传低 8 位,顺序搞反了,点会画到莫名其妙的位置去。我踩过这个坑,所以现在写这类代码都会加注释提醒自己。

4.2 RGB565 颜色编码——16位色的秘密

为什么用 RGB565 而不是 RGB888?说白了就两个字——省空间。嵌入式系统的 RAM 和 Flash 都是稀缺资源,能省一点是一点。

RGB565 用 16 位(2 字节)表示一个像素颜色:

  • 红色(R): 5 位,取值范围 0-31
  • 绿色(G): 6 位,取值范围 0-63
  • 蓝色(B): 5 位,取值范围 0-31

你看,绿色多给了一位。为什么?因为人眼对绿色最敏感,多一位能让绿色渐变更平滑。这是硬件工程师们多年经验总结出来的。

颜色模式 位数 颜色总数 每像素字节数 适用场景
RGB565 16 65,536 2 嵌入式 LCD 主流
RGB888 24 16,777,216 3 高清显示、桌面系统
RGB444 12 4,096 1.5 低端屏、节省内存

RGB565 的颜色值怎么拼出来?看这个公式:

// RGB565 颜色编码
// 参数:R(0-31), G(0-63), B(0-31)
// 返回:16位颜色值
uint16_t RGB565(uint8_t r, uint8_t g, uint8_t b)
{
    return ((r & 0x1F) << 11) |   // 红色占 bit15-bit11
           ((g & 0x3F) << 5)  |   // 绿色占 bit10-bit5
           (b & 0x1F);             // 蓝色占 bit4-bit0
}

// 常用颜色宏定义
#define WHITE         0xFFFF
#define BLACK         0x0000
#define RED           0xF800
#define GREEN         0x07E0
#define BLUE          0x001F
#define YELLOW        0xFFE0
#define CYAN          0x07FF
#define MAGENTA       0xF81F
注意:如果你从 PC 端图片处理软件(如 Photoshop)导出的颜色值是 RGB888 格式,需要先转换才能用在 LCD 上。转换公式很简单:R5 = R8 >> 3,G6 = G8 >> 2,B5 = B8 >> 3。我曾经直接拿 RGB888 的值往 LCD 里写,结果颜色完全不对,查了半天才发现是位数没对齐。

4.3 缓冲区管理——显存的设计哲学

画点函数可以直接写屏,但这样做有个大问题——效率低。每次画一个点都要通过 SPI 发一堆命令和数据,SPI 速度再快也扛不住频繁的单点操作。

我个人的习惯是:能批量操作就别单点操作。这就引出了缓冲区管理的概念。

4.3.1 全屏缓冲区 vs 局部缓冲区

两种方案,各有优劣:

方案 内存占用 刷新速度 适用场景
全屏缓冲区 240×320×2 = 153.6 KB 快(一次 DMA 传输) 有外部 SRAM 或大内存 MCU
局部缓冲区 几 KB 到几十 KB 中等(分块刷新) 内存紧张的 MCU(如 STM32F103)
无缓冲区(直接写屏) 0 慢(单点操作) 调试阶段、简单显示

对于 STM32F103 这种只有 20KB 或 64KB RAM 的芯片,全屏缓冲区基本不现实。我一般用局部缓冲区,比如开一个 240×16 像素的行缓冲区,逐行刷新。

4.3.2 缓冲区操作流程

有了缓冲区,画点函数就要改一改了——先写缓冲区,再统一刷到屏幕:

// 显存缓冲区(局部缓冲区示例:240x16 像素)
#define BUFFER_ROWS  16
uint16_t lcd_buffer[LCD_WIDTH * BUFFER_ROWS];

// 带缓冲区的画点函数
void LCD_DrawPoint_Buffered(uint16_t x, uint16_t y, uint16_t color)
{
    if (x >= LCD_WIDTH || y >= LCD_HEIGHT) return;

    // 计算缓冲区索引
    uint32_t index = y * LCD_WIDTH + x;
    lcd_buffer[index] = color;
}

// 将缓冲区内容刷到 LCD
void LCD_FlushBuffer(uint16_t start_y, uint16_t end_y)
{
    uint16_t height = end_y - start_y;
    uint32_t total_pixels = LCD_WIDTH * height;

    // 设置窗口区域
    LCD_WriteCmd(0x2A);
    LCD_WriteData(0); LCD_WriteData(0);
    LCD_WriteData((LCD_WIDTH-1) >> 8); LCD_WriteData((LCD_WIDTH-1) & 0xFF);

    LCD_WriteCmd(0x2B);
    LCD_WriteData(start_y >> 8); LCD_WriteData(start_y & 0xFF);
    LCD_WriteData((end_y-1) >> 8); LCD_WriteData((end_y-1) & 0xFF);

    // 批量发送像素数据
    LCD_WriteCmd(0x2C);
    for (uint32_t i = 0; i < total_pixels; i++)
    {
        LCD_WriteData(lcd_buffer[i] >> 8);
        LCD_WriteData(lcd_buffer[i] & 0xFF);
    }
}
性能提示:如果 MCU 支持 DMA,批量刷屏时一定要用 DMA + SPI 传输。CPU 只需要启动一次 DMA,剩下的时间可以去做其他事情。我在一个项目里用 DMA 刷 240x320 的全屏,刷新率从 15fps 直接飙到了 45fps,效果非常明显。

4.4 本章知识结构图

下面这张图把本章的核心知识点串起来了,你可以对照着理清思路:

像素操作基础 - 知识结构图 像素操作 画点函数 RGB565 颜色编码 缓冲区管理 坐标边界检查 SPI 命令序列 数据写入 R:5位 G:6位 B:5位 颜色宏定义 RGB888 转 RGB565 全屏 vs 局部缓冲区 缓冲区读写接口 DMA 批量刷屏 三者协同:画点函数使用 RGB565 颜色,操作缓冲区,最终刷到 LCD

4.5 实战要点总结

这一章的内容偏基础,但非常重要。我总结几个关键点,你写代码时可以对照着检查:

  1. 画点函数一定要做边界检查——这是防呆设计,能省掉你大量调试时间。
  2. RGB565 的颜色值不要搞混位顺序——高位是红色,中间是绿色,低位是蓝色。记不住就查宏定义。
  3. 缓冲区大小要量力而行——先算清楚 MCU 还剩多少 RAM,再决定用全屏还是局部缓冲区。
  4. 批量操作优先——能用 DMA 就用 DMA,能一次发一帧就别一次发一个点。
避坑提醒:有些 LCD 驱动芯片(比如 ST7789)的坐标设置顺序是 x 在前 y 在后,但有些芯片(比如 SSD1306)是 y 在前 x 在后。换芯片时一定要看数据手册,别想当然。我曾经因为没仔细看手册,把 ST7789 的驱动直接套到 SSD1306 上,折腾了两天才发现是坐标顺序反了。