rivet/timer.rs
1//! Fixed-size timer queue backing [`crate::time::Sleep`].
2//!
3//! A task calling `Sleep::<MICROS>::new().await` registers a deadline here
4//! instead of busy-polling — the arch timer ISR (`riscv::timer_tick` /
5//! `cortex_m::systick_handler`) calls [`poll_timers`] on every tick, which
6//! wakes any task whose deadline has passed. This is what makes
7//! `port::arch::idle()` (WFI) a real power-saving wait instead of a spin loop:
8//! between ticks, no task is marked ready, so the executor actually sleeps.
9//!
10//! Slots are `u64`-deadline `UnsafeCell`s guarded by [`crate::critical`]
11//! rather than atomics, since RV32 has no native 64-bit atomic ops (even
12//! with the `A` extension, which is 32-bit/pointer-width only).
13
14use core::cell::UnsafeCell;
15
16/// Maximum number of outstanding timers (RIVET_MAX_TIMERS; one per task
17/// blocked in `Sleep`, so this should be >= `MAX_TASKS` if every task might
18/// sleep concurrently).
19pub const MAX_TIMERS: usize = crate::config::MAX_TIMERS;
20
21struct TimerSlot {
22 /// Deadline in microseconds. 0 = slot unused.
23 deadline: UnsafeCell<u64>,
24 task: UnsafeCell<crate::task::TaskId>,
25}
26
27// Safety: all access goes through `critical::enter`, which disables
28// interrupts (single-core), so there is no concurrent access.
29unsafe impl Sync for TimerSlot {}
30
31// Inline const avoids a named `const` item with interior mutability
32// (clippy::declare_interior_mutable_const).
33static TIMER_SLOTS: [TimerSlot; MAX_TIMERS] = [const {
34 TimerSlot {
35 deadline: UnsafeCell::new(0),
36 task: UnsafeCell::new(crate::task::TaskId::new(0, 0)),
37 }
38}; MAX_TIMERS];
39
40/// Handle to a registered timer slot; carries the slot index and the
41/// registered deadline so a stale [`cancel_deadline`] (after the slot was
42/// already freed and reused) is a harmless no-op (plan.md [B7]).
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct TimerHandle {
45 slot: u8,
46 deadline: u64,
47}
48
49/// Register a wake-up for `(priority, index)` at `deadline_us`.
50/// Called by `Sleep::poll` on first poll.
51///
52/// Returns a handle that can be used to cancel the registration (plan.md
53/// [B7]: a dropped `Sleep` must not leak its slot), or
54/// [`TimerQueueFull`] when every slot is in use.
55///
56/// Exposed beyond `pub(crate)` only under `feature = "test-support"`, so
57/// the property tests (plan.md §1.4) can drive the queue directly.
58#[cfg(not(feature = "test-support"))]
59pub(crate) fn register_deadline(
60 deadline_us: u64,
61 task: crate::task::TaskId,
62) -> Result<TimerHandle, TimerQueueFull> {
63 register_deadline_impl(deadline_us, task)
64}
65
66/// See [`register_deadline`].
67#[cfg(feature = "test-support")]
68pub fn register_deadline(
69 deadline_us: u64,
70 task: crate::task::TaskId,
71) -> Result<TimerHandle, TimerQueueFull> {
72 register_deadline_impl(deadline_us, task)
73}
74
75fn register_deadline_impl(
76 deadline_us: u64,
77 task: crate::task::TaskId,
78) -> Result<TimerHandle, TimerQueueFull> {
79 crate::critical::enter(|| {
80 for (i, slot) in TIMER_SLOTS.iter().enumerate() {
81 // SAFETY: all access to TIMER_SLOTS goes through
82 // `critical::enter` (interrupts disabled on single-core
83 // targets), so no concurrent access is possible.
84 unsafe {
85 if *slot.deadline.get() == 0 {
86 *slot.task.get() = task;
87 *slot.deadline.get() = deadline_us;
88 return Ok(TimerHandle {
89 slot: i as u8,
90 deadline: deadline_us,
91 });
92 }
93 }
94 }
95 Err(TimerQueueFull)
96 })
97}
98
99/// Cancel a registered deadline. Safe to call with a stale handle: the
100/// slot is only cleared if its deadline still matches the handle's
101/// (plan.md [B7] — a cancelled `Sleep` must not free a *new* registration
102/// that reused its slot).
103pub(crate) fn cancel_deadline(handle: TimerHandle) {
104 if let Some(slot) = TIMER_SLOTS.get(handle.slot as usize) {
105 crate::critical::enter(|| {
106 // SAFETY: guarded by critical::enter (see register).
107 unsafe {
108 if *slot.deadline.get() == handle.deadline {
109 *slot.deadline.get() = 0;
110 }
111 }
112 });
113 }
114}
115
116/// Scan for expired timers and wake their tasks. Call from the platform
117/// timer ISR on every tick.
118pub fn poll_timers(now_us: u64) {
119 let mut woke_any = false;
120 crate::critical::enter(|| {
121 for slot in &TIMER_SLOTS {
122 // SAFETY: all access to TIMER_SLOTS goes through
123 // `critical::enter` (interrupts disabled on single-core
124 // targets), so no concurrent access is possible.
125 unsafe {
126 let d = *slot.deadline.get();
127 if d != 0 && now_us >= d {
128 *slot.deadline.get() = 0;
129 crate::waker::mark_ready(*slot.task.get());
130 woke_any = true;
131 }
132 }
133 }
134 });
135 // plan.md Phase 24, found on real dual-core hardware: only one hart
136 // ever calls this function (whichever owns the periodic tick — see
137 // every board's own `tick_start` docs), but the task a cooperative
138 // waker just marked ready could be hosted by the async executor
139 // running on any hart, including one that's currently idling
140 // (`waiti`/`wfi`) waiting for exactly this kind of news. `mark_ready`
141 // itself only flips bitmap flags; nothing else here was telling that
142 // *other* hart to wake up and look — on real dual-core hardware, an
143 // executor task idling on the non-tick-owning core would never
144 // notice its `Sleep` had expired, waiting in `waiti` forever even
145 // though the work was genuinely ready (confirmed: `smp_test.rs`'s
146 // monitor task hung exactly this way; a single-core `Sleep` test,
147 // where the tick-owning hart and the only hart are trivially the
148 // same one, passed cleanly, isolating this as *specifically* the
149 // missing piece). No per-task hart affinity is tracked, so this
150 // broadcasts to every other hart rather than targeting one — a
151 // spurious wake on a hart with nothing to do is a cheap, harmless
152 // no-op (it just re-checks and goes back to idling); a real wake
153 // that's never delivered is not.
154 if woke_any {
155 let hart = crate::port::arch::hart_id();
156 for other in 0..crate::config::MAX_HARTS {
157 if other != hart {
158 crate::port::arch::request_reschedule_on(other);
159 }
160 }
161 }
162 poll_ptask_deadlines(now_us);
163}
164
165// ── Preemptive-task block-with-timeout deadlines ────────────────────
166//
167// Backs `PriorityMutex::lock_timeout` (and later `Semaphore`/`Channel`
168// timeouts): one deadline slot per preemptive task id (indexed by id), so
169// a blocked task is unblocked by the tick when its deadline passes. The
170// cooperative tier has its own wake mechanism (the waker bitmap); this is
171// the preemptive-tier analog, keyed by task id and unblocking via
172// `sched::unblock`.
173
174/// Deadline slots, indexed by task id (0 = no deadline registered).
175struct PtaskDeadline {
176 deadline: UnsafeCell<u64>,
177}
178
179// SAFETY: all access goes through `critical::enter` (interrupts disabled,
180// single-core), so there is no concurrent access.
181unsafe impl Sync for PtaskDeadline {}
182
183// Inline const avoids a named `const` item with interior mutability
184// (clippy::declare_interior_mutable_const).
185static PTASK_DEADLINES: [PtaskDeadline; crate::preempt::tcb::MAX_PTASKS] = [const {
186 PtaskDeadline {
187 deadline: UnsafeCell::new(0),
188 }
189};
190 crate::preempt::tcb::MAX_PTASKS];
191
192/// Register a wake-up deadline for a blocked preemptive task. Replaces any
193/// previous registration for the same task.
194pub(crate) fn register_ptask_deadline(deadline_us: u64, task: usize) -> Result<(), TimerQueueFull> {
195 let Some(slot) = PTASK_DEADLINES.get(task) else {
196 return Err(TimerQueueFull);
197 };
198 crate::critical::enter(|| {
199 // SAFETY: guarded by critical::enter; single writer per task slot
200 // (the blocking task), reader is the tick ISR.
201 unsafe {
202 *slot.deadline.get() = deadline_us;
203 }
204 });
205 Ok(())
206}
207
208/// Cancel a preemptive task's block deadline (e.g. it acquired the
209/// resource before the deadline). No-op if none registered.
210pub(crate) fn cancel_ptask_deadline(task: usize) {
211 if let Some(slot) = PTASK_DEADLINES.get(task) {
212 crate::critical::enter(|| {
213 // SAFETY: guarded by critical::enter (see register).
214 unsafe {
215 *slot.deadline.get() = 0;
216 }
217 });
218 }
219}
220
221/// Wake preemptive tasks whose block deadline has passed.
222fn poll_ptask_deadlines(now_us: u64) {
223 let mut woke_any = false;
224 crate::critical::enter(|| {
225 for (task, slot) in PTASK_DEADLINES.iter().enumerate() {
226 // SAFETY: guarded by critical::enter (see register).
227 unsafe {
228 let d = *slot.deadline.get();
229 if d != 0 && now_us >= d {
230 *slot.deadline.get() = 0;
231 crate::preempt::sched::unblock(task);
232 woke_any = true;
233 }
234 }
235 }
236 });
237 // Same reasoning, same fix as `poll_timers`'s identical broadcast
238 // above (plan.md Phase 24) — a preemptive task's own blocking
239 // timeout (`PriorityMutex::lock_timeout` etc.) can unblock a task
240 // that's not "current" on this hart at all.
241 if woke_any {
242 let hart = crate::port::arch::hart_id();
243 for other in 0..crate::config::MAX_HARTS {
244 if other != hart {
245 crate::port::arch::request_reschedule_on(other);
246 }
247 }
248 }
249}
250
251/// Queue-full error returned by timer registration APIs (plan.md §4.3).
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253pub struct TimerQueueFull;
254
255/// Test-only: clear every timer slot. Part of the global reset done by
256/// [`crate::kernel_test!`].
257#[cfg(feature = "test-support")]
258pub(crate) fn reset_for_test() {
259 crate::critical::enter(|| {
260 for slot in &TIMER_SLOTS {
261 // SAFETY: all access to TIMER_SLOTS goes through
262 // `critical::enter` (interrupts disabled, single-core), so no
263 // concurrent access is possible here.
264 unsafe {
265 *slot.deadline.get() = 0;
266 }
267 }
268 for slot in &PTASK_DEADLINES {
269 // SAFETY: same guard as above.
270 unsafe {
271 *slot.deadline.get() = 0;
272 }
273 }
274 });
275}
276
277/// Count of timer slots currently in use. Used by host-test reset/
278/// inspection helpers and by [`crate::report`].
279pub fn slots_in_use() -> usize {
280 crate::critical::enter(|| {
281 TIMER_SLOTS
282 .iter()
283 // SAFETY: all access to TIMER_SLOTS goes through
284 // `critical::enter` (interrupts disabled), so reads are
285 // exclusive.
286 .filter(|slot| unsafe { *slot.deadline.get() != 0 })
287 .count()
288 })
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn register_and_expire() {
297 crate::kernel_test! {
298 register_deadline(1000, crate::task::TaskId::new(3, 2)).unwrap();
299 poll_timers(500); // not yet
300 assert_eq!(crate::waker::next_ready(), None);
301
302 poll_timers(1000); // now expired
303 assert_eq!(crate::waker::next_ready(), Some(crate::task::TaskId::new(3, 2)));
304 }
305 }
306
307 #[test]
308 fn multiple_timers_independent() {
309 crate::kernel_test! {
310 register_deadline(100, crate::task::TaskId::new(1, 0)).unwrap();
311 register_deadline(200, crate::task::TaskId::new(2, 0)).unwrap();
312
313 poll_timers(150);
314 assert_eq!(crate::waker::next_ready(), Some(crate::task::TaskId::new(1, 0)));
315 assert_eq!(crate::waker::next_ready(), None);
316
317 poll_timers(250);
318 assert_eq!(crate::waker::next_ready(), Some(crate::task::TaskId::new(2, 0)));
319 }
320 }
321}