在使用 LVGL 开发图形界面时,经常会遇到两个关键函数:lv_tick_inc()lv_timer_handler()。很多开发者可能会疑惑,既然已经通过硬件定时器调用了 lv_tick_inc() 来更新时间基准,为什么还需要手动调用 lv_timer_handler()?本文将从原理、设计思路和实际应用的角度,详细解答这个问题。


核心问题

问:为什么不在每次“告诉” LVGL 时间时(调用 lv_tick_inc())就检查任务,而是需要我手动调用 lv_timer_handler()


LVGL 的时钟机制

LVGL 是一个轻量级的图形库,它依赖于系统提供的时间基准来驱动其内部逻辑,例如动画、输入设备轮询、屏幕刷新等。以下是 LVGL 使用时钟的关键点:

  1. 时间基准 (lv_tick_inc)

    • LVGL 需要一个全局的时间基准,通常以毫秒为单位递增。

    • 这个时间基准由用户实现,通常是通过一个硬件定时器或系统定时器定期调用 lv_tick_inc(ms) 来更新。

  2. 计时器处理 (lv_timer_handler)

    • LVGL 内部维护了一组软件计时器(例如用于动画、延迟操作等)。

    • 这些计时器需要定期检查和处理,这通过调用 lv_timer_handler() 来完成。

    • lv_timer_handler() 会遍历所有注册的计时器,并根据当前时间基准执行到期的任务。


为什么需要分开 lv_tick_inclv_timer_handler

为了回答这个问题,我们需要理解 LVGL 的设计理念和实际应用场景。

1. 减少中断上下文的负担

  • 硬件定时器运行在中断上下文中 硬件定时器(如 esp_timer)通常每 1 毫秒触发一次中断,调用 lv_tick_inc(1) 更新时间基准。中断上下文是一个非常敏感的环境,要求代码尽可能短小高效。

  • 任务处理可能较耗时 如果每次调用 lv_tick_inc(1) 时都直接检查并处理任务(即调用 lv_timer_handler()),那么可能会执行一些复杂的逻辑(比如动画计算、屏幕刷新等)。这些操作会显著增加中断的执行时间,可能导致系统性能下降甚至崩溃。

  • 分离的好处 将时间更新和任务处理分开后,中断只需要简单地调用 lv_tick_inc(1),而任务处理可以在主循环或低优先级任务中完成,避免了对中断上下文的影响。


2. 灵活性:允许用户控制任务处理频率

  • 时间基准可以很高频 硬件定时器通常以固定的高频(如每 1 毫秒)触发,确保时间基准精确。

  • 任务处理不需要高频 实际上,LVGL 的任务处理并不需要每 1 毫秒执行一次。例如:

    • 动画的刷新频率通常是 30Hz 或 60Hz(即每 33 毫秒或 16 毫秒刷新一次)。

    • 输入设备轮询可能只需要每 10 毫秒执行一次。

    如果每次时间更新都检查任务,会导致大量不必要的计算,浪费 CPU 资源。

  • 用户可以根据需求调整频率 通过让用户手动调用 lv_timer_handler(),LVGL 把任务处理的频率交给用户控制。例如,你可以选择每 10 毫秒调用一次 lv_timer_handler(),这样既能保证响应速度,又不会过度占用 CPU。


3. 多任务系统的适配性

  • 嵌入式系统通常使用 RTOS(实时操作系统) 在嵌入式系统中,任务通常由 RTOS 调度,每个任务有自己的优先级和执行时机。如果 LVGL 直接在中断中处理任务,可能会干扰其他高优先级任务的执行。

  • 任务处理更适合放在主循环或低优先级任务中 lv_timer_handler() 放在主循环或低优先级任务中,可以让系统更灵活地管理资源。例如,当系统负载较高时,可以适当降低 lv_timer_handler() 的调用频率,从而为其他任务腾出更多 CPU 时间。


4. 简化设计:职责分离

  • 单一职责原则 LVGL 的设计遵循“单一职责原则”,即每个模块只负责一件事情:

    • 硬件定时器负责提供时间基准。

    • 用户负责调用 lv_timer_handler() 来处理任务。

    这种设计让 LVGL 更加通用,能够适应不同的硬件和系统环境。

  • 用户有更多的控制权 通过让用户手动调用 lv_timer_handler(),LVGL 提供了更大的灵活性。例如,你可以根据系统的实际负载动态调整 lv_timer_handler() 的调用频率,或者在某些情况下完全暂停任务处理。


类比日常生活

为了更好地理解,我们可以用一个生活中的场景来类比:

  1. 时间更新 厨房里有一个挂钟,每 1 分钟发出一次滴答声,告诉你时间过去了 1 分钟。这相当于硬件定时器调用 lv_tick_inc(1)

  2. 任务处理 你需要根据食谱完成一系列任务,比如切菜、煮汤、炒菜等。这些任务需要你定期查看挂钟,并判断是否到了该执行的时间。

    如果每次挂钟滴答时,你都停下来检查食谱并开始做菜,效率会非常低。相反,你可以每隔几分钟才检查一次食谱(相当于调用 lv_timer_handler()),然后集中处理多个任务。


总结

LVGL 的设计将“时间更新”和“任务处理”分开,主要有以下几个原因:

  1. 减少中断上下文的负担:避免在中断中执行复杂任务,提升系统稳定性。

  2. 灵活性:允许用户根据需求调整任务处理频率。

  3. 多任务系统的适配性:更好地与 RTOS 协同工作。

  4. 简化设计:职责分离,让 LVGL 更加通用和灵活。

通过这种设计,LVGL 不仅能够高效运行,还能适应各种不同的硬件和系统环境。希望这篇文章能帮助你更好地理解 LVGL 的时钟机制!


本站由 小马 使用 Stellar 创建。