【奶茶Beta专项】【LVGL9.4源码分析】06-tick时间管理
- [1 概述](#1 概述)
-
- [1.1 文档目的](#1.1 文档目的)
- [1.2 代码版本与范围](#1.2 代码版本与范围)
- [2 设计意图与总体定位](#2 设计意图与总体定位)
-
- [2.1 tick 的角色:单一"单调时间源"](#2.1 tick 的角色:单一“单调时间源”)
- [2.2 设计目标](#2.2 设计目标)
- [3 tick 模块接口与实现](#3 tick 模块接口与实现)
-
- [3.1 核心接口](#3.1 核心接口)
- [3.2 tick 的更新方式](#3.2 tick 的更新方式)
-
- [3.2.1 裸机 / RTOS:在定时中断中调用 lv_tick_inc](#3.2.1 裸机 / RTOS:在定时中断中调用 lv_tick_inc)
- [3.2.2 Linux / Windows:利用系统定时器或独立线程](#3.2.2 Linux / Windows:利用系统定时器或独立线程)
- [3.3 与 lv_timer 的关系](#3.3 与 lv_timer 的关系)
- [4 设计优势与缺点](#4 设计优势与缺点)
-
- [4.1 优势](#4.1 优势)
- [4.2 潜在缺点与注意事项](#4.2 潜在缺点与注意事项)
- [5 API 速查表](#5 API 速查表)
- [6 Linux 上的 tick 对接与时间体系](#6 Linux 上的 tick 对接与时间体系)
-
- [6.1 Linux 时间的几个概念](#6.1 Linux 时间的几个概念)
- [6.2 方案一:独立 tick 线程 + lv_tick_inc(官方示例风格)](#6.2 方案一:独立 tick 线程 + lv_tick_inc(官方示例风格))
-
- [6.2.1 实现示例](#6.2.1 实现示例)
- [6.2.2 优势](#6.2.2 优势)
- [6.2.3 劣势](#6.2.3 劣势)
- [6.3 方案二:自定义 tick,直接读取单调时钟(零线程方案)](#6.3 方案二:自定义 tick,直接读取单调时钟(零线程方案))
-
- [6.3.1 LV_TICK_CUSTOM 直接读时钟](#6.3.1 LV_TICK_CUSTOM 直接读时钟)
- [6.3.2 或在主循环中顺带维护 tick](#6.3.2 或在主循环中顺带维护 tick)
- [6.3.3 优势](#6.3.3 优势)
- [6.3.4 劣势](#6.3.4 劣势)
- [6.4 开机时间、世界时间、本地时间、夏令时](#6.4 开机时间、世界时间、本地时间、夏令时)
- [7 RTC 与 tick 的协作](#7 RTC 与 tick 的协作)
-
- [7.1 职责划分](#7.1 职责划分)
- [7.2 典型实践建议](#7.2 典型实践建议)
- [8 小结](#8 小结)
- [9 附录](#9 附录)
-
- [A 参考文档(外部)](#A 参考文档(外部))
- [B 相关资源(CSDN 系列)](#B 相关资源(CSDN 系列))
- [C 联系方式](#C 联系方式)
文档版本 : 1.0
更新日期 : 2025年12月
适用对象: LVGL9.4 在多平台下移植与时间相关功能设计的工程师
1 概述
1.1 文档目的
本篇围绕 library/lvgl/src/tick 目录,对 LVGL9.4 的 tick 时间管理机制 做源码级分析,重点回答几个问题:
- LVGL 的"时间"是如何定义和管理的?tick 与系统时钟、RTC、世界时间之间是什么关系?
- 在不同平台(特别是 Linux)下,应如何对接 LVGL 的 tick?
lv_tick_*与lv_timer、动画、输入设备刷新之间如何协同?- 这些设计的优势与局限在哪里,工程实践中有哪些常见踩坑点?
1.2 代码版本与范围
- 仓库路径:
https://github.com/lvgl/lvgl.git - 版本:v9.4.0
- commit:
c016f72d4c125098287be5e83c0f1abed4706ee5 - 涉及目录与文件:
src/tick/lv_tick.h/lv_tick.c:tick 核心接口与实现;src/tick/lv_tick_custom.c(如存在):平台自定义 tick 钩子示例;- 关联模块:
src/misc/lv_timer.c、src/core/lv_obj_tree.c等使用 tick 的组件。
2 设计意图与总体定位
2.1 tick 的角色:单一"单调时间源"
LVGL 内部大量逻辑依赖时间:
lv_timer定时器链表(重绘、动画、周期任务);- 输入设备去抖动、长按/重复触发判断;
- 滚动/动画插值计算、过渡效果;
- 系统监控(FPS、CPU 占用需要时间差)。
为了在不同平台上统一这些逻辑,LVGL 定义了 单一的"单调递增时间源":
- 单位:毫秒(ms);
- 语义:单调不减(monotonic),不能因为系统时间校准、时区、夏令时而回退;
- 使用接口:
lv_tick_get()/lv_tick_elaps()/lv_tick_inc(ms)。
注意:这个 tick 不是"世界时间""本地时间",而是仅用于 LVGL 内部相对时间计算的 逻辑时钟。
2.2 设计目标
- 平台无关:无论是裸机、RTOS 还是 Linux/Windows,只要能提供一个毫秒级的单调递增时间,就能驱动 LVGL。
- 实现简单 :大部分平台只需在定时中断或 OS 定时器回调中周期性调用
lv_tick_inc(period_ms)即可。 - 与世界时间解耦:上层 UI 组件可以通过 RTC / 系统时间管理世界时间,但 LVGL 核心不会直接依赖 RTC 或本地时间。
- 可调试:通过统一 tick 源,很容易在 sysmon 等模块中做时间相关统计。
3 tick 模块接口与实现
3.1 核心接口
lv_tick.h 中定义的典型接口如下(伪代码形式):
c
uint32_t lv_tick_get(void);
uint32_t lv_tick_elaps(uint32_t prev_tick);
void lv_tick_inc(uint32_t ms);
lv_tick_get()- 返回当前 LVGL tick 值,单位 ms;
- 类型通常为
uint32_t,自然溢出后依靠减法计算时间差。
lv_tick_elaps(prev)- 返回自
prev以来经过的时间:(lv_tick_get() - prev),自动考虑 32 位溢出。 - 常用于"是否超时""动画经过时长"等计算。
- 返回自
lv_tick_inc(ms)- 由应用或平台层调用,用来通知 LVGL "又过去了 ms 毫秒";
- 实际实现:内部全局 tick 变量加上
ms。
3.2 tick 的更新方式
3.2.1 裸机 / RTOS:在定时中断中调用 lv_tick_inc
最常见的方式是:在周期性定时器(如 SysTick、硬件定时器、RTOS tick hook)中调用:
c
/* 例如 1ms 中断一次 */
void SysTick_Handler(void)
{
lv_tick_inc(1);
}
或在 OS 调度器 tick hook 中:
c
void vApplicationTickHook(void)
{
lv_tick_inc(1);
}
关键点:
- 中断频率 ×
lv_tick_inc传入的ms要与实际时间对齐,否则会影响动画速度与定时器精度; lv_tick_inc必须在中断/高优先级上下文里调用,以保证 tick 单调且不受应用阻塞影响。
3.2.2 Linux / Windows:利用系统定时器或独立线程
在有 OS 且有高精度定时器的平台,常见几种接入方式:
- 使用 POSIX 定时器 /
timerfd/evtimer,定期触发回调调用lv_tick_inc(period_ms); - 创建一个专门的 tick 线程:
c
void *tick_thread(void *arg)
{
const uint32_t period_ms = 1;
while(running) {
usleep(period_ms * 1000);
lv_tick_inc(period_ms);
}
return NULL;
}
要点:
- 推荐使用单调时钟 (如
clock_gettime(CLOCK_MONOTONIC, ...))来驱动 sleep/定时器,保证不会因为系统时间调整而出现"跳跃"或"倒退"; - 不建议直接用
gettimeofday/time(NULL)计算 sleep 间隔,因为这些接口都会受到 NTP 校时、时区和夏令时切换的影响。
3.3 与 lv_timer 的关系
lv_timer 是 LVGL 内部所有定时任务的统一调度器,其核心思路是:
- 每个 timer 记录一个"下次触发时间"字段(基于
lv_tick_get()); - 在
lv_timer_handler()中:- 通过
lv_tick_get()获得当前 tick; - 遍历 timer 链表,找出已经到期或超期的定时器执行其回调;
- 重新计算下次触发时间。
- 通过
因此:
lv_tick_inc→ 更新内部时间;lv_tick_get/lv_tick_elaps→ 为lv_timer提供时间判断基础;lv_timer_handler→ 依据 tick 值执行具体任务(包括刷新显示、动画推进等)。
这一层关系决定了:tick 的精度和稳定性直接决定 LVGL 的响应速度和动画流畅度。
4 设计优势与缺点
4.1 优势
- 高度可移植:只要平台能提供一个"单调递增毫秒计数",就能接入 LVGL tick;
- 与世界时间解耦:tick 不受本地时间/时区/夏令时影响,避免 UI 逻辑因系统时间跳变而错乱;
- 实现简单:接口数量少、语义清晰,移植代价低;
- 统一调度 :所有 timer/动画/刷新都走
lv_timer,调优和问题排查更集中。
4.2 潜在缺点与注意事项
- 精度受限:默认以毫秒为单位,对于需要亚毫秒精度的场景可能不够;
- 依赖外部正确驱动 :如果
lv_tick_inc调用频率不稳定或被阻塞,动画和定时器都会被拖慢; - 与 OS 系统时钟割裂:如果上层业务期望直接用 LVGL tick 表示"真实时间",需要额外做映射与校准逻辑。
5 API 速查表
| 类别 | 接口 | 功能说明 |
|---|---|---|
| tick 读写 | lv_tick_get() |
获取当前 LVGL tick(ms),单调递增 |
| tick 读写 | lv_tick_elaps(prev) |
计算自 prev 以来经过的时间,处理溢出 |
| tick 维护 | lv_tick_inc(ms) |
由平台/应用调用,通知 LVGL"时间前进了 ms" |
实际接口名称/数量需以当前 LVGL 版本源码为准,这里列出的是最核心、最常用的一组。
6 Linux 上的 tick 对接与时间体系
本节扩展讨论在 Linux 等有完整时间子系统的平台上,如何合理对接 LVGL tick,并与"开机时间/世界时间/本地时间/夏令时"等概念协同。
6.1 Linux 时间的几个概念
在 Linux 中,常见时间概念包括:
- 系统单调时钟 :
CLOCK_MONOTONIC- 从系统启动开始持续递增,不受手动/网络校时影响;
- 适合做定时器、超时判断。
- 真实时间(实时时钟) :
CLOCK_REALTIME/time(NULL)- 表示带时区/夏令时的"世界时间/本地时间";
- 可能因为 NTP 或手动校时发生跳变。
- RTC(Real-Time Clock)硬件时钟
- 通常通过
/dev/rtc*或hwclock工具访问; - 在断电后仍能保持时间(电池供电)。
- 通常通过
- 进程启动时间 / uptime
/proc/uptime、CLOCK_BOOTTIME等,常用于统计运行时长。
对于 LVGL 而言:
- tick 应当基于单调时间源(
CLOCK_MONOTONIC/ BOOTTIME); - 世界时间、本地时间、夏令时处理应交给上层业务/RTC 模块,不应直接混入 tick。
6.2 方案一:独立 tick 线程 + lv_tick_inc(官方示例风格)
6.2.1 实现示例
一种常见做法是起一个专门的 tick 线程,内部基于单调时钟计算 delta_ms 并调用 lv_tick_inc:
c
static void *lvgl_tick_thread(void *arg)
{
struct timespec last, now;
clock_gettime(CLOCK_MONOTONIC, &last);
while(running) {
struct timespec req = { .tv_sec = 0, .tv_nsec = 1 * 1000 * 1000 }; /* 1ms */
nanosleep(&req, NULL);
clock_gettime(CLOCK_MONOTONIC, &now);
uint32_t delta_ms =
(uint32_t)((now.tv_sec - last.tv_sec) * 1000u +
(now.tv_nsec - last.tv_nsec) / 1000000u);
last = now;
lv_tick_inc(delta_ms);
}
return NULL;
}
要点:
- 避免直接假定"每次 sleep 1ms 就一定过去 1ms",而是用单调时钟实测;
- 即便系统发生负载抖动或短暂 sleep 误差,tick 仍然能保持大致准确。
6.2.2 优势
- 结构清晰:tick 维护逻辑集中在一个线程里,主循环代码保持简单;
- 与裸机/RTOS 教学示例一致:便于照抄官方 demo,在不同平台之间迁移思路;
- 修改局部 :不需要动 LVGL 源码,只要在移植层起线程并调用
lv_tick_inc即可。
6.2.3 劣势
- 占用一个额外线程:在资源紧张或线程数严格受限的系统中,这个线程本身是一笔成本;
- 上下文切换开销:即便线程很轻量,也会带来一定调度和唤醒负担;
- 在某些简单场景(单线程主循环 + 事件驱动)下显得有些"重"。
6.3 方案二:自定义 tick,直接读取单调时钟(零线程方案)
6.3.1 LV_TICK_CUSTOM 直接读时钟
另一种更"工程化"的做法,是开启 LVGL 的自定义 tick(如 LV_TICK_CUSTOM),让 lv_tick_get() 直接读取 OS 提供的单调时钟,不再维护内部自增 tick:
c
uint32_t lv_tick_get(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint32_t)(ts.tv_sec * 1000u + ts.tv_nsec / 1000000u);
}
在这种方案下:
- 不再需要
lv_tick_inc(); - 所有地方通过
lv_tick_get()+lv_tick_elaps()做时间差计算。
6.3.2 或在主循环中顺带维护 tick
如果已经有一个固定频率的主循环线程,也可以不改 lv_tick_get,而是在主循环里根据单调时钟计算 delta 并调用 lv_tick_inc,避免额外线程:
c
static uint32_t last_ms;
static uint32_t monotonic_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint32_t)(ts.tv_sec * 1000u + ts.tv_nsec / 1000000u);
}
void main_loop(void)
{
last_ms = monotonic_ms();
while(running) {
uint32_t now = monotonic_ms();
uint32_t delta = now - last_ms;
last_ms = now;
lv_tick_inc(delta);
lv_timer_handler();
usleep(1000); /* 根据项目需求调整 sleep 时间 */
}
}
6.3.3 优势
- 无需额外线程:节省一个专门的 tick 线程,减少调度和内存占用;
- 调用点更集中 :要么集中在
lv_tick_get,要么集中在主循环,更容易调试时间相关问题; - 对于 PC/Linux 这类平台,一次
clock_gettime的系统调用开销通常是可接受的。
6.3.4 劣势
- 实现略偏"高级" :需要修改/重载
lv_tick_get或显式在主循环中维护delta,对刚接触 LVGL 的开发者不如"起一个线程"直观; - 若
lv_tick_get()频繁被调用,直接读系统时钟会带来一定 syscall 开销,需要结合实际频率评估; - 在多线程场景中,需要确保只有一个地方负责推进 tick,避免出现双重更新时间源。
6.4 开机时间、世界时间、本地时间、夏令时
LVGL tick 只关心"开机后经过了多少毫秒",与以下概念解耦:
- 世界时间(UTC) :
- 通常由 NTP 或手动配置;
- 可通过
time(NULL)/CLOCK_REALTIME获取; - 会因校时而跳变。
- 本地时间(含时区/夏令时) :
- 由
localtime_r/strftime等转换获得; - 完全是展示层问题,UI 可以在显示层做转换。
- 由
- 夏令时 :
- 只是本地时间表示的一种规则,对 LVGL tick 不应产生影响。
推荐做法:
- tick 只基于单调时间源;
- RTC / 世界时间 / 本地时间由独立模块负责,并在需要时与 LVGL UI 层交互(例如"显示当前时间"控件),而不是改变 LVGL tick。
7 RTC 与 tick 的协作
7.1 职责划分
在典型嵌入式系统中:
- RTC 模块 负责:
- 在断电/重启后保持世界时间(或本地时间);
- 开机时提供一个"基准时间点";
- 可能与外部时钟源(GPS/NTP)做同步。
- LVGL tick 模块 负责:
- 自系统启动以来的相对时间管理;
- 只有"从 0 开始增长的毫秒数"这一视角。
两者的协作方式可以是:
- 开机时,从 RTC 读出当前 UTC 时间戳,记录为
t_rtc_boot; - 从 OS 获取 uptime 或 LVGL tick 基准时间
t_boot_ms; - 之后可以通过:
current_utc = t_rtc_boot + lv_tick_get()/1000(需考虑 drift 与校时);- 或结合 OS 的
CLOCK_MONOTONIC/CLOCK_REALTIME做更精细映射。
7.2 典型实践建议
- 不要用 LVGL tick 直接替代 RTC:tick 溢出、重启都会重置,不能承担长期时间保持责任;
- LVGL tick 用于"短时间尺度"(秒~小时级)的定时任务与动画即可;
- RTC/系统时间用于"长时间尺度"(天~年级)的日程、日志时间戳等业务逻辑;
- UI 中若需要显示"当前时间""闹钟"等,应通过单独的时间服务模块向 LVGL 提供已经处理好时区/夏令时的时间字符串。
8 小结
library/lvgl/src/tick 为 LVGL9.4 提供了一套简洁但关键的时间管理基础设施:
- 通过
lv_tick_get/lv_tick_elaps/lv_tick_inc这组 API,统一了多平台的相对时间语义; - 所有定时器、动画、输入设备去抖和刷新都通过该 tick 驱动,形成了稳定的时间轴;
- tick 明确与"世界时间/本地时间/夏令时/RTC"解耦,只承担单调时间源角色。
在移植与系统设计时,建议:
- 在底层用"单调时钟"实现 LVGL tick;
- 在上层用独立时间服务管理 RTC/世界时间/本地时间,必要时再将结果传给 LVGL UI 层展示;
- 避免让 NTP 校时、时区或夏令时逻辑直接影响
lv_tick_inc调用节奏。
9 附录
A 参考文档(外部)
B 相关资源(CSDN 系列)
- 【奶茶Beta专项】【LVGL9.4源码分析】01-目录结构
- 【奶茶Beta专项】【LVGL9.4源码分析】02-编译框架-Cmake详解
- 【奶茶Beta专项】【LVGL9.4源码分析】03-显示框架-display
- 【奶茶Beta专项】【LVGL9.4源码分析】04-OS抽象层
- 【奶茶Beta专项】【LVGL9.4源码分析】05-标准库 (编号示例,实际链接请按发布结果调整)
C 联系方式
- 维护者: 妙核科技
- 最后更新: 2025年12月
- 适用版本: LVGL 9.4+