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