tips:LVGL 定时器触发周期不准确(实际间隔 设定间隔)问题排查与解决方案

news/2025/11/27 20:54:19/文章来源:https://www.cnblogs.com/YouEmbedded/p/19279444

问题现象

在使用 LVGL 库开发嵌入式 GUI 时,创建了一个设定为 1 秒(1000ms)触发一次的定时器,但实际观察到定时器回调函数的执行间隔明显超过 1 秒,导致依赖该定时器的功能(如时间显示更新)变得缓慢。

问题根源分析

要理解这个问题,必须先掌握 LVGL 定时器的工作原理。

  • LVGL 定时器的工作原理

    LVGL 的定时器是 基于 “节拍(Tick)” 工作的,而不是直接基于硬件时钟。

    • 节拍(Tick):LVGL 内部维护一个全局的毫秒级计数器,我们称之为 “节拍”。这个计数器的值通过 lv_tick_inc(x) 函数来手动递增,其中 x 是你告诉 LVGL “自上次调用以来,已经过去了 x 毫秒”。
    • 定时器触发条件:创建一个定时器(如 lv_timer_create(cb, 1000, NULL)),LVGL 会记录一个 “目标节拍值”(当前节拍值 + 1000)。每当 lv_timer_handler() 函数被调用时,它会检查当前的全局节拍值是否已经达到或超过了某个定时器的 “目标节拍值”。如果达到,就触发该定时器的回调函数,并为它设置下一个 “目标节拍值”。

    核心结论LVGL 定时器的精度完全依赖于 lv_tick_inc(x) 函数被调用的频率和 x 值的准确性。

  • 问题代码分析

    LVGL的主循环代码如下:

    while(1) {lv_timer_handler();   // 处理LVGL的定时器和任务lv_tick_inc(5);       // 告诉LVGL,过去了5毫秒usleep(5000);         // 让CPU休眠5毫秒
    }
    

    我们来拆解这个循环的实际执行情况:

    • lv_timer_handler():执行耗时不确定,取决于当前有多少 LVGL 任务要处理,但通常非常短(微秒到毫秒级)。
    • lv_tick_inc(5):这是问题的关键。它强制告诉 LVGL,自从上一次调用 lv_tick_inc 以来,已经过去了 5 毫秒
    • usleep(5000):请求操作系统将当前进程挂起 5000 微秒(即 5 毫秒)。

    实际循环周期

    • 一个完整的循环周期 T_real 约等于 usleep(5000) 的时间加上 lv_timer_handler() 执行的时间。我们近似认为 T_real ≈ 5ms。
    • 在每一个大约 5ms 的真实时间里,lv_tick_inc(5) 都向 LVGL 报告 “时间过去了 5ms”。

    对定时器的影响

    • 如果设定了一个 1 秒(1000ms)的定时器。LVGL 需要累计收到 1000 个 “虚拟毫秒” 才会触发它。
    • 按照当前的循环,LVGL 每 5ms 收到 5 个 “虚拟毫秒”。要收到 1000 个,需要 1000ms / 5ms_per_loop = 200 次循环。
    • 200 次循环的真实时间是 200 * T_real(≈5ms) = 1000ms

    为什么实际会超过 1 秒?

    • 上述计算是理想情况。实际上,lv_timer_handler() 函数本身需要消耗时间,usleep(5000) 的实际休眠时间也可能因为系统调度等原因略有偏差。
    • 因此,T_real 会略大于 5ms,导致 200 次循环的总时间略大于 1 秒。
    • 如果在循环中加入了其他耗时操作,这个偏差会更加明显。

    总结:上述代码创建了一个 “虚拟” 的 5ms 时间单位 ,并用它来驱动 LVGL 的时间系统。LVGL 的定时器基于这个虚拟单位工作,因此它的实际触发间隔被循环周期 “拉长” 了。

解决方案

方案一:精确的时间增量计算(推荐)

核心思路

放弃 “猜测” 时间过去了多久,改为精确测量每次循环的真实耗时,然后将这个真实的耗时告诉 LVGL。

这需要借助一个比 usleep 更精确的系统计时器,来测量两次循环之间的时间差。在类 Unix 系统(如 Linux、FreeRTOS 等)中,通常使用 gettimeofday() 函数,它可以提供微秒级的时间精度。

代码实现

#include <sys/time.h> // 引入 gettimeofday() 的头文件// ...// 在主循环开始前,记录一个初始的时间点
struct timeval last_time;
gettimeofday(&last_time, NULL);while(1) {// 记录当前时间点struct timeval current_time;gettimeofday(&current_time, NULL);// 计算与上一次记录的时间差(单位:毫秒)long diff_us = (current_time.tv_sec - last_time.tv_sec) * 1000000 +(current_time.tv_usec - last_time.tv_usec);int diff_ms = diff_us / 1000;// 如果时间差大于0,就告知LVGL真实流逝的时间if (diff_ms > 0) {lv_tick_inc(diff_ms);}// 更新“上一次时间”为“当前时间”,为下一次循环做准备last_time = current_time;// 处理LVGL的定时器和任务lv_timer_handler();// 短暂休眠,降低CPU占用率。这个值可以很小,因为我们已经有了精确的时间测量。usleep(1000); // 休眠1毫秒
}

代码解析

  • struct timeval:这是一个结构体,用于存储时间。tv_sec 是秒数,tv_usec 是微秒数(1 秒 = 1000000 微秒)。
  • gettimeofday(&current_time, NULL):获取当前的精确时间,并存储在 current_time 变量中。
  • diff_us 计算
    • (current_time.tv_sec - last_time.tv_sec) * 1000000:计算秒数差,并转换为微秒。
    • (current_time.tv_usec - last_time.tv_usec):计算微秒数差。
    • 两者相加得到总的时间差(微秒)。
  • diff_ms = diff_us / 1000:将微秒转换为毫秒,因为 lv_tick_inc 的参数是毫秒。
  • if (diff_ms > 0):这是一个健壮性检查。虽然罕见,但在某些情况下(如系统时间被调整),current_time 可能会小于 last_time,导致 diff_ms 为负数。我们只在时间确实向前推进时才调用 lv_tick_inc
  • last_time = current_time;:这是关键的一步。它将当前时间保存下来,作为下一次循环计算时间差的基准。
  • usleep(1000):这里的休眠时间不再影响定时器精度,它的主要作用是防止 CPU 在空循环中 100% 占用。即使这里休眠 10ms,gettimeofday 也能精确计算出真实的耗时(约 10ms)并传递给 lv_tick_inc

方案优势

  • 精确性:定时器的触发间隔将严格遵守你设定的时间(如 1 秒),不受循环内其他代码执行时间的影响。
  • 健壮性:即使系统负载很高,或者循环中加入了其他耗时操作,gettimeofday 也能准确地测量出真实的时间流逝,保证 LVGL 时间系统的稳定。
  • 标准实践:这是在嵌入式系统中集成 LVGL 或其他 GUI 库时,处理时间问题的标准和推荐方法。它将 GUI 的虚拟时间与硬件的真实时间解耦。

方案二:简单匹配循环周期与节拍(不推荐,仅作对比)

核心思路

既然问题是因为 lv_tick_inc(x) 中的 x 值与实际循环周期不匹配,那么一个简单粗暴的解决方法就是调整 x 值和 usleep 时间,让它们与 LVGL 的期望(1ms per tick)相匹配

LVGL 的定时器系统设计期望是每 1 毫秒调用一次 lv_tick_inc(1)

代码实现

while(1) {lv_timer_handler();   // 处理LVGL的定时器和任务lv_tick_inc(1);       // 告诉LVGL,过去了1毫秒usleep(1000);         // 让CPU休眠1毫秒
}

代码解析

  • lv_tick_inc(1):我们现在告诉 LVGL,每次循环只过去了 1 毫秒。

  • usleep(1000):我们尝试让循环的实际周期也接近 1 毫秒。

    如果这个循环能够精确地每 1 毫秒执行一次,那么 LVGL 的时间系统就会完美运行。1 秒的定时器就会正好在 1 秒后触发。

方案缺点

  • 精度依赖 usleepusleep() 函数的精度不高,尤其是在系统负载较高时,操作系统可能无法保证精确地在 1 毫秒后唤醒进程。如果 usleep(1000) 实际休眠了 1.2 毫秒,那么你的循环周期就变成了 1.2 毫秒,LVGL 的时间还是会慢。
  • 受循环内其他代码影响:如果 lv_timer_handler() 的执行时间变长,或者你在循环中加入了其他任何耗时操作,都会导致整个循环周期 T_real 大于 1 毫秒。这会直接导致 LVGL 的时间变慢。
  • CPU 占用率可能较高:为了接近 1ms 的精度,usleep 的时间不能设得太大,这可能导致 CPU 在空转和休眠之间快速切换,占用率相对较高。

方案对比与选择建议

特性 方案一:精确时间测量 (推荐) 方案二:简单周期匹配 (不推荐)
核心原理 测量真实时间差,动态调整 lv_tick_inc 的参数。 假设循环周期固定为 1ms,固定调用 lv_tick_inc(1)
时间精度 。定时器触发间隔与设定值高度一致。 。精度依赖于 usleep 的准确性和系统负载,定时器容易变慢。
健壮性 。能自适应系统负载变化和循环内代码耗时的波动。 。循环周期的任何微小变化都会直接影响定时器精度。
CPU 占用 可控。可以通过设置一个合理的 usleep 时间(如 1ms 或 5ms)来平衡精度和 CPU 占用。 较高。为了追求 1ms 的精度,usleep 时间通常较短,导致 CPU 频繁唤醒。
实现复杂度 稍高。需要引入 gettimeofday 等时间测量函数和差值计算。 极低。只需修改两个数字即可。
适用场景 所有场景,尤其是对定时器精度有要求的复杂项目。 仅适用于非常简单系统负载极低、且对时间精度要求不高的演示或原型项目。

最终建议:无论你的项目当前复杂度如何,都应优先选择方案一。它虽然实现上稍显复杂,但带来的是稳定、精确和健壮的时间系统,能从根本上避免此类问题再次发生,并为未来项目的扩展打下坚实基础。方案二只能作为临时的、权宜之计,不应在正式或复杂的项目中使用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/978964.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

docker离线安装emqx(麒麟aarch64)

最近需要在麒麟系统安装emqx,由于emqx没有麒麟系统的安装包且源码编译依赖版本较难管理,因此采用docker容器化部署,现在回忆总结一下emqx的docker离线部署步骤。这里是麒麟系统离线安装docker和docker-compose的步骤…

云斗学院 NOIP 考前练手公益赛 Round 1 题目分析

应该比CSP-S还简单。 T1 分讨+贪心即可。 T2 简单题,贪心 T3 原题,做过,USACO的题目,大概是只吃 \([l,r]\) 的,然后留一个 \(k\) 最后给一个牛吃。题单里面有。 T4 好玩。 我们假设我们并没有看到数据随机。 题目…

第6章 基于应变的单轴疲劳分析 11

引言 局部应变-寿命法的核心假设是:缺口构件的裂纹形核与小裂纹扩展阶段寿命,与光滑实验室试样在相同循环变形条件(即裂纹起始部位的局部应变控制材料行为)下的寿命一致。如图6.1所示,基于这一概念,若已知试样局…

C++写有一个2D 小游戏(贪吃蛇)

用 C++ 开发游戏需要结合图形库、输入处理、游戏逻辑等模块,以下以2D 小游戏(贪吃蛇)为例,展示 C++ 游戏开发的核心流程,使用跨平台图形库SFML(简单易用,适合入门)。 一、准备工作 1. 安装 SFML 库下载地址:S…

NOIP day -2 笔记

马上NOIP了,写点笔记攒一下rp P2824[排序] 十分巧妙的数据结构题,关键点在于如何正确的处理排序的结果。 直接暴力做显然会被T飞,由此我们需要考虑用一种复杂度更低的方法去标记排序。 对于一个点 \(x\) 做包含 \(x…

专精

要在自己喜欢的方向去专精,专精的本质就是:通过极致的"慢" 和"专注", 在一个狭小的领域里,积攒出能瞬间破解,别人没有的穿透力 比如 哲学 : 哲学史那么大,全研究,可能就是个知道分子,但如…

对比说明Java NIO框架和传统的IO框架的优缺点

Java NIO 框架与传统 IO(BIO)框架的优缺点对比,核心围绕性能、易用性、适用场景展开,以下从双方视角分别分析: 一、传统 IO(BIO)框架的优缺点 优点:API 简单直观,开发成本低BIO 基于 “流” 模型设计(如Inpu…

CF2157C Meximum Array 2

限制分开讨论。 首先对于一个位置,如果两个地方的限制都有,那么填 \(k + 1\),因为此时不能填 \(< k\) 的数,也不能填 \(k\),因此填 \(k + 1\)。 如果什么限制都没有,那当然是填什么无所谓。 重要的就是只有两…

如何在实际项目中选择使用Java NIO框架还是传统IO框架?

在实际项目中选择 Java NIO 框架还是传统 IO(BIO)框架,核心是匹配项目的技术场景、性能需求、开发成本三者的平衡。以下从决策维度、场景分类、选型建议三个层面给出具体方案: 一、核心决策维度 选择前需明确以下关…

AT_fps_24_b 整数の組

生成函数是简单的,列出生成函数 然后对后两个的分母因式分解发现能和前两项消掉,最后是 \([x^n]\frac{1}{(x-1)^2}\)。 还有一种是枚举前两种,然后 \(d\) 跟 \(t=n-a-b\) 模 2 同余,且满足 \(3d\le t\) 所以看 \(\…

详细介绍:【数据结构初阶】单链表

详细介绍:【数据结构初阶】单链表pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco&…

第五十篇

今天是11月27号,上了体育和数据结构

每日随笔

今天背了第四单元的英语单词,看了一会《代码大全2》

2025年日语自学软件推荐:最适合零基础与进阶者的优质口碑选择

2025年日语自学软件推荐:最适合零基础与进阶者的优质口碑选择学习一门新语言时,合适的自学工具往往能让我们少走弯路。对于选择自学的日语爱好者来说,如何在众多日语自学软件中找到真正适合自己的那一款日语自学软件…

ABC386 VP总结

比赛链接 ResultE 题没开 LL 爽挂 3 发,F 题咋是压哨过的 Solution F - Operate K 令 \(dp_{i,j}\) 为 \(S\) 的前 \(i\) 位和 \(T\) 的前 \(j\) 为的最小编辑距离,转移是显然的。因为 \(dp_{i,j}\) 只会从 \(dp_{i,…

探究Spring Boot框架中访问不存在的接口时触发对error路径的访问

先说结论 默认情况下在Spring Boot框架中访问不存在的接口时会触发对"/error"路径的访问,这是由Spring Boot框架的默认错误处理机制导致的,核心是ErrorMvcAutoConfiguration自动配置类在起作用。 追根溯源…

tarjan 强连通分量、缩点、点双、割点、割边(桥)

有向图 强连通分量、缩点 取 cmin(low[u], dfn[v]) 时 v 一定要在栈里。 弹栈时要将 u 也弹出。 int dfn[N], low[N], dfnp, st[N], sp, vis[N], bl[N], blp; void tarjan(int u) {vis[st[++sp] = u] = 1;dfn[u] = low…

2025最新智慧停车与门禁系统解决方案推荐——骏通智能,专注出入口控制与智能化管理,车牌识别、道闸管理、门禁解决方案、通道闸、停车场服务、人脸门禁一站式解决

随着智慧城市建设的加速推进,智慧停车与智能门禁系统已成为现代建筑、社区及商业场所的标配设施。在2025年的出入口控制与智能化管理领域,骏通智能凭借多年技术沉淀与创新实力,为各类场景提供高效、安全、智能的解决…

我踩坑后总结:企业微信客服API接入客服系统,90%的人都搞错了!

vx:llike620 gofly.v1kf.com 最近在配置企业微信客服时,我在域名备案这个问题上踩了不少坑,结果发现大多数人的理解都存在误区。今天就把我的实战经验分享给大家,帮你少走弯路! 两个后台,两种不同的规则 首先必须…

香橙派上进行MQTT数据存储客户端开发(一)基本环境配置

香橙派上进行MQTT数据存储客户端开发(一)基本环境配置基本信息 云服务器配置:EMQX Cloud 类型为 Serverless 主机:Orangepi5max 16G (ARM64 架构) + 32G tf卡 系统:Orangepi5max_1.0.0_ubuntu_jammy_desktop_xf…