User Guide¶
Overview¶
chumicro-timing provides two things:
- Tick helpers —
ticks_ms(),ticks_diff(), andticks_add()that handle counter wraparound correctly across all three Python runtimes. - Heartbeat — a periodic timer that tells you when a time interval has elapsed, without blocking.
These are the building blocks for non-blocking timing on microcontrollers. Instead of calling time.sleep() (which blocks everything), you capture a timestamp once per loop and check heartbeat.poll(now) for each component.
Getting started¶
Basic heartbeat¶
The most common pattern is a periodic action in a main loop:
from chumicro_timing import Heartbeat, ticks_ms
led_heartbeat = Heartbeat(period_ms=500)
while True:
now = ticks_ms()
if led_heartbeat.poll(now):
# This runs twice per second
toggle_led()
poll(now_ms) returns True once per elapsed period and advances the internal timer. Calling it again with the same timestamp returns False until the next period elapses.
Shared timestamps¶
Always capture ticks_ms() once per loop iteration and pass the same value to every component. This prevents drift between independent clock reads:
from chumicro_timing import Heartbeat, ticks_ms
fast = Heartbeat(period_ms=100) # 10 Hz
slow = Heartbeat(period_ms=5000) # every 5 seconds
while True:
now = ticks_ms() # ONE reading per iteration
if fast.poll(now):
read_sensor()
if slow.poll(now):
send_report()
On a slow microcontroller, calling ticks_ms() separately for each component would return slightly different values. A heartbeat that should fire at the same moment as another might not. Sharing the timestamp eliminates this class of bug.
Checking without consuming¶
is_due(now_ms) tells you whether the period has elapsed without advancing the timer. This is useful when you need to check timing state without committing to an action:
now = ticks_ms()
if heartbeat.is_due(now):
# Period has elapsed, but the timer hasn't been reset yet.
# Calling is_due(now) again will still return True.
pass
Call poll(now) when you're ready to consume the beat and start the next period.
Resetting¶
reset(now_ms) restarts the timer from the given timestamp:
now = ticks_ms()
heartbeat.reset(now)
# The next beat is now period_ms from this moment,
# regardless of when the last beat was.
Using ticks directly¶
For custom timing logic that doesn't fit the heartbeat pattern, use the tick functions directly:
from chumicro_timing import ticks_ms, ticks_diff, ticks_add
# Record a timestamp
start = ticks_ms()
# ... do work ...
# Check elapsed time (handles wraparound correctly)
elapsed = ticks_diff(ticks_ms(), start)
# Compute a deadline
deadline = ticks_add(start, 3000) # 3 seconds from start
Important: Do not use plain subtraction (end - start) on tick values. The counter wraps every ~6.2 days, and plain subtraction gives wrong results near the boundary. Always use ticks_diff().
Wraparound details¶
The tick counter uses a 2²⁹ ms period (~6.2 days). This keeps all arithmetic within small integers, avoiding heap-allocated big integers on boards without big-int support.
ticks_diff() is correct as long as the two timestamps are no more than ~3.1 days apart (half the period). For any practical embedded timing, this is more than sufficient.
ticks_add() rejects deltas at or beyond the half-period (±2²⁸ ms) with an OverflowError.
Platform behavior¶
The tick source is selected automatically at import time:
| Priority | Source | Runtime |
|---|---|---|
| 1 | supervisor.ticks_ms |
CircuitPython 7+ |
| 2 | time.ticks_ms |
MicroPython, some CircuitPython builds |
| 3 | time.monotonic_ns |
CPython, some CircuitPython boards |
| 4 | time.monotonic |
Final fallback (float seconds → int ms) |
All sources are masked to the 2²⁹ period, so behavior is identical regardless of which source is used.
Using with Runner¶
For applications with many components, chumicro-runner provides a Runner that captures the timestamp for you and services active components. See the chumicro-runner guide for details.
Integration with a tick-based scheduler¶
Heartbeat is designed to be polled from a main loop or tick-based scheduler — it never blocks. A typical pattern:
from chumicro_timing import Heartbeat, ticks_ms
heartbeat = Heartbeat(period_ms=1000)
def on_tick() -> None:
"""Called once per scheduler tick."""
now = ticks_ms()
if heartbeat.poll(now):
do_periodic_work()
Examples¶
The examples directory contains complete runnable scripts:
| Example | What it shows |
|---|---|
heartbeat_blink.py |
Basic heartbeat in a main loop (the embedded hello world) |
multiple_heartbeats.py |
Several heartbeats at different rates sharing one timestamp |
timeout_check.py |
Using ticks_diff() for deadline-based timeout detection |
debounce.py |
Button debounce using ticks_ms() and ticks_diff() |
periodic_tick.py |
Manual periodic action — what Heartbeat does under the hood |
circuitpython_blink.py |
LED blink on CircuitPython hardware |
circuitpython_debounce.py |
Button debounce on CircuitPython hardware |
micropython_blink.py |
LED blink on MicroPython hardware |
micropython_debounce.py |
Button debounce on MicroPython hardware |
Simulated examples run on CPython. Hardware examples (circuitpython_* / micropython_*) require a real board — see the setup notes in each file.
What's new¶
- 0.1.24: Documentation sync and CI build performance improvements.