2、LVGL基础架构与显示驱动:从软件分层到点亮屏幕

好,我们直接进入正题。这一章要讲的是LVGL的骨架——软件架构和显示驱动。说白了,就是搞清楚LVGL内部怎么组织代码,以及我们怎么让它在屏幕上真正画出东西来。

我个人习惯把LVGL看作一个三层蛋糕。最上层是应用层,你写UI逻辑的地方;中间是核心层,处理所有图形计算和事件分发;最底层是硬件抽象层,负责跟屏幕、触摸屏打交道。今天重点就在最底层——显示驱动。

核心观点:LVGL的显示驱动框架设计得非常巧妙。它不直接操作硬件,而是通过一个叫“帧缓冲”的内存区域来间接控制屏幕。你只需要把像素数据填进这块内存,剩下的交给驱动和硬件。

2.1 LVGL软件架构概览

先看一张整体结构图,我亲手画的,帮你建立全局认知。

LVGL软件架构分层图 LVGL 软件架构分层 应用层 (Application Layer) 你的UI逻辑:按钮回调、页面切换、数据绑定 核心层 (Core Layer) 对象管理、样式系统、事件分发、动画引擎 布局计算、绘图命令生成 硬件抽象层 (HAL) 显示驱动接口、输入设备驱动接口 帧缓冲管理、硬件定时器 硬件层 (LCD/SPI/Parallel) 你只需要关注这一层

看到最右边那个说明框了吗?作为移植者,你只需要关注硬件抽象层。核心层和应用层LVGL已经帮你写好了,你动都不用动。

2.2 显示驱动框架:lv_disp_drv_t

LVGL的显示驱动核心是一个结构体——lv_disp_drv_t。我刚开始接触时觉得它字段太多,后来发现其实就几个关键点。

字段名 类型 作用 必填?
flush_cb 函数指针 刷新回调,把帧缓冲数据刷到屏幕 是
draw_buf 指针 指向帧缓冲区的指针 是
hor_res uint16_t 屏幕水平分辨率 是
ver_res uint16_t 屏幕垂直分辨率 是
rotated uint8_t 屏幕旋转方向 否
color_depth uint8_t 颜色深度(16/32位) 否

我的经验:很多新手在flush_cb里犯同一个错误——忘记调用lv_disp_flush_ready()。这个函数告诉LVGL“我已经刷完了,你可以继续画下一块了”。不调用的话,屏幕会卡死,画面只刷新一次就停了。我曾经排查了一整个下午才发现是这个问题。

2.3 帧缓冲(FrameBuffer)概念

帧缓冲是什么?说白了就是一块内存区域,里面存着屏幕上每个像素的颜色值。LVGL把所有绘图操作都在这块内存里完成,然后一次性刷到屏幕上。

你想想看,如果每画一个像素就直接操作硬件,那效率得多低?帧缓冲就是中间缓存层,让LVGL可以“离线”绘制,再“批量”提交。

帧缓冲有三种常见模式:

  • 单缓冲模式:一块缓冲区,LVGL画完就刷。简单省内存,但可能有撕裂现象。
  • 双缓冲模式:两块缓冲区,一块用于显示,一块用于绘制。交替使用,画面流畅无撕裂。
  • 局部缓冲模式:缓冲区只覆盖屏幕的一部分,LVGL分块绘制。适合内存紧张的MCU。

注意:双缓冲虽然效果好,但内存消耗翻倍。比如一个320x240的16位色屏幕,单缓冲需要320×240×2 = 150KB,双缓冲就是300KB。对于STM32F103这种只有64KB RAM的芯片,根本跑不动。我建议先评估你的硬件资源再选模式。

2.4 初始化显示驱动:手把手教你写代码

理论说完了,直接上代码。这是我在实际项目中反复使用的初始化模板,你直接拿去改分辨率就行。

/* 1. 定义帧缓冲区 */
#define HOR_RES  320
#define VER_RES  240
#define COLOR_DEPTH 16

static lv_color_t buf1[HOR_RES * VER_RES];  /* 单缓冲 */
/* 如果内存够,可以再加一个 buf2 做双缓冲 */

/* 2. 初始化显示驱动 */
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);  /* 先初始化结构体 */

/* 3. 设置关键参数 */
disp_drv.hor_res = HOR_RES;
disp_drv.ver_res = VER_RES;
disp_drv.flush_cb = my_flush_callback;  /* 你的刷新函数 */
disp_drv.draw_buf = &buf1;              /* 绑定缓冲区 */

/* 4. 注册驱动 */
lv_disp_t *disp = lv_disp_drv_register(&disp_drv);

/* 5. 实现刷新回调 */
static void my_flush_callback(lv_disp_drv_t *drv, 
                              const lv_area_t *area, 
                              lv_color_t *color_p)
{
    /* 把 color_p 指向的数据刷到屏幕的 (area->x1, area->y1) 到 (area->x2, area->y2) 区域 */
    /* 这里调用你的硬件接口,比如 SPI 发送命令 */
    
    /* 重要!通知LVGL刷新完成 */
    lv_disp_flush_ready(drv);
}

嗯,这里要注意一点。flush_cb里的area参数告诉你要刷哪个矩形区域,不是全屏每次都刷。LVGL很聪明,只刷变化的部分,节省带宽。

2.5 避坑指南:我踩过的三个坑

这部分是我用真金白银换来的教训,你记一下。

  1. 字节序问题:16位色深下,RGB565的字节序在不同MCU上可能不同。STM32是大端,有些屏幕控制器是小端。颜色显示不对时,先检查这个。我曾经调了一个星期的颜色偏紫问题,最后发现是字节序反了。
  2. 缓冲区对齐:有些DMA控制器要求缓冲区地址4字节对齐。如果你用uint8_t数组做缓冲区,可能不对齐,导致DMA传输异常。我建议用lv_color_t数组,它自带对齐属性。
  3. 刷新频率匹配:LVGL默认的刷新频率是30fps,如果你的屏幕刷新太慢(比如SPI时钟只有10MHz),LVGL会等不及,导致画面卡顿。这时候需要调低LVGL的刷新频率,或者优化硬件传输。

一句话总结:显示驱动移植的核心就是三件事——分配帧缓冲、实现刷新回调、注册驱动。其他都是细节,但细节决定成败。

2.6 本章小结

我们这一章把LVGL的软件架构拆开了,重点看了显示驱动框架和帧缓冲。你知道了LVGL分三层,你只需要管最底层;你知道了lv_disp_drv_t怎么配置;你知道了帧缓冲的三种模式;你还拿到了可以直接用的初始化代码。