3、LVGL基础入门:LVGL架构与核心概念

好,从这一章开始,我们正式进入LVGL的世界。说实话,我第一次接触LVGL的时候,也被它那一堆概念搞得有点懵——对象、图层、事件、样式……感觉像在学一个新的操作系统。但别担心,咱们一步步来,把这些东西拆开揉碎了讲清楚。

我个人习惯,学一个新框架之前,先看它的整体架构。就像你要去一个陌生的城市,先看地图,再走街串巷,心里才有底。

3.1 LVGL的整体架构

LVGL的架构,说白了就是三层结构:

  • 底层:硬件驱动层,负责跟屏幕、触摸、定时器打交道。
  • 中间层:核心库层,对象管理、事件分发、绘图引擎都在这里。
  • 上层:控件层,按钮、标签、滑块、图表……你看到的UI元素。

你想想看,这三层各司其职。底层把像素点亮,中间层决定怎么画、画什么,上层提供现成的“积木块”。

核心要点: LVGL的架构设计遵循“分层解耦”原则。你写应用代码时,基本只跟上层控件打交道,底层驱动配置好一次就不用再动了。

我在项目中遇到过一个问题:有人把底层驱动和UI逻辑混在一起写,结果换了个屏幕,整个代码都要重写。嗯,这就是没理解分层的好处。

3.2 核心概念:对象、图层、事件

这三个概念,是LVGL的基石。搞懂了它们,你就掌握了LVGL的一半。

3.2.1 对象(lv_obj_t)

LVGL里的一切都是对象。按钮是对象,标签是对象,甚至屏幕本身也是一个对象。每个对象都有自己的属性:位置、大小、颜色、透明度……

对象之间是父子关系。比如,一个按钮对象是屏幕对象的子对象。子对象会继承父对象的一些属性,比如裁剪区域、透明度等。

// 创建一个屏幕对象
lv_obj_t * scr = lv_obj_create(NULL);
// 在屏幕上创建一个按钮对象
lv_obj_t * btn = lv_btn_create(scr);
// 在按钮上创建一个标签对象
lv_obj_t * label = lv_label_create(btn);
lv_label_set_text(label, "点击我");

这段代码,就是典型的“对象树”结构。屏幕是根,按钮是子,标签是孙。你想想看,这种树形结构管理UI,是不是很清晰?

小技巧: 我习惯把对象树画在纸上,尤其是复杂界面。先画树,再写代码,逻辑不会乱。

3.2.2 图层(Layer)

图层这个概念,很多人容易忽略。LVGL默认有三个图层:

图层 名称 用途
底层 LV_LAYER_TYPE_BOTTOM 背景、壁纸等
主层 LV_LAYER_TYPE_TOP 普通UI控件
顶层 LV_LAYER_TYPE_SYS 系统级覆盖层(如弹窗)

说白了,图层就是Z轴顺序。底层在最下面,顶层在最上面。你平时创建的控件默认都在主层。如果你想让某个控件永远在最前面,比如一个“加载中”的提示,可以把它放到顶层。

我曾经踩过一个坑:弹窗被其他控件挡住了。后来才发现,弹窗默认在主层,而主层里控件的Z轴顺序是按创建时间排的。解决办法很简单,把弹窗放到顶层图层就好了。

// 创建一个顶层图层对象
lv_obj_t * sys_layer = lv_layer_sys_create();
// 在顶层图层上创建弹窗
lv_obj_t * msgbox = lv_msgbox_create(sys_layer, NULL);

3.2.3 事件(Event)

事件,就是用户操作和系统状态变化的“通知”。点击、长按、滑动、释放……这些都是事件。

LVGL的事件处理机制,我特别喜欢。它用的是“事件监听+回调”模式。你给一个对象注册一个事件回调函数,当事件发生时,LVGL自动调用这个函数。

// 事件回调函数
static void btn_event_cb(lv_event_t * e) {
    lv_event_code_t code = lv_event_get_code(e);
    if(code == LV_EVENT_CLICKED) {
        LV_LOG_USER("按钮被点击了!");
    }
}

// 注册事件回调
lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, NULL);

这里要注意,事件是可以冒泡的。子对象的事件会向上传递给父对象。你想想看,这有什么用?比如,你有一个容器,里面有很多按钮。你不需要给每个按钮都注册事件,只需要给容器注册一个事件,然后判断事件源是谁就行了。

避坑指南: 我曾经在事件回调里做了耗时操作,导致界面卡死。记住,事件回调里不要做延时、循环、文件读写等阻塞操作。如果必须做,请使用LVGL的任务系统(lv_task)或异步处理。

3.3 创建第一个LVGL工程

理论说完了,咱们动手。创建一个LVGL工程,其实就三步:

  1. 下载源码:从GitHub或官网下载LVGL源码。
  2. 配置工程:把源码文件添加到你的IDE工程里。
  3. 编写初始化代码:调用lv_init()初始化库,然后配置显示驱动和输入驱动。

我建议你直接用LVGL官方提供的模板工程,省时省力。如果你用的是STM32,官方有STM32的模板;如果你用的是ESP32,也有对应的模板。

#include "lvgl.h"

int main(void) {
    // 1. 初始化LVGL库
    lv_init();
    
    // 2. 初始化显示驱动(后面详细讲)
    my_display_init();
    
    // 3. 初始化输入驱动(触摸或按键)
    my_input_init();
    
    // 4. 创建UI
    lv_obj_t * label = lv_label_create(lv_scr_act());
    lv_label_set_text(label, "Hello, LVGL!");
    lv_obj_center(label);
    
    // 5. 主循环
    while(1) {
        lv_timer_handler();  // 处理LVGL任务
        my_delay(5);         // 延时5ms
    }
}

这段代码,就是LVGL工程的“骨架”。你以后所有的项目,都是在这个骨架基础上填充内容。

小提示: lv_timer_handler() 是LVGL的心脏,它负责处理所有定时任务、动画、事件等。你必须定期调用它,一般每5ms调用一次。我习惯在主循环里放一个5ms的延时,然后调用它。

3.4 屏幕与显示驱动配置

显示驱动配置,是LVGL移植中最关键的一步。说白了,就是告诉LVGL:你的屏幕长什么样,怎么往上面画像素。

LVGL的显示驱动,核心是一个结构体:lv_disp_drv_t。你需要填充这个结构体,然后注册它。

字段 说明 必填
hor_res 屏幕水平分辨率 是
ver_res 屏幕垂直分辨率 是
flush_cb 刷新回调函数 是
draw_buf 绘图缓冲区 是
user_data 用户自定义数据 否

最重要的就是 flush_cb,这个函数负责把LVGL绘制好的缓冲区内容,刷新到屏幕上。

// 显示缓冲区
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf[LV_HOR_RES_MAX * 10];  // 10行缓冲区

// 刷新回调函数
static void my_flush_cb(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) {
    // 将color_p指向的缓冲区数据,刷新到屏幕的area区域
    // 这里调用你的屏幕驱动函数
    my_display_fill(area->x1, area->y1, area->x2, area->y2, color_p);
    
    // 通知LVGL刷新完成
    lv_disp_flush_ready(disp_drv);
}

// 显示驱动初始化
void my_display_init(void) {
    // 初始化绘图缓冲区
    lv_disp_draw_buf_init(&draw_buf, buf, NULL, LV_HOR_RES_MAX * 10);
    
    // 初始化显示驱动
    static lv_disp_drv_t disp_drv;
    lv_disp_drv_init(&disp_drv);
    disp_drv.hor_res = 320;   // 屏幕宽度
    disp_drv.ver_res = 240;   // 屏幕高度
    disp_drv.flush_cb = my_flush_cb;
    disp_drv.draw_buf = &draw_buf;
    
    // 注册显示驱动
    lv_disp_drv_register(&disp_drv);
}

这里有个细节要注意:lv_disp_flush_ready() 必须调用。如果不调用,LVGL会认为刷新还没完成,就不会继续绘制下一帧。我曾经忘了加这一句,结果屏幕一直黑着,查了半天才发现。

关键点: 显示缓冲区的大小直接影响性能。缓冲区越大,刷新效率越高,但占用内存也越多。对于资源受限的MCU,我建议使用“行缓冲区”,比如10行。这样内存占用小,性能也够用。

嗯,到这里,LVGL的基础架构和核心概念就讲完了。你可能会觉得内容有点多,但别急,这些都是基本功。下一节我们会用这些知识,真正跑起来一个LVGL程序。

LVGL架构与核心概念 上层:控件层 按钮、标签、滑块、图表、列表…… 直接面向用户,提供现成UI组件 lv_btn_create() / lv_label_create() / lv_slider_create() 中间层:核心库层 对象管理、事件分发、绘图引擎、样式系统 负责UI逻辑、布局、动画、内存管理 lv_obj_t / lv_event_t / lv_style_t / lv_draw_ctx_t 底层:硬件驱动层 显示驱动、触摸驱动、定时器、文件系统 与硬件直接交互,提供底层能力 lv_disp_drv_t / lv_indev_drv_t / lv_tick_get() 对象(lv_obj_t) 图层(Layer) 事件(Event)