【奶茶Beta专项】【LVGL9.4源码分析】06-tick时间管理

【奶茶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.csrc/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/uptimeCLOCK_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 系列)

C 联系方式

  • 维护者: 妙核科技
  • 最后更新: 2025年12月
  • 适用版本: LVGL 9.4+
相关推荐
Bigan(安)1 小时前
【奶茶Beta专项】【LVGL9.4源码分析】04-OS抽象层
linux·c语言·mcu·arm·unix
2301_793069821 小时前
Linux Ubuntu/Windows 双系统 分区挂载指南
linux·windows·ubuntu
好风凭借力,送我上青云1 小时前
哈夫曼树和哈夫曼编码
c语言·开发语言·数据结构·c++·算法·霍夫曼树
道路与代码之旅1 小时前
Windows 10 中以 WSL 驱 Ubuntu 记
linux·windows·ubuntu
ULTRA??1 小时前
动态内存管理:C语言malloc极简封装方案(修正版,可申请二维数组)
c语言·开发语言
say_fall1 小时前
C++ 入门第一课:命名空间、IO 流、缺省参数与函数重载全解析
c语言·开发语言·c++
DeeplyMind1 小时前
第5章:并发与竞态条件-13:Fine- Versus Coarse-Grained Locking
linux·驱动开发·ldd
赖small强1 小时前
【Linux C/C++开发】C++多态特性深度解析:从原理到实践
linux·c语言·c++·多态·虚函数表
huangyuchi.1 小时前
【Linux 网络】基于TCP的Socket编程:通过协议定制,实现网络计算器
linux·网络·tcp/ip·linux网络·协议定制·josncpp库·序列与反序列化