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 crate::waker::broadcast_reschedule();
156 }
157 poll_ptask_deadlines(now_us);
158}
159
160// ── Preemptive-task block-with-timeout deadlines ────────────────────
161//
162// Backs `PriorityMutex::lock_timeout` (and later `Semaphore`/`Channel`
163// timeouts): one deadline slot per preemptive task id (indexed by id), so
164// a blocked task is unblocked by the tick when its deadline passes. The
165// cooperative tier has its own wake mechanism (the waker bitmap); this is
166// the preemptive-tier analog, keyed by task id and unblocking via
167// `sched::unblock`.
168
169/// Deadline slots, indexed by task id (0 = no deadline registered).
170struct PtaskDeadline {
171 deadline: UnsafeCell<u64>,
172}
173
174// SAFETY: all access goes through `critical::enter` (interrupts disabled,
175// single-core), so there is no concurrent access.
176unsafe impl Sync for PtaskDeadline {}
177
178// Inline const avoids a named `const` item with interior mutability
179// (clippy::declare_interior_mutable_const).
180static PTASK_DEADLINES: [PtaskDeadline; crate::preempt::tcb::MAX_PTASKS] = [const {
181 PtaskDeadline {
182 deadline: UnsafeCell::new(0),
183 }
184};
185 crate::preempt::tcb::MAX_PTASKS];
186
187/// Register a wake-up deadline for a blocked preemptive task. Replaces any
188/// previous registration for the same task.
189pub(crate) fn register_ptask_deadline(deadline_us: u64, task: usize) -> Result<(), TimerQueueFull> {
190 let Some(slot) = PTASK_DEADLINES.get(task) else {
191 return Err(TimerQueueFull);
192 };
193 crate::critical::enter(|| {
194 // SAFETY: guarded by critical::enter; single writer per task slot
195 // (the blocking task), reader is the tick ISR.
196 unsafe {
197 *slot.deadline.get() = deadline_us;
198 }
199 });
200 Ok(())
201}
202
203/// Cancel a preemptive task's block deadline (e.g. it acquired the
204/// resource before the deadline). No-op if none registered.
205pub(crate) fn cancel_ptask_deadline(task: usize) {
206 if let Some(slot) = PTASK_DEADLINES.get(task) {
207 crate::critical::enter(|| {
208 // SAFETY: guarded by critical::enter (see register).
209 unsafe {
210 *slot.deadline.get() = 0;
211 }
212 });
213 }
214}
215
216/// Wake preemptive tasks whose block deadline has passed.
217fn poll_ptask_deadlines(now_us: u64) {
218 let mut woke_any = false;
219 crate::critical::enter(|| {
220 for (task, slot) in PTASK_DEADLINES.iter().enumerate() {
221 // SAFETY: guarded by critical::enter (see register).
222 unsafe {
223 let d = *slot.deadline.get();
224 if d != 0 && now_us >= d {
225 *slot.deadline.get() = 0;
226 crate::preempt::sched::unblock(task);
227 woke_any = true;
228 }
229 }
230 }
231 });
232 // Same reasoning, same fix as `poll_timers`'s identical broadcast
233 // above (plan.md Phase 24) — a preemptive task's own blocking
234 // timeout (`PriorityMutex::lock_timeout` etc.) can unblock a task
235 // that's not "current" on this hart at all.
236 if woke_any {
237 let hart = crate::port::arch::hart_id();
238 for other in 0..crate::config::MAX_HARTS {
239 if other != hart {
240 crate::port::arch::request_reschedule_on(other);
241 }
242 }
243 }
244}
245
246/// Queue-full error returned by timer registration APIs (plan.md §4.3).
247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248pub struct TimerQueueFull;
249
250/// Test-only: clear every timer slot. Part of the global reset done by
251/// [`crate::kernel_test!`].
252#[cfg(feature = "test-support")]
253pub(crate) fn reset_for_test() {
254 crate::critical::enter(|| {
255 for slot in &TIMER_SLOTS {
256 // SAFETY: all access to TIMER_SLOTS goes through
257 // `critical::enter` (interrupts disabled, single-core), so no
258 // concurrent access is possible here.
259 unsafe {
260 *slot.deadline.get() = 0;
261 }
262 }
263 for slot in &PTASK_DEADLINES {
264 // SAFETY: same guard as above.
265 unsafe {
266 *slot.deadline.get() = 0;
267 }
268 }
269 });
270}
271
272/// Count of timer slots currently in use. Used by host-test reset/
273/// inspection helpers and by [`crate::report`].
274pub fn slots_in_use() -> usize {
275 crate::critical::enter(|| {
276 TIMER_SLOTS
277 .iter()
278 // SAFETY: all access to TIMER_SLOTS goes through
279 // `critical::enter` (interrupts disabled), so reads are
280 // exclusive.
281 .filter(|slot| unsafe { *slot.deadline.get() != 0 })
282 .count()
283 })
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn register_and_expire() {
292 crate::kernel_test! {
293 register_deadline(1000, crate::task::TaskId::new(3, 2)).unwrap();
294 poll_timers(500); // not yet
295 assert_eq!(crate::waker::next_ready(), None);
296
297 poll_timers(1000); // now expired
298 assert_eq!(crate::waker::next_ready(), Some(crate::task::TaskId::new(3, 2)));
299 }
300 }
301
302 #[test]
303 fn multiple_timers_independent() {
304 crate::kernel_test! {
305 register_deadline(100, crate::task::TaskId::new(1, 0)).unwrap();
306 register_deadline(200, crate::task::TaskId::new(2, 0)).unwrap();
307
308 poll_timers(150);
309 assert_eq!(crate::waker::next_ready(), Some(crate::task::TaskId::new(1, 0)));
310 assert_eq!(crate::waker::next_ready(), None);
311
312 poll_timers(250);
313 assert_eq!(crate::waker::next_ready(), Some(crate::task::TaskId::new(2, 0)));
314 }
315 }
316}