2、LVGL基础架构与显示驱动:从软件分层到点亮屏幕
好,我们直接进入正题。这一章要讲的是LVGL的骨架——软件架构和显示驱动。说白了,就是搞清楚LVGL内部怎么组织代码,以及我们怎么让它在屏幕上真正画出东西来。
我个人习惯把LVGL看作一个三层蛋糕。最上层是应用层,你写UI逻辑的地方;中间是核心层,处理所有图形计算和事件分发;最底层是硬件抽象层,负责跟屏幕、触摸屏打交道。今天重点就在最底层——显示驱动。
核心观点:LVGL的显示驱动框架设计得非常巧妙。它不直接操作硬件,而是通过一个叫“帧缓冲”的内存区域来间接控制屏幕。你只需要把像素数据填进这块内存,剩下的交给驱动和硬件。
2.1 LVGL软件架构概览
先看一张整体结构图,我亲手画的,帮你建立全局认知。
看到最右边那个说明框了吗?作为移植者,你只需要关注硬件抽象层。核心层和应用层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 避坑指南:我踩过的三个坑
这部分是我用真金白银换来的教训,你记一下。
- 字节序问题:16位色深下,RGB565的字节序在不同MCU上可能不同。STM32是大端,有些屏幕控制器是小端。颜色显示不对时,先检查这个。我曾经调了一个星期的颜色偏紫问题,最后发现是字节序反了。
- 缓冲区对齐:有些DMA控制器要求缓冲区地址4字节对齐。如果你用
uint8_t数组做缓冲区,可能不对齐,导致DMA传输异常。我建议用lv_color_t数组,它自带对齐属性。 - 刷新频率匹配:LVGL默认的刷新频率是30fps,如果你的屏幕刷新太慢(比如SPI时钟只有10MHz),LVGL会等不及,导致画面卡顿。这时候需要调低LVGL的刷新频率,或者优化硬件传输。
一句话总结:显示驱动移植的核心就是三件事——分配帧缓冲、实现刷新回调、注册驱动。其他都是细节,但细节决定成败。
2.6 本章小结
我们这一章把LVGL的软件架构拆开了,重点看了显示驱动框架和帧缓冲。你知道了LVGL分三层,你只需要管最底层;你知道了lv_disp_drv_t怎么配置;你知道了帧缓冲的三种模式;你还拿到了可以直接用的初始化代码。