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 | 无 | 打开显示 |
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 | 无 | 打开显示 |
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);
}
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 验证显示是否正常
代码写完了,怎么验证?我的做法是分三步走:
- 全屏填充测试: 先填一种纯色(比如红色),看屏幕是不是全红了。如果有条纹或黑块,说明初始化或区域设置有问题。
- 边界测试: 画一个矩形边框,确认坐标映射是否正确。我曾经遇到过x和y反了的情况,画出来的图形是镜像的。
- 图案测试: 显示一个简单的图案,比如棋盘格或条纹,检查像素是否完整。
4.5 本章知识结构图
下面这张图总结了点亮LCD屏的完整流程,从初始化到验证:
4.6 避坑指南
最后,分享几个我踩过的坑,希望能帮你省点时间:
- 上电时序: 有些屏对VCC和IO的上电顺序有要求。比如先VCC再IO,或者需要等待一段时间。数据手册里通常有 “Power On Sequence” 章节,别跳过。
- 复位引脚: 如果屏有硬件复位引脚(RST),初始化前一定要拉低再拉高。我见过有人忘了拉复位,结果初始化命令发过去没反应。
- 命令/数据切换: 对于I2C接口,发送命令和数据时,控制字节不同。SSD1306的命令控制字节是0x00,数据是0x40。搞反了屏幕不会有任何反应。
- 延时等待: 有些命令(比如退出睡眠)需要等待几十毫秒才能生效。别急着发下一条命令,给芯片一点反应时间。