Skip to content

soc_esp32::poll

Busy-wait on a hardware condition, with a bound, spelled once.

A dozen drivers wrote the same loop:

let mut spins = 0;
while reg::read(r) & DONE == 0 {
spins += 1;
if spins > SPINS { return Err(..Timeout); }
core::hint::spin_loop();
}

Individually fine; together, twelve places to get the polarity or the bound wrong. [until] is that loop, once, returning [Timeout] the driver maps into its own error.

[until_us] bounds the wait by wall-clock time and is what a driver should reach for. [until]’s max_spins is a raw loop count, so its real duration depends on the CPU clock — the same number means one thing at 80 MHz and a third of it at 240 MHz, and nothing at all on another core. It remains for the loops that run before the clock is installed, or with the instruction cache off (a flash erase), where the microsecond clock cannot be read.

The clock the microsecond bound reads is installed by the kernel at boot: its timer is a Layer-1 driver a Layer-0 SoC crate may not depend on, so the kernel hands the reader down rather than this crate reaching up for it — the same shape as esp32_flash::set_interrupt_hooks.

struct Deadline { /* private fields */ }

A microsecond deadline for a hand-rolled poll loop whose several exit branches [until_us]’s single condition cannot express — an I2C wait that must tell a NAK from an arbitration loss from a timeout, say.

Construct it once before the loop, then call expired once per iteration where a spin counter used to be checked. Before the clock is installed it falls back to the same [DEFAULT_SPINS] budget [until] uses, so a loop that races boot still terminates.

struct Timeout;

A poll gave up before its condition held.

Implements: Clone, Copy, Debug, Eq, PartialEq, StructuralPartialEq

fn set_clock(now_us: fn(..))

Install the monotonic microsecond clock the [until_us] bound reads.

Called once from boot, before any driver polls. Passing a null-equivalent is impossible: a fn pointer is never zero, so the “not installed” sentinel can never collide with a real reader.

fn until<impl FnMut() -> bool>(ready: impl FnMut, max_spins: u32) -> Result<(), Timeout>

Spin until ready() returns true, or max_spins iterations pass.

Returns [Timeout] on expiry. ready is polled at least once. Prefer [until_us] unless the wait runs before the clock is installed or with the cache off.

References: Timeout

fn until_us<impl FnMut() -> bool>(timeout_us: u64, ready: impl FnMut) -> Result<(), Timeout>

Spin until ready() returns true, or timeout_us microseconds pass — a bound that means the same wall-clock time at any CPU frequency, unlike [until].

ready is polled at least once. Before the kernel installs a clock (early boot) it falls back to [until] with [DEFAULT_SPINS], so a poll that races boot still terminates.

References: Timeout

const DEFAULT_SPINS: u32;

A generous default iteration bound for [until]. A 12-bit ADC conversion or a short frame is microseconds; this absorbs interrupts and still fails dead hardware.

const DEFAULT_TIMEOUT_US: u64;

A generous default timeout for [until_us], in microseconds. Absorbs an interrupt storm and still fails dead hardware quickly, at any CPU frequency.