3、显示驱动框架:framebuffer原理、显示驱动接口、初始化显示控制器、实现打点函数

好,到了第三章。这一章是真正开始跟硬件打交道的地方。很多初学者一上来就急着写打点函数,结果屏幕死活不亮,然后就开始怀疑人生。我当年也干过这种事——焊好线、烧好程序,屏幕一片漆黑,折腾了两天才发现是时钟配置错了。

所以这一章,咱们把显示驱动框架拆开揉碎了讲。你理解了 framebuffer 的本质,搞清了显示控制器的初始化流程,再写打点函数就是水到渠成的事。

3.1 什么是 framebuffer?

Framebuffer,直译过来就是「帧缓冲区」。说白了,它就是一块内存区域,里面存着屏幕上每个像素的颜色数据。CPU 往这块内存里写数据,显示控制器就自动把数据刷到屏幕上。

你可以把它想象成一个二维数组:

// 假设屏幕分辨率是 800x480,每个像素 2 字节(RGB565)
uint16_t framebuffer[480][800];  // 行 × 列

你修改 framebuffer[100][200] = 0xF800;,屏幕上 (200, 100) 那个点就变成红色。就是这么简单粗暴。

核心概念:Framebuffer 是 CPU 和显示控制器之间的「共享内存」。CPU 只管写,显示控制器只管读。两者通过硬件同步机制(比如 VSync 中断)来避免撕裂。

我在项目里见过有人用单缓冲,也有人用双缓冲。单缓冲简单省内存,但会有撕裂现象——屏幕上半部分显示旧帧,下半部分显示新帧。双缓冲虽然多占一倍内存,但画面流畅得多。LVGL 默认支持双缓冲,后面我们会讲到怎么配置。

3.2 显示驱动接口长什么样?

不管底层硬件是 SPI、RGB 还是 MIPI,LVGL 只认三个接口:

接口函数 作用 参数说明
disp_init() 初始化显示控制器和 GPIO 无参数,通常在 lv_port_disp_init() 中调用
disp_flush() 将一块矩形区域的数据刷到屏幕 x1, y1, x2, y2, color_p
disp_rounder()(可选) 对齐刷新区域边界 某些硬件要求坐标是 2 或 4 的倍数

你想想看,LVGL 其实不关心你的屏幕是怎么接的。它只调用 disp_flush(),把一块矩形区域的颜色数据扔给你。你怎么把数据送到屏幕上,那是你自己的事。

我的习惯:在写 disp_flush() 之前,先写一个裸机的打点函数测试一下。确认屏幕能亮、能画点,再封装成 flush 接口。这样出了问题,你能快速定位是硬件问题还是 LVGL 配置问题。

3.3 初始化显示控制器

初始化这一步,不同芯片差异很大。但核心流程是通用的:

  1. 配置 GPIO:把数据线、时钟线、控制线(CS、DC、RST、BL)设为正确的功能模式。
  2. 配置时钟:给 LCD 控制器提供正确的时钟源和分频系数。
  3. 配置时序参数:HBP、HFP、VBP、VFP、Hsync、Vsync 这些参数必须跟屏幕数据手册一致。
  4. 发送初始化命令:通过 SPI 或并行接口向 LCD 驱动 IC 写入寄存器配置。
  5. 点亮背光:拉高 BL 引脚,或者通过 PWM 调节亮度。

我拿一个常见的 ILI9341 屏幕举个例子:

void lcd_init(void) {
    // 1. 复位屏幕
    gpio_set_level(PIN_RST, 0);
    delay_ms(10);
    gpio_set_level(PIN_RST, 1);
    delay_ms(120);

    // 2. 发送初始化命令序列
    lcd_write_cmd(0x01);  // 软件复位
    delay_ms(120);

    lcd_write_cmd(0x11);  // 退出睡眠模式
    delay_ms(120);

    lcd_write_cmd(0x36);  // 内存访问控制
    lcd_write_data(0x48); // 根据你的屏幕方向调整

    lcd_write_cmd(0x3A);  // 像素格式
    lcd_write_data(0x55); // RGB565

    lcd_write_cmd(0x29);  // 开启显示
    delay_ms(10);

    // 3. 点亮背光
    gpio_set_level(PIN_BL, 1);
}

我曾经踩过的坑:初始化命令序列里的延时不能省。有些屏幕对时序很敏感,延时不够会导致初始化失败。我遇到过一块屏幕,复位后必须等 120ms 才能发命令,否则它根本不响应。查数据手册查了两天才发现。

3.4 实现打点函数

打点函数是所有显示操作的基础。它的逻辑很简单:

  1. 设置光标位置(告诉屏幕你要往哪个坐标写数据)。
  2. 写入像素颜色值。

对于 ILI9341 这类 SPI 接口的屏幕,打点函数大概长这样:

void lcd_draw_pixel(uint16_t x, uint16_t y, uint16_t color) {
    // 设置列地址
    lcd_write_cmd(0x2A);
    lcd_write_data(x >> 8);
    lcd_write_data(x & 0xFF);

    // 设置行地址
    lcd_write_cmd(0x2B);
    lcd_write_data(y >> 8);
    lcd_write_data(y & 0xFF);

    // 写像素数据
    lcd_write_cmd(0x2C);
    lcd_write_data(color >> 8);
    lcd_write_data(color & 0xFF);
}

注意这里的数据格式。RGB565 下,一个像素占 2 个字节。高位在前还是低位在前,取决于你的屏幕和 MCU 的字节序。我习惯先发高字节再发低字节,大部分屏幕都认这个顺序。

3.5 从打点到 flush 接口

有了打点函数,封装 disp_flush() 就很简单了。LVGL 每次刷新会传一个矩形区域,你只需要遍历这个区域内的所有像素:

void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) {
    int32_t x, y;
    for (y = area->y1; y <= area->y2; y++) {
        for (x = area->x1; x <= area->x2; x++) {
            lcd_draw_pixel(x, y, color_p->full);
            color_p++;
        }
    }
    lv_disp_flush_ready(disp);  // 通知 LVGL 刷新完成
}

嗯,这里要注意。上面的代码虽然能工作,但效率很低。每个像素都要发三条命令,SPI 通信开销巨大。实际项目中,我们会用「写矩形」命令来优化:

void lcd_fill_rect(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t *colors) {
    uint32_t size = (x2 - x1 + 1) * (y2 - y1 + 1);

    // 设置矩形区域
    lcd_write_cmd(0x2A);
    lcd_write_data(x1 >> 8); lcd_write_data(x1 & 0xFF);
    lcd_write_data(x2 >> 8); lcd_write_data(x2 & 0xFF);

    lcd_write_cmd(0x2B);
    lcd_write_data(y1 >> 8); lcd_write_data(y1 & 0xFF);
    lcd_write_data(y2 >> 8); lcd_write_data(y2 & 0xFF);

    // 连续写入所有像素数据
    lcd_write_cmd(0x2C);
    lcd_write_data_bulk(colors, size * 2);  // 批量写,不拆包
}

这样一次命令就能刷完整个矩形区域,速度能提升几十倍。

性能建议:如果你的 MCU 支持 DMA,一定要用 DMA 来传输像素数据。CPU 只需要启动传输,然后就可以去干别的事。LVGL 的刷新帧率能从 15fps 直接跳到 60fps。

3.6 本章知识体系

下面这张图总结了显示驱动框架的核心流程:

显示驱动框架流程图 显示驱动框架核心流程 Framebuffer 内存 LVGL 渲染引擎 disp_flush() 回调 打点 / 矩形填充 显示控制器 → 屏幕 关键点 • 数据流向:内存 → 渲染 → 刷新 • 优化重点:减少 SPI 命令次数 • 性能瓶颈:像素传输带宽 常见接口 • SPI:引脚少,速度较慢 • RGB:速度快,引脚多 • MCU 8080:并行,中等速度

从这张图可以看得很清楚:数据从 framebuffer 出发,经过 LVGL 渲染引擎处理,通过 disp_flush() 回调进入你的驱动层,最终由显示控制器送到屏幕上。每一层都有优化的空间,但最底层的打点函数是基础中的基础。

我个人习惯先把打点函数调通,再往上封装。这样出了问题,你能确定是底层驱动的问题,还是上层配置的问题。别问我怎么知道的——当年我同时改了驱动和 LVGL 配置,结果屏幕不亮,排查了整整一个下午。