3. LCD初始化序列:解析初始化命令表、配置显示窗口、设置像素格式
LCD上电后不会直接显示内容,它需要一套完整的初始化流程。我刚开始接触LCD驱动时,以为只要把数据扔过去就能显示,结果屏幕一片漆黑。后来才明白——初始化序列没走对,后面全是白费功夫。
这一节,我们就来拆解LCD初始化的三个核心环节:命令表解析、显示窗口配置、像素格式设置。说白了,就是告诉LCD芯片「你该醒醒了,准备好接数据」。
3.1 初始化命令表的结构与执行逻辑
大多数SPI接口的LCD控制器(比如常见的ILI9341、ST7789、ST7735)都内置了一套寄存器。初始化就是通过SPI发送一系列命令+参数,把这些寄存器设置成我们想要的状态。
命令表通常是一个二维数组,结构如下:
| 字段 | 说明 | 典型值 |
|---|---|---|
| 命令字节 | 寄存器地址,8位 | 0x11(退出睡眠) |
| 参数长度 | 该命令携带的参数字节数 | 0 或 N |
| 参数列表 | 连续存放的参数字节 | {0x00, 0x78} |
| 延时标记 | 命令执行后是否需要等待 | 0 或 毫秒值 |
我个人习惯把命令表定义成这样的结构体:
typedef struct {
uint8_t cmd;
uint8_t len;
uint8_t data[16];
uint16_t delay_ms;
} lcd_init_cmd_t;
然后整个初始化序列就是一个const数组:
const lcd_init_cmd_t st7789_init[] = {
{0x01, 0, {}, 120}, // 软件复位,等120ms
{0x11, 0, {}, 120}, // 退出睡眠模式
{0x36, 1, {0x00}, 0}, // 设置扫描方向
{0x3A, 1, {0x05}, 0}, // 像素格式:16位色
{0x21, 0, {}, 0}, // 反色关闭
{0x13, 0, {}, 10}, // 正常显示模式
{0x29, 0, {}, 10}, // 开启显示
};
执行时,驱动函数逐条取出命令,先发命令字节,再发参数,最后延时。流程很简单,但顺序不能乱。
3.2 配置显示窗口:CASET与RASET
显示窗口,也叫「显存访问窗口」。LCD控制器的GRAM(显存)通常比屏幕分辨率大。比如一块240×240的屏,GRAM可能是320×320。我们需要告诉芯片:「我只用这块矩形区域,数据往这里填」。
配置窗口用到两个关键命令:
- CASET(列地址设置):命令码0x2A,设置X轴起始和结束坐标
- RASET(行地址设置):命令码0x2B,设置Y轴起始和结束坐标
每个命令携带4个参数,分两对16位坐标:
// 设置窗口为全屏:0,0 到 239,239
void lcd_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) {
uint8_t data[4];
// CASET
data[0] = x0 >> 8;
data[1] = x0 & 0xFF;
data[2] = x1 >> 8;
data[3] = x1 & 0xFF;
lcd_send_cmd(0x2A, data, 4);
// RASET
data[0] = y0 >> 8;
data[1] = y0 & 0xFF;
data[2] = y1 >> 8;
data[3] = y1 & 0xFF;
lcd_send_cmd(0x2B, data, 4);
// 设置完成后,后续RAMWR(0x2C)写入的数据只填充这个窗口
lcd_send_cmd(0x2C, NULL, 0);
}
你想想看,如果窗口设置错了,会出现什么现象?最常见的就是显示错位——图像跑到屏幕外面去了,或者只显示一半。我在做滚动显示时,就是靠不断调整窗口位置来实现平滑滚动的。
3.3 像素格式设置:COLMOD命令
像素格式决定了每个像素用多少位来表示。常见的有:
| 参数值 | 像素格式 | 每像素字节数 | 颜色深度 |
|---|---|---|---|
| 0x02 | 8位色 | 1 | 256色 |
| 0x03 | 12位色 | 1.5(需对齐) | 4096色 |
| 0x05 | 16位色 | 2 | 65536色 |
| 0x06 | 18位色 | 3 | 262144色 |
命令码是0x3A(COLMOD),带一个参数。最常用的是0x05,即RGB565格式——红色5位、绿色6位、蓝色5位,共16位,占用2个字节。
为什么RGB565最流行?说白了,16位色在视觉上已经足够细腻,而且每个像素只占2字节,显存占用小,SPI传输也快。18位色虽然色彩更丰富,但传输量大了50%,对刷新率影响明显。
// 设置16位色模式
lcd_send_cmd(0x3A, (uint8_t[]){0x05}, 1);
设置完像素格式后,后续所有写入GRAM的数据都必须按这个格式打包。比如一个红色像素在RGB565下是:
// 红色:R=31, G=0, B=0
// 二进制:11111 000000 00000
// 合并为16位:0xF800
uint16_t red_pixel = 0xF800;
3.4 完整初始化流程与SVG图解
把上面三个环节串起来,一个典型的LCD初始化流程如下:
- 硬件复位:拉低RST引脚至少10ms,再拉高
- 发送初始化命令表:逐条执行,注意延时
- 配置显示窗口:设置CASET和RASET为全屏
- 设置像素格式:COLMOD设为0x05(16位色)
- 开启显示:发送0x29命令
- 填充背景色:向GRAM写入全屏颜色数据
下面这张图展示了整个初始化流程的脉络:
核心要点:初始化序列是LCD驱动的「第一道关卡」。命令表顺序、延时时间、窗口边界、像素格式——这四个要素只要有一个出错,屏幕就可能不亮、花屏或显示异常。我建议你在调试时,先用逻辑分析仪抓SPI波形,确认每个命令和参数都正确发出,再怀疑硬件问题。
3.5 实际项目中的初始化代码示例
下面是一个完整的初始化函数,基于ST7789驱动芯片,240×240分辨率:
void lcd_init(void) {
// 1. 硬件复位
LCD_RST_LOW();
delay_ms(10);
LCD_RST_HIGH();
delay_ms(120);
// 2. 发送初始化命令表
const lcd_init_cmd_t init_cmds[] = {
{0x01, 0, {}, 120}, // 软件复位
{0x11, 0, {}, 120}, // 退出睡眠
{0x36, 1, {0x00}, 0}, // 扫描方向:默认
{0x3A, 1, {0x05}, 0}, // 16位色
{0xB2, 5, {0x0C,0x0C,0x00,0x33,0x33}, 0}, // porch设置
{0xB7, 1, {0x35}, 0}, // 栅极驱动
{0xBB, 1, {0x28}, 0}, // VCOM
{0xC0, 1, {0x0C}, 0}, // 电源控制
{0xC2, 2, {0x01,0xFF}, 0}, // VDVH
{0xD0, 2, {0xA4,0xA1}, 0}, // VDV/VRH
{0x21, 0, {}, 0}, // 反色关闭
{0x13, 0, {}, 10}, // 正常显示
{0x29, 0, {}, 10}, // 开启显示
};
for (int i = 0; i < sizeof(init_cmds)/sizeof(init_cmds[0]); i++) {
lcd_send_cmd(init_cmds[i].cmd, init_cmds[i].data, init_cmds[i].len);
if (init_cmds[i].delay_ms > 0) {
delay_ms(init_cmds[i].delay_ms);
}
}
// 3. 设置全屏窗口
lcd_set_window(0, 0, 239, 239);
// 4. 填充黑色背景
uint16_t black = 0x0000;
for (uint32_t i = 0; i < 240 * 240; i++) {
lcd_send_data((uint8_t*)&black, 2);
}
}