rivet/lib.rs
1//! Rivet RTOS — zero-allocation, dual-tier RTOS for microcontrollers.
2//!
3//! # Architecture
4//!
5//! Two tiers of concurrency, unified under one priority scheduler:
6//!
7//! - **Preemptive tier** (`#[rivet::ptask]` / [`spawn_ptask!`]): each task
8//! gets its own stack. The timer tick can suspend a running task at *any*
9//! point (not just at a yield/await) and resume a higher-priority one
10//! instead — real priority preemption, with priority inheritance on
11//! [`preempt::PriorityMutex`] to avoid priority inversion.
12//! - **Cooperative tier** (`#[rivet::task]`): `async fn` tasks compiled to
13//! `Future` state machines, polled on a single shared stack — zero
14//! per-task stack cost, ideal for I/O-bound/event-driven logic. This
15//! tier runs as an ordinary preemptive task at the lowest priority, so
16//! any real preemptive task immediately preempts it; it fills otherwise
17//! idle CPU time and only calls `WFI` when nothing anywhere is ready.
18//!
19//! # Targets
20//!
21//! - **ARM Cortex-M** (M0+/M3/M4/M7/M33) — via the `arch-cortex-m` feature
22//! - **RISC-V** (RV32) — via the `arch-riscv` feature
23//!
24//! # Example
25//!
26//! ```ignore
27//! use rivet::sync::Semaphore;
28//!
29//! static SEM: Semaphore<1> = Semaphore::new(0);
30//!
31//! // Cooperative: fine for I/O-bound logic.
32//! #[rivet::task(priority = 0)]
33//! async fn background() {
34//! loop {
35//! SEM.acquire().await;
36//! }
37//! }
38//!
39//! // Preemptive: genuinely can't be starved by a lower/equal priority
40//! // task that never yields.
41//! static CFG: u32 = 42;
42//! fn critical_task(cfg: &'static u32) -> ! {
43//! loop {
44//! // real work, no .await required anywhere
45//! }
46//! }
47//!
48//! fn main() -> ! {
49//! rivet::init();
50//! rivet::spawn_ptask!(stack = 2048, priority = 5, entry = critical_task, arg = CFG);
51//! rivet::run();
52//! }
53//! ```
54
55#![no_std]
56#![forbid(clippy::undocumented_unsafe_blocks)]
57
58// Test-support (feature = "test-support") uses `std::sync::Mutex` to
59// serialize host tests that share kernel globals. The feature is only ever
60// enabled from this crate's own `[dev-dependencies]`, so `std` is only
61// linked into test builds, never embedded builds.
62#[cfg(feature = "test-support")]
63extern crate std;
64
65pub mod config;
66pub mod console;
67pub mod critical;
68pub mod deadlines;
69pub mod exec_time;
70pub mod executor;
71pub mod irq;
72pub mod latency;
73pub mod fault;
74pub mod log;
75pub mod port;
76pub mod preempt;
77pub mod report;
78/// Dump kernel state to the console. See the [`report`] module docs for
79/// exactly what's included and what's deliberately left out.
80pub use report::report;
81pub mod sync;
82pub mod task;
83pub mod time;
84pub mod timer;
85pub mod trace;
86pub mod waker;
87pub mod watchdog;
88
89#[cfg(feature = "test-support")]
90pub mod test_support;
91
92/// Declare a static async (cooperative-tier) task. See the [`task`] module
93/// docs and the crate-level example. Lives in the macro namespace, so it
94/// coexists with the `task` module (`rivet::task::TaskCell` etc.) at the
95/// same path.
96pub use rivet_macros::task;
97
98/// Declare the application entry point. See [`rivet_macros::main`] for the
99/// full docs and an example.
100pub use rivet_macros::main;
101
102/// Serialize + reset a host test. Every test that touches kernel globals
103/// (task registry, waker bitmaps, timer slots, scheduler state) must be
104/// wrapped in this macro: `cargo test` runs test fns on parallel threads
105/// and the shared statics otherwise race (observed flake: 1/8 runs of
106/// `cargo test -p rivet`).
107///
108/// ```ignore
109/// #[test]
110/// fn my_test() {
111/// rivet::kernel_test! {
112/// // ... test body ...
113/// }
114/// }
115/// ```
116#[cfg(feature = "test-support")]
117#[macro_export]
118macro_rules! kernel_test {
119 ($($body:tt)*) => {{
120 let __rivet_test_guard = $crate::test_support::acquire();
121 $crate::test_support::reset_all();
122 $($body)*
123 }};
124}
125
126/// Crate version.
127pub const VERSION: &str = env!("CARGO_PKG_VERSION");
128
129/// Stack reserved for the cooperative-tier task (the async executor,
130/// spawned automatically at priority 0 by [`init`]).
131///
132/// Aligned to its own size (unlike [`preempt::Stack`]'s general 16-byte
133/// alignment, which is enough for a context-switch frame but not for
134/// this): this is the one task stack in the kernel that bypasses the
135/// pool's own size-aligned carving (`stack_pool::alloc_stack`) — spawned
136/// directly from a fixed `'static` buffer in [`init`] — yet still gets
137/// handed to `port::arch::on_switch_to`, which on Cortex-M reprograms an
138/// MPU region sized to it. An MPU region's base must be aligned to its
139/// own size; a plain `.bss` array has no such guarantee (found via a
140/// board with a different `.bss` layout than the one this was first
141/// written against — same class of bug the pool's alignment math exists
142/// to prevent, just missed for this one non-pool stack).
143const ASYNC_IDLE_STACK_SIZE: usize = 4096;
144#[repr(align(4096))]
145struct AlignedIdleStack([u8; ASYNC_IDLE_STACK_SIZE]);
146static mut ASYNC_IDLE_STACK: AlignedIdleStack = AlignedIdleStack([0; ASYNC_IDLE_STACK_SIZE]);
147static ASYNC_IDLE_ARG: () = ();
148
149fn async_idle_entry(_arg: &'static ()) -> ! {
150 // Safety: EXECUTOR.init() was called in `init()`, before this task can
151 // possibly run (the preemptive scheduler doesn't start until `run()`).
152 unsafe {
153 core::ptr::addr_of!(executor::EXECUTOR)
154 .as_ref()
155 .unwrap()
156 .run();
157 }
158}
159
160/// Initialize the kernel: set up the arch layer, discover `#[rivet::task]`
161/// (cooperative) tasks, and spawn the async executor as the lowest-priority
162/// preemptive task. Call [`spawn_ptask!`] for any additional preemptive
163/// tasks after this, then call [`run`].
164pub fn init() {
165 port::arch::init();
166 // Before `port::board::init()`: a board's init may call
167 // `console::enable_irq_tx()` (plan.md Phase 14), which needs the TX/
168 // RX rings already split. Calling it earlier than that would be
169 // silently harmless today (`write_bytes_irq` falls back to polling
170 // if `TX_SENDER` isn't set yet) but fragile to rely on.
171 console::init();
172 port::board::init();
173 port::board::tick_start(config::TICK_HZ);
174 log::init();
175 // Safety: EXECUTOR is only accessed here at boot, before run().
176 unsafe {
177 core::ptr::addr_of_mut!(executor::EXECUTOR)
178 .as_mut()
179 .unwrap()
180 .init();
181 }
182 // Safety: ASYNC_IDLE_STACK and ASYNC_IDLE_ARG are static, `'static`
183 // data never aliased anywhere else; the executor task is spawned
184 // exactly once here, before `run()` starts the scheduler, so no other
185 // task can observe the half-initialized registration.
186 unsafe {
187 #[allow(static_mut_refs)]
188 let _ = preempt::spawn(
189 &mut ASYNC_IDLE_STACK.0,
190 0, // lowest priority: any real preemptive task preempts this
191 async_idle_entry,
192 &ASYNC_IDLE_ARG,
193 );
194 }
195}
196
197/// Start the preemptive scheduler. Never returns.
198/// Must be called after [`init`] (and any [`spawn_ptask!`] calls).
199/// Set by [`run`], just before entering the scheduler (plan.md Phase 19):
200/// the signal secondary harts spin on before bringing up their own arch
201/// state and calling [`run_secondary_hart`]. `init()` plus every
202/// boot-time [`spawn_ptask!`] call is guaranteed complete by the time this
203/// is set, since both happen (by construction, in every binary this
204/// workspace builds) before the app calls `run()`.
205static KERNEL_READY: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);
206
207/// Whether hart 0 has finished boot and is running the scheduler (plan.md
208/// Phase 19). `rivet-rt`'s secondary-hart bring-up spins on this before
209/// calling [`run_secondary_hart`]. Always `false` (and irrelevant) on a
210/// single-hart board — nothing there ever calls the secondary-hart path.
211pub fn kernel_ready() -> bool {
212 KERNEL_READY.load(core::sync::atomic::Ordering::Acquire)
213}
214
215pub fn run() -> ! {
216 KERNEL_READY.store(true, core::sync::atomic::Ordering::Release);
217 preempt::start();
218}
219
220/// Bring up the preemptive scheduler on a **secondary** hart (plan.md
221/// Phase 19, RISC-V `virt` under `-smp > 1` only): per-hart arch bring-up
222/// (trap vector, ISR stack slice, PMP catch-all — everything
223/// [`port::arch::init`] does, all genuinely per-hart CSR/register state)
224/// followed by [`preempt::start_secondary_hart`]. Deliberately does
225/// **not** repeat [`init`]'s other steps (console/board/log setup, the
226/// async-idle-task spawn) — those are global, one-time facts owned by
227/// hart 0. Call only after [`kernel_ready`] is true, from a hart other
228/// than the one that called [`run`]. Never returns.
229pub fn run_secondary_hart() -> ! {
230 port::arch::init();
231 preempt::start_secondary_hart();
232}
233
234/// Voluntarily give up the CPU: request an immediate reschedule
235/// opportunity, same as a mutex unlock waking a higher-priority waiter.
236/// Safe to call from task or ISR context.
237pub fn yield_now() {
238 port::arch::request_reschedule();
239}
240
241/// Terminate successfully. Never returns. Under QEMU this reduces to the
242/// board's exit device / semihosting path (the `xtask` test harness
243/// asserts on the resulting exit code); on real hardware, boards typically
244/// map this to a reset or halt.
245pub fn exit_success() -> ! {
246 port::board::exit(0)
247}
248
249/// Terminate with a distinguishable non-zero failure code. Never returns.
250pub fn exit_failure(code: u32) -> ! {
251 port::board::exit(code)
252}
253
254/// Trigger a system reset (watchdog / fault-policy recovery). Never
255/// returns.
256pub fn system_reset() -> ! {
257 port::board::reset()
258}