3. 架构设计对比:LVGL的组件化架构 vs TouchGFX的MVC架构

聊到嵌入式GUI,很多人上来就比控件多、动画炫。但我个人觉得,架构设计才是决定项目能走多远的关键。你想想看,一个跑在MCU上的界面,如果任务调度乱成一锅粥,事件循环卡死,再漂亮的UI也是白搭。

这一章,我们就来扒开LVGL和TouchGFX的底层骨架。看看它们各自是怎么组织代码、怎么调度任务、怎么处理事件的。

3.1 LVGL的组件化架构:一切皆对象

LVGL的设计哲学很直接——万物皆组件。屏幕是一个组件,按钮是一个组件,标签也是一个组件。它们都继承自同一个基类 lv_obj_t。

我在做智能家居面板时,就用这种思路搭过界面。每个传感器卡片都是一个独立组件,修改一个不会影响其他,爽得很。

3.1.1 组件树与父子关系

LVGL里所有组件构成一棵树。根节点是屏幕(lv_scr_act()),下面挂载各种容器和控件。子组件的位置、旋转、透明度,都会受父组件影响。

// 创建一个屏幕
lv_obj_t *scr = lv_obj_create(NULL);
lv_scr_load(scr);

// 创建一个容器(父组件)
lv_obj_t *cont = lv_obj_create(scr);
lv_obj_set_size(cont, 200, 150);

// 创建一个按钮(子组件)
lv_obj_t *btn = lv_btn_create(cont);
lv_obj_set_pos(btn, 10, 10);

这种树状结构的好处是:布局管理非常直观。你只需要关心父子关系,不用手动计算全局坐标。

3.1.2 组件的生命周期

每个组件从创建到销毁,经历四个阶段:

阶段 触发时机 典型操作
创建 lv_xxx_create() 分配内存、初始化属性
布局 父组件大小变化时 自动调用 lv_obj_set_pos/size
渲染 每帧刷新时 绘制背景、边框、内容
销毁 lv_obj_del() 释放内存、移除事件
注意:我曾经在项目中忘记调用 lv_obj_del_async() 在中断中删除组件,结果直接死机。LVGL的组件操作不是线程安全的,一定要在主循环或任务中执行。

3.2 TouchGFX的MVC架构:分离的艺术

TouchGFX走的是另一条路——MVC模式。Model、View、Controller各司其职。说白了,就是把数据、界面、逻辑拆得干干净净。

我第一次接触TouchGFX时,觉得这玩意儿太“重”了。但后来做一个需要频繁更新数据的仪表盘项目,才发现MVC真香。数据变了,界面自动刷新,不用手动去翻控件。

3.2.1 三层的职责划分

  • Model(模型):存放业务数据。比如温度值、开关状态。不关心怎么显示。
  • View(视图):负责界面显示。由TouchGFX Designer生成的UI代码都在这里。
  • Controller(控制器):处理用户交互,更新Model,通知View刷新。
// Model 层
class Model {
public:
    int getTemperature() { return temperature; }
    void setTemperature(int t) {
        temperature = t;
        // 通知监听器
        if (listener) listener->modelChanged();
    }
private:
    int temperature = 25;
    ModelListener *listener;
};

// View 层(由 Designer 生成)
class ScreenView : public View {
public:
    void updateTemperature(int value) {
        // 更新文本控件
        textArea.setValue(value);
    }
};

// Controller 层
class ScreenPresenter : public Presenter {
public:
    void buttonClicked() {
        model->setTemperature(model->getTemperature() + 1);
    }
};

3.2.2 数据绑定与自动刷新

TouchGFX通过 ModelListener 接口实现数据绑定。Model变化时,自动调用View的更新方法。你不需要在循环里轮询数据。

核心差异:LVGL是“你主动去改控件”,TouchGFX是“数据变了,控件自己改”。前者灵活,后者省心。

3.3 任务调度机制对比

嗯,这里要注意。两个框架的任务调度思路完全不同,直接决定了你的主循环怎么写。

3.3.1 LVGL:手动滴答驱动

LVGL不依赖RTOS。它只需要你定期调用两个函数:

// 1. 提供系统心跳(通常每1ms调用一次)
lv_tick_inc(1);

// 2. 在主循环中调用任务处理
while(1) {
    lv_task_handler();  // 处理所有LVGL内部任务
    // 你的其他业务代码
    delay(5);
}

lv_task_handler() 内部会遍历所有注册的任务(动画、刷新、输入处理),按优先级和时间片执行。默认情况下,它每5ms处理一次。

我的习惯:在FreeRTOS中,我会单独创建一个低优先级任务来跑 lv_task_handler(),避免阻塞高优先级的中断或通信任务。

3.3.2 TouchGFX:OS封装与事件队列

TouchGFX天生为RTOS设计。它内部维护一个事件队列,所有交互都通过队列传递。

// TouchGFX 的典型主循环(在OS任务中)
while(1) {
    // 处理来自触摸、按键的事件
    osMessageQueueGet(queue, &msg, NULL, portMAX_DELAY);
    
    // 将事件分发到当前屏幕
    presenter->handleEvent(msg);
    
    // 触发渲染
    hal->vSync();
}

TouchGFX的渲染由VSync信号驱动。它不会像LVGL那样持续刷新,而是只在需要时重绘。这大大降低了CPU占用。

特性 LVGL TouchGFX
调度方式 手动调用 lv_task_handler() OS事件队列 + VSync驱动
RTOS依赖 可选(裸机也能跑) 强烈推荐(裸机需自己实现队列)
刷新策略 周期性全刷新 按需局部刷新
CPU占用 较高(持续刷新) 较低(事件驱动)

3.4 事件循环机制深度拆解

事件循环是GUI的“心脏”。两个框架的处理方式,体现了它们的设计哲学差异。

3.4.1 LVGL:事件冒泡与分组

LVGL的事件机制类似Web的DOM事件。点击一个按钮,事件会从按钮冒泡到父容器,直到被处理或到达屏幕。

// 为按钮添加点击事件
lv_obj_add_event_cb(btn, btn_click_cb, LV_EVENT_CLICKED, NULL);

// 事件回调
static void btn_click_cb(lv_event_t *e) {
    lv_obj_t *btn = lv_event_get_target(e);
    LV_LOG_USER("Button clicked!");
}

事件类型有几十种:LV_EVENT_PRESSED、LV_EVENT_VALUE_CHANGED、LV_EVENT_REFRESH 等等。你可以为同一个组件注册多个事件回调。

避坑指南:我曾经在事件回调里直接调用 lv_obj_del() 删除触发事件的组件,导致野指针崩溃。正确的做法是用 lv_event_send() 延迟处理,或者用 lv_async_call()。

3.4.2 TouchGFX:虚拟函数与屏幕栈

TouchGFX的事件处理更“C++化”。每个屏幕(View)重写虚函数来处理特定事件:

class ScreenView : public View {
public:
    // 处理触摸事件
    virtual void handleClickEvent(const ClickEvent &evt) {
        if (evt.getType() == ClickEvent::RELEASED) {
            // 调用Presenter处理业务逻辑
            presenter->onButtonReleased();
        }
    }
    
    // 处理按键事件
    virtual void handleKeyEvent(uint8_t key) {
        if (key == KEY_ENTER) {
            presenter->onConfirm();
        }
    }
};

TouchGFX维护一个屏幕栈。切换屏幕时,旧屏幕入栈,新屏幕出栈。返回上一屏时,直接从栈顶弹出,状态自动恢复。

3.5 架构对比总结:一张图看懂

下面这张SVG图,把两个框架的架构差异画得明明白白。我建议你保存下来,做项目选型时对照着看。

LVGL vs TouchGFX 架构对比 LVGL 组件化架构 lv_obj_t(基类组件) lv_btn / lv_label / lv_slider 事件冒泡机制 lv_task_handler() 手动调度 裸机 / RTOS 均可 TouchGFX MVC架构 Model(数据模型) View(界面视图) Presenter(控制器) ModelListener 数据绑定 OS事件队列 + VSync 强烈依赖RTOS 架构差异

3.6 选型建议:什么时候用哪个?

说了这么多,到底怎么选?我根据自己的项目经验,给你几个判断标准:

  • 资源受限(Flash < 512KB,RAM < 128KB):选LVGL。它能在裸机上跑,内存占用可控。
  • 需要复杂数据绑定和屏幕切换:选TouchGFX。MVC架构让代码更清晰,尤其适合多页面应用。
  • 团队C++功底强:TouchGFX的虚函数和继承体系用起来很顺手。
  • 团队习惯C语言:LVGL的纯C接口更友好,学习成本低。
  • 项目需要频繁OTA更新UI:LVGL的组件化设计更容易做动态加载。
一句话总结:LVGL是“搭积木”,灵活自由;TouchGFX是“造房子”,结构严谨。没有绝对的好坏,只有合不合适的项目。