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);
}
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
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);
}
}
4.4 本章知识结构图
下面这张图把本章的核心知识点串起来了,你可以对照着理清思路:
4.5 实战要点总结
这一章的内容偏基础,但非常重要。我总结几个关键点,你写代码时可以对照着检查:
- 画点函数一定要做边界检查——这是防呆设计,能省掉你大量调试时间。
- RGB565 的颜色值不要搞混位顺序——高位是红色,中间是绿色,低位是蓝色。记不住就查宏定义。
- 缓冲区大小要量力而行——先算清楚 MCU 还剩多少 RAM,再决定用全屏还是局部缓冲区。
- 批量操作优先——能用 DMA 就用 DMA,能一次发一帧就别一次发一个点。