ESC
输入关键词搜索文章标题和内容

LVGL嵌入式GUI零基础入门:从上手Demo到MVP架构工程实战

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

LVGL嵌入式GUI零基础入门:从上手Demo到MVP架构工程实战

导读:LVGL是嵌入式领域最流行的开源轻量GUI库,最低64KB Flash、16KB RAM就能跑起来,C语言开发,支持按钮、图表、动画等丰富控件。本文从零教你跑通第一个Demo,再过渡到MVP分层架构,最后搞定图片资源的工程化处理,完整走通全流程。

一、LVGL原理简析

LVGL(Light and Versatile Graphics Library)是一个用纯C写的开源嵌入式图形库,MIT协议,完全免费。它的核心设计思路是"低内存、高可移植、零依赖"——不绑定任何操作系统、芯片或显示屏,裸机、FreeRTOS、RT-Thread上都能跑。

核心抽象只有四层:

  • 对象(lv_obj_t):按钮、文字、图片、滑块等所有UI元素,本质上都是对象。每个对象有坐标、尺寸、样式、父子关系、事件回调队列,形成一棵对象树
  • 显示设备(lv_disp_t):通过 lv_disp_drv_t 注册的逻辑输出通道,支持多屏、分屏、离屏渲染
  • 输入设备(lv_indev_t):统一抽象触摸屏、按键、编码器、鼠标等输入,底层差异被封装为标准化上报接口
  • 主循环(lv_timer_handler):LVGL的生命线,按毫秒级精度调度定时任务、动画帧更新、输入去抖、屏幕刷新,不是简单的 while(1) 轮询

二、实操步骤:从零跑通第一个Demo

2.1 环境准备

  • 硬件:任意支持LVGL的开发板(STM32、ESP32均可),带一块TFT屏幕
  • 软件:移植好 lv_port_disp.c(显示驱动)和 lv_port_indev.c(触摸驱动)
  • 参考:LVGL官方移植文档 docs.lvgl.io

2.2 核心Demo代码

#include "lvgl.h"
#include "lv_port_disp.h"
#include "lv_port_indev.h"

// 按钮点击回调
static void btn_event_cb(lv_event_t *e)
{
    lv_obj_t *label = lv_event_get_user_data(e);
    lv_label_set_text(label, "已点击!");
}

int main(void)
{
    // 1. 初始化LVGL核心
    lv_init();
    // 2. 初始化屏幕和触摸驱动
    lv_port_disp_init();
    lv_port_indev_init();

    // 3. 创建居中文字
    lv_obj_t *label = lv_label_create(lv_scr_act());
    lv_label_set_text(label, "Hello LVGL!");
    lv_obj_align(label, LV_ALIGN_CENTER, 0, -30);

    // 4. 创建按钮
    lv_obj_t *btn = lv_btn_create(lv_scr_act());
    lv_obj_set_size(btn, 120, 50);
    lv_obj_align(btn, LV_ALIGN_CENTER, 0, 30);

    // 5. 按钮文字
    lv_obj_t *btn_label = lv_label_create(btn);
    lv_label_set_text(btn_label, "Click Me");

    // 6. 绑定点击事件
    lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, label);

    // 7. LVGL主循环
    while (1) {
        lv_timer_handler();
        usleep(5000);
    }
    return 0;
}

2.3 运行效果

屏幕中央显示"Hello LVGL!"文字,下方有一个"Click Me"按钮。点击按钮后,上方文字变为"已点击!"。这就是LVGL最基础的界面交互逻辑——创建对象、绑定事件、刷新显示。

三、原生开发 vs MVP架构对比

对比项 原生LVGL开发 MVP架构开发
代码结构 UI和业务代码混在一个文件,改一处牵全身 View/Model/Presenter三层分离,各司其职
可测试性 无法单独测试UI或业务逻辑 三层独立,可分别编写单元测试
可复用性 耦合度高,换硬件或改功能成本极大 界面和业务完全分离,跨平台复用方便
多人协作 分工不清,极易代码冲突 目录清晰,前端/后端/调度独立开发
维护成本 项目越大越难维护 结构规范,新人上手快,bug定位快
适用规模 简单Demo、单一功能小项目 中大型项目、需要长期迭代的产品

四、核心流程图

4.1 LVGL对象模型层级

flowchart TB subgraph 屏幕层["lv_scr_act() 活动屏幕"] direction TB S1["屏幕是根容器"] end subgraph 控件层["lv_obj_t 对象树"] direction TB O1["lv_btn_create()"] O2["lv_label_create()"] O3["lv_img_create()"] O1 --> O2 end subgraph 事件层["lv_event 事件回调"] direction TB E1["LV_EVENT_CLICKED"] E2["LV_EVENT_PRESSED"] E3["LV_EVENT_VALUE_CHANGED"] end subgraph 渲染层["lv_timer_handler 主循环"] direction TB L1["定时器调度"] L2["动画帧更新"] L3["屏幕刷新"] end 屏幕层 --> 控件层 控件层 --> 事件层 事件层 --> 渲染层 L3 -.->|"循环刷新"| 屏幕层 style 屏幕层 fill:#E3F2FD,stroke:#1976D2 style 控件层 fill:#E8F5E9,stroke:#388E3C style 事件层 fill:#FFF8E1,stroke:#F57C00 style 渲染层 fill:#F3E5F5,stroke:#7B1FA2

4.2 MVP架构执行流程

flowchart LR subgraph View["View 视图层"] direction TB V1["创建UI控件"] V2["注册事件监听"] V3["刷新界面显示"] V1 --> V2 end subgraph Presenter["Presenter 调度层"] direction TB P1["接收事件通知"] P2["调用Model处理"] P3["指令View刷新"] end subgraph Model["Model 模型层"] direction TB M1["传感器读取"] M2["网络通信"] M3["数据存储"] end V2 -->|"用户点击事件"| P1 P1 --> P2 P2 --> M1 P2 --> M2 M1 -->|"数据回调"| P2 M2 -->|"数据回调"| P2 P2 --> P3 P3 -->|"更新UI"| V3 style View fill:#E3F2FD,stroke:#1976D2,color:#0D47A1 style Presenter fill:#FFF8E1,stroke:#F57C00,color:#E65100 style Model fill:#E8F5E9,stroke:#388E3C,color:#1B5E20

五、MVP架构在LVGL中的落地

5.1 三层职责一句话

层级 职责 不做什么
View 画控件、排布局、调样式、播动画 不碰任何业务逻辑,不调用传感器/网络
Model 传感器读取、网络通信、数据存储、协议解析 不依赖LVGL,不含任何UI代码
Presenter 接收View事件 → 调Model处理 → 通知View刷新 不直接操作硬件,不直接画控件

5.2 项目目录结构

project/
├── view/           # 界面布局代码(lv_obj_t声明 + lv_event_cb_t实现)
│   ├── main_page.c
│   └── setting_page.c
├── model/          # 业务功能代码(HAL驱动 + 协议解析 + 存储)
│   ├── sensor.c
│   └── storage.c
├── presenter/      # 中间调度代码(协调逻辑 + 状态映射)
│   ├── main_presenter.c
│   └── setting_presenter.c
└── lvgl/           # LVGL图形库

5.3 实战案例:X-TRACK开源项目

X-TRACK(GitHub: FASTSHIFT/X-TRACK)是一个基于LVGL + MVP架构的开源GPS自行车码表项目,4.5k+ stars。它的架构设计是MVP在嵌入式领域的经典参考:

  • 4层架构:硬件层 → HAL抽象层 → 应用逻辑层 → LVGL UI层
  • DataCenter发布订阅机制:组件间通过消息总线解耦,PingPongBuffer实现无锁核间通信
  • HAL接口:同一套应用代码,不修改即可在MCU硬件和PC模拟器上编译运行
  • 单内存池:所有动态内存分配统一走LVGL内存池,避免碎片化,方便调试

六、帧动画实现 + 图片资源工程化

6.1 逐帧动画原理

把动画拆成多张连续静态图片,用定时器按固定间隔切换 lv_img_set_src() 的图片源,就形成流畅动画。LVGL原生支持两种动画方式:

  • 属性动画(lv_anim_t):自动对对象属性(坐标、透明度、大小)做补间插值
  • 帧动画(lv_img_set_src + 定时器):手动切换图片数组,适合表情、加载动画等

6.2 图片转C数组:工程化方案

嵌入式开发中,PNG/JPG图片无法直接使用,必须转成C数组。LVGL官方提供了两个工具:

方案一:LVGL在线转换器

地址:https://lvgl.io/tools/imageconverter

上传图片 → 选择颜色格式(推荐RGB565节省空间)→ 点击Convert → 下载 .c 文件。在代码中声明即可使用:

来自 linuxros.cn · linuxROS
LV_IMG_DECLARE(my_icon);
lv_obj_t *img = lv_img_create(lv_scr_act());
lv_img_set_src(img, &my_icon);

方案二:Python脚本批量转换(推荐工程使用)

安装 lvgl-imgconverter 包,写脚本遍历资源目录,一键批量转换:

import os
import subprocess

image_dir = "./assets/images/"
output_dir = "./src/images/"

for filename in os.listdir(image_dir):
    if filename.endswith(('.png', '.jpg')):
        name = os.path.splitext(filename)[0]
        subprocess.run([
            "lv_img_converter",
            os.path.join(image_dir, filename),
            "-o", os.path.join(output_dir, f"{name}.c"),
            "-f", "RGB565"
        ])

脚本优势:

  • 自动生成合法C变量名,不会语法报错
  • 标准化输出LVGL兼容格式,直接 #include 即可
  • 支持增量更新,图片没变就不重复生成,省编译时间
  • 批量处理比手动写数组快几十倍,零人为失误

6.3 帧动画播放代码

// 帧数组
static const lv_img_dsc_t *anim_frames[] = {
    &img_frame_01, &img_frame_02, &img_frame_03,
    &img_frame_04, &img_frame_05, &img_frame_06,
};
#define FRAME_COUNT 6

// 定时器回调:切换帧
static void anim_timer_cb(lv_timer_t *timer)
{
    lv_obj_t *img = lv_timer_get_user_data(timer);
    static int frame_idx = 0;
    lv_img_set_src(img, anim_frames[frame_idx]);
    frame_idx = (frame_idx + 1) % FRAME_COUNT;
}

// 启动动画
lv_timer_t *anim = lv_timer_create(anim_timer_cb, 100, img_obj);

七、常见问题解决

问题一:LVGL界面卡顿、不流畅

检查 lv_timer_handler() 的调用间隔,建议5ms一次。如果主循环里有耗时操作(如传感器轮询),把耗时操作移到单独的任务或中断中,别阻塞主循环。另外,确认 lv_conf.h 中 LV_MEM_SIZE 分配足够(建议至少32KB)。

问题二:图片显示花屏或颜色不对

LVGL的颜色格式(RGB565 vs ARGB8888)和屏幕驱动的颜色格式必须一致。检查 lv_conf.h 中的 LV_COLOR_DEPTH 配置,以及图片转换时选择的颜色格式。如果屏幕显示颜色反了,检查 lv_port_disp.c 中 flush_cb 函数的字节序。

问题三:MVP架构中View和Presenter如何通信

View通过 lv_event_cb_t 回调通知Presenter,Presenter通过函数指针或消息队列通知View刷新。不要在View回调中直接调Model,也不要在Model中直接调LVGL API——所有跨层调用都经过Presenter。

八、总结

LVGL用64KB Flash、16KB RAM就能跑出专业级嵌入式GUI,是目前嵌入式图形开发的首选方案。

  • Demo阶段:直接写就行,代码短、上手快,适合验证原型
  • 项目阶段:上MVP架构,View/Model/Presenter三层分离,改UI不动业务、改业务不动UI,维护成本跳水
  • 资源处理:用官方在线转换器或Python脚本批量转图片,告别手写数组的噩梦

这套LVGL + MVP + 脚本工具链的组合,从简单Demo到带动画的产品级项目,都能应对。


版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页