The time on a computer passes in small steps. If these values are stored or processed using a fixed size representation then care is needed to prevent issues. For example:
- A clock that ticks 100 times a second (100Hz), represented by a signed 32bit integer can only reach
(2^31 - 1) / 24 / 3600 / 100 = 248.55
days before overflowing. E.g. FAA Airworthiness Directive 2018-20-15 for Boeing 787 (pdf) - A 4000Hz clock using the same representation will overflow at
(2^31 - 1) / 3600 / 4000 = 149.13
hours. E.g. EASA Airworthiness Directive 2017-0129R1 for Airbus A350 (pdf).
CircuitPython time functions
CircuitPython provides a subset of functions from the time library. The monotonic in their name refers to a guarantee that time will not go backwards. For comparison, gettimeofday on Linux does not provide this guarantee and naive use for timing durations can result in negative values!
time.monotonic()
Python and CircuitPython's time.monotonic()
returns time in seconds as a float
variable. This return value increases over time reducing the resolution available to represent the fractional part of the value. The effect is far more significant for the 30bit storage representation used by CircuitPython (based on single precision floating point) in combination with the epoch time of 0.0 at power-up. This lowers the precision of the millisecond portion as the program continues to run and increases the granularity. This effect can be very significant if the program runs for days or weeks.
time.monotonic_ns()
Python (3.5+) and CircuitPython's time.monotonic_ns()
(available on all boards bar Gemma M0, Trinket M0 and Feather M0 non-Express) is the equivalent of time.monotonic()
with anĀ int
return value in nanoseconds (one billionth of a second). This value is not subject to reduced precision over time as long as the value is not accidentally converted to a float
. Examples of correct and incorrect usage are shown below.
import time # Good - int value preserved start = time.monotonic_ns() time.sleep(0.005) ms_duration = (((time.monotonic_ns() - start) + 500000) // 1000000) print(ms_duration) # Good - int subtraction then the small value # duration value is ok to convert to a float start = time.monotonic_ns() time.sleep(0.005) ms_duration = round((time.monotonic_ns() - start) / 1e6, 1) print(ms_duration) # BAD - accidental/premature conversion to # float reducing accuracy of calculation start_ms = time.monotonic_ns() / 1e6 # BAD time.sleep(0.005) ms_duration = round(time.monotonic_ns() / 1e6 # BAD - start_ms, 1) print(ms_duration)
The first example will print 5
. The second example prints values like 4.6
or 4.7
. The third example will print 0.0
or infrequently 32.0
for a board that's been running for one day!
Care is needed when using the exponential notation like 1e6
for 1000000
. In Python, 1e6
is always a float
. The example below from REPL shows how unintended type promotion to float
spoils the accuracy of the addition.
>>> t1 = time.monotonic_ns() >>> t1 167333909366000 >>> t1 + 1e9 1.67335e+14 >>> int(t1 + 1e9) 167334878642176 >>> t1 + 1000000000 167334909366000
Using the correct division operator for floor division (//
) and ensuring it's used with int
types is also critical to avoid trouble from float
s.
>>> smaller = 1034567890 >>> larger = smaller + 10 >>> if larger > smaller: print("all ok - integer calculation/comparison") ... all ok - integer calculation/comparison >>> if larger // 10 > smaller // 10: print("all ok - integer calculation/comparison") ... all ok - integer calculation/comparison >>> if larger / 10 > smaller // 10: print("not ok - float division") ... >>> if larger // 1e1 > smaller // 10: print("not ok - integer division used with float") ...
Making Large Numbers Readable
The 1e9
representation is useful for one billion because
the number 9 is easier to read and validate compared to visually counting the number of zeroes in 1000000000
. The previous section mentions the type of 1e9
is a float
.
The language Ada introduced a representation of numeric literals (numbers) with optional underscores to allow a number to visually broken up. From Honeywell Bull's successful "GREEN" proposal for Ada, "Rationale for the Design of the GREEN Programming Language (1979)":
The underscore is permitted within a number to break up long sequences of digits, a requirement that has long been recognized by printers.
Java introduced this with V7 in 2011, Python introduced this in 3.6 in 2016. CircuitPython implements this, an example on REPL can be seen below showing how large int
values can be more clearly represented.
Adafruit CircuitPython 5.0.0 on 2020-03-02; Adafruit CLUE nRF52840 Express with nRF52840 >>> >>> an_int = 1_000_000_000 >>> print(an_int, ... an_int // 1000_000, an_int // 1_00_00_00, ... int("-1_234_567")) 1000000000 1000 1000 -1234567
Performance Variability
In general, interpreted languages tend to offer less predictable performance than compiled languages so there may be some variation in this timing. CircuitPython performance will be occasionally affected by:
- garbage collection (a few ms),
- various other background tasks like USB activity including CIRCUITPY file system operations,
- automatic
displayio
screen refreshes (can be 100+ ms).
Demonstration of time.monotonic() Granularity
This is a short program which demonstrates the degradation in sub-second time.monotonic()
resolution. The output will differ on boards which have been powered up for a long time showing how time becomes more granular.
import board, time # Disable updates to LCD screen which # will occasionally introduce very large # delays between statement execution board.DISPLAY.auto_refresh = False print(*[("{:" + str(hdr[0]) + "s}").format(hdr[1])[:hdr[0]] for hdr in ((3, "num"), (15, "monotonic_ns()"), (12, "monotonic()"), (8, "fraction"), (9, "offset")) ], sep=" | ") count = 1 previous = time.monotonic() # Print the time when it changes according # to time.monotonic() to inspect the # sub-second precision while True: now = time.monotonic() now_ns = time.monotonic_ns() if now != previous: now_ns_str = str(now_ns) now_frac = now - int(now) print("{:3d}".format(count), now_ns_str[:-9] + "." + now_ns_str[-9:], "{:.6f}".format(now), "{:.6f}".format(now_frac), "{:.6f}".format(int(now_ns_str[-9:-3]) / 1e6 - now_frac), sep=" | ") previous = now count = 1 else: count += 1
Ten Minutes
A CLUE board which is approaching 10 minutes of uptime prints the time on every iteration of the loop and has sub-millisecond precision from time.monotonic()
.
num | monotonic_ns() | monotonic() | fraction | offset 1 | 563.305638000 | 563.304901 | 0.304932 | 0.000706 1 | 563.310520000 | 563.309813 | 0.309814 | 0.000705 1 | 563.315276000 | 563.314915 | 0.314941 | 0.000335 1 | 563.319893000 | 563.318825 | 0.318848 | 0.001045 1 | 563.324549000 | 563.323975 | 0.323975 | 0.000574 1 | 563.329326000 | 563.328838 | 0.328857 | 0.000468 1 | 563.333935000 | 563.332987 | 0.333008 | 0.000927 1 | 563.338627000 | 563.337898 | 0.337891 | 0.000736 1 | 563.343376000 | 563.343000 | 0.343018 | 0.000358 1 | 563.348121000 | 563.347864 | 0.347900 | 0.000221
Two Days
After almost two days the sub-second precision has dramatically dropped - the granularity is now 1/16th of a second (62.5ms).
num | monotonic_ns() | monotonic() | fraction | offset 1 | 149621.908317000 | 149621.868134 | 0.875000 | 0.033317 179 | 149621.929066000 | 149621.939659 | 0.937500 | -0.008434 584 | 149621.993094000 | 149622.001648 | 0.000000 | 0.993094 663 | 149622.057075000 | 149622.058868 | 0.062500 | -0.005425 663 | 149622.121106000 | 149622.130394 | 0.125000 | -0.003894 662 | 149622.185034000 | 149622.192383 | 0.187500 | -0.002466 663 | 149622.249074000 | 149622.249603 | 0.250000 | -0.000926 663 | 149622.313108000 | 149622.306824 | 0.312500 | 0.000608 472 | 149622.360060000 | 149622.383118 | 0.375000 | -0.014940 581 | 149622.424027000 | 149622.430801 | 0.437500 | -0.013473 663 | 149622.488075000 | 149622.497559 | 0.500000 | -0.011925 663 | 149622.552114000 | 149622.559547 | 0.562500 | -0.010386 662 | 149622.616032000 | 149622.621536 | 0.625000 | -0.008968 663 | 149622.680073000 | 149622.688293 | 0.687500 | -0.007427 663 | 149622.744107000 | 149622.750282 | 0.750000 | -0.005893 660 | 149622.808095000 | 149622.812271 | 0.812500 | -0.004405 582 | 149622.872057000 | 149622.869492 | 0.875000 | -0.002943 663 | 149622.936033000 | 149622.941017 | 0.937500 | -0.001467 663 | 149623.000109000 | 149623.003006 | 0.000000 | 0.000109 495 | 149623.049088000 | 149623.060226 | 0.062500 | -0.013412
The loop is typically running over 600 times before time.monotonic()
changes. This is because only 4 bits are left for the fractional component of the time.
Page last edited March 08, 2024
Text editor powered by tinymce.