2、LVGL架构总览:软件架构、模块划分、核心组件与渲染管线

好,咱们进入第二章。上一章我们搭好了环境,跑起了第一个demo。现在该把LVGL的“骨架”拆开看看了。

我刚开始接触LVGL时,第一反应是“这玩意儿代码量不小啊”。但真正理清它的架构后,发现设计得相当清晰。说白了,LVGL就是一个面向对象的嵌入式GUI框架,用C语言模拟了类的继承和多态。

2.1 软件架构:三层结构

LVGL的软件架构可以分成三层。我个人习惯把它想象成一个汉堡:

  • 底层(面包底):硬件驱动 + 操作系统抽象层。负责屏幕刷新、触摸输入、定时器、内存管理。
  • 中间层(肉饼):核心库。包括对象管理、事件系统、样式系统、布局引擎、动画引擎、绘图API。
  • 上层(面包顶):控件库(Widgets)。所有你看到的按钮、标签、滑块、图表,都在这一层。

关键点:上层控件不直接操作硬件,它们只调用中间层的绘图API。中间层再通过底层驱动把像素刷到屏幕上。这种分层的好处是——换屏幕驱动?改底层就行,上层代码不用动。

LVGL 三层软件架构 上层:控件库 (Widgets) 按钮 | 标签 | 滑块 | 图表 | 列表 | 键盘 | 画布 ... 中间层:核心库 (Core) 对象管理 | 事件系统 | 样式系统 | 布局引擎 动画引擎 | 绘图API | 字体/图片管理 底层:硬件驱动 + OS抽象层 显示驱动 | 触摸驱动 | 定时器 | 内存管理 | 文件系统 上层调用中间层API,中间层通过底层驱动操作硬件。层与层之间通过接口解耦。

2.2 模块划分:核心模块一览

LVGL的源码目录结构,其实就是模块划分的直接体现。我列一下最重要的几个模块:

模块 目录/前缀 职责
核心对象 lv_obj 所有控件的基类。创建、删除、移动、层级管理。
样式系统 lv_style 管理颜色、边框、阴影、圆角等视觉属性。支持CSS-like级联。
事件系统 lv_event 触摸、按键、自定义事件的注册、分发、冒泡。
布局引擎 lv_layout Flexbox和Grid布局。自动计算子控件位置。
绘图引擎 lv_draw 绘制矩形、圆、文字、图片、弧线。支持抗锯齿。
动画引擎 lv_anim 属性插值、缓动函数、动画路径、自动回弹。
字体/图片 lv_font / lv_img 字体加载、图片解码、缓存管理。
输入设备 lv_indev 触摸、鼠标、键盘、编码器的抽象接口。
控件库 lv_btn, lv_label ... 几十种现成控件,开箱即用。

我的经验:刚开始不用把所有模块都啃完。先掌握 lv_obj、lv_style、lv_event 这三个,就能应付80%的开发场景。其他模块用到再查。

2.3 核心组件:对象、样式、事件

这三个组件是LVGL的“三驾马车”。我分别说一下。

2.3.1 对象(lv_obj)

LVGL里所有可见的东西都是对象。按钮是对象,标签是对象,甚至屏幕本身也是一个对象。对象之间可以形成父子关系:

lv_obj_t *scr = lv_scr_act();              // 获取当前屏幕(父对象)
lv_obj_t *btn = lv_btn_create(scr);        // 在屏幕上创建按钮(子对象)
lv_obj_t *label = lv_label_create(btn);    // 在按钮上创建标签(子对象的子对象)

父子关系决定了:

  • 位置:子对象坐标相对于父对象。
  • 裁剪:子对象超出父对象范围的部分会被裁剪。
  • 事件冒泡:事件可以从子对象冒泡到父对象。
  • 生命周期:删除父对象,所有子对象自动删除。

避坑指南:我曾经在项目中忘记设置父对象,直接把按钮创建在NULL上。结果按钮显示不出来,排查了半天。记住:lv_btn_create(NULL) 会创建一个“孤儿对象”,它不在任何屏幕上,自然看不见。

2.3.2 样式(lv_style)

样式就是控件的“皮肤”。LVGL的样式系统支持级联——你可以给屏幕设置一个全局样式,给按钮组设置一个组样式,再给某个按钮单独设置样式。优先级是:局部 > 组 > 全局。

lv_style_t style;
lv_style_init(&style);
lv_style_set_bg_color(&style, lv_color_hex(0x2196F3));
lv_style_set_radius(&style, 8);

lv_obj_add_style(btn, &style, LV_STATE_DEFAULT);

这里有个细节:样式可以绑定到不同的状态(LV_STATE_DEFAULT、LV_STATE_PRESSED、LV_STATE_DISABLED等)。按下的按钮和禁用的按钮可以有不同的样式。

2.3.3 事件(lv_event)

事件是用户交互的入口。LVGL的事件模型很简单:注册一个回调,当事件发生时回调被调用。

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);

事件类型很多,常用的有:LV_EVENT_CLICKED(点击)、LV_EVENT_PRESSING(按住中)、LV_EVENT_VALUE_CHANGED(值改变,比如滑块拖动)。

2.4 渲染管线概览:从事件到像素

好,现在我们把所有模块串起来,看看一次点击事件到屏幕像素变化,到底经历了什么。这就是LVGL的渲染管线。

  1. 输入采集:底层驱动检测到触摸按下,上报坐标。
  2. 事件分发:输入设备模块找到触摸点下的对象,触发 LV_EVENT_PRESSED 事件。
  3. 应用逻辑:你的事件回调执行。比如修改了某个标签的文字。
  4. 标记脏区域:LVGL自动检测到对象属性变化,标记该对象所在的矩形区域为“需要重绘”。
  5. 重绘调度:在 lv_timer_handler() 中,LVGL遍历所有脏区域,调用绘图引擎进行重绘。
  6. 绘图引擎执行:绘图引擎根据对象的样式、内容(文字、图片等),在内存中的缓冲区绘制像素。
  7. 刷新到屏幕:绘图完成后,调用显示驱动的 flush_cb,将缓冲区内容复制到屏幕显存。
LVGL 渲染管线(一次点击事件的全流程) ① 输入采集 触摸/按键检测 ② 事件分发 找到目标对象 ③ 应用逻辑 修改属性/数据 ④ 标记脏区域 需要重绘的区域 ⑤ 重绘调度 lv_timer_handler() ⑥ 绘图引擎 绘制到缓冲区 ⑦ 刷新到屏幕 flush_cb() 整个流程在 lv_timer_handler() 中循环执行 每次循环只处理一个脏区域,避免卡顿

嗯,这里要注意:LVGL并不是每帧都全屏重绘。它只重绘“脏区域”——也就是属性发生变化的那一小块。这大大提升了性能。我在一个只有64KB RAM的MCU上跑过LVGL,只要脏区域控制得好,帧率完全能接受。

2.5 小结

这一章我们把LVGL的架构拆了一遍。你记住三个数字就行:三层架构、九大模块、七步渲染管线。后面每一章都会围绕这些模块展开。

说白了,LVGL的设计哲学就是:分层解耦 + 脏区域重绘。理解了这个,你就能明白为什么它能在资源受限的嵌入式设备上跑得这么流畅。