Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

MicroPython for Microcontrollers  Module 5 – Time, timers, interrupts and watchdogs

Time on a chip: ticks, sleeps and the RTC

Time intervals with ticks_ms() and ticks_diff(), wait without blocking or drifting, and keep calendar time in UTC with the RTC and ntptime in MicroPython.

  • Beginner
  • 25 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 25 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Measure intervals with ticks_ms(), ticks_us() and ticks_diff(), within the half period each counter allows
  • Choose between sleep_ms(), sleep_us() and a deadline loop for a wait
  • Schedule periodic work from the previous deadline so that it never drifts
  • Set the RTC with a tuple in its own order, and store times in UTC, converting to local time only for display

Before you start

What you’ll learn · Before you start

What you will learn

  • Measure intervals with ticks_ms(), ticks_us() and ticks_diff(), within the half period each counter allows
  • Choose between sleep_ms(), sleep_us() and a deadline loop for a wait
  • Schedule periodic work from the previous deadline so that it never drifts
  • Set the RTC with a tuple in its own order, and store times in UTC, converting to local time only for display

Before you start

On this page

A Pico that takes a reading every 10 seconds, with time.sleep(10) after 30 to 50 ms of work, takes 8,606 readings a day instead of 8,640, and its last reading of the day starts 343 seconds after its slot. After every power-up the same board’s calendar clock says it is the first second of 2021, so each timestamp it writes is years out until something sets the clock.

A microcontroller keeps time in two ways, tick counters for intervals and a calendar clock for dates, and each mistake above uses one of them badly. This lesson times intervals with the tick counters and their limits, chooses how to wait, runs a periodic job from deadlines so that it keeps its rhythm, and sets the calendar clock so that the board stores and sends UTC. Every program runs here, on a model of a Pico’s clock built from MicroPython 1.29’s documentation and the rp2 port’s source.

Tick counters time intervals, until half their period

time.ticks_ms() counts milliseconds from a point the documentation calls arbitrary: on a Pico, the moment the board started. One reading means nothing; the difference between two readings is an interval. ticks_us() counts microseconds the same way, and ticks_cpu() counts in the finest unit a port offers, which each port decides: on a Pico it is the same 1 MHz timer as ticks_us(). All three return ticks: The counts that time.ticks_ms(), ticks_us() and ticks_cpu() return: they wrap to 0 at a power of two, 2**30 on 32-bit ports, so two readings are compared only with time.ticks_diff(). More in “Pins: outputs, inputs, pull-ups and Signal” with one period, a power of two, which MicroPython’s configuration sets to 2³⁰ on 32-bit ports such as rp2 and esp32, so that every value is a small integer: An integer that MicroPython keeps inside the object word itself, with no heap allocation: on most 32-bit ports one from -2**30 to just below 2**30. Larger integers stay exact but are stored on the heap. More in “Numbers, bytes and text on small chips” and reading the clock never touches the heap.

So ticks_diff(end, start) turns two readings into an interval, never end - start, and ticks_add(t, ms) makes a deadline. This program does the arithmetic at the two edges of the period, the way MicroPython’s source does it:

Where each counter wraps, and where ticks_diff() stops being right

ticks/counters.py

"""How long the tick counters of a Pico or an ESP32 run before they wrap, and where ticks_diff() stops being right."""
from ticks import TICKS_PERIOD, ticks_add, ticks_diff

print("TICKS_MAX =", ticks_add(0, -1))
half = TICKS_PERIOD // 2
day_ms = 24 * 60 * 60 * 1000
minute_us = 60 * 1000 * 1000
print("ticks_ms(): wraps after %.2f days, ticks_diff() right up to %.2f days" % (TICKS_PERIOD / day_ms, (half - 1) / day_ms))
print("ticks_us(): wraps after %.2f minutes, ticks_diff() right up to %.2f minutes" % (TICKS_PERIOD / minute_us, (half - 1) / minute_us))
print("ticks_diff(%d, 0) = %d" % (half - 1, ticks_diff(half - 1, 0)))
print("ticks_diff(%d, 0) = %d" % (half, ticks_diff(half, 0)))
try:
    ticks_add(0, half)
except OverflowError as e:
    print("ticks_add(0, %d): OverflowError: %s" % (half, e))

Output

TICKS_MAX = 1073741823
ticks_ms(): wraps after 12.43 days, ticks_diff() right up to 6.21 days
ticks_us(): wraps after 17.90 minutes, ticks_diff() right up to 8.95 minutes
ticks_diff(536870911, 0) = 536870911
ticks_diff(536870912, 0) = -536870912
ticks_add(0, 536870912): OverflowError: ticks interval overflow

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 counters.py

ticks.py (the arithmetic) · ticks/ticks.py

"""MicroPython's tick arithmetic, written out for CPython (extmod/modtime.c, v1.29.0).

The rp2 and esp32 ports keep tick values as small integers, so their tick counters wrap at 2**30
(MICROPY_PY_TIME_TICKS_PERIOD in py/mpconfig.h). Not MicroPython: on a board, time.ticks_add() and
time.ticks_diff() are built in. counter_after() is not an API at all: it stands for the hardware counter.
"""
TICKS_PERIOD = 1 << 30
_HALF = TICKS_PERIOD // 2


def ticks_add(ticks, delta):
    """The tick value delta ticks after (or before) ticks."""
    if not -_HALF < delta < _HALF:
        raise OverflowError("ticks interval overflow")
    return (ticks + delta) & (TICKS_PERIOD - 1)


def ticks_diff(ticks1, ticks2):
    """ticks1 - ticks2 as a signed number, right while they are less than half a period apart."""
    return ((ticks1 - ticks2 + _HALF) & (TICKS_PERIOD - 1)) - _HALF


def counter_after(ticks, elapsed):
    """What the counter reads elapsed ticks after it read ticks, however long that is."""
    return (ticks + elapsed) % TICKS_PERIOD

ticks_diff() answers within one period centred on zero, so it is right only while the two readings are less than half a period apart: 6.21 days for ticks_ms(), 8.95 minutes for ticks_us(). At exactly half a period the answer turns negative, and ticks_add() refuses to make a deadline that far away. The documentation compares this to a 12-hour clock face: look at it again after 13 hours and it seems that one hour has passed. Here is the same mistake on a board that checks a one-hour deadline after longer and longer gaps. Step through it and watch late:

Interactive · line by line A one-hour deadline, checked after longer and longer gaps Each pass reads the counter a longer time after the start. Watch late, the answer of ticks_diff() in milliseconds, on the third and fourth pass.

Program · ticks/rare_check.py

from ticks import ticks_add
from ticks import ticks_diff
from ticks import counter_after

HOUR = 3_600_000  # ms
DAY = 24 * HOUR
start = 1_000_000  # ticks_ms()
due = ticks_add(start, HOUR)
gaps = [2 * HOUR, 6 * DAY,
        7 * DAY, 13 * DAY]
for gap in gaps:
    now = counter_after(start, gap)
    late = ticks_diff(now, due)
    passed = gap // HOUR
    hours = round(late / HOUR, 1)
    print(passed, "h:", hours)

Variables

After line 11: ticks_add is ticks_add(ticks, delta); ticks_diff is ticks_diff(ticks1, ticks2); counter_after is counter_after(ticks, elapsed); HOUR is 3600000; DAY is 86400000; start is 1000000; due is 4600000; gaps is [7200000, 518400000, 604800000, 1123200000] (the loop stopped at position 4); gap is 1123200000; now is 50458176; late is 45858176; passed is 312; hours is 12.7.ticks_addticks_add(ticks, delta)ticks_diffticks_diff(ticks1, ticks2)counter_aftercounter_after(ticks, elapsed)HOUR3600000DAY86400000start1000000due4600000gaps7200000051840000016048000002112320000034endgap1123200000now50458176late45858176passed312hours12.7
  • just taken by the loop
  • changed in this step
  • passed by the loop
  • read next
  • where the loop stopped

Printed so far

2 h: 1.0
144 h: 143.0
168 h: -131.3
312 h: 12.7

Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.

The drawing shows the variables when the program ends, and the program's 35 steps are listed below.

Line run 11
Next line end
Loop pass 4
Every step as text
  1. Start. No line has run yet, so there are no variables. Python starts with line 1.
  2. Line 1. from ticks import ticks_add loads ticks_add.
  3. Line 2. from ticks import ticks_diff loads ticks_diff.
  4. Line 3. from ticks import counter_after loads counter_after.
  5. Line 5. HOUR = 3_600_000 sets HOUR to 3600000.
  6. Line 6. DAY = 24 * HOUR sets DAY to 86400000.
  7. Line 7. start = 1_000_000 sets start to 1000000.
  8. Line 8. due = ticks_add(start, HOUR) sets due to 4600000. due is 4,600,000: one hour, 3,600,000 ms, after the reading at 1,000,000.
  9. Line 9. gaps = [2 * HOUR, 6 * DAY, 7 * DAY, 13 * DAY] runs.
  10. Line 9. gaps = [2 * HOUR, 6 * DAY, 7 * DAY, 13 * DAY] makes gaps a list of 4 items.
  11. Line 11. for gap in gaps: takes the item at position 0 of gaps: gap is 7200000.
  12. Line 12. now = counter_after(start, gap) sets now to 8200000.
  13. Line 13. late = ticks_diff(now, due) sets late to 3600000. Two hours have passed, so the deadline is one hour overdue: late is 3,600,000 ms.
  14. Line 14. passed = gap // HOUR sets passed to 2.
  15. Line 15. hours = round(late / HOUR, 1) sets hours to 1.0.
  16. Line 16. print(passed, "h:", hours) prints 2 h: 1.0.
  17. Line 11. for gap in gaps: takes the item at position 1 of gaps: gap is 518400000.
  18. Line 12. now = counter_after(start, gap) sets now to 519400000 (it was 8200000).
  19. Line 13. late = ticks_diff(now, due) sets late to 514800000 (it was 3600000).
  20. Line 14. passed = gap // HOUR sets passed to 144 (it was 2).
  21. Line 15. hours = round(late / HOUR, 1) sets hours to 143.0 (it was 1.0).
  22. Line 16. print(passed, "h:", hours) prints 144 h: 143.0.
  23. Line 11. for gap in gaps: takes the item at position 2 of gaps: gap is 604800000.
  24. Line 12. now = counter_after(start, gap) sets now to 605800000 (it was 519400000).
  25. Line 13. late = ticks_diff(now, due) sets late to -472541824 (it was 514800000). The bug Seven days are 604,800,000 ms, more than half the period, 536,870,912 ms. ticks_diff() wraps to a negative number and says the deadline is still 131.3 hours away.
  26. Line 14. passed = gap // HOUR sets passed to 168 (it was 144).
  27. Line 15. hours = round(late / HOUR, 1) sets hours to -131.3 (it was 143.0).
  28. Line 16. print(passed, "h:", hours) prints 168 h: -131.3.
  29. Line 11. for gap in gaps: takes the item at position 3 of gaps: gap is 1123200000.
  30. Line 12. now = counter_after(start, gap) sets now to 50458176 (it was 605800000).
  31. Line 13. late = ticks_diff(now, due) sets late to 45858176 (it was -472541824). The bug After 13 days the counter has wrapped once, and ticks_diff() says the deadline passed 12.7 hours ago. It passed 311 hours ago.
  32. Line 14. passed = gap // HOUR sets passed to 312 (it was 168).
  33. Line 15. hours = round(late / HOUR, 1) sets hours to 12.7 (it was -131.3).
  34. Line 16. print(passed, "h:", hours) prints 312 h: 12.7.
  35. Line 11. for gap in gaps: finds no item at position 4 of gaps (it has 4 items), so the loop ends. That was the last line, so the program ends. The checks after 2 hours and after 6 days were right. The two after more than 6.2 days were wrong, and nothing raised an error.

After two hours and after six days the answers are right. After seven days the deadline seems 131.3 hours away, and after thirteen it seems 12.7 hours overdue, when it is in fact 311 hours overdue. A loop that keeps a ticks deadline must check it well within half a period; a job that may wait for days takes its time from the calendar clock instead.

Keeping ticks across a reset

A tick value saved to a file and compared after a restart compares two counts with different starting points: after a hard reset a Pico’s ticks_ms() starts again from 0. Use ticks only inside one run of the program, and store time.time() for anything that must outlive a reset.

Checkpoint

Check: Tick counters and their half period

  1. Question 1 of 2 A program compares two ticks_us() readings with ticks_diff() on a Pico. What is the longest interval, in minutes, that it still measures correctly?

    Type a number.

    A hint

    The period is 2**30 microseconds, and ticks_diff() is right only below half of it.

    Show the answer: Question 1 of 2

    Answer: 8.95 minutes (anything from 8.9 to 9 counts)

    The counter wraps every 2**30 µs, 1,073.7 seconds or 17.9 minutes. ticks_diff() answers within one period centred on zero, so it is right up to half a period minus one tick: 536,870,911 µs, 8.95 minutes.

  2. Question 2 of 2 What does time.ticks_cpu() count on a Pico, an RP2040, running MicroPython 1.29.0?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Microseconds, from the same 1 MHz timer as ticks_us()

    ticks_cpu() promises the finest unit a port has, and the documentation says to avoid it in portable code. On the RP2040 the rp2 port's source returns time_us_32(), the same count ticks_us() reads.

A sleep stops the loop; a deadline keeps it listening

A sleep is the simplest way to wait and the bluntest. On the rp2 port, time.sleep_ms(500) lets the processor rest until something happens or the time is up, and gives pending scheduled callbacks their turn: work that timers and interrupts hand to the main program, which the next two lessons cover. It waits at least 500 ms, and longer when interrupt handlers or threads need the processor. sleep_us() is for short, exact waits: up to 1,000 µs it is a busy-wait: A wait in which the processor runs a tight loop that checks the time until it is up, doing nothing else meanwhile. On a Pico, time.sleep_us() waits this way for delays of up to 1,000 microseconds., which keeps the processor busy and runs no scheduled callbacks. sleep() takes seconds, a fraction too on boards with floats, but keeps only whole milliseconds. This model follows the rp2 port’s source call by call:

Six sleeps on a Pico: how long, how, and whether callbacks run

sleeps/sleeps.py

"""What each sleep call does on a Pico: how long it waits, how, and whether scheduled callbacks get their turn."""
import rp2_time as time  # on a board: import time

calls = [
    ("sleep(1.5)", lambda: time.sleep(1.5)),
    ("sleep(0.0004)", lambda: time.sleep(0.0004)),
    ("sleep_ms(0)", lambda: time.sleep_ms(0)),
    ("sleep_ms(-5)", lambda: time.sleep_ms(-5)),
    ("sleep_us(400)", lambda: time.sleep_us(400)),
    ("sleep_us(2500)", lambda: time.sleep_us(2500)),
]
print("%-15s %9s %9s %9s  %s" % ("call", "waits us", "idle us", "spins us", "callbacks run"))
for text, call in calls:
    time.reset()
    call()
    ran = "yes" if time.callback_runs else "no"
    print("%-15s %9d %9d %9d  %s" % (text, time.clock_us, time.idle_us, time.busy_us, ran))

Output

call             waits us   idle us  spins us  callbacks run
sleep(1.5)        1500000   1500000         0  yes
sleep(0.0004)           0         0         0  yes
sleep_ms(0)             0         0         0  yes
sleep_ms(-5)            0         0         0  no
sleep_us(400)         400         0       400  no
sleep_us(2500)       2500      2000       500  yes

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sleeps.py

rp2_time.py (the model) · sleeps/rp2_time.py

"""time.sleep(), sleep_ms() and sleep_us() of MicroPython's rp2 port (v1.29.0), on a simulated clock.

As extmod/modtime.c and ports/rp2/mphalport.c define them:
  - sleep(s) multiplies s by 1000 and keeps the whole milliseconds, then waits like sleep_ms();
  - sleep_ms(ms) does nothing for a negative ms; otherwise it runs pending scheduled callbacks, then waits for
    events with the CPU idle, again and again, until at least ms milliseconds have passed;
  - sleep_us(us) does nothing unless us > 0; over 1,000 us it first waits the whole milliseconds like sleep_ms(),
    then it spins in a tight loop until the time is up.
Not MicroPython: on a board these functions are built into the time module.
"""

clock_us = 0  # the board's microsecond timer
idle_us = 0  # time spent waiting for events
busy_us = 0  # time spent spinning
callback_runs = 0  # how often pending callbacks were given their turn


def reset():
    global clock_us, idle_us, busy_us, callback_runs
    clock_us = idle_us = busy_us = callback_runs = 0


def _wait_for_events(ms):
    # mp_event_wait_ms(): run pending callbacks, then wait (WFE) until an event or the timeout
    global clock_us, idle_us, callback_runs
    callback_runs += 1
    clock_us += ms * 1000
    idle_us += ms * 1000


def _delay_ms(ms):
    start = clock_us
    while True:  # a do-while loop in the C source
        _wait_for_events(ms - (clock_us - start) // 1000)
        if (clock_us - start) // 1000 >= ms:
            break


def sleep(seconds):
    _delay_ms(int(1000 * seconds))


def sleep_ms(ms):
    if ms >= 0:
        _delay_ms(ms)


def sleep_us(us):
    global clock_us, busy_us
    if us > 0:
        end = clock_us + us
        if us > 1000:
            _delay_ms(us // 1000)
        busy_us += end - clock_us  # the tight loop until the end
        clock_us = end

Two lines catch people out: sleep(0.0004) waits no time at all, because 0.4 ms becomes 0, and sleep_us(400) runs no callbacks while it spins. sleep_us(2500) rests for its 2 whole milliseconds and spins only for the last 500 µs.

While a sleep runs, the loop cannot see a button, a byte on the UART or the end of another job. A deadline loop waits differently: it makes a deadline with ticks_add(), compares the time with ticks_diff() on every pass, does whatever else is ready, and rests between passes with machine.idle(), which stops the processor’s clock until the next interrupt, or for at most 1 ms. Three things about your case decide which wait fits. Answer them in the table:

Interactive · answer and compare Which way of waiting fits your loop? Answer three questions about the wait. The table marks the ways that fit and says why the others do not.
Must the program notice anything else, such as a button or a byte, during the wait?
How long is the wait?
Does the board run on a battery?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Four ways to wait on a Pico against the four things that decide between them
Wait Other work meanwhile Precision Power while waiting Scheduled callbacks
sleep_ms(n) How it waits: rests until n ms have passed Poor the loop does nothing else until the wait ends Mixed at least n ms, and later when handlers or threads need the CPU Good the CPU waits for events Good they run while it waits
sleep_us(n) How it waits: spins in a tight loop up to 1 ms Poor nothing else runs, and up to 1,000 µs it spins Good microseconds, from the 1 MHz timer Poor the CPU runs flat out for waits of up to 1 ms Poor none run during a wait of up to 1,000 µs
Deadline loop with idle() How it waits: checks a deadline and rests between passes Good buttons, bytes and other jobs are checked on every pass Mixed up to about 1 ms late: idle() lasts until an interrupt or 1 ms Good the CPU clock is gated between passes Good they run between the bytecodes of the loop
Deadline loop that spins How it waits: checks a deadline as fast as it can Good other work is checked on every pass Good as fine as one pass of the loop Poor the CPU never rests Good they run between the bytecodes of the loop
How the answers decide
  • “Must the program notice anything else, such as a button or a byte, during the wait?” Yes, it must keep listening: rules out sleep_ms(n) and sleep_us(n). A sleep holds the loop until it ends, so a press or a byte that arrives meanwhile waits for it.
  • “How long is the wait?” Under 1 ms: rules out sleep_ms(n) and adds a caveat to Deadline loop with idle(). sleep_ms() counts whole milliseconds, and idle() can return up to 1 ms late: a wait of 200 µs needs sleep_us() or a deadline in ticks_us().
  • “How long is the wait?” 1 ms or more: adds a caveat to Deadline loop that spins and sleep_us(n). Spinning for tens of milliseconds does nothing useful and burns power; sleep_us() of more than 1 ms rests only for its whole milliseconds.
  • “Does the board run on a battery?” Yes: rules out Deadline loop that spins. A spinning CPU draws full current for the whole wait; on a battery, rest with sleep_ms() or idle() between checks.

In short: sleep_ms() when the loop has nothing else to watch, sleep_us() for waits under a millisecond inside a driver, and a deadline loop with machine.idle() whenever the board must keep listening.

Checkpoint

Check: Sleeps and deadline loops

  1. Question 1 of 2 How long does this line wait on a Pico?

    Read the code, then choose one answer.

    time.sleep(0.0004)
    Show the answer: Question 1 of 2

    Answer: No time at all, because sleep() keeps whole milliseconds and 0.4 ms becomes 0

    In the model of the lesson, sleep(0.0004) waited 0 µs and gave the callbacks their turn. For a wait under a millisecond, use sleep_us() or a deadline in ticks_us().

  2. Question 2 of 2 A timer has just scheduled a callback while the main program runs time.sleep_us(400) on a Pico. When does the callback run?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: After the sleep, because a wait of up to 1,000 µs spins and runs no scheduled callbacks

    The model's sleep_us(400) spun for 400 µs with no callback runs. A handler that must answer within microseconds runs as a hard interrupt instead, the subject of the next lessons.

Run a periodic job from its last deadline, not from now

A loop that sleeps for its period after its work runs a period plus the work on every pass. This program models one day of the 10-second logger from the start of the lesson, with work of 30 to 50 ms a reading, under two loops: one sleeps 10 s after the work, and one waits for a deadline made from the previous one:

One day of a 10-second logger, two ways

drift/drift.py

"""One day of a logger that takes a reading every 10 s, on a simulated millisecond clock.

Each reading's work takes 30 to 50 ms (a seeded random draw, the same in every run). The first loop sleeps 10 s
after the work, as time.sleep(10) would; the second waits for the next deadline, made from the last one.
"""
import random

from ticks import ticks_add, ticks_diff

PERIOD = 10_000  # ms
DAY = 24 * 60 * 60 * 1000  # ms


def work_times():
    rng = random.Random(7)
    while True:
        yield rng.randint(30, 50)


def sleep_after_work():
    now, starts, work = 0, [], work_times()
    while now < DAY:
        starts.append(now)
        now += next(work)  # the reading
        now += PERIOD  # time.sleep(10)
    return starts


def deadline_loop():
    now, due, starts, work = 0, 0, [], work_times()
    while now < DAY:
        starts.append(now)
        now = ticks_add(now, next(work))  # the reading
        due = ticks_add(due, PERIOD)  # the next slot, from the last one
        wait = ticks_diff(due, now)
        if wait > 0:
            now = ticks_add(now, wait)  # time.sleep_ms(wait)
    return starts


for name, loop in (("sleep(10) after the work", sleep_after_work), ("deadline from the last slot", deadline_loop)):
    starts = loop()
    last = len(starts) - 1
    behind = (starts[last] - last * PERIOD) / 1000
    print("%-28s %5d readings in 24 h, the last one %6.1f s after its slot" % (name, len(starts), behind))

Output

sleep(10) after the work      8606 readings in 24 h, the last one  343.0 s after its slot
deadline from the last slot   8640 readings in 24 h, the last one    0.0 s after its slot

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 drift.py

ticks.py (the arithmetic) · drift/ticks.py

"""MicroPython's tick arithmetic, written out for CPython (extmod/modtime.c, v1.29.0).

The rp2 and esp32 ports keep tick values as small integers, so their tick counters wrap at 2**30
(MICROPY_PY_TIME_TICKS_PERIOD in py/mpconfig.h). Not MicroPython: on a board, time.ticks_add() and
time.ticks_diff() are built in. counter_after() is not an API at all: it stands for the hardware counter.
"""
TICKS_PERIOD = 1 << 30
_HALF = TICKS_PERIOD // 2


def ticks_add(ticks, delta):
    """The tick value delta ticks after (or before) ticks."""
    if not -_HALF < delta < _HALF:
        raise OverflowError("ticks interval overflow")
    return (ticks + delta) & (TICKS_PERIOD - 1)


def ticks_diff(ticks1, ticks2):
    """ticks1 - ticks2 as a signed number, right while they are less than half a period apart."""
    return ((ticks1 - ticks2 + _HALF) & (TICKS_PERIOD - 1)) - _HALF


def counter_after(ticks, elapsed):
    """What the counter reads elapsed ticks after it read ticks, however long that is."""
    return (ticks + elapsed) % TICKS_PERIOD

The sleeping loop took 34 readings fewer and fell 343 seconds behind; the deadline loop took all 8,640, each on its slot. Press Edit, set PERIOD to 60_000 and run it again: a logger that reads once a minute drifts in the same way. The whole difference is one line, due = ticks_add(due, PERIOD): the next deadline is the last one plus the period, so the work time is never added to the rhythm, and the loop sleeps only for what is left, ticks_diff(due, now). An error that grows with every pass, as the sleeping loop’s does, is drift: A timing error that grows with every repetition, as when a loop sleeps for its whole period after its work, so that each run starts a little later than the one before..

Making the next deadline from the time now

due = time.ticks_add(time.ticks_ms(), PERIOD) written after the work adds the work time to every period: the sleeping loop’s drift with more code. Make it from the last deadline, due = time.ticks_add(due, PERIOD).

A deadline loop still has a decision to make when one pass takes longer than a period, for a Wi-Fi reconnect or a slow sensor: run every missed slot at once, or skip them and keep to the grid. Step through a loop that skips them, with readings that take 40, 35, 3,400 and 45 ms:

Interactive · line by line A deadline loop that keeps to its grid after a slow reading Each pass is one reading. Watch due, the next slot, and the while loop on the third pass, when one reading takes 3,400 ms.

Program · drift/every_loop.py

from ticks import ticks_add
from ticks import ticks_diff

PERIOD = 1000  # ms between readings
work = [40, 35, 3400, 45]  # ms each
now = 0  # ticks_ms()
due = 0
for ms in work:
    now = ticks_add(now, ms)
    due = ticks_add(due, PERIOD)
    while ticks_diff(now, due) > 0:
        due = ticks_add(due, PERIOD)
    wait = ticks_diff(due, now)
    now = ticks_add(now, wait)
    print("start at", now)

Variables

After line 8: ticks_add is ticks_add(ticks, delta); ticks_diff is ticks_diff(ticks1, ticks2); PERIOD is 1000; work is [40, 35, 3400, 45] (the loop stopped at position 4); now is 7000; due is 7000; ms is 45; wait is 955.ticks_addticks_add(ticks, delta)ticks_diffticks_diff(ticks1, ticks2)PERIOD1000work400351340024534endnow7000due7000ms45wait955
  • just taken by the loop
  • changed in this step
  • passed by the loop
  • read next
  • where the loop stopped

Printed so far

start at 1000
start at 2000
start at 6000
start at 7000

Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.

The drawing shows the variables when the program ends, and the program's 42 steps are listed below.

Line run 8
Next line end
Loop pass 4
Every step as text
  1. Start. No line has run yet, so there are no variables. Python starts with line 1.
  2. Line 1. from ticks import ticks_add loads ticks_add.
  3. Line 2. from ticks import ticks_diff loads ticks_diff.
  4. Line 4. PERIOD = 1000 sets PERIOD to 1000.
  5. Line 5. work = [40, 35, 3400, 45] makes work a list of 4 items.
  6. Line 6. now = 0 sets now to 0.
  7. Line 7. due = 0 sets due to 0.
  8. Line 8. for ms in work: takes the item at position 0 of work: ms is 40.
  9. Line 9. now = ticks_add(now, ms) sets now to 40 (it was 0).
  10. Line 10. due = ticks_add(due, PERIOD) sets due to 1000 (it was 0). Key point The next slot is the last slot plus the period, 1000, not the time now plus the period, 1040: the 40 ms of work do not move the rhythm.
  11. Line 11. while ticks_diff(now, due) > 0: tests its condition: it is false from the start (now is 40 and due is 1000), so the body never runs.
  12. Line 13. wait = ticks_diff(due, now) sets wait to 960.
  13. Line 14. now = ticks_add(now, wait) sets now to 1000 (it was 40). The loop sleeps for what is left, 960 ms, so the next reading starts at 1000 exactly.
  14. Line 15. print("start at", now) prints start at 1000.
  15. Line 8. for ms in work: takes the item at position 1 of work: ms is 35.
  16. Line 9. now = ticks_add(now, ms) sets now to 1035 (it was 1000).
  17. Line 10. due = ticks_add(due, PERIOD) sets due to 2000 (it was 1000).
  18. Line 11. while ticks_diff(now, due) > 0: tests its condition: it is false from the start (now is 1035 and due is 2000), so the body never runs.
  19. Line 13. wait = ticks_diff(due, now) sets wait to 965 (it was 960).
  20. Line 14. now = ticks_add(now, wait) sets now to 2000 (it was 1035).
  21. Line 15. print("start at", now) prints start at 2000.
  22. Line 8. for ms in work: takes the item at position 2 of work: ms is 3400.
  23. Line 9. now = ticks_add(now, ms) sets now to 5400 (it was 2000). This reading started at 2000 and took 3,400 ms, so its work ends at 5400, long after the slot at 3000.
  24. Line 10. due = ticks_add(due, PERIOD) sets due to 3000 (it was 2000).
  25. Line 11. while ticks_diff(now, due) > 0: tests its condition: it is true (now is 5400 and due is 3000), so the body runs.
  26. Line 12. due = ticks_add(due, PERIOD) sets due to 4000 (it was 3000).
  27. Line 11. while ticks_diff(now, due) > 0: tests its condition: it is true (now is 5400 and due is 4000), so the body runs again (pass 2).
  28. Line 12. due = ticks_add(due, PERIOD) sets due to 5000 (it was 4000).
  29. Line 11. while ticks_diff(now, due) > 0: tests its condition: it is true (now is 5400 and due is 5000), so the body runs again (pass 3).
  30. Line 12. due = ticks_add(due, PERIOD) sets due to 6000 (it was 5000). Slots 3000, 4000 and 5000 passed during the work, so the loop skips them: the next slot is 6000, still on the grid.
  31. Line 11. while ticks_diff(now, due) > 0: tests its condition: it is false (now is 5400 and due is 6000), so the loop ends.
  32. Line 13. wait = ticks_diff(due, now) sets wait to 600 (it was 965).
  33. Line 14. now = ticks_add(now, wait) sets now to 6000 (it was 5400).
  34. Line 15. print("start at", now) prints start at 6000. Key point The reading after the slow one starts at 6000, not at 6400: the rhythm is kept, and the skipped slots are not run in a burst.
  35. Line 8. for ms in work: takes the item at position 3 of work: ms is 45.
  36. Line 9. now = ticks_add(now, ms) sets now to 6045 (it was 6000).
  37. Line 10. due = ticks_add(due, PERIOD) sets due to 7000 (it was 6000).
  38. Line 11. while ticks_diff(now, due) > 0: tests its condition: it is false from the start (now is 6045 and due is 7000), so the body never runs.
  39. Line 13. wait = ticks_diff(due, now) sets wait to 955 (it was 600).
  40. Line 14. now = ticks_add(now, wait) sets now to 7000 (it was 6045).
  41. Line 15. print("start at", now) prints start at 7000.
  42. Line 8. for ms in work: finds no item at position 4 of work (it has 4 items), so the loop ends. That was the last line, so the program ends. Every start the program printed, 1000, 2000, 6000 and 7000, is on the 1,000 ms grid, although one reading took 3.4 seconds.

The slow reading ended at 5,400 ms, after the slots at 3,000, 4,000 and 5,000. The loop skipped all three, and the next reading started at 6,000, on the grid. A logger usually wants this. A job that must run once for every slot, such as one that totals pulses per second, catches up instead and records how late each run was.

Key idea

Add the period to the last deadline, sleep only for what is left, and decide in advance what a late pass does with the slots it missed.

Checkpoint

Check: Periodic jobs without drift

  1. Question 1 of 2 A loop sleeps 60 s after each reading's 50 ms of work. About how many seconds behind its schedule is it after one day?

    Type a number.

    A hint

    Each pass takes 60.05 s instead of 60 s. How many passes fit in a day?

    Show the answer: Question 1 of 2

    Answer: 72 s (anything from 70 to 74 counts)

    A day of 86,400 s holds about 86,400 ÷ 60.05 ≈ 1,439 readings, and each one adds 0.05 s, so the last starts about 1,438 × 0.05 ≈ 72 s after its slot. A deadline loop would keep every reading on its slot.

  2. Question 2 of 2 A deadline loop with a 1,000 ms period skips the slots a slow pass missed. A reading starts at 2,000 ms and takes 3,400 ms. When does the next reading start?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: At 6000

    The traced loop printed 1000, 2000, 6000 and 7000: every start on the grid, with the three missed slots skipped.

The RTC keeps the calendar, and a Pico’s starts at 2021

Intervals come from ticks; dates come from the RTC: Real-time clock: the part of a microcontroller that keeps the calendar date and time, read and set with machine.RTC().datetime(). A Pico has no battery for it, so it starts again from a fixed date at every power-up.. machine.RTC().datetime() returns eight numbers in its own order, (year, month, day, weekday, hours, minutes, seconds, subseconds), with weekday 0 for Monday. A Pico has no battery for its RTC: at every power-up and every hard reset the rp2 port starts it at 2021-01-01 00:00:00, and on a Pico the subseconds always read 0.

The time module turns the RTC into seconds and tuples. time.time() counts whole seconds from the port’s epoch: The moment from which a clock counts seconds: the start of 1970 on the rp2 and unix ports and in CPython, the start of 2000 on esp32 and other embedded ports. time.gmtime(0)[0] gives its year., and time.gmtime() and time.localtime() return (year, month, mday, hour, minute, second, weekday, yearday): almost the same fields, in another order. That difference is the commonest way to set a clock wrong. The model below sets the RTC once from a time tuple and once in the RTC’s own order:

A Pico's RTC after power-up, set two ways

rtc/rtc_tuples.py

"""A Pico's RTC after power-up, set once with the wrong tuple and once with the right one."""
import board_time as time  # on a board: import time
from board_time import RTC  # on a board: from machine import RTC


def show(label, value):
    print("%-18s %s" % (label, value))


rtc = RTC()
show("after power-up:", rtc.datetime())
show("time.time():", time.time())
show("epoch year:", time.gmtime(0)[0])

tm = time.gmtime(1_800_000_930)  # a UTC time tuple
show("time tuple:", tm)

rtc.datetime(tm)  # the wrong order
show("set from tm:", rtc.datetime())

y, mo, d, h, mi, s, wd, yd = tm
rtc.datetime((y, mo, d, wd, h, mi, s, 0))
show("set in RTC order:", rtc.datetime())
show("time.time():", time.time())
show("localtime() == gmtime():", time.localtime() == time.gmtime())

Output

after power-up:    (2021, 1, 1, 4, 0, 0, 0, 0)
time.time():       1609459200
epoch year:        1970
time tuple:        (2027, 1, 15, 8, 15, 30, 4, 15)
set from tm:       (2027, 1, 15, 4, 15, 30, 4, 0)
set in RTC order:  (2027, 1, 15, 4, 8, 15, 30, 0)
time.time():       1800000930
localtime() == gmtime(): True

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rtc_tuples.py

board_time.py (the model) · rtc/board_time.py

"""The calendar side of a Pico's clock: machine.RTC and the time module's date functions (MicroPython 1.29.0, rp2).

From the rp2 port's source: at every hard start main.c sets the RTC to 2021-01-01 00:00:00 (ports/rp2/main.c);
RTC.datetime() reads (year, month, day, weekday, hours, minutes, seconds, 0), and a tuple it is given must have 8
items, of which it uses the year, month and day (items 0 to 2) and the hours, minutes and seconds (items 4 to 6),
with no range check, so 25 hours become 1 o'clock the next day (ports/rp2/machine_rtc.c, shared/timeutils). The
time module counts from the start of 1970 on this port and has no time zones: gmtime and localtime are the same
function (extmod/modtime.c). Not MicroPython: a model for computers. power_up() stands for a hard reset.
"""
import calendar
import time as _host_time

_seconds = 0  # what the RTC holds: seconds since the start of 1970


def power_up():
    global _seconds
    _seconds = calendar.timegm((2021, 1, 1, 0, 0, 0))


def _tuple(secs):
    return tuple(_host_time.gmtime(secs)[:8])


class RTC:
    def datetime(self, dt=None):
        global _seconds
        if dt is None:
            y, mo, d, h, mi, s, wd, _ = _tuple(_seconds)
            return (y, mo, d, wd, h, mi, s, 0)
        if len(dt) != 8:
            raise ValueError("requested length 8 but object has length %d" % len(dt))
        days = calendar.timegm((dt[0], dt[1], dt[2], 0, 0, 0)) // 86400
        _seconds = days * 86400 + dt[4] * 3600 + dt[5] * 60 + dt[6]


def time():
    return _seconds


def gmtime(secs=None):
    return _tuple(_seconds if secs is None else secs)


localtime = gmtime  # the same function on the rp2 port: no time zones


def mktime(tm):
    return calendar.timegm(tuple(tm[:6]))


power_up()

Given the time tuple, the RTC read the hour where it expects the weekday, which it ignores, took the minutes for hours and so on, and kept 15:30:04 for 08:15:30 UTC, without a word. The two orders, position by position:

Position RTC().datetime() time.gmtime() and time.localtime()
0 year year
1 month month
2 day of the month day of the month
3 weekday, 0 = Monday, ignored when set hour
4 hours minute
5 minutes second
6 seconds weekday, 0 = Monday
7 subseconds, 0 on a Pico day of the year

Epochs differ as well. The rp2 port counts from the start of 1970, as Unix, CPython and the unix port do; the esp32 port, like most other embedded ports, counts from the start of 2000, so the same moment is 946,684,800 seconds smaller on an ESP32. time.gmtime(0)[0] tells a program which epoch it runs on.

One range of dates on every port since 1.26

MicroPython 1.26 made time(), localtime() and mktime() work on the same range of dates on every port, from 1970 up to at least 2099, whichever epoch the port counts from.

Something has to set the RTC after every start. Follow a Pico W from power-up to timestamps it can trust:

Interactive · stage by stage How a Pico W comes to know the time Step through a start from power-up to timestamps the board can trust, then the two kinds of reset.
How a Pico W comes to know the time, step 7 of 7. Showing Power-up or hard reset, boot.py and main.py run, Join Wi-Fi, ntptime.settime(), Timestamps are right. In focus: hard.soft reset keeps ithard reset: 2021 againPower-up or hard resetRTC: start of 2021boot.py and main.py runtimestamps say 2021Join Wi-FiPico W or ESP32ntptime.settime()RTC set to UTCTimestamps are righttime.time(), gmtime()

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The clock starts at 2021. At power-up, and after every hard reset, the rp2 port starts the RTC at the first second of 2021. A Pico has no battery for its clock, so nothing earlier survives.
  2. The app starts. boot.py and main.py run. A timestamp taken now says 2021, because time.time() is the RTC's count of seconds since the start of 1970.
  3. The network first. The app joins Wi-Fi. Until it has, nothing can tell the board what time it is.
  4. Ask a time server. ntptime.settime() asks an NTP server, pool.ntp.org unless ntptime.host says otherwise, waits up to ntptime.timeout, 1 s, and writes the answer into the RTC in UTC. No answer raises OSError.
  5. Now trust it. From here time.time(), gmtime() and localtime() are right, and all three give UTC, because the board has no time zones.
  6. A soft reset. Ctrl-D or machine.soft_reset() leaves the RTC alone, and the boot files run again with the right time.
  7. A hard reset. machine.reset(), the reset button or a watchdog starts the clock at 2021 again, so the app must set it again before it trusts a timestamp.

A board’s time is right only after the last stage, and a hard reset sends it back to the first. So an app that stamps data keeps a flag that says whether the clock has been set since the start, and until it has, it marks its readings unsynchronised or holds them back.

Checkpoint

Check: The RTC, its tuple and the epoch

  1. Question 1 of 2 tm holds the current time in UTC. Which call sets a Pico's RTC to it?

    Read the code, then choose one answer.

    tm = time.gmtime()
    rtc = machine.RTC()
    Show the answer: Question 1 of 2

    Answer: rtc.datetime((tm[0], tm[1], tm[2], tm[6], tm[3], tm[4], tm[5], 0))

    In the model, rtc.datetime(tm) kept 15:30:04 for 08:15:30 UTC. Moving the weekday to position 3 and the time fields after it gives the right clock.

  2. Question 2 of 2 An ESP32's time.time() reads 853,316,130. What is the same moment in Unix time, in seconds since the start of 1970?

    Type a number.

    A hint

    The esp32 port counts from the start of 2000.

    Show the answer: Question 2 of 2

    Answer: 1800000930 s

    The esp32 port's epoch is the start of 2000, 946,684,800 s after the start of 1970, so the Unix time is 853,316,130 + 946,684,800 = 1,800,000,930, the reading of the lesson's example.

Store UTC on the device, and show local time only to people

A board has no time-zone database. On the rp2 and esp32 ports time.gmtime and time.localtime are one and the same function, so local time is whatever was written into the RTC. On boards with a network, the Pico W and the ESP32, the firmware carries ntptime from micropython-lib: ntptime.settime() asks an NTP: Network Time Protocol: a device asks a time server for the current time over UDP port 123. MicroPython's ntptime module uses it to set a board's RTC to UTC. server, pool.ntp.org unless ntptime.host names another, waits up to ntptime.timeout, 1 second, and writes UTC: Coordinated Universal Time, the reference time scale that servers and devices use to agree on a moment. Local times are fixed offsets from it, such as UTC+05:30 in India. into the RTC, because, as its source says, MicroPython has no time-zone support.

Keep it that way: store and send UTC, as seconds since the start of 1970 or as an RFC 3339 timestamp ending in Z, and convert to a local time only where a person reads it. This program, which runs unchanged on a board, does it for one reading, as a Pico and as an ESP32 count it:

One reading's time: counted, stored, sent and shown Python · utc/utc_ist.py
"""One reading's time, as a Pico and an ESP32 count it, as it is stored and sent, and as a person in India reads it.

The same code runs on a board: the time module there counts from 1970 (rp2) or from 2000 (esp32), and
time.gmtime(0)[0] says which.
"""
import time

IST = 5 * 3600 + 30 * 60  # India: UTC+05:30, all year
EPOCH_2000 = 946_684_800  # seconds from the start of 1970 to the start of 2000


def unix_time(board_secs, epoch_year):
    """Seconds since the start of 1970, from a board's time.time()."""
    return board_secs + (EPOCH_2000 if epoch_year == 2000 else 0)


def iso_utc(t):
    return "%04d-%02d-%02dT%02d:%02d:%02dZ" % tuple(time.gmtime(t)[:6])


def iso_ist(t):
    return "%04d-%02d-%02dT%02d:%02d:%02d+05:30" % tuple(time.gmtime(t + IST)[:6])


pico = 1_800_000_930  # time.time() on a Pico
esp32 = pico - EPOCH_2000  # the same moment on an ESP32
print("Pico time.time():  ", pico)
print("ESP32 time.time(): ", esp32)
print("both as Unix time: ", unix_time(pico, 1970), unix_time(esp32, 2000))
print("stored and sent:   ", iso_utc(pico))
print("shown in India:    ", iso_ist(pico))

# mpremote rtc --set copies the computer's local time into the RTC.
# On a laptop in India, the board's RTC then runs 5 h 30 min ahead of UTC.
wrong = pico + IST
print("after mpremote rtc --set in India:", iso_utc(wrong), "(%d s ahead)" % (wrong - pico))

Output

Pico time.time():   1800000930
ESP32 time.time():  853316130
both as Unix time:  1800000930 1800000930
stored and sent:    2027-01-15T08:15:30Z
shown in India:     2027-01-15T13:45:30+05:30
after mpremote rtc --set in India: 2027-01-15T13:45:30Z (19800 s ahead)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 utc_ist.py

Both counts are the same moment once the ESP32’s is moved from its epoch to 1970, and the stored form names its zone, Z. The last line is a trap of a tool you use every day: mpremote rtc --set copies the computer’s local clock into the board’s RTC, and the board, with no zones, takes it for UTC.

IST is UTC+05:30, all year

IST runs five and a half hours ahead of UTC throughout the year, and India has no daylight saving time, so showing a UTC time in India always adds 19,800 seconds. A board set with mpremote rtc --set from a laptop in India runs 19,800 seconds ahead of UTC, and a server that reads its timestamps as UTC places every reading five and a half hours in the future.

Follow that trap from the laptop to the server:

Interactive · message by message A board set from a laptop in India Step through one clock setting and one reading, from the laptop to the server.
A board set from a laptop in India, step 5 of 5. Messages so far: Laptop in India to Pico: 13:45:30; Pico to Server: 13:45:30Z.Laptop in Indiaits clock shows ISTPicono time zonesServerreads times as UTC13:45:3013:45:30Z

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Two clocks agree. The laptop's clock shows 13:45:30, India time. The same moment is 08:15:30 UTC.
  2. rtc --set. mpremote rtc --set reads the laptop's local time with Python's datetime.now() and writes 13:45:30 into the board's RTC, with no zone attached.
  3. The board believes it. The board has no time zones, so its RTC now says 13:45:30 UTC, and time.time() runs 19,800 s, five and a half hours, ahead.
  4. A reading goes out. The board stamps a reading with time.time() and sends it as 13:45:30Z, the Z meaning UTC.
  5. In the future. The server shows the reading at 19:15:30 India time, five and a half hours after it was taken.

The cure is to set the clock in UTC: ntptime.settime() on a board with a network, or, on a board without one, an mpremote exec that writes a UTC tuple, in the RTC’s order, into machine.RTC().datetime().

Timestamp Converter Turn a board's time.time() value into a date in UTC or IST, and back.

Key idea

Keep UTC wherever a machine reads the time, and convert to IST, or any other zone, only on the screen a person looks at.

Checkpoint

Check: UTC on the device, local time on screen

  1. Question 1 of 2 How many seconds do you add to a UTC timestamp to show it in India Standard Time?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 19800 s

    IST is UTC+05:30 all year: 5 × 3,600 + 30 × 60 = 19,800 seconds, with no daylight saving time to account for.

  2. Question 2 of 2 Your laptop's clock shows India time, and you run mpremote rtc --set. What does the board's RTC hold afterwards?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The laptop's local time, which the board takes for UTC, so its time.time() runs 19,800 s ahead

    The lesson's example shows such a board stamping 13:45:30Z for a reading taken at 08:15:30 UTC. Set the clock with ntptime.settime(), or write a UTC tuple yourself.

Practise: a periodic job on a wrapping clock

Write the helper a deadline loop asks on every pass: is the job due now? It keeps the slots on their grid whatever the loop does, skips and counts the slots a stall missed, and stays right when the counter wraps. The two tick functions sit at the top of the starter, copied from MicroPython’s arithmetic, and the sample tests drive a fake clock:

Exercise · Medium · Python

A periodic job that never drifts and never bursts

Write the Every class in every.py. A main loop asks it, as often as it likes, whether a job that repeats every period_ms milliseconds is due, so the loop never has to sleep through the time between two runs.

Every(period_ms, clock) reads the clock once and starts the first period there: clock() returns tick values in milliseconds, as time.ticks_ms() does on a board. due() returns True once for each slot that has come, where the slots lie exactly period_ms apart from that start, and False otherwise. A call made late still keeps the next slot on that grid, so the job never drifts. When several slots passed without a call, due() returns True once, skips the others and adds how many it skipped to missed, so a long stall never turns into a burst of runs.

Tick values wrap at TICKS_PERIOD, 2**30, as on a Pico: compare them only with the ticks_diff() and ticks_add() at the top of the file, which work as MicroPython's do. A period_ms below 1 raises ValueError. The sample tests drive a fake clock, and one of them starts 5 ms before the wrap.

Starter code · every.py

TICKS_PERIOD = 1 << 30  # ticks wrap here on 32-bit ports such as the Pico's


def ticks_add(ticks, delta):
    """MicroPython's time.ticks_add(): the tick value delta ms after ticks."""
    return (ticks + delta) & (TICKS_PERIOD - 1)


def ticks_diff(ticks1, ticks2):
    """MicroPython's time.ticks_diff(): ticks1 - ticks2, right across the wrap."""
    half = TICKS_PERIOD // 2
    return ((ticks1 - ticks2 + half) & (TICKS_PERIOD - 1)) - half


class Every:
    """Says when a job that repeats every period_ms is due, without drift and without bursts."""

    def __init__(self, period_ms, clock):
        self.period = period_ms
        self.clock = clock
        self.missed = 0

    def due(self):
        """True once for each slot that has come; slots that passed unseen are skipped and counted in missed."""
        return False
The sample tests · test_every.py
from every import TICKS_PERIOD, Every


class FakeClock:
    def __init__(self, t=0):
        self.t = t

    def __call__(self):
        return self.t


def calls(every, clock, times):
    seen = []
    for t in times:
        clock.t = t
        seen.append(every.due())
    return seen


def test_first_slot():
    """is due one period after it starts, and not a millisecond before"""
    clock = FakeClock(0)
    job = Every(1000, clock)
    assert calls(job, clock, [1, 999, 1000]) == [False, False, True]


def test_once_per_slot():
    """says True once per slot when the loop asks every millisecond"""
    clock = FakeClock(0)
    job = Every(1000, clock)
    times = [t for t in range(0, 3501) if calls(job, clock, [t])[0]]
    assert times == [1000, 2000, 3000]


def test_no_drift():
    """keeps the slots on the grid when a call comes late"""
    clock = FakeClock(0)
    job = Every(1000, clock)
    assert calls(job, clock, [1007, 1990, 2000, 2999, 3002]) == [True, False, True, False, True]


def test_skips_missed_slots():
    """runs once after a long stall, counts the skipped slots and keeps the grid"""
    clock = FakeClock(0)
    job = Every(1000, clock)
    assert calls(job, clock, [5400, 5400, 5999, 6000]) == [True, False, False, True]
    assert job.missed == 4


def test_across_the_wrap():
    """keeps time when the tick counter wraps to 0"""
    start = TICKS_PERIOD - 5
    clock = FakeClock(start)
    job = Every(10, clock)
    assert calls(job, clock, [TICKS_PERIOD - 1, 4, 5, 14, 15]) == [False, False, True, False, True]


def test_bad_period():
    """refuses a period below 1 ms"""
    for bad in (0, -10):
        try:
            Every(bad, FakeClock(0))
        except ValueError:
            continue
        raise AssertionError("Every(%r, clock) did not raise ValueError" % (bad,))
A hint

Keep the tick value of the next slot in the object. In due(), late = ticks_diff(clock(), self.next): a negative late means not yet. Otherwise late // period_ms slots after the next one have passed as well: count them in missed, and move self.next on by all of them plus one with ticks_add().

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Then a self-check on ticks, waits, drift, the RTC and UTC: it asks about your weakest idea first, and explains every answer as soon as you pick it:

Practice that adapts to you

Practise this lesson

12 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 12 A greenhouse controller sets a ticks_ms() deadline one hour ahead, then sleeps between checks for up to 7 days at a time. What can happen?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: After 7 days ticks_diff() can say the deadline is still more than 100 hours away, so the job is not run

    In the traced program the check after 7 days gave −131.3 hours, a deadline apparently in the future. A job that may wait for days takes its time from the RTC.

  2. Question 2 of 12 What does this print on a Pico?

    Read the code, then choose one answer.

    import time
    print(time.ticks_add(0, -1))
    Show the answer: Question 2 of 12

    Answer: 1073741823

    The counters example printed TICKS_MAX = 1073741823. The value is the same for ticks_ms(), ticks_us() and ticks_cpu(), which share one period.

  3. Question 3 of 12 An app writes time.ticks_ms() to a file every minute, and after a power cut works out how long the board was off with ticks_diff(now, saved). Why is the answer worthless?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: Ticks count from an arbitrary point, the board's start on a Pico, which the power cut moved

    Ticks belong to one run of the program. A board that must know how long it was off compares time.time() values from an RTC that kept running, or that was set again from NTP.

  4. Question 4 of 12 A battery-powered Pico must blink an LED every 500 ms and react to a button within 5 ms. How should it wait?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: In a deadline loop that checks the button and the blink deadline on every pass and rests with machine.idle()

    Of the four waits in the table, only the deadline loop with idle() keeps listening, rests the CPU and reacts within about a millisecond.

  5. Question 5 of 12 What does time.sleep_ms(0) do on a Pico?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: Gives pending scheduled callbacks their turn, then returns at once

    A long loop that never sleeps can call sleep_ms(0) on each pass to let scheduled callbacks run without waiting.

  6. Question 6 of 12 Which line, in place of the comment, keeps the readings on their slots without drift?

    Read the code, then choose one answer.

    due = time.ticks_ms()
    while True:
        read_and_send()
        ...  # the next deadline
        wait = time.ticks_diff(due, time.ticks_ms())
        if wait > 0:
            time.sleep_ms(wait)
    Show the answer: Question 6 of 12

    Answer: due = time.ticks_add(due, PERIOD)

    The drift example's deadline loop used this line and took all 8,640 readings of a day on their slots.

  7. Question 7 of 12 A job totals the pulses of a flow meter for every one-second slot and sends one total per slot. What should its deadline loop do after a pass that took 3.4 s?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: Run the three missed slots at once, each with its own total, then go on along the grid

    Skipping and catching up are both correct; the job decides. Decide it before the first slow pass, and count the passes that ran late.

  8. Question 8 of 12 A Pico's app set the RTC from NTP an hour ago. Then a watchdog resets the board. What does the RTC read when main.py starts again?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: The first second of 2021, until the app sets it again

    After a hard reset an app sets the clock again, from NTP or another source, before it trusts a timestamp.

  9. Question 9 of 12 A Pico is given rtc.datetime((2027, 1, 15, 0, 8, 15, 30, 0)), with weekday 0, Monday, for a date that is a Friday. What happens?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: The time is set and the weekday ignored, so reading it back gives weekday 4, Friday

    The model of the lesson reads back (2027, 1, 15, 4, 8, 15, 30, 0) after a setting like this one.

  10. Question 10 of 12 A Pico W in Chennai has run ntptime.settime(). What does time.localtime() return?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: The same as time.gmtime(), the time in UTC

    To show India time, add 19,800 s to the UTC seconds before you format them, as the example's iso_ist() does.

  11. Question 11 of 12 Boards in Pune and in Berlin send readings to one server, which draws charts for people in both cities. What should each reading's timestamp be?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: UTC, as Unix seconds or an RFC 3339 timestamp ending in Z, converted for each viewer when the chart is drawn

    Store and send UTC; convert at the screen. The example's iso_utc() and iso_ist() show both forms of one reading.

  12. Question 12 of 12 What does drift.py print for one day of the 10-second logger under its two loops?

    What does this program print? Choose one answer.

    """One day of a logger that takes a reading every 10 s, on a simulated millisecond clock.
    
    Each reading's work takes 30 to 50 ms (a seeded random draw, the same in every run). The first loop sleeps 10 s
    after the work, as time.sleep(10) would; the second waits for the next deadline, made from the last one.
    """
    import random
    
    from ticks import ticks_add, ticks_diff
    
    PERIOD = 10_000  # ms
    DAY = 24 * 60 * 60 * 1000  # ms
    
    
    def work_times():
        rng = random.Random(7)
        while True:
            yield rng.randint(30, 50)
    
    
    def sleep_after_work():
        now, starts, work = 0, [], work_times()
        while now < DAY:
            starts.append(now)
            now += next(work)  # the reading
            now += PERIOD  # time.sleep(10)
        return starts
    
    
    def deadline_loop():
        now, due, starts, work = 0, 0, [], work_times()
        while now < DAY:
            starts.append(now)
            now = ticks_add(now, next(work))  # the reading
            due = ticks_add(due, PERIOD)  # the next slot, from the last one
            wait = ticks_diff(due, now)
            if wait > 0:
                now = ticks_add(now, wait)  # time.sleep_ms(wait)
        return starts
    
    
    for name, loop in (("sleep(10) after the work", sleep_after_work), ("deadline from the last slot", deadline_loop)):
        starts = loop()
        last = len(starts) - 1
        behind = (starts[last] - last * PERIOD) / 1000
        print("%-28s %5d readings in 24 h, the last one %6.1f s after its slot" % (name, len(starts), behind))
    Show the answer: Question 12 of 12

    Answer: it prints

    sleep(10) after the work      8606 readings in 24 h, the last one  343.0 s after its slot
    deadline from the last slot   8640 readings in 24 h, the last one    0.0 s after its slot

    The sleeping loop took 8,606 readings and ended 343.0 s behind; the deadline loop took 8,640, the last on its slot.

Interview questions

Warm-up (fresher): What is the difference between time.ticks_ms() and time.time() on a microcontroller?

ticks_ms() is a millisecond counter with no fixed starting point: on a Pico it counts from the moment the board started, and on 32-bit ports it wraps to 0 every 2³⁰ ms, about 12.4 days. It is for intervals and deadlines, compared only with ticks_diff() and moved only with ticks_add(). time.time() is whole seconds since the port’s epoch, the start of 1970 on rp2 and of 2000 on esp32, read from the RTC. It is for timestamps, and it is right only once something has set the RTC, for example ntptime.settime(), because a Pico’s RTC starts at the beginning of 2021 after every power-up. Use ticks inside one run of the program and time.time(), in UTC, for anything stored or sent.

Two longer interview questions on time start from this lesson:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    Sensor boards in a greenhouse send a reading every minute. After 30 days the newest readings from the Pico boards are 26 minutes behind the server's clock, and every reading from the ESP32 boards appears about 30 years in the past. Explain both faults and how you would fix them.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A Pico W opens an irrigation valve at 06:00 India time every day. Design how the board knows the time, from power-up to the daily trigger, and say what it does when the network is down or the clock has just been corrected.

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. For how long is ticks_diff() right?

    While the two readings are less than half a period apart: 6.21 days for ticks_ms() and 8.95 minutes for ticks_us() on 32-bit ports.

  2. Can a tick value saved before a reset be compared after it?

    No: after a hard reset a Pico's ticks_ms() counts from 0 again. Use ticks within one run of the program, and store time.time() for anything that must outlive a reset.

  3. How long does time.sleep(0.0004) wait on a Pico?

    No time at all: sleep() keeps only whole milliseconds, so 0.4 ms becomes 0. sleep_us(400) does wait, as a busy-wait that runs no scheduled callbacks.

  4. How does a periodic job avoid drift?

    It makes each deadline from the last one, due = ticks_add(due, PERIOD), and sleeps only for what is left, ticks_diff(due, now). Sleeping a whole period after the work adds the work's time to every pass.

  5. In what order does RTC().datetime() take and give its fields?

    (year, month, day, weekday, hours, minutes, seconds, subseconds), with weekday 0 for Monday. time.gmtime() puts the hour where the RTC expects the weekday, so passing its tuple sets the clock wrong without a word.

  6. What time does a Pico's RTC show after power-up?

    The first second of 2021: with no battery, it starts there again at each power-up and each hard reset, so the board's timestamps are years out until something sets the clock.

  7. Which epochs do the rp2 and esp32 ports count from?

    rp2 from the start of 1970, as Unix and CPython do; esp32 from the start of 2000, so the same moment is 946,684,800 seconds smaller. time.gmtime(0)[0] tells a program which.

  8. Why keep a board's clock in UTC, and what does mpremote rtc --set do?

    Boards have no time zones, and ntptime.settime() writes UTC. mpremote rtc --set copies the computer's local clock, so a laptop in India sets the board 19,800 seconds ahead of UTC.

Key takeaways

  • The tick counters time intervals: compare two readings with ticks_diff() and make deadlines with ticks_add(), and check a deadline well within half the period, 6.21 days for ticks_ms() and 8.95 minutes for ticks_us().
  • sleep_ms() rests and gives scheduled callbacks their turn but holds the loop; sleep_us() spins for waits up to 1 ms; a deadline loop with machine.idle() waits and keeps listening.
  • A periodic job made from its last deadline never drifts, while sleeping for the period after the work drifted 343 seconds in a day, and a deadline loop decides in advance whether it skips missed slots or catches up.
  • RTC().datetime() puts the weekday in position 3, unlike time.gmtime(), a Pico’s RTC starts at 2021 after every hard reset, and an ESP32’s epoch is 2000, 946,684,800 seconds after the start of 1970.
  • Boards have no time zones: set the RTC to UTC with ntptime.settime(), store and send UTC, and add 19,800 seconds only to show a time in India.

Terms in this lesson

busy-wait
A wait in which the processor runs a tight loop that checks the time until it is up, doing nothing else meanwhile. On a Pico, time.sleep_us() waits this way for delays of up to 1,000 microseconds.
drift
A timing error that grows with every repetition, as when a loop sleeps for its whole period after its work, so that each run starts a little later than the one before.
epoch
The moment from which a clock counts seconds: the start of 1970 on the rp2 and unix ports and in CPython, the start of 2000 on esp32 and other embedded ports. time.gmtime(0)[0] gives its year.
NTP
Network Time Protocol: a device asks a time server for the current time over UDP port 123. MicroPython's ntptime module uses it to set a board's RTC to UTC.
RTC
Real-time clock: the part of a microcontroller that keeps the calendar date and time, read and set with machine.RTC().datetime(). A Pico has no battery for it, so it starts again from a fixed date at every power-up.
UTC
Coordinated Universal Time, the reference time scale that servers and devices use to agree on a moment. Local times are fixed offsets from it, such as UTC+05:30 in India.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.