4. 单缓冲刷新机制
做GUI双缓冲,说白了你得先让屏幕亮起来、能画点东西。不然缓冲区里数据再漂亮,LCD上啥也看不见。
这一章咱们就啃LCD驱动最核心的三块:硬件接口怎么选、初始化序列怎么写、画点函数怎么实现。我当年第一次调LCD驱动,花了整整两天才点亮第一个像素——后来发现是初始化时序里一个延时参数写错了。嗯,这种坑咱们今天一并填上。
4.1 两种主流接口:SPI vs I8080
嵌入式LCD常用的接口就两种:SPI和I8080(也叫MCU接口、并行接口)。选哪个,取决于你的屏尺寸、刷新率需求和MCU引脚资源。
| 对比项 | SPI(串行) | I8080(并行) |
|---|---|---|
| 引脚数 | 4~5线(CS、SCLK、MOSI、MISO、DC) | 8~16数据线 + 控制线(RD、WR、CS、RS) |
| 传输速率 | 一般10~40MHz,受限于SPI时钟 | 可达80~160MHz(16位并行) |
| 适用场景 | 小屏(<3.5寸)、低刷新率、引脚紧张 | 大屏、高刷新率、双缓冲场景 |
| 驱动复杂度 | 简单,软件模拟也容易 | 稍复杂,时序要求严格 |
我的建议:如果你的GUI刷新率目标在30fps以上,或者分辨率超过320x240,老老实实用I8080。SPI在高速下容易受干扰,而且刷全屏时CPU占用率极高。
4.2 接口时序基础
不管哪种接口,核心都是「写命令」和「写数据」两个操作。区别在于:SPI靠DC引脚区分,I8080靠RS引脚区分。
4.2.1 SPI写时序
SPI模式下,DC=0表示当前字节是命令,DC=1表示是数据。CS拉低选中设备,SCLK每个上升沿锁存数据。
// SPI写命令
static void lcd_write_cmd(uint8_t cmd) {
LCD_CS_LOW();
LCD_DC_LOW(); // 命令模式
spi_transfer_byte(cmd);
LCD_CS_HIGH();
}
// SPI写数据
static void lcd_write_data(uint8_t data) {
LCD_CS_LOW();
LCD_DC_HIGH(); // 数据模式
spi_transfer_byte(data);
LCD_CS_HIGH();
}
小技巧:很多SPI LCD支持连续写数据模式——先发一次命令,然后连续发多个数据字节,中间不用拉高CS。刷全屏时效率能提升30%以上。
4.2.2 I8080写时序
I8080接口更直接:RS=0写命令,RS=1写数据。WR上升沿锁存数据总线上的值。
// I8080写命令(16位数据总线)
static void lcd_write_cmd(uint16_t cmd) {
LCD_RS_LOW(); // 命令
LCD_CS_LOW();
LCD_WR_LOW();
DATA_BUS = cmd;
LCD_WR_HIGH(); // 上升沿锁存
LCD_CS_HIGH();
}
// I8080写数据
static void lcd_write_data(uint16_t data) {
LCD_RS_HIGH(); // 数据
LCD_CS_LOW();
LCD_WR_LOW();
DATA_BUS = data;
LCD_WR_HIGH();
LCD_CS_HIGH();
}
注意:I8080的WR脉冲宽度有最小要求,一般在20~50ns。用GPIO模拟时,连续几条赋值语句之间最好加个空操作(__NOP())保证时序。
4.3 初始化序列——LCD的「开机流程」
LCD上电后不会自动工作,必须按数据手册的初始化序列发送一串命令。不同驱动IC的序列不同,但套路基本一致。
我整理了一个通用初始化流程,以常见的ILI9341为例:
- 硬件复位:RST引脚拉低10ms以上,再拉高等待120ms
- 退出睡眠模式:发命令0x11(SLPOUT),等待150ms
- 设置像素格式:发命令0x3A,参数0x55(16位色)或0x66(18位色)
- 设置内存访问控制:发命令0x36,参数控制RGB/BGR、扫描方向
- 设置显示窗口:发命令0x2A(列地址)、0x2B(行地址)
- 开启显示:发命令0x29(DISPON)
// ILI9341初始化示例(精简版)
void lcd_init(void) {
// 1. 硬件复位
LCD_RST_LOW();
delay_ms(10);
LCD_RST_HIGH();
delay_ms(120);
// 2. 退出睡眠
lcd_write_cmd(0x11);
delay_ms(150);
// 3. 像素格式:16位色
lcd_write_cmd(0x3A);
lcd_write_data(0x55);
// 4. 内存访问控制:RGB顺序,从上到下从左到右
lcd_write_cmd(0x36);
lcd_write_data(0x48);
// 5. 开启显示
lcd_write_cmd(0x29);
delay_ms(50);
}
避坑指南:我曾经在初始化序列里漏了退出睡眠模式后的延时,结果屏幕一直黑着。查了两天数据手册才发现——0x11命令后必须等150ms才能发后续命令,否则驱动IC不响应。
4.4 画点函数——GUI的原子操作
画点函数是所有GUI操作的基础。它的核心逻辑就三步:设置写窗口 → 移动光标到目标位置 → 写入像素颜色。
以ILI9341为例,画点函数实现如下:
// 设置写窗口(矩形区域)
static void lcd_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) {
lcd_write_cmd(0x2A); // 列地址
lcd_write_data(x0 >> 8);
lcd_write_data(x0 & 0xFF);
lcd_write_data(x1 >> 8);
lcd_write_data(x1 & 0xFF);
lcd_write_cmd(0x2B); // 行地址
lcd_write_data(y0 >> 8);
lcd_write_data(y0 & 0xFF);
lcd_write_data(y1 >> 8);
lcd_write_data(y1 & 0xFF);
lcd_write_cmd(0x2C); // 开始写内存
}
// 画点函数
void lcd_draw_pixel(uint16_t x, uint16_t y, uint16_t color) {
// 边界检查
if (x >= LCD_WIDTH || y >= LCD_HEIGHT) return;
lcd_set_window(x, y, x, y); // 窗口大小为1x1
lcd_write_data(color); // 写入像素颜色
}
性能优化:画点函数每次都要设置窗口,效率很低。批量画图时(比如填充矩形),应该只设置一次窗口,然后连续写多个像素数据。双缓冲的刷新函数就是这么干的。
4.5 知识体系总览
下面这张图把LCD驱动的基础知识串起来了,方便你对照理解:
4.6 写在最后
LCD驱动这块,说白了就是「接口选对、时序写对、初始化别漏、画点函数别错」。你只要把这四个点吃透了,后面做双缓冲刷新就是水到渠成的事。
我个人习惯把画点函数封装成宏或者内联函数,因为它在GUI里调用频率极高——每帧画面可能调用几万次。函数调用开销累积起来,帧率能差好几帧。
嗯,这一章就到这儿。代码示例都是实际项目里验证过的,你直接拿去用,注意把引脚定义和延时函数换成你自己的平台就行。