LVGL调度

前言

在做项目的过程中使用到了LVGL,遇到了一些问题,于LVGL的调度有关,所以这里写一篇笔记记录LVGL调度相关内容

LVGL任务调度

任务处理介绍

LVGL通过定时器轮询机制周期性检查并执行所有任务(动画、事件、刷新等),由主循环中持续调用lv_task_handler() 处理任务。

所有动画、控件刷新、事件分发等任务操作,都是在该函数内执行,并且最终都是通过定时器机制驱动。

我们必须在循环中间隔5~20ms持续调用lv_task_handler() 来处理任务,

在使用LVGL时,通常伴随RTOS的使用,所以我们都是将lvgl任务处理单独由一个任务执行,保证lv_task_handler()周期性的被调用处理所有的事件(UI刷新,输入检测等)

1
2
3
4
5
6
7
8
9
10
/* LVGL Handler task,驱动LVGL运行 */
void LvglHandlerTask(void *argument)
{
while(1)
{
lv_task_handler(); // 启动lvgl的事务处理
osDelay(5);
}
}

定时器机制

LVGL内部维护一个定时器链表,在每次调用lv_task_handler()时,会对整个定时器链表进行一个遍历的操作,并对链表中所有的已经到期的定时器执行定时器回调函数(我们注册的输入设备回调函数等等)进行一个执行。最终达到驱动系统更新的目的,属于非阻塞的延迟执行机制

任务执行方式

LVGL采用协作式但线程调度,任务(定时器回调、动画、事件处理)按以下规则运行:

  • 顺序执行:每个到期定时器的回调函数会一次性完整执行,完成后才会处理下一个任务,不会被抢占
  • 优先级控制:定时器可设置优先级,但仅影响执行顺序

因为没有时间片划分,每次任务都要执行完成才会执行下一个任务,所以任务必须快速结束,否则会影响其他任务的执行!!!

调度流程源码分析

1.在初始化阶段,对于各个设备(显示、输入设备),我们会对其进行创建对象和设置对应的回调函数

image-20250807184751990

image-20250807185944898

2.其中创建对象的时候,会跟随创建一个定时器,并将对应对象的回调函数作为参数传入

image-20250807185233119

这就是对应设备的回调函数lv_indev_read_timer_cb内部实现(这里是输入设备的), 最终就是调用我们初始化时注册的indev->read_cb

image-20250807185640233

image-20250807185700055

  1. 定时器创建的内部会设置定时器回调函数设置为我们对应类型设备的回调函数,同时作为结点插入到我们的定时器链表当中

image-20250807185417317

4.每隔5~10ms,我们调用的lv_timer_handler时,会对整个定时器链表进行一个遍历的操作,并对链表中所有的已经到期的定时器执行定时器回调函数(我们注册的包括输入设备采集、动画、事件分发等所有的设备和事件)进行一个执行。最终达到驱动系统更新的目的

image-20250807190324653

​ 每个定时器都会判断是否到期,只要到期才会执行对应定时器的timer_cb回调函数

image-20250807191952078

​ 每个timer都有上次运行时间戳timer->last_run,定时器间隔timer->period

image-20250807192222125

以下是为 lv_timer_handler() 添加的详细中文注释版,注释尽量保留原有逻辑结构,方便后续阅读和调试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// LVGL 的定时器调度主函数,每次被调用会遍历所有注册的定时器并执行到期的任务
LV_ATTRIBUTE_TIMER_HANDLER uint32_t lv_timer_handler(void)
{
LV_TRACE_TIMER("begin");

lv_timer_state_t * state_p = &state;

// 避免定时器调度函数被重复进入(多线程或中断中可能会并发调用)
if(state_p->already_running) {
LV_TRACE_TIMER("already running, concurrent calls are not allow, returning");
return 1;
}
state_p->already_running = true;

// 如果定时器已被暂停,直接返回
if(state_p->lv_timer_run == false) {
state_p->already_running = false; // 释放“互斥锁”
return 1;
}

LV_PROFILER_BEGIN;
lv_lock(); // 进入临界区,保护链表等资源不被同时访问

// 获取当前时间(单位 ms)
uint32_t handler_start = lv_tick_get();

// 检查是否调用了 lv_tick_inc()(用于更新系统 tick)
if(handler_start == 0) {
state.run_cnt++;
if(state.run_cnt > 100) {
state.run_cnt = 0;
LV_LOG_WARN("It seems lv_tick_inc() is not called.");
}
}

/*************** 开始遍历并执行定时器任务 ***************/

lv_timer_t * next;
lv_timer_t * timer_active;
lv_ll_t * timer_head = timer_ll_p; // 定时器链表头指针

do {
// 每轮遍历前先清空“状态标志”
state_p->timer_deleted = false;
state_p->timer_created = false;

// 获取第一个定时器
timer_active = lv_ll_get_head(timer_head);
while(timer_active) {
// 提前获取下一个指针(因为本 timer 执行后可能会被删除)
next = lv_ll_get_next(timer_head, timer_active);

// 如果当前定时器已经到期,则执行回调函数
if(lv_timer_exec(timer_active)) {
// 如果执行期间创建/删除了定时器,链表结构可能被破坏,必须重新从头遍历
if(state_p->timer_created || state_p->timer_deleted) {
LV_TRACE_TIMER("Start from the first timer again because a timer was created or deleted");
break;
}
}

// 进入下一个定时器
timer_active = next;
}
} while(timer_active); // 如果因链表被修改跳出,则重新遍历

/*************** 计算下次定时器触发时间 ***************/

uint32_t time_until_next = LV_NO_TIMER_READY; // 默认无任务准备
next = lv_ll_get_head(timer_head);
while(next) {
if(!next->paused) {
// 获取该定时器距离下一次触发还需等待的时间
uint32_t delay = lv_timer_time_remaining(next);
if(delay < time_until_next)
time_until_next = delay; // 保留最短时间
}
next = lv_ll_get_next(timer_head, next);
}

/*************** 统计“空闲率”用于性能分析 ***************/

state_p->busy_time += lv_tick_elaps(handler_start); // 本轮花费时间
uint32_t idle_period_time = lv_tick_elaps(state_p->idle_period_start);

if(idle_period_time >= IDLE_MEAS_PERIOD) {
// 计算过去一段时间内的 CPU 空闲率(100% - 忙碌率)
state_p->idle_last = (state_p->busy_time * 100) / idle_period_time;
state_p->idle_last = state_p->idle_last > 100 ? 0 : 100 - state_p->idle_last;

// 重置统计值
state_p->busy_time = 0;
state_p->idle_period_start = lv_tick_get();
}

// 保存本次计算的“下次触发间隔”
state_p->timer_time_until_next = time_until_next;

state_p->already_running = false; // 解锁,允许下次进入

LV_TRACE_TIMER("finished (%" LV_PRIu32 " ms until the next timer call)", time_until_next);
lv_unlock(); // 退出临界区
LV_PROFILER_END;

return time_until_next; // 返回最短等待时间,供外部调度参考
}

LVGL时基

时基介绍

LVGL需要一个系统滴答(系统时基)作为它的核心时间管理机制,来获取动画和其他任务经过的事件,以便驱动LVGL内部任务lv_task_handler()的正常执行(管理动画(如按钮点击的过渡效果)、处理输入设备(如触摸屏、按键)、定时更新 UI)

如果缺少时基,LVGL就认为时间静止了,LVGL涉及时间相关的内容就不会生效,比如:

  • 动画卡住或不动
  • 定时器、超时、刷新全部失效
  • 触摸/输入响应延迟或无效

配置时基

我们在移植lvgl时,需要完成改时基的配置

为LVGL提供时基的方法有两种:使用定时器、FreeRTOS的Tick钩子函数

Tick 接口 — LVGL 文档:官方建议使用FreeRTOS的话,就用Tick钩子函数提供时基,实现告知LVGL现在距离上一次多少毫秒过去了的功能

FreeRTOS钩子函数

FreeRTOS的钩子函数vApplicationTickHook()会在每次系统时钟节拍(tick)中断时被调用(通常是SysTick中断),详细介绍见笔记或官方文档

下面我们来介绍如何进行配置

1.在FreeRTOSConfig.h中配置configUSE_IDLE_HOOK1,启用钩子函数

image-20250807222131884

运行周期:由configTICK_RATE_HZ决定, 一般设置为1000,也就是1ms

image-20250807222225838

  1. 自己实现vApplicationTickHook,里面调用lv_tick_inc(1),告诉LVGL已经过去了1ms
1
2
3
4
5
void vApplicationTickHook()
{
// 告诉lvgl已经过去了1毫秒
lv_tick_inc(1);
}

注意:vApplicationTickHook() 从ISR内执行,因此必须非常短,且不能被阻塞!

定时器

LVGL调度相关API

我们在移植使用lvgl时就需要对两个部分:lv_task_handlerlv_tick_inc()进行分别移植调用!

  • lv_task_handler()

    image-20250807190509527

    • 说明:lv_task_handler()内部调用lv_timer_handler()

    • 作用:驱动LVGL的主循环,刷新UI、执行动画、处理输入设备等任务。

    • 用法:使用LVGL必须在主循环或RTOS独立任务中每5~20ms调用一次。

以下就是在FreeRTOS中单独的一个任务用于驱动LVGL,官方推荐执行lvgl的最小栈空间>2KB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
osThreadId_t LvglHandlerTaskHandle;
const osThreadAttr_t LvglHandlerTask_attributes = {
.name = "LvglHandlerTaskHandle",
.stack_size = 128 * 24, // lvgl官方推荐栈空间至少2K
.priority = (osPriority_t) osPriorityNormal,
};

LvglHandlerTaskHandle = osThreadNew(LvglHandlerTask, NULL, &LvglHandlerTask_attributes);

/* LVGL Handler task,驱动LVGL运行 */
void LvglHandlerTask(void *argument)
{
while(1)
{
lv_task_handler(); // 启动lvgl的事务处理
osDelay(5);
}
}

  • lv_tick_inc()

image-20250807202001571

作用:为LVGL提供毫秒级的时间流逝(时基)。

用法:通常每1ms调用一次,比如RTOS tick钩子、定时器中断。

用法见上方配置时基部分