rivet_rt/lib.rs
1//! Rivet RTOS boot glue: `_start`/`Reset`, bss/data initialization, default
2//! exception handlers, and a default panic handler — everything that was
3//! previously copy-pasted into every example binary's `main.rs`.
4//!
5//! Link this crate (`use rivet_rt as _;`) alongside one `rivet-arch-*` and
6//! one `rivet-bsp-*` crate, declare your entry point with
7//! [`rivet::main`](../rivet_macros/attr.main.html), and that's a complete
8//! binary:
9//!
10//! ```ignore
11//! #![no_std]
12//! #![no_main]
13//!
14//! use rivet_bsp_qemu_virt as _;
15//! use rivet_rt as _;
16//!
17//! #[rivet::main]
18//! fn main() -> ! {
19//! rivet::println!("hello");
20//! rivet::run()
21//! }
22//! ```
23//!
24//! Unlike `rivet`/`rivet-arch-*`/`rivet-bsp-*`, this crate legitimately
25//! knows the target architecture (`#[cfg(target_arch = ...)]`) — it is
26//! boot glue for a *known* target, the same role `cortex-m-rt`/`riscv-rt`
27//! play in the wider embedded-Rust ecosystem, not kernel or board logic.
28//! It reaches no MMIO of its own beyond what every binary needs to boot at
29//! all (stack pointer, bss/data, and — on Cortex-M — the small set of
30//! exception vectors every board needs *something* installed at).
31
32#![no_std]
33
34extern "C" {
35 // Referenced by symbol name from the RISC-V global_asm! `_start` (not
36 // a Rust-level call there, so rustc's dead-code analysis doesn't see
37 // it as used on that target) and by direct call from Cortex-M's
38 // `Reset` below.
39 #[allow(dead_code)]
40 fn rivet_main() -> !;
41}
42
43#[cfg(target_arch = "riscv32")]
44mod riscv {
45 /// The compile-time-configured hart ceiling, exposed as a real linked
46 /// symbol so `_start`'s `global_asm!` (which can only reference
47 /// linker symbols, not Rust consts) can compare `mhartid` against it
48 /// (plan.md Phase 19). `1` on every board that hasn't opted into
49 /// `RIVET_MAX_HARTS > 1` — harts `1..` then immediately park, exactly
50 /// the pre-Phase-19 behavior.
51 #[no_mangle]
52 static __rivet_max_harts: u32 = rivet::config::MAX_HARTS as u32;
53
54 // `mhartid` guard: harts other than 0 must never touch shared kernel
55 // statics (bss zeroing, `rivet_main`'s `rivet::init()`) — those are
56 // hart-0-only, one-time facts. Before plan.md Phase 19 the guard
57 // simply parked every other hart forever (`-smp N > 1` ran N kernel
58 // copies otherwise, over one set of kernel statics — Rivet's
59 // multi-core story was AMP-or-nothing). Phase 19 gives harts
60 // `1..RIVET_MAX_HARTS` a real bring-up path instead: each gets its
61 // own boot stack (`__hart_n_stack_top`, sized per hart in the linker
62 // script) and spins on `rivet::kernel_ready()` before calling
63 // `rivet_secondary_main`, which does per-hart arch init and enters
64 // the scheduler. Harts `>= RIVET_MAX_HARTS` (a build might run under
65 // `-smp` higher than it was configured for) still park forever —
66 // there is no kernel state sized for them.
67 //
68 // `.data` copy: `__data_load` is `__data_start` itself (a `PROVIDE`
69 // default every board's linker script gets from `rivet-rt`'s shared
70 // fragment) on a single-RAM-region target with no separate flash load
71 // address (QEMU virt) — this loop is then a harmless self-copy. A
72 // real XIP board (ESP32-C6: flash-backed `.data`, copied to RAM at
73 // boot, same shape Cortex-M/Xtensa already need) overrides
74 // `__data_load` to its real flash address in its own linker script,
75 // and this exact loop does the real work, no separate boot path
76 // needed.
77 core::arch::global_asm!(
78 ".section .text._start",
79 ".global _start",
80 "_start:",
81 " csrr t0, mhartid",
82 " bnez t0, secondary_entry",
83 " la sp, __stack_top",
84 " la t0, __data_start",
85 " la t1, __data_end",
86 " la t2, __data_load",
87 "3:",
88 " bgeu t0, t1, 4f",
89 " lw t3, 0(t2)",
90 " sw t3, 0(t0)",
91 " addi t0, t0, 4",
92 " addi t2, t2, 4",
93 " j 3b",
94 "4:",
95 " la t0, __bss_start",
96 " la t1, __bss_end",
97 "1:",
98 " bgeu t0, t1, 2f",
99 " sw zero, 0(t0)",
100 " addi t0, t0, 4",
101 " j 1b",
102 "2:",
103 " call rivet_main",
104 // rivet_main is `-> !`; this is unreachable in practice, kept only
105 // so a hypothetical return doesn't fall off the end of .text.
106 " j park_hart",
107 // A secondary hart (mhartid != 0): if it's within the configured
108 // hart ceiling, give it its own boot stack (one 1K slice per hart,
109 // `.secondary_stacks`, indexed by mhartid — never shared with hart
110 // 0's `__stack_top` or with each other) and hand off to Rust,
111 // which spins on `rivet::kernel_ready()` before touching any
112 // kernel state. Out-of-range harts park immediately, same as
113 // before Phase 19.
114 "secondary_entry:",
115 " la t1, __rivet_max_harts",
116 " lw t1, 0(t1)",
117 " bgeu t0, t1, park_hart",
118 " la t2, __secondary_stacks_top",
119 " slli t3, t0, 9", // t3 <- mhartid * 512 (power-of-two shift,
120 // matches link-qemu-virt.ld's per-hart
121 // .secondary_stacks slice size; avoids
122 // needing the M extension)
123 " sub sp, t2, t3",
124 " call rivet_secondary_main",
125 " j park_hart",
126 "park_hart:",
127 " wfi",
128 " j park_hart",
129 );
130
131 /// Hart bring-up on a secondary hart, called from `_start`'s asm once
132 /// it has its own boot stack: spin for `rivet::kernel_ready()`
133 /// (hart 0's signal that `rivet::init()` and every boot-time
134 /// `spawn_ptask!` have completed), then hand off to
135 /// `rivet::run_secondary_hart()` (per-hart arch bring-up — trap
136 /// vector, ISR stack slice, PMP — followed by the scheduler). Never
137 /// returns.
138 ///
139 /// # Safety
140 /// Must only be reached from `_start`'s asm, on a hart whose id is
141 /// `< RIVET_MAX_HARTS` (the asm already checked this), with that
142 /// hart's own boot stack already installed as `sp`.
143 #[no_mangle]
144 unsafe extern "C" fn rivet_secondary_main() -> ! {
145 while !rivet::kernel_ready() {
146 core::hint::spin_loop();
147 }
148 rivet::run_secondary_hart();
149 }
150}
151
152#[cfg(target_arch = "arm")]
153mod cortex_m {
154 extern "C" {
155 static __data_load: u8;
156 static __data_start: u8;
157 static __data_end: u8;
158 static __bss_start: u8;
159 static __bss_end: u8;
160 }
161
162 /// # Safety
163 /// Runs at power-on reset as the vector-table Reset entry; performs
164 /// the `.data` copy and `.bss` zeroing, then starts the kernel.
165 #[no_mangle]
166 pub unsafe extern "C" fn Reset() -> ! {
167 // SAFETY: `__data_*`/`__bss_*` are the board linker script's
168 // (via rivet-rt's common linker fragment) data/bss bounds; this
169 // runs once, before any other code, with nothing else touching
170 // that memory yet.
171 unsafe {
172 let data_load = core::ptr::addr_of!(__data_load);
173 let data_start = core::ptr::addr_of!(__data_start);
174 let data_end = core::ptr::addr_of!(__data_end);
175 let count = data_end as usize - data_start as usize;
176 for i in 0..count {
177 core::ptr::write(
178 (data_start as *mut u8).add(i),
179 core::ptr::read(data_load.add(i)),
180 );
181 }
182
183 let bss_start = core::ptr::addr_of!(__bss_start);
184 let bss_end = core::ptr::addr_of!(__bss_end);
185 let bss_count = bss_end as usize - bss_start as usize;
186 for i in 0..bss_count {
187 core::ptr::write((bss_start as *mut u8).add(i), 0);
188 }
189
190 super::rivet_main()
191 }
192 }
193
194 /// Shared fallback for exception vectors no board/test overrides:
195 /// prints a marker and halts. A binary that wants to observe a
196 /// specific fault (e.g. the fault-isolation test suite) defines its
197 /// own `#[no_mangle] extern "C" fn HardFault()` etc., which — being a
198 /// strong symbol — takes priority over this crate's; the linker
199 /// script only falls back to `DefaultHandler` via `PROVIDE` for
200 /// vectors nothing else defines.
201 #[no_mangle]
202 pub extern "C" fn DefaultHandler() {
203 rivet::console::write_str("HARD_FAULT\n");
204 // This handler runs at HardFault's fixed, always-highest
205 // exception priority and then spins forever without returning —
206 // no lower-priority interrupt (including a board's
207 // interrupt-driven console TX ISR, plan.md Phase 14) can ever
208 // preempt it, so a message queued into that ring here would
209 // never drain on its own.
210 rivet::console::flush_sync();
211 loop {
212 core::hint::spin_loop();
213 }
214 }
215}
216
217#[cfg(target_arch = "xtensa")]
218mod xtensa {
219 // `xtensa-lx-rt` (linked in by `rivet-arch-xtensa`) owns `Reset`,
220 // bss/data init, and the vector table entirely (plan.md Phase 21/22 —
221 // see `rivet-arch-xtensa`'s module docs for why that boot-glue layer
222 // is sourced from there rather than hand-written, same reasoning as
223 // `riscv-rt`/`cortex-m-rt` for the other two arches, just one layer
224 // further out here). All this crate needs to provide is the `main`
225 // symbol `xtensa-lx-rt`'s own `Reset` calls once bss/data are ready.
226 #[no_mangle]
227 pub extern "C" fn main() -> ! {
228 // SAFETY: called exactly once, by `xtensa-lx-rt`'s `Reset`, after
229 // bss/data init and before anything else runs.
230 unsafe { super::rivet_main() }
231 }
232
233 /// Fallback for every peripheral interrupt vector the `esp32s3` PAC's
234 /// `device.x` doesn't have a specific handler bound for (`PROVIDE(X =
235 /// DefaultHandler)` for every named vector) — same role as Cortex-M's
236 /// `DefaultHandler` in this same crate.
237 #[no_mangle]
238 pub extern "C" fn DefaultHandler() {
239 rivet::console::write_str("UNHANDLED_INTERRUPT\n");
240 loop {
241 core::hint::spin_loop();
242 }
243 }
244}
245
246/// Default panic handler: prints the location and message via
247/// [`rivet::console`], then exits with a distinguishable failure code.
248/// Disable with `default-features = false` to supply your own.
249#[cfg(feature = "panic-handler")]
250mod panic {
251 use core::fmt::Write;
252 use core::panic::PanicInfo;
253
254 struct ConsoleWriter;
255 impl Write for ConsoleWriter {
256 fn write_str(&mut self, s: &str) -> core::fmt::Result {
257 rivet::console::write_str(s);
258 Ok(())
259 }
260 }
261
262 #[panic_handler]
263 fn panic(info: &PanicInfo) -> ! {
264 rivet::console::write_str("PANIC: ");
265 if let Some(loc) = info.location() {
266 let _ = write!(ConsoleWriter, "{}:{}", loc.file(), loc.line());
267 } else {
268 rivet::console::write_str("(no location)");
269 }
270 rivet::console::write_str(": ");
271 let _ = write!(ConsoleWriter, "{}", info.message());
272 rivet::console::write_str("\n");
273 rivet::exit_failure(0xFF);
274 }
275}