4. 点亮第一块LCD屏:LCD驱动IC初始化与显示验证

终于到了最让人兴奋的环节——点亮屏幕。说实话,我第一次调LCD驱动时,心里也没底。数据手册翻了好几遍,寄存器列表看得眼花缭乱。但后来我发现,只要抓住几个关键步骤,这事儿其实没那么玄乎。

这一章,我们就拿最常见的SSD1306(OLED)和ILI9341(TFT)来举例。我会带着你一步步走完初始化序列、设置显示区域、填充数据,最后验证屏幕是不是真的亮了。

4.1 驱动IC初始化序列分析

每个LCD驱动IC上电后,都处于一个“默认状态”。但这个默认状态通常不是我们想要的——比如对比度太低、扫描方向不对、或者干脆就是黑屏。所以,我们需要通过SPI或I2C发送一串配置命令,把它“唤醒”并设置好。

初始化序列,说白了就是一张命令表。芯片厂商会在数据手册里给出推荐序列。我个人习惯,拿到一个新屏,第一件事就是去数据手册里找 “Initialization Sequence” 或 “Example Code” 章节。

4.1.1 SSD1306 初始化流程

SSD1306是128x64的单色OLED驱动。它的初始化命令不多,但顺序很重要。我整理了一个典型序列:

步骤 命令 参数 说明
1 0xAE 无 关闭显示(先关再配)
2 0xD5 0x80 设置振荡频率
3 0xA8 0x3F 设置多路复用比(64行)
4 0xD3 0x00 显示偏移=0
5 0x40 无 起始行=0
6 0x8D 0x14 启用电荷泵(关键!)
7 0x20 0x00 设置内存寻址模式(水平)
8 0xA1 无 段重映射(左右镜像修正)
9 0xC8 无 COM扫描方向(上下镜像修正)
10 0xDA 0x12 COM引脚配置
11 0x81 0xCF 设置对比度
12 0xD9 0xF1 预充电周期
13 0xDB 0x40 VCOMH电压
14 0xA4 无 全局显示(非黑屏)
15 0xA6 无 正常显示(非反色)
16 0xAF 无 打开显示
注意: 第6步的电荷泵(0x8D 0x14)是SSD1306最容易踩的坑。我曾经遇到过一块屏死活不亮,查了两天,最后发现是电荷泵没开。芯片内部升压电路不工作,屏幕当然没反应。

4.1.2 ILI9341 初始化流程

ILI9341是320x240的TFT彩屏驱动,命令比SSD1306多得多。但核心思路一样:关显示 → 配置电源/时序 → 配置显示参数 → 开显示。这里我只列出关键命令:

命令 参数 作用
0x01 无 软件复位
0x11 无 退出睡眠模式
0xCF 0x00, 0x83, 0x30 电源控制B
0xED 0x64, 0x03, 0x12, 0x81 电源序列控制
0xE8 0x85, 0x01, 0x79 驱动时序控制A
0x3A 0x55 像素格式(16位色)
0x36 0x48 内存访问控制(扫描方向)
0x29 无 打开显示
我的经验: ILI9341的初始化序列,不同厂家生产的模组可能略有差异。比如0x36(扫描方向)这个寄存器,有的屏默认是横屏,有的是竖屏。我建议你拿到屏后,先跑一遍厂商提供的示例代码,确认显示方向对了再改。

4.2 编写LCD初始化函数

知道了命令序列,接下来就是写代码。我习惯把初始化封装成一个函数,比如 LCD_Init()。底层通信我用的是I2C,但SPI的写法也类似,只是发送函数不同。

先看SSD1306的初始化代码:

// SSD1306 I2C 初始化函数
void SSD1306_Init(void) {
    // 等待上电稳定
    HAL_Delay(100);
    
    // 关闭显示
    SSD1306_WriteCmd(0xAE);
    
    // 设置振荡频率
    SSD1306_WriteCmd(0xD5);
    SSD1306_WriteCmd(0x80);
    
    // 设置多路复用比
    SSD1306_WriteCmd(0xA8);
    SSD1306_WriteCmd(0x3F);  // 64行
    
    // 设置显示偏移
    SSD1306_WriteCmd(0xD3);
    SSD1306_WriteCmd(0x00);
    
    // 设置起始行
    SSD1306_WriteCmd(0x40);
    
    // 启用电荷泵(重要!)
    SSD1306_WriteCmd(0x8D);
    SSD1306_WriteCmd(0x14);
    
    // 设置内存寻址模式
    SSD1306_WriteCmd(0x20);
    SSD1306_WriteCmd(0x00);  // 水平寻址
    
    // 段重映射
    SSD1306_WriteCmd(0xA1);
    
    // COM扫描方向
    SSD1306_WriteCmd(0xC8);
    
    // COM引脚配置
    SSD1306_WriteCmd(0xDA);
    SSD1306_WriteCmd(0x12);
    
    // 设置对比度
    SSD1306_WriteCmd(0x81);
    SSD1306_WriteCmd(0xCF);
    
    // 预充电周期
    SSD1306_WriteCmd(0xD9);
    SSD1306_WriteCmd(0xF1);
    
    // VCOMH电压
    SSD1306_WriteCmd(0xDB);
    SSD1306_WriteCmd(0x40);
    
    // 全局显示
    SSD1306_WriteCmd(0xA4);
    
    // 正常显示
    SSD1306_WriteCmd(0xA6);
    
    // 打开显示
    SSD1306_WriteCmd(0xAF);
}

这里 SSD1306_WriteCmd() 是底层发送单字节命令的函数。对于I2C,它需要先发控制字节(0x00表示命令),再发命令字节。数据手册里叫它 “Command Mode”。

再看ILI9341的初始化,代码会长一些,但结构一样:

void ILI9341_Init(void) {
    // 软件复位
    ILI9341_WriteCmd(0x01);
    HAL_Delay(5);
    
    // 退出睡眠
    ILI9341_WriteCmd(0x11);
    HAL_Delay(120);  // 需要等待较长时间
    
    // 电源控制B
    ILI9341_WriteCmd(0xCF);
    ILI9341_WriteData(0x00);
    ILI9341_WriteData(0x83);
    ILI9341_WriteData(0x30);
    
    // ... 其他配置省略 ...
    
    // 像素格式:16位色
    ILI9341_WriteCmd(0x3A);
    ILI9341_WriteData(0x55);
    
    // 内存访问控制
    ILI9341_WriteCmd(0x36);
    ILI9341_WriteData(0x48);
    
    // 打开显示
    ILI9341_WriteCmd(0x29);
}
关键区别: SSD1306是单色屏,每个像素只占1位;ILI9341是彩屏,每个像素占2字节(RGB565)。所以后续的填充函数,数据格式完全不同。

4.3 设置显示区域与填充数据

初始化完了,屏幕还是黑的。我们需要告诉驱动IC:“我要往哪个区域写数据”。这就是设置显示区域(Set Address Window)。

4.3.1 SSD1306 的显示区域设置

SSD1306的内存是逐列扫描的。设置显示区域需要指定列起始、列结束、页起始、页结束。页的概念要理解一下:SSD1306把128列分成8页,每页8个像素(行)。

void SSD1306_SetCursor(uint8_t x, uint8_t y) {
    // 设置列地址范围
    SSD1306_WriteCmd(0x21);  // 设置列地址命令
    SSD1306_WriteCmd(x);     // 列起始
    SSD1306_WriteCmd(127);   // 列结束
    
    // 设置页地址范围
    SSD1306_WriteCmd(0x22);  // 设置页地址命令
    SSD1306_WriteCmd(y);     // 页起始
    SSD1306_WriteCmd(7);     // 页结束
}

填充数据时,我们直接往这个区域写字节。每个字节的8个位,对应一列上的8个像素。比如写0xFF,这一列就全亮。

void SSD1306_Fill(uint8_t data) {
    uint8_t i, j;
    for (i = 0; i < 8; i++) {       // 8页
        SSD1306_SetCursor(0, i);    // 每页从第0列开始
        for (j = 0; j < 128; j++) { // 128列
            SSD1306_WriteData(data);
        }
    }
}

4.3.2 ILI9341 的显示区域设置

ILI9341的设置更直观:指定矩形区域的左上角和右下角坐标。

void ILI9341_SetWindow(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
    // 设置列地址
    ILI9341_WriteCmd(0x2A);
    ILI9341_WriteData(x1 >> 8);
    ILI9341_WriteData(x1 & 0xFF);
    ILI9341_WriteData(x2 >> 8);
    ILI9341_WriteData(x2 & 0xFF);
    
    // 设置行地址
    ILI9341_WriteCmd(0x2B);
    ILI9341_WriteData(y1 >> 8);
    ILI9341_WriteData(y1 & 0xFF);
    ILI9341_WriteData(y2 >> 8);
    ILI9341_WriteData(y2 & 0xFF);
    
    // 准备写内存
    ILI9341_WriteCmd(0x2C);
}

填充颜色时,每个像素需要2字节(RGB565格式)。比如红色是0xF800,绿色是0x07E0,蓝色是0x001F。

void ILI9341_FillColor(uint16_t color) {
    uint32_t i;
    uint32_t total = 320 * 240;  // 全屏像素数
    
    ILI9341_SetWindow(0, 0, 319, 239);
    for (i = 0; i < total; i++) {
        ILI9341_WriteData(color >> 8);
        ILI9341_WriteData(color & 0xFF);
    }
}

4.4 验证显示是否正常

代码写完了,怎么验证?我的做法是分三步走:

  1. 全屏填充测试: 先填一种纯色(比如红色),看屏幕是不是全红了。如果有条纹或黑块,说明初始化或区域设置有问题。
  2. 边界测试: 画一个矩形边框,确认坐标映射是否正确。我曾经遇到过x和y反了的情况,画出来的图形是镜像的。
  3. 图案测试: 显示一个简单的图案,比如棋盘格或条纹,检查像素是否完整。
一个小技巧: 如果你手头没有逻辑分析仪,可以用示波器看I2C或SPI的时钟线和数据线。初始化时,波形应该是一串连续的脉冲。如果只有几个脉冲就停了,说明代码可能卡在某个地方了。

4.5 本章知识结构图

下面这张图总结了点亮LCD屏的完整流程,从初始化到验证:

LCD驱动初始化与显示验证流程 LCD驱动初始化与显示验证流程 1. 上电 & 硬件复位 2. 发送初始化命令序列 3. 设置显示区域 (Window) 4. 填充像素数据 5. 验证显示效果 常见验证方法 • 全屏纯色填充 • 矩形边框测试 • 棋盘格/条纹图案 常见问题 • 电荷泵未开启 • 扫描方向错误 • 像素格式不匹配

4.6 避坑指南

最后,分享几个我踩过的坑,希望能帮你省点时间:

  • 上电时序: 有些屏对VCC和IO的上电顺序有要求。比如先VCC再IO,或者需要等待一段时间。数据手册里通常有 “Power On Sequence” 章节,别跳过。
  • 复位引脚: 如果屏有硬件复位引脚(RST),初始化前一定要拉低再拉高。我见过有人忘了拉复位,结果初始化命令发过去没反应。
  • 命令/数据切换: 对于I2C接口,发送命令和数据时,控制字节不同。SSD1306的命令控制字节是0x00,数据是0x40。搞反了屏幕不会有任何反应。
  • 延时等待: 有些命令(比如退出睡眠)需要等待几十毫秒才能生效。别急着发下一条命令,给芯片一点反应时间。
总结一下: 点亮LCD屏,本质上就是“初始化 → 设窗口 → 填数据”这三板斧。只要初始化序列没错,区域设置对了,数据格式匹配了,屏幕一定能亮。如果没亮,别慌,用逻辑分析仪抓一下波形,看看命令有没有发出去,参数对不对。调试多了,你自然就有感觉了。