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为例:

  1. 硬件复位:RST引脚拉低10ms以上,再拉高等待120ms
  2. 退出睡眠模式:发命令0x11(SLPOUT),等待150ms
  3. 设置像素格式:发命令0x3A,参数0x55(16位色)或0x66(18位色)
  4. 设置内存访问控制:发命令0x36,参数控制RGB/BGR、扫描方向
  5. 设置显示窗口:发命令0x2A(列地址)、0x2B(行地址)
  6. 开启显示:发命令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驱动的基础知识串起来了,方便你对照理解:

LCD驱动基础知识体系 接口选择 SPI接口 4线/5线 · 引脚少 · 速度一般 I8080接口 8/16位并行 · 速度快 · 引脚多 初始化序列 硬件复位 退出睡眠 像素格式 开启显示 画点函数 设置写窗口 移动光标 写入像素颜色 核心:画点函数是GUI所有绘图操作的基础,双缓冲最终也要靠它把数据刷到屏幕上

4.6 写在最后

LCD驱动这块,说白了就是「接口选对、时序写对、初始化别漏、画点函数别错」。你只要把这四个点吃透了,后面做双缓冲刷新就是水到渠成的事。

我个人习惯把画点函数封装成宏或者内联函数,因为它在GUI里调用频率极高——每帧画面可能调用几万次。函数调用开销累积起来,帧率能差好几帧。

嗯,这一章就到这儿。代码示例都是实际项目里验证过的,你直接拿去用,注意把引脚定义和延时函数换成你自己的平台就行。