kernel::alarm
A one-shot alarm at an absolute microsecond deadline.
[crate::clock] answers “what time is it”. This answers “wake me at”. They
are different questions and want different hardware: a free-running counter
for the first, a programmable compare and an interrupt for the second.
Who wants it
Section titled “Who wants it”The radio blobs. Espressif’s Wi-Fi driver arms ETSTimers in
microseconds — channel dwell, frame timing, retry windows — and gets them
back through five entries in wifi_osi_funcs_t. The first implementation
of those served them from a task that woke every scheduler tick and polled,
which made a 100 µs timer fire up to ten times late. All three references
use an alarm instead: esp-idf’s esp_timer programs the LAC timer’s
compare register and dispatches from its interrupt, NuttX reaches the same
code, and Arduino ships esp-idf unchanged.
Which timer, and why the kernel owns it
Section titled “Which timer, and why the kernel owns it”TG0’s LAC counter. It is a fifth counter inside timer group 0, separate
from the four general-purpose timers — which matters, because all four are
spoken for: TIMG1/T1 is [crate::clock], and TIMG0/T0, TIMG0/T1 and
TIMG1/T0 drive on-target self-tests. esp-idf picked the same counter for
the same reason.
It lives here rather than in the crate that wanted it because of the layer
rules, exactly as [crate::clock] does: radio/* may name kernel,
hal, soc/* and lib/* but not drivers/physical/*, so the radio
adapter cannot reach TIMG at all.
The callback runs in trap context
Section titled “The callback runs in trap context”[set_handler] takes a fn() and calls it from the interrupt. It must not
block and must not take a lock a task could be holding — the usual top-half
contract. The radio’s use of it is to signal a service task, which is what
esp-idf’s timer_alarm_isr does too: it clears the interrupt and notifies.
The handler is registered as IRAM-safe, so it keeps running while a
flash operation has the cache off. That is a promise about the handler, not
something this module can check, and it is why the handler is a fn()
rather than a closure: whatever is behind it has to be in IRAM.
Functions
Section titled “Functions”cancel
Section titled “cancel”fn cancel()Cancel a pending alarm. The counter keeps running.
is_available
Section titled “is_available”fn is_available() -> boolWhether init succeeded.
set_after_us
Section titled “set_after_us”fn set_after_us(after_us: u64)Fire the alarm after_us microseconds from now.
Replaces any alarm already set — there is one compare register, so this is
a single deadline rather than a queue. The caller keeps the queue and arms
the earliest, which is what radio_esp32::ets_timer does.
A delay of zero fires as soon as the hardware can, not never.
set_handler
Section titled “set_handler”fn set_handler(f: Option<fn(..)>) -> Option<fn(..)>Install what the alarm calls. Returns the previous handler.
The handler runs in trap context. See the module docs.
fn wait(timeout_ms: u32) -> boolBlock until the alarm fires or timeout_ms passes. true if woken.
The timeout is a backstop, not the mechanism: an alarm that is missed makes the caller late rather than never, and on a host — where there is no LAC timer — it is the only thing that returns at all.
One waiter. A second task calling this would be woken by the same alarm and there is nothing here to say which; the intended caller is a single service task.