4. TouchGFX入门
好,咱们进入架构对比的第一部分。今天重点聊LVGL。
很多初学者拿到LVGL,第一反应是:“这玩意儿怎么跑起来的?” 点一下屏幕,控件动了,再点一下,页面切换了。感觉像变魔术。其实背后就是两件事:事件驱动和任务调度。
我个人习惯把LVGL的架构理解成一个“事件循环工厂”。它不依赖操作系统,也能自己转起来。你给它一个tick,它还你一个流畅的UI。
4.1 事件驱动模型:谁在监听你的手指?
LVGL里,一切用户交互都归结为“事件”。触摸、按键、编码器转动……统统打包成事件,丢进队列里。
核心逻辑就三步:
- 输入设备读取:底层驱动把物理信号转成坐标或键值。
- 事件对象创建:LVGL把原始数据包装成
lv_event_t结构体。 - 事件分发:沿着对象树,从子到父,一路冒泡,找到注册了回调的控件。
关键点:LVGL的事件传递是“冒泡”机制。子控件先处理,处理不完再往上抛。你可以用 LV_EVENT_STOP 手动掐断冒泡。
举个例子,你点了一个按钮:
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_t * btn = lv_btn_create(lv_scr_act());
lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, NULL);
这段代码我写过无数次。但有个坑——回调里别做耗时操作。我曾经在一个点击回调里写了延时200ms的Flash擦除,结果界面直接卡死。为什么?因为事件处理是在LVGL的主循环里同步执行的,你卡住了循环,整个UI就停了。
避坑指南:事件回调里不要用 lv_delay() 或 while(1) 等待。需要异步处理的任务,请用LVGL的任务调度器或扔到后台线程。
4.2 任务调度机制:LVGL的“心脏”
LVGL内部维护了一个软件定时器链表。所有周期性任务——比如动画刷新、屏幕更新、输入设备轮询——都挂在这个链表上。
调度逻辑很简单:
- 每个任务有一个
period(周期)和last_run(上次执行时间)。 - 主循环每次调用
lv_timer_handler(),遍历链表,检查当前时间是否 >=last_run + period。 - 如果到期,执行回调,更新
last_run。
你想想看,这不就是一个微型操作系统的心跳吗?
下面这张图能帮你理清整个流程:
4.3 如何创建和管理任务?
LVGL提供了 lv_timer_create() 接口。用法很直接:
lv_timer_t * timer = lv_timer_create(my_timer_cb, 100, NULL);
static void my_timer_cb(lv_timer_t * t) {
static uint32_t cnt = 0;
cnt++;
LV_LOG_USER("定时器触发 %d 次", cnt);
}
第二个参数是周期,单位毫秒。第三个参数是用户数据指针,可以传结构体进去。
我记得有个项目需要每隔50ms采集一次传感器数据并更新UI。我直接建了个定时器,回调里读传感器、刷新仪表盘。跑起来很顺,没卡顿。为什么?因为传感器读取只花了2ms,不影响UI刷新。
经验之谈:定时器回调的执行时间,建议控制在周期时间的10%以内。比如周期100ms,回调执行别超过10ms。否则任务会堆积,界面响应变慢。
4.4 任务优先级与动态调整
LVGL的任务调度是非抢占式的。所有任务按注册顺序排队,没有优先级概念。但你可以通过 lv_timer_set_period() 动态调整周期,变相实现“优先级”:
- 重要任务:周期设短一点,比如10ms。
- 次要任务:周期设长一点,比如500ms。
另外,lv_timer_pause() 和 lv_timer_resume() 可以暂停/恢复任务。我在做低功耗项目时,屏幕息屏后就把所有动画定时器暂停了,功耗直接降了30%。
注意:不要频繁创建和删除定时器。LVGL的定时器链表是动态分配的,频繁操作会导致内存碎片。我建议在初始化阶段就把所有定时器建好,运行时只做暂停/恢复或修改周期。
4.5 与裸机/RTOS的配合
LVGL不强制要求操作系统。裸机环境下,你只需要在 main() 里放一个死循环:
while(1) {
lv_timer_handler();
my_delay_ms(5); // 让出CPU
}
如果跑RTOS,我习惯给LVGL单独开一个任务:
void lvgl_task(void * arg) {
while(1) {
lv_timer_handler();
vTaskDelay(pdMS_TO_TICKS(5));
}
}
这里有个细节:lv_tick_inc() 需要在另一个定时器中断或低优先级任务里调用,保证LVGL的时间基准准确。我见过有人把 lv_tick_inc() 放在高优先级中断里,结果中断频繁触发,影响了UI流畅度。后来改到低优先级任务里,问题就解决了。
4.6 性能调优:别让调度成为瓶颈
最后聊点实战调优。LVGL的任务调度本身开销很小,但如果你注册了太多定时器,遍历链表的时间会线性增长。我建议:
- 定时器数量控制在20个以内。
- 能用动画库实现的,别自己写定时器。
- 周期小于10ms的任务,考虑用硬件定时器+标志位。
说白了,LVGL的调度器是为UI设计的,不是为硬实时任务准备的。把实时性要求高的逻辑放到RTOS任务里,LVGL只负责界面刷新,这样分工最合理。
嗯,这一章就到这里。LVGL的事件驱动和任务调度,核心就是“同步、轮询、单线程”。理解了这个,你就能驾驭它的行为,而不是被它牵着走。