4. 像素操作核心:设置窗口函数、写像素点、区域填充与颜色格式转换
各位同学,今天我们来啃一块硬骨头——像素操作。说白了,LCD 屏幕上的一切图形,最终都要落到「怎么把颜色填到正确的位置」这个问题上。我刚开始做 LCD 驱动时,总觉得画点函数写出来就完事了,结果一跑起来满屏花屏……后来才发现,窗口设置和颜色格式这两个坑,踩一次就记住了。
4.1 窗口函数:告诉屏幕「我要画哪里」
先问一个问题:你往屏幕上写数据,LCD 控制器怎么知道这些数据对应哪个像素?
答案就是窗口函数。它相当于在屏幕上画一个矩形框,之后所有写入的数据都只填充这个框内。我习惯叫它「裁剪区」——你想想看,没有这个框,每次写像素都要计算绝对坐标,效率低得吓人。
4.1.1 Set Column Address 和 Set Page Address
这两个命令通常成对出现:
- Set Column Address:定义 X 轴范围(列起始 ~ 列结束)
- Set Page Address:定义 Y 轴范围(页起始 ~ 页结束)
以 ILI9341 为例,命令格式如下:
// 设置列范围:列起始 SC,列结束 EC
void LCD_SetColumn(uint16_t sc, uint16_t ec) {
LCD_WriteCmd(0x2A); // 列地址命令
LCD_WriteData(sc >> 8); // 高字节
LCD_WriteData(sc & 0xFF); // 低字节
LCD_WriteData(ec >> 8); // 高字节
LCD_WriteData(ec & 0xFF); // 低字节
}
// 设置页范围:页起始 SP,页结束 EP
void LCD_SetPage(uint16_t sp, uint16_t ep) {
LCD_WriteCmd(0x2B); // 页地址命令
LCD_WriteData(sp >> 8);
LCD_WriteData(sp & 0xFF);
LCD_WriteData(ep >> 8);
LCD_WriteData(ep & 0xFF);
}
0x2C(写存储器命令),让 LCD 控制器进入数据写入模式。否则有些驱动 IC 会卡在命令解析状态。
4.1.2 窗口设置的典型流程
- 关闭自动递增(如果需要)
- 发送列地址命令 + 4 字节参数
- 发送页地址命令 + 4 字节参数
- 发送写存储器命令(0x2C)
- 连续写入像素数据
我曾经在项目里犯过一个低级错误:列和页的起始/结束参数搞反了。结果屏幕上画出来的矩形是镜像的,调试了整整一个下午……嗯,从那以后我每次写窗口函数都会在纸上先画一遍坐标。
4.2 写像素点函数:最基础的原子操作
窗口设置好之后,写像素就简单了。但这里有个关键点:颜色格式。你送出去的数据是 16 位还是 24 位,直接决定了写像素函数的实现。
4.2.1 RGB565 格式下的写像素
// 在 (x, y) 位置画一个 RGB565 颜色点
void LCD_DrawPixel(uint16_t x, uint16_t y, uint16_t color) {
// 1. 设置窗口为单像素大小
LCD_SetColumn(x, x);
LCD_SetPage(y, y);
// 2. 写入颜色数据
LCD_WriteCmd(0x2C);
LCD_WriteData(color >> 8); // 高字节
LCD_WriteData(color & 0xFF); // 低字节
}
你可能会问:为什么窗口要设成单像素?直接写坐标不行吗?
答案是不行。大多数 LCD 控制器内部维护一个「写指针」,窗口函数的作用就是重置这个指针的位置。如果不设窗口,写指针会从上一次的位置继续递增,结果就是颜色写到别的地方去了。
4.3 区域填充函数:批量操作的威力
区域填充的本质就是「设大窗口 + 批量写数据」。以填充一个矩形为例:
// 填充矩形区域 (x0,y0) 到 (x1,y1),颜色为 color
void LCD_FillRect(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) {
uint32_t pixel_count = (x1 - x0 + 1) * (y1 - y0 + 1);
uint32_t i;
// 设置窗口
LCD_SetColumn(x0, x1);
LCD_SetPage(y0, y1);
LCD_WriteCmd(0x2C);
// 连续写入像素
for (i = 0; i < pixel_count; i++) {
LCD_WriteData(color >> 8);
LCD_WriteData(color & 0xFF);
}
}
这里有个优化点:如果颜色是固定的,可以用硬件自动递增模式。有些 LCD 控制器支持「内存写自动加 1」,你只需要在初始化时配置好,之后连续写数据即可,不需要每次手动计算地址。
4.4 颜色格式转换:RGB565 与 RGB888
这是最容易出 bug 的地方。我见过太多人把颜色值搞错,导致屏幕显示偏色严重。
4.4.1 两种格式的位分布
| 格式 | 红色 | 绿色 | 蓝色 | 总位数 |
|---|---|---|---|---|
| RGB565 | 5 位 (bit15~11) | 6 位 (bit10~5) | 5 位 (bit4~0) | 16 位 |
| RGB888 | 8 位 (bit23~16) | 8 位 (bit15~8) | 8 位 (bit7~0) | 24 位 |
4.4.2 转换函数实现
// RGB888 -> RGB565
uint16_t RGB888_to_RGB565(uint8_t r, uint8_t g, uint8_t b) {
return ((r >> 3) << 11) | // 红:8 位截断为 5 位
((g >> 2) << 5) | // 绿:8 位截断为 6 位
(b >> 3); // 蓝:8 位截断为 5 位
}
// RGB565 -> RGB888(扩展)
void RGB565_to_RGB888(uint16_t rgb565, uint8_t *r, uint8_t *g, uint8_t *b) {
*r = ((rgb565 >> 11) & 0x1F) << 3; // 5 位扩展到 8 位
*g = ((rgb565 >> 5) & 0x3F) << 2; // 6 位扩展到 8 位
*b = (rgb565 & 0x1F) << 3; // 5 位扩展到 8 位
}
4.5 知识结构总览
下面这张图把本章的核心逻辑串起来了:
4.6 实战中的避坑指南
- 窗口参数越界:LCD 的列和页范围通常是 0~239 和 0~319(以 240×320 为例)。传入超出范围的值,轻则显示错位,重则死机。我建议在窗口函数内部加参数校验。
- 字节序问题:RGB565 数据在 16 位总线上是高位在前还是低位在前?不同 LCD 控制器不一样。ILI9341 默认是高位在前,但有些国产屏是低位在前。查数据手册的「Memory Write Cycle」章节。
- 颜色格式不匹配:如果你用 RGB888 的图片数据直接喂给 RGB565 的屏,颜色会严重失真。反过来,RGB565 的数据喂给 RGB888 的屏,颜色会偏暗。转换函数一定要写对。
4.7 小结
像素操作是 LCD 驱动的基石。窗口函数决定了「画在哪里」,写像素函数决定了「画什么」,区域填充提升了批量效率,颜色格式转换保证了颜色准确。这四个函数写好了,后面画线、画圆、显示图片都会轻松很多。
嗯,记住一点:先验证窗口,再验证颜色。我每次移植新屏驱动,都会先画一个全屏的纯色矩形——如果颜色不对,先查格式;如果位置不对,先查窗口。这个习惯帮我省了无数调试时间。