Chapter 15: embassy-time
September 17, 2026 · View on GitHub
Introduction
The blink driver's heartbeat is Timer::after_millis(controller.delay_ms()),
and the button driver samples on Timer::after_millis(DEBOUNCE_DELAY_MS). Both
calls suspend the task for a measured interval without consuming the core. This
chapter opens embassy-time: the time driver beneath it, the Instant and
Duration types that express times, and the Timer future that waits on them.
The Time Driver
embassy-time itself knows no hardware. It is a portable API backed by a
time driver that embassy-rp provides when the time-driver feature is
enabled (Chapter 8). The driver owns the hardware timer and answers one question:
what time is it now?
+------------------+ +--------------------+
| embassy-time | --> | embassy-rp driver |
| generic API | | (RTC + timer IRQ) |
+------------------+ +--------------------+
The driver ticks in microseconds. Timing resolution of a few microseconds is far finer than our 500 ms blink period — but the same driver can schedule a byte-timeout of 100 us or a watchdog of 5 seconds without changing anything in application code.
Instant and Duration
Two types carry the units:
Instant— A point in time on the driver's timeline. Created byInstant::now(); comparable and subtractable.Duration— A span of time. Built from named constructors:Duration::from_micros,::from_millis,::from_secs, and friends.
Instant minus Instant gives a Duration — the amount of time that passed:
let start = Instant::now();
read_sensor();
let elapsed = start.elapsed(); // Duration
if elapsed < Duration::from_millis(10) {
// still inside the window
}
Instant/Duration are Copy, so they pass around freely in our controller
structs — a pattern you can use to add timeouts to any driver.
Timer
Timer is the wait primitive. Its two most common forms:
Timer::after_millis(500).await; // wait 500 ms from now
Timer::after(Duration::from_secs(1)).await; // wait 1 second from now
Timer::after(...) and Timer::at(instant) hang a future off the ticking
clock. Polling the future returns Pending until the deadline passes, then
Ready(()). While it is Pending the executor runs other tasks — the exact
cooperative behavior of Chapter 12:
Task: set_led_high -> await Timer 500ms -> set_led_low ...
| |
v v
Executor: other work or wfi (idle)
after_millis in Our Drivers
The blink loop's timing comes straight from its own state — the delay_ms
field of LedController:
loop {
let state = controller.toggle();
if led_state_to_level(state) {
led.set_high();
} else {
led.set_low();
}
Timer::after_millis(controller.delay_ms()).await;
}
Every iteration: toggle the state, drive the pin, and sleep for the configured
interval. Because the delay is a method, the timing policy lives inside the
controller; main just reads it out.
The button driver samples on its own interval:
Timer::after_millis(DEBOUNCE_DELAY_MS).await;
Five milliseconds apart, the loop feeds fresh pin samples into the debounce
filter (Chapters 22–23). Both loops do the same thing: .await on a Timer
turns "wait" into "yield," and the executor proves it by running the leftover
time on useful work or nothing at all.
Summary
embassy-timeis a portable API backed by the RP2350 time driver (time-driverfeature).Instantis a point in time;Durationis a span; both areCopy.Timer::after_*returns a future that becomesReadywhen the deadline passes.- Waiting on a
Timeryields the core — the cooperative heart of every driver. - The blink and button loops both derive their period from configuration constants read at runtime.
Next: the HAL. How embassy_rp::init hands us the whole processor, safely.