Skip to main content

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}