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},         // 开启显示
};

执行时,驱动函数逐条取出命令,先发命令字节,再发参数,最后延时。流程很简单,但顺序不能乱。

我曾经踩过的坑: 退出睡眠模式(0x11)之后必须等待至少120ms,否则后续命令会被忽略。有些芯片手册写的是5ms,但实际测试发现不够稳。我后来统一加了120ms延时,再没出过问题。

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);
}

你想想看,如果窗口设置错了,会出现什么现象?最常见的就是显示错位——图像跑到屏幕外面去了,或者只显示一半。我在做滚动显示时,就是靠不断调整窗口位置来实现平滑滚动的。

小技巧: 窗口坐标是包含边界的。全屏240×240时,结束坐标是239,不是240。写错边界会导致多写或少写一行像素,显示时会出现一条亮线或黑线。

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初始化流程如下:

  1. 硬件复位:拉低RST引脚至少10ms,再拉高
  2. 发送初始化命令表:逐条执行,注意延时
  3. 配置显示窗口:设置CASET和RASET为全屏
  4. 设置像素格式:COLMOD设为0x05(16位色)
  5. 开启显示:发送0x29命令
  6. 填充背景色:向GRAM写入全屏颜色数据

下面这张图展示了整个初始化流程的脉络:

LCD初始化序列流程图 LCD初始化序列流程图 1. 上电 & 硬件复位 2. 逐条执行初始化命令表 3. CASET / RASET 窗口配置 4. COLMOD 像素格式设置 5. 0x29 开启显示 6. 填充背景色(可选) 关键注意事项 • 退出睡眠后必须延时 • 窗口坐标包含边界 • 像素格式与数据对齐 • 命令顺序不可颠倒

核心要点:初始化序列是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);
    }
}
调试建议: 初始化完成后,先填充一种纯色(比如红色0xF800),看看屏幕是否全屏显示正确颜色。如果只有部分区域变色,说明窗口配置有问题;如果颜色不对,检查像素格式和SPI时序。