time —— 与时间相关的函数

time 模块提供获取当前时间和日期、测量时间间隔以及执行延时的函数。

时间纪元(Time Epoch):在所有平台上都是 1970 年 1 月 1 日 00:00:00(UTC)。可以通过 gmtime(0)[0] 获取纪元年份。

维护实际日历日期/时间:这需要实时时钟(RTC)。在具有底层操作系统(包括某些 RTOS) 的系统上,RTC 可能是隐含存在的。设置和维护实际日历时间是 OS/RTOS 的职责,并在 MicroPython 之外完成,MicroPython 仅通过操作系统 API 查询日期/时间。而在裸机端口上,系统时间依赖 machine.RTC() 对象。可以使用 machine.RTC().datetime(tuple) 函数设置当前日历时间,并通过以下方式进行维持:

  • 通过后备电池供电(对于某些开发板,这可能是一个额外的可选组件)。

  • 使用网络时间协议进行同步(需要由端口或用户进行配置)。

  • 由用户在每次上电时手动设置(许多开发板在硬复位后会保持 RTC 时间,尽管有些板子在这种情况下可能需要重新设置)。

如果系统或 MicroPython 的 RTC 未维护实际日历时间,下面那些需要参考当前绝对时间的函数可能不会按照预期工作。

MicroPython 示例:

 1# SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
 2#
 3# SPDX-License-Identifier: MIT
 4
 5import os, sys, io
 6import M5
 7from M5 import *
 8import time
 9
10
11label0 = None
12label1 = None
13label2 = None
14label3 = None
15
16
17def setup():
18    global label0, label1, label2, label3
19
20    M5.begin()
21    Widgets.fillScreen(0x222222)
22    label0 = Widgets.Label("GMT-0 time:", 2, 20, 1.0, 0xFFFFFF, 0x222222, Widgets.FONTS.DejaVu18)
23    label1 = Widgets.Label("label1", 2, 60, 1.0, 0xFFFFFF, 0x222222, Widgets.FONTS.DejaVu18)
24    label2 = Widgets.Label("GMT-2 time:", 2, 100, 1.0, 0xFFFFFF, 0x222222, Widgets.FONTS.DejaVu18)
25    label3 = Widgets.Label("label3", 2, 140, 1.0, 0xFFFFFF, 0x222222, Widgets.FONTS.DejaVu18)
26
27    time.timezone("GMT0")
28    label1.setText(str(time.localtime()))
29    time.timezone("GMT-8")
30    label3.setText(str(time.localtime()))
31
32
33def loop():
34    global label0, label1, label2, label3
35    M5.update()
36
37
38if __name__ == "__main__":
39    try:
40        setup()
41        while True:
42            loop()
43    except (Exception, KeyboardInterrupt) as e:
44        try:
45            from utility import print_error_msg
46
47            print_error_msg(e)
48        except ImportError:
49            print("please update to latest firmware")

UiFlow2 示例:

example.png

cores3_time_example.m5f2

Functions

time.timezone([tz])

当不传入任何参数时,获取当前时区并返回表示该时区的字符串。

UIFLOW2:

timezone.png

传入一个时区字符串来设置时区。tz 的可选值可以在此 链接 中找到。

UIFLOW2:

timezone1.png

timezone2.png

time.gmtime([secs])
time.localtime([secs])

将自时间纪元(见上文)起的秒数 secs 转换为一个包含 8 个元素的元组:(year, month, mday, hour, minute, second, weekday, yearday)。如果未提供 secs 或其为 None,则使用 RTC 中的当前时间。

gmtime() 函数返回 UTC 的日期时间元组,而 localtime() 返回本地时间的日期时间元组。

该 8 元组中各项的含义如下:

  • year 含有世纪信息(例如 2014)。

  • month 的取值范围为 1–12。

  • mday 的取值范围为 1–31。

  • hour 的取值范围为 0–23。

  • minute 的取值范围为 0–59。

  • second 的取值范围为 0–59。

  • weekday 的取值范围为 0–6,分别表示周一到周日。

  • yearday 的取值范围为 1–366。

UIFLOW2:

gmtime.png

localtime.png

time.mktime()

这是 localtime() 的反函数。其参数是一个完整的 8 元组,表示一个与 localtime() 相同格式的时间。返回值为自 1970 年 1 月 1 日以来的秒数(整数)。

UIFLOW2:

mktime.png

mktime1.png

time.sleep(seconds)

按给定的秒数休眠。某些开发板可能接受浮点数形式的 seconds,以便休眠小数秒。需要注意的是,其他开发板可能不接受浮点参数,为了兼容它们,请使用 sleep_ms()sleep_us() 函数。

UIFLOW2:

sleep.png

time.sleep_ms(ms)

按给定的毫秒数延时,取值应为正数或 0。

该函数至少会延时给定的毫秒数,但如果系统还需要执行其他处理(例如中断处理程序或其他线程),实际延时可能更长。即使传入的 ms 为 0,其他处理仍然可以执行。若需要更精确的延时,请使用 sleep_us()

UIFLOW2:

sleep_ms.png

time.sleep_us(us)

按给定的微秒数延时,取值应为正数或 0。

该函数尝试提供至少 us 微秒的精确延时,但如果系统还有其他更高优先级的处理需要执行,实际延时可能更长。

UIFLOW2:

sleep_us.png

time.ticks_ms()

返回一个不断递增的毫秒计数器,其参考起点是任意的,并且在达到某个值后会回绕。

回绕阈值不会被显式暴露,我们将其记为 TICKS_MAX 以便讨论。计数值的周期为 TICKS_PERIOD = TICKS_MAX + 1TICKS_PERIOD 一定是 2 的幂,但在不同移植之间可能不同。所有 ticks_ms()ticks_us()ticks_cpu() 函数使用相同的周期值(为简化处理)。因此,这些函数会返回范围为 [0 .. TICKS_MAX](含端点)的值,总共 TICKS_PERIOD 种取值。注意只使用非负值。在大多数情况下,你应当将这些函数返回的值视为不透明值。对这些值唯一可用的操作是下面介绍的 ticks_diff()ticks_add() 函数。

注意:直接对这些值执行标准算术运算(+、-)或关系运算符(<、<=、>、>=)会导致错误结果。先做算术运算再将结果作为参数传给 ticks_diff()ticks_add() 也会导致这些函数返回错误结果。

UIFLOW2:

ticks_ms.png

time.ticks_us()

与上面的 ticks_ms() 类似,但单位为微秒。

UIFLOW2:

ticks_ms.png

time.ticks_cpu()

类似于 ticks_ms()ticks_us(),但具有系统中尽可能高的分辨率。通常使用的是 CPU 时钟,这也是该函数命名的原因。但它不一定必须是 CPU 时钟,也可以使用系统中其它可用的高分辨率计时源(例如高精度定时器)。该函数的精确时间单位(分辨率)在 time 模块层面未做规定,但特定移植版本的文档可能会给出更具体的信息。该函数适用于非常精细的基准测试或非常紧凑的实时循环。请避免在可移植代码中使用它。

可用性:并非所有移植都实现了该函数。

UIFLOW2:

ticks_cpu.png

time.ticks_add(ticks, delta)

将 ticks 值按给定的偏移量进行平移,该偏移量可以为正也可以为负。给定一个 ticks 值,该函数允许根据模运算定义(见上文 ticks_ms())计算在该值之前或之后 delta 个 ticks 的值。ticks 参数必须是 ticks_ms()ticks_us()ticks_cpu() 函数(或前一次 ticks_add() 调用)直接返回的结果。然而,delta 可以是任意整数或数值表达式。ticks_add() 适用于计算事件/任务的截止时间。(注意:你必须使用 ticks_diff() 函数来处理这些截止时间。)

示例:

# Find out what ticks value there was 100ms ago
print(ticks_add(time.ticks_ms(), -100))

# Calculate deadline for operation and test for it
deadline = ticks_add(time.ticks_ms(), 200)
while ticks_diff(deadline, time.ticks_ms()) > 0:
    do_a_little_of_something()

# Find out TICKS_MAX used by this port
print(ticks_add(0, -1))

UIFLOW2:

ticks_add.png

time.ticks_diff(ticks1, ticks2)

计算由 ticks_ms()ticks_us()ticks_cpu() 函数返回值之间的差值,并以有符号数形式返回,该值可能会发生回绕。

参数顺序与减法运算符相同,ticks_diff(ticks1, ticks2) 的含义等同于 ticks1 - ticks2。然而,由 ticks_ms() 等函数返回的值可能发生回绕,直接对它们做减法会产生错误结果。这就是需要 ticks_diff() 的原因:它实现了模(更具体地说是环)运算,即使在值发生回绕时(只要它们之间的间隔不太大,见下文),也能给出正确结果。该函数返回范围为 [-TICKS_PERIOD/2 .. TICKS_PERIOD/2-1] 的**有符号**值(这是二进制补码有符号整数的典型取值范围)。如果结果为负,表示 ticks1 发生在 ticks2 之前;否则表示 ticks1 发生在 ticks2 之后。这**仅在** ticks1ticks2 之间相隔不超过 TICKS_PERIOD/2-1 个 ticks 时成立。如果不满足该条件,将返回错误结果。具体来说,如果两个 tick 值相隔 TICKS_PERIOD/2-1 个 ticks,函数会返回该值。但如果两次调用之间实际经过了 TICKS_PERIOD/2 个实时 ticks,函数则会返回 -TICKS_PERIOD/2,即结果会回绕到可能取值的负区间。

上述约束条件背后的非正式理由是这样的:假设你被关在一个房间里,监测时间流逝的唯一工具是一只标准的 12 刻度钟表。如果你现在看了一眼表盘,然后在 13 小时内都没有再看(例如,你睡了很久),当你再次看表时,可能会误以为只过去了 1 小时。为了避免这种错误,你应该定期查看钟表。你的应用程序也应该如此。“睡得太久”的比喻可以直接映射到应用行为:不要让应用中的任何单个任务运行得太久。应当将任务拆分成多个步骤,并在其间进行时间记录。

ticks_diff() 被设计为可以适配多种使用模式,其中包括:

  • 带超时的轮询。在这种情况下,事件的发生顺序是已知的,你只需要处理 ticks_diff() 的正结果:

    # Wait for GPIO pin to be asserted, but at most 500us
    start = time.ticks_us()
    while pin.value() == 0:
        if time.ticks_diff(time.ticks_us(), start) > 500:
            raise TimeoutError
    
  • 事件调度。在这种情况下,如果某个事件已经过期,ticks_diff() 的结果可能为负数:

    # This code snippet is not optimized
    now = time.ticks_ms()
    scheduled_time = task.scheduled_time()
    if ticks_diff(scheduled_time, now) > 0:
        print("Too early, let's nap")
        sleep_ms(ticks_diff(scheduled_time, now))
        task.run()
    elif ticks_diff(scheduled_time, now) == 0:
        print("Right at time!")
        task.run()
    elif ticks_diff(scheduled_time, now) < 0:
        print("Oops, running late, tell task to run faster!")
        task.run(run_faster=true)
    

注意:不要将 time() 的返回值传给 ticks_diff(),你应该对它们使用普通的数学运算。但也要注意,time() 也可能(并且将会)溢出。这就是著名的 https://en.wikipedia.org/wiki/Year_2038_problem

UIFLOW2:

ticks_diff.png

time.time()

返回自时间纪元以来的秒数(整数),前提是底层 RTC 已按前文所述正确设置并维护。如果未设置 RTC,则该函数返回自端口特定参考时间点以来的秒数(对于没有电池供电 RTC 的嵌入式开发板,通常是自上电或复位以来)。如果你希望编写可移植的 MicroPython 应用,不应依赖该函数提供高于秒级的精度。若需要更高精度的绝对时间戳,请使用 time_ns()。如果只需要相对时间,请使用 ticks_ms()ticks_us()。如果需要日历时间,调用不带参数的 gmtime()localtime() 会是更好的选择。

与 CPython 的差异

在 CPython 中,该函数返回自 Unix 纪元(1970-01-01 00:00 UTC)以来的秒数,类型为浮点数,通常具有微秒级精度。在 MicroPython 中,只有 Unix 端口使用相同的纪元,并且在浮点精度允许的情况下返回子秒级精度。嵌入式硬件通常没有足够的浮点精度同时表示很长的时间范围和子秒精度,因此会使用秒级精度的整数值。一些嵌入式硬件也缺少电池供电的 RTC,因此会返回自上次上电或其它相对且与硬件相关的时间点(例如复位)以来的秒数。

UIFLOW2:

time.png

time.time_ns()

time() 类似,但返回自时间纪元以来的纳秒数,类型为整数(通常是大整数,因此会在堆上分配)。

UIFLOW2:

None