Skip to main content

rivet_arch_cortex_m/
lib.rs

1//! Rivet RTOS — ARM Cortex-M ISA port.
2//!
3//! Implements the Group A (`rivet::port::arch`) symbol contract for
4//! Cortex-M targets: PendSV context switch, MemManage fault handling, MPU
5//! programming, SysTick tick source. Contains **no board/MMIO knowledge**
6//! beyond what's genuinely part of the Cortex-M architecture (SCB, MPU,
7//! SysTick, and their fixed System Control Space addresses — identical on
8//! every Cortex-M3/4/7/33). A board's clock rate, console, and exit/reset
9//! path are supplied separately by a `rivet-bsp-*` crate.
10//!
11//! # Preemptive context switch
12//!
13//! Tasks run in Thread mode using PSP (Process Stack Pointer); exceptions
14//! (SysTick, PendSV, everything else) always run in Handler mode using MSP
15//! (Main Stack Pointer) — automatic Cortex-M behavior. That split matters:
16//! a PendSV handler's own nested Rust calls (the scheduler, atomics, etc.)
17//! run on MSP, never touching a task's PSP-based stack — no RISC-V-style
18//! risk of the scheduler's own call chain competing for space with
19//! whatever a task had reserved for itself.
20//!
21//! Following ARM's recommended pattern: SysTick only *requests* a
22//! reschedule (`SCB.ICSR.PENDSVSET`); the actual register save/restore and
23//! scheduling decision happen in PendSV, which — being the lowest-priority
24//! exception — never preempts a higher-priority ISR mid-flight.
25
26#![no_std]
27
28// This crate's context-switch/SVC asm assumes a soft-float ABI
29// throughout: `rivet_svc_handler`'s EXC_RETURN decode checks the fixed
30// byte pattern for "no FP frame" (`0xFD`/`0xF9`) rather than testing
31// EXC_RETURN bit 2 (the FP-context-active flag), and neither it nor the
32// `PendSV` handler save/restore `s16-s31`/`FPSCR`. Both are silently
33// correct only because a soft-float target (`thumbv7em-none-eabi`, not
34// `-eabihf`) never sets the FPU's context-active state (`FPCA`) in the
35// first place — no VFP instruction is ever emitted, so there's no FP
36// context to lose. Catch the unsupported configuration at compile time
37// instead of producing a corrupted stack frame (garbage SP from
38// `rivet_svc_handler` reading the wrong exception-frame location) the
39// first time a task is spawned from inside another task on a hardfloat
40// build.
41#[cfg(target_feature = "vfp2")]
42compile_error!(
43    "rivet-arch-cortex-m assumes a soft-float ABI (build for e.g. \
44     thumbv7em-none-eabi, not -eabihf) — see this crate's own module \
45     docs for what a hardfloat port would additionally need"
46);
47
48pub mod dwt;
49pub mod mpu;
50#[cfg(feature = "nvic")]
51pub mod nvic;
52pub mod semihosting;
53#[cfg(feature = "systick")]
54pub mod systick;
55
56/// Minimum task stack: the PendSV frame (32 bytes r4-r11 + 32 bytes
57/// hardware-stacked r0-r3/r12/lr/pc/xPSR) plus slack for the entry
58/// trampoline.
59pub const MIN_TASK_STACK: usize = 64 + 64;
60
61#[no_mangle]
62extern "Rust" fn __rivet_arch_init() {
63    // SCB.VTOR's reset value is architecturally 0x00000000 — correct for
64    // every board in this workspace that happens to load its flash at
65    // address 0 (QEMU's `lm3s6965evb`/`mps2-an385`), a no-op write here,
66    // but *wrong* for a board whose vector table lives somewhere else
67    // (a real chip's actual flash base, e.g. the STM32F401RE's
68    // 0x08000000): without this, exceptions vector through whatever
69    // VTOR defaults to instead of the board's real table, which reads
70    // as "boot works, then total silence the instant the first
71    // interrupt (SysTick) would otherwise fire" — no fault, no crash,
72    // because the hardware is faithfully doing exactly what it's told
73    // to, just not with the table this kernel actually built. Confirmed
74    // by bisection on real STM32F401RE hardware (see
75    // rivet-bsp-stm32f401re/link-stm32f401re.ld's own doc for the full
76    // story). `__vector_table` is provided by every Cortex-M board's
77    // linker script in this workspace, so this is unconditional, not
78    // feature-gated.
79    unsafe extern "C" {
80        static __vector_table: u32;
81    }
82    // SAFETY: `SCB::PTR` is the statically-known System Control Block
83    // base; `__vector_table` is a linker-defined symbol (its address,
84    // not its value, is what VTOR needs — matches every other `la
85    // __symbol`-style linker-script constant this workspace uses).
86    unsafe {
87        (*cortex_m::peripheral::SCB::PTR)
88            .vtor
89            .write(core::ptr::addr_of!(__vector_table) as u32);
90    }
91
92    mpu::init();
93    dwt::init();
94
95    // PendSV must run at the lowest possible priority so it never preempts
96    // a higher-priority ISR mid-flight — it only runs once everything else
97    // has finished, which is what makes it safe to do the actual stack
98    // switch there. Set SHPR3.PRI_14 (PendSV) and SHPR3.PRI_15 (SysTick)
99    // to the lowest priority (0xFF, all implemented priority bits set).
100    //
101    // SAFETY: `SCB::PTR` is the statically-known System Control Block
102    // base, valid on every Cortex-M; these SHPR/SHCSR writes are volatile
103    // MMIO accesses and the SCB is exclusively owned by this module.
104    unsafe {
105        let scb = &*cortex_m::peripheral::SCB::PTR;
106        scb.shpr[10].write(0xFF); // PendSV priority (SHPR3 byte 2)
107        scb.shpr[11].write(0xFF); // SysTick priority (SHPR3 byte 3)
108                                  // Enable the dedicated Bus/Usage/MemManage fault handlers; without
109                                  // this they escalate straight to HardFault, hiding the real cause.
110        scb.shcsr.write(
111            (1 << 16) // MEMFAULTENA
112            | (1 << 17) // BUSFAULTENA
113            | (1 << 18), // USGFAULTENA
114        );
115    }
116
117    // Every external NVIC IRQ resets to priority 0 — the *highest*
118    // configurable priority, strictly above PendSV/SysTick's 0xFF. Left
119    // alone, any future peripheral IRQ that touches kernel state
120    // (`rivet::irq::dispatch` calling `unblock`/a waker, or anything
121    // else that ends up in `sched`/`timer`) could preempt PendSV or
122    // SysTick *mid-reschedule* — the two-separate-atomics
123    // `READY_BITMAP`/`QUEUES` update `sched::ready_add`/`ready_remove`
124    // do is exactly the kind of thing that isn't safe to interrupt.
125    // Floor every implemented IRQ to PendSV/SysTick's own 0xFF so
126    // nothing outranks the scheduler unless a board *deliberately*
127    // raises one (every board that registers its own IRQ already does,
128    // explicitly, via `rivet::irq::set_priority` — matching this floor,
129    // not fighting it). `NVIC::PTR.ipr` covers the architectural maximum
130    // (240 IRQs); writing entries a given chip doesn't implement is
131    // architecturally safe (unimplemented IPR bits/registers are
132    // fixed/ignored, never a fault).
133    //
134    // SAFETY: `NVIC::PTR` is the statically-known NVIC base, valid on
135    // every Cortex-M; IPR is byte-addressable, plain volatile MMIO,
136    // and this runs once, before any IRQ is enabled.
137    unsafe {
138        let nvic = &*cortex_m::peripheral::NVIC::PTR;
139        for ipr in nvic.ipr.iter() {
140            ipr.write(0xFF);
141        }
142    }
143}
144
145#[no_mangle]
146extern "Rust" fn __rivet_arch_idle() {
147    cortex_m::asm::wfi();
148}
149
150#[no_mangle]
151extern "Rust" fn __rivet_arch_min_task_stack() -> usize {
152    MIN_TASK_STACK
153}
154
155/// No hardware minimum: the CM3 MPU denies the whole task-stack pool with
156/// one region rather than a per-stack guard (see this crate's own
157/// `on_switch_to`), so this value is unused for actual protection —
158/// still a real power of two, matching the historical guard size, since
159/// `rivet::preempt::stack_pool`'s layout math needs *some* value.
160#[no_mangle]
161extern "Rust" fn __rivet_arch_min_guard_size() -> usize {
162    64
163}
164
165#[no_mangle]
166extern "Rust" fn __rivet_arch_cycle_count() -> u64 {
167    dwt::cycle_count()
168}
169
170/// plan.md Phase 13: these three are hard-required by the port contract
171/// (every existing binary must still link even if it never enables the
172/// `nvic` feature), so they're defined unconditionally here rather than
173/// only inside `nvic.rs` — a board that doesn't enable `nvic` gets a
174/// harmless no-op instead of a link error naming a symbol it doesn't need.
175#[no_mangle]
176extern "Rust" fn __rivet_arch_irq_enable(_irq_num: u32) {
177    #[cfg(feature = "nvic")]
178    nvic::enable(_irq_num);
179}
180
181#[no_mangle]
182extern "Rust" fn __rivet_arch_irq_disable(_irq_num: u32) {
183    #[cfg(feature = "nvic")]
184    nvic::disable(_irq_num);
185}
186
187#[no_mangle]
188extern "Rust" fn __rivet_arch_irq_set_priority(_irq_num: u32, _priority: u8) {
189    #[cfg(feature = "nvic")]
190    nvic::set_priority(_irq_num, _priority);
191}
192
193/// plan.md Phase 19: every Cortex-M board this workspace targets is
194/// QEMU-modeled strictly single-core (confirmed empirically — `-smp 4`
195/// is rejected outright by both `lm3s6965evb` and `mps2-an385`), so this
196/// is always hart 0.
197#[no_mangle]
198extern "Rust" fn __rivet_arch_hart_id() -> usize {
199    0
200}
201
202/// plan.md Phase 19: never called with `hart != 0` on a single-core
203/// board (see `__rivet_arch_hart_id`'s docs above) — aliasing straight to
204/// the self-reschedule path keeps the contract satisfiable without a
205/// separate no-op that would silently swallow a real bug if it ever were
206/// called with a nonzero hart.
207#[no_mangle]
208extern "Rust" fn __rivet_arch_request_reschedule_on(hart: usize) {
209    debug_assert_eq!(hart, 0, "rivet-arch-cortex-m: single-core, hart must be 0");
210    __rivet_arch_request_reschedule();
211}
212
213/// plan.md Phase 12: cycle stamp at the moment a reschedule was
214/// requested, consumed by `rivet_pendsv_rust` to record `IrqEntry`
215/// latency — the single trigger point below covers both the tick-driven
216/// and voluntary-yield paths uniformly (unlike RISC-V, Cortex-M has no
217/// separate "just entered the handler" asm hook that's safe to touch
218/// without risking the hand-tuned PendSV register-save sequence).
219#[cfg(feature = "latency-histograms")]
220static RESCHEDULE_REQUESTED_AT: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
221
222/// Set PendSV pending. Single trigger for every context switch, whether
223/// tick-driven or a voluntary yield.
224#[no_mangle]
225extern "Rust" fn __rivet_arch_request_reschedule() {
226    #[cfg(feature = "latency-histograms")]
227    RESCHEDULE_REQUESTED_AT.store(dwt::cycle_count() as u32, core::sync::atomic::Ordering::Relaxed);
228    // SAFETY: `SCB::PTR` is the statically-known System Control Block
229    // base, valid on every Cortex-M; `ICSR` write is a volatile MMIO
230    // access.
231    unsafe {
232        let scb = &*cortex_m::peripheral::SCB::PTR;
233        scb.icsr.write(1 << 28); // PENDSVSET
234    }
235}
236
237#[no_mangle]
238extern "Rust" fn __rivet_arch_irq_save() -> usize {
239    // `Primask::is_active()` means "exceptions are active", i.e.
240    // interrupts are currently *enabled* (PRIMASK bit clear) — no
241    // negation here, unlike a naive reading of the name might suggest.
242    let was_enabled = cortex_m::register::primask::read().is_active();
243    cortex_m::interrupt::disable();
244    was_enabled as usize
245}
246
247#[no_mangle]
248extern "Rust" fn __rivet_arch_irq_restore(token: usize) {
249    if token != 0 {
250        // SAFETY: re-enabling interrupts only if they were enabled at the
251        // matching `__rivet_arch_irq_save` call.
252        unsafe { cortex_m::interrupt::enable() };
253    }
254}
255
256#[no_mangle]
257extern "Rust" fn __rivet_arch_on_switch_to(stack_base: usize, stack_size: usize) {
258    mpu::set_current_stack(stack_base, stack_size);
259}
260
261#[no_mangle]
262extern "Rust" fn __rivet_arch_guard_register(_guard_base: usize, _slot: usize) {
263    // No per-task locked guard on Cortex-M: the two-region MPU design
264    // (whole-pool deny + current-stack allow) already gives full mutual
265    // stack isolation without per-task PMP-style entries.
266}
267
268#[no_mangle]
269extern "Rust" fn __rivet_arch_scratch_open(base: usize, size: usize) {
270    mpu::allow_scratch(base, size);
271}
272
273#[no_mangle]
274extern "Rust" fn __rivet_arch_scratch_close() {
275    mpu::clear_scratch();
276}
277
278// ── Preemptive tier: PendSV context switch ────────────────────────
279
280/// Rust-side PendSV logic. Called from the asm handler with `interrupted_sp`
281/// (the interrupted task's PSP, pointing at its saved r4-r11 frame). Saves
282/// the interrupted task's registers (already on the stack), asks the
283/// scheduler what to run next, and returns the stack pointer to resume.
284#[no_mangle]
285unsafe extern "C" fn rivet_pendsv_rust(interrupted_sp: usize) -> usize {
286    #[cfg(feature = "latency-histograms")]
287    {
288        let requested_at = RESCHEDULE_REQUESTED_AT.load(core::sync::atomic::Ordering::Relaxed);
289        let now = dwt::cycle_count() as u32;
290        rivet::latency::record(
291            rivet::latency::Kind::IrqEntry,
292            now.wrapping_sub(requested_at) as u64,
293        );
294    }
295    rivet::preempt::on_tick(interrupted_sp)
296}
297
298core::arch::global_asm!(
299    ".section .text.rivet_task_exit",
300    ".global rivet_task_exit",
301    ".thumb_func",
302    "rivet_task_exit:",
303    "  bl   rivet_task_exit_core", // r0/r1 carry the return value
304    "1:",
305    "  b    1b",
306);
307
308core::arch::global_asm!(
309    ".section .text.PendSV",
310    ".global PendSV",
311    ".thumb_func",
312    "PendSV:",
313    // A lone `push {{lr}}` (one word) leaves MSP 4-mod-8 across the `bl`
314    // below, an AAPCS 8-byte-alignment violation — harmless on M3 (no
315    // LDRD/VLDR here), latent on M4F/M7. Fixed with an explicit `sub sp,
316    // #4` instead of padding the push list with a second register: r4-r11
317    // are semantically live across this function (r4 in particular gets
318    // overwritten by `ldmia` below with the *new* task's value, so
319    // pushing/popping it here would restore the *old* task's stale r4
320    // right before returning — a real bug caught in review, not shipped).
321    "  push {{lr}}",
322    "  sub  sp, sp, #4",
323    "  mrs  r0, psp",
324    "  subs r0, r0, #32",
325    "  stmia r0, {{r4-r11}}",
326    "  bl   rivet_pendsv_rust",
327    "  ldmia r0, {{r4-r11}}",
328    "  adds r0, r0, #32",
329    "  msr  psp, r0",
330    "  add  sp, sp, #4",
331    "  pop  {{lr}}",
332    // Symbol for the GDB context-switch verification script (tests/gdb):
333    // r4-r11 have been restored from the frame; frame base = psp - 32.
334    ".global rivet_pendsv_resume",
335    "rivet_pendsv_resume:",
336    "  bx   lr",
337);
338
339// ── First task start / initial stack frame ────────────────────────
340
341/// Set up the initial stack frame for a new task, then start the first
342/// task's execution. Called once, from `preempt::start`, with the first
343/// task's already-built stack frame.
344#[no_mangle]
345unsafe extern "Rust" fn __rivet_arch_start_first_task(sp: usize) -> ! {
346    // SAFETY: `sp` is the freshly-built initial frame of the first task;
347    // PSP is set exactly once here, before any interrupt can fire.
348    let frame = sp as *const u32;
349    let arg = unsafe { core::ptr::read(frame.add(8)) };
350    let entry_fn = unsafe { core::ptr::read(frame.add(14)) };
351
352    unsafe {
353        core::arch::asm!(
354            "msr psp, {sp}",
355            "movs r2, #2",
356            "msr control, r2", // SPSEL=1 (use PSP in Thread mode), stay privileged
357            "isb",
358            sp = in(reg) sp,
359            out("r2") _,
360        );
361    }
362
363    // PSP is valid now — safe to let SysTick/PendSV start firing.
364    #[cfg(feature = "systick")]
365    systick::enable();
366
367    // Root cause (plan.md Phase 24), found via a real regression on real
368    // Cortex-M hardware: `rivet::preempt::start()` now wraps its call
369    // into this function in `port::arch::critical_section` (masking
370    // interrupts for the whole gap between the scheduling decision and
371    // this function actually consuming the picked task's state — closes
372    // a real race found on Xtensa dual-core, plan.md Phase 24's own
373    // section has the full story). That wrapper's own interrupt-restore
374    // never runs, because its closure diverges into this `-> !`
375    // function — every arch's `start_first_task` is now responsible for
376    // re-enabling interrupts itself as part of dispatch. RISC-V's
377    // `mret`-based resume already does this implicitly (the fabricated
378    // context's own `mstatus` carries `MIE = 1`); this port had no
379    // equivalent, so the very first task's interrupts silently never
380    // came back — the whole system froze the instant any code past this
381    // point needed a tick or exception. `cortex_m::interrupt::enable()`
382    // is the same primitive `__rivet_arch_irq_restore` above already
383    // uses.
384    unsafe {
385        cortex_m::interrupt::enable();
386    }
387
388    unsafe {
389        core::arch::asm!(
390            "mov r0, {arg}",
391            "bx {entry}",
392            arg = in(reg) arg,
393            entry = in(reg) entry_fn,
394            options(noreturn)
395        );
396    }
397}
398
399/// Frame layout (aligned to 8 bytes, 64 bytes total):
400/// ```text
401/// [sp+0]  r4
402/// [sp+4]  r5
403/// [sp+8]  r6
404/// [sp+12] r7
405/// [sp+16] r8
406/// [sp+20] r9
407/// [sp+24] r10
408/// [sp+28] r11
409/// [sp+32] r0   <- arg
410/// [sp+36] r1
411/// [sp+40] r2
412/// [sp+44] r3
413/// [sp+48] r12
414/// [sp+52] lr   <- entry_fn (with Thumb bit set)
415/// [sp+56] pc   <- entry_fn (with Thumb bit set)
416/// [sp+60] xPSR <- 0x01000000 (Thumb mode)
417/// ```
418/// The PendSV handler restores r4-r11 from the first 32 bytes; the
419/// hardware un-stacks the remaining 32 bytes on exception return, resuming
420/// at `entry_fn` with `r0 = arg`.
421unsafe fn init_task_stack_impl(stack: &mut [u8], entry_fn: usize, arg: usize) -> usize {
422    const FRAME_WORDS: usize = 16; // 8 (r4-r11) + 8 (hw frame)
423    const STACK_ALIGN: usize = 16;
424
425    // SAFETY: `stack` is a valid mutable slice of at least MIN_TASK_STACK
426    // bytes (the caller guarantees this); the writes below initialize the
427    // frame INSIDE the slice (at the top, aligned down).
428    unsafe {
429        let base = stack.as_mut_ptr() as usize;
430        let top = base + stack.len();
431        let frame_start = (top - FRAME_WORDS * 4) & !(STACK_ALIGN - 1);
432        let frame = frame_start as *mut u32;
433
434        for i in 0..FRAME_WORDS {
435            core::ptr::write(frame.add(i), 0);
436        }
437        core::ptr::write(frame.add(8), arg as u32); // r0
438                                                    // r1,r2,r3,r12 (words 9-12) stay 0
439        extern "C" {
440            fn rivet_task_exit();
441        }
442        core::ptr::write(frame.add(13), rivet_task_exit as *const () as usize as u32); // lr
443        core::ptr::write(frame.add(14), entry_fn as u32); // pc
444        core::ptr::write(frame.add(15), 0x0100_0000); // xPSR: Thumb bit (T=1) set
445
446        frame_start
447    }
448}
449
450/// SVC-vectored kernel call: builds a new task's initial stack frame from
451/// *Handler* mode, where the MPU does not apply the way it does in Thread
452/// mode. Thread-mode code cannot write another task's stack: MPU region 6
453/// denies the whole `.task_stacks` pool and region 7 only permits the
454/// *current* task's stack — a spawner faulting on the new task's stack is
455/// exactly what an unprivileged `init_task_stack` would hit.
456///
457/// Naked (no prologue): the exception frame base must be read from `sp`
458/// *before* the compiler pushes anything, and the exception return value
459/// in `lr` must be preserved across the `bl rivet_svc_core` call so the
460/// handler returns with `bx lr` (EXC_RETURN), not a normal branch.
461///
462/// Preserves `lr` with a real `push`/`pop` on this handler's own stack
463/// (MSP — Handler mode always uses it), the same shape `PendSV`'s own
464/// asm below already uses for the identical alignment reason, rather
465/// than stashing it in a register across the call. An earlier version
466/// used `mov r4, lr` instead: `r4` is AAPCS callee-saved, but this naked
467/// handler has no prologue to actually save/restore the *caller's*
468/// (i.e. the interrupted code's) live `r4` — so it silently clobbered
469/// whatever value the compiler's register allocator happened to be
470/// keeping there, live across the SVC boundary, the instant it made
471/// that choice (confirmed by disassembling a real build where it did
472/// exactly that). `r12` isn't a fix either: it's AAPCS *caller*-saved,
473/// so `bl rivet_svc_core` is free to clobber it too. `r1` here is dead
474/// (only used for the EXC_RETURN low-byte check, already done) and just
475/// rides along as the push/pop's 8-byte-alignment partner.
476///
477/// # Safety
478/// Exception entry point; installed via the board's vector table
479/// (`rivet-rt`); never called directly.
480#[unsafe(naked)]
481#[no_mangle]
482unsafe extern "C" fn rivet_svc_handler() {
483    // SAFETY: naked handler with no stack frame; the register-level
484    // protocol with `rivet_svc_core` is documented in the doc comment.
485    core::arch::naked_asm!(
486        "uxtb r1, lr",    // EXC_RETURN 0xFFFFFFFD = taken from thread
487        "cmp  r1, #0xfd", // mode with PSP (spawn from a running task);
488        "bne  1f",        // 0xF9 = thread mode with MSP (boot context)
489        "mrs  r0, psp",   // frame on PSP
490        "b    2f",
491        "1:",
492        "mov  r0, sp", // frame on MSP — computed before the push below
493                       // touches *this* handler's own (MSP) stack
494        "2:",
495        "push {{r1, lr}}", // preserve EXC_RETURN across the call
496        "bl   rivet_svc_core",
497        "pop  {{r1, lr}}",
498        "bx   lr", // exception return
499    );
500}
501
502/// Rust half of [`rivet_svc_handler`]: `frame` is the exception stack
503/// frame ({r0,r1,r2,r3,r12,lr,pc,xPSR}) pushed by the `svc 0` issued from
504/// `__rivet_arch_init_task_stack`.
505#[no_mangle]
506fn rivet_svc_core(frame: *mut u32) {
507    // SAFETY: the caller guarantees `frame` points at the live exception
508    // stack frame ({r0,r1,r2,r3,...}) pushed by the `svc 0`; all four
509    // slots are valid, word-aligned reads.
510    let (stack_ptr, stack_len, entry, arg) = unsafe {
511        (
512            *frame.add(0) as *mut u8,
513            *frame.add(1) as usize,
514            *frame.add(2) as usize,
515            *frame.add(3) as usize,
516        )
517    };
518
519    // SAFETY: the caller passed a valid `&mut [u8]` slice split across
520    // r0/r1 (as_mut_ptr / len).
521    let sp = unsafe {
522        // The ARMv7-M MPU applies in Handler mode too, so the write into
523        // the denied `.task_stacks` pool would fault even here. Disable
524        // the MPU for the duration of the frame write (real RTOSes do the
525        // same); the SVC handler runs at the highest configurable priority
526        // so nothing can preempt us mid-window.
527        let saved = mpu::disable_for_scope();
528        let sp = init_task_stack_impl(
529            core::slice::from_raw_parts_mut(stack_ptr, stack_len),
530            entry,
531            arg,
532        );
533        mpu::restore_after_scope(saved);
534        sp
535    };
536    // Deliver the result via the exception frame's saved r0.
537    // SAFETY: `frame` points at the live exception stack frame on MSP.
538    unsafe {
539        core::ptr::write_volatile(frame, sp as u32);
540    }
541}
542
543/// Issue `init_task_stack_impl` from Handler mode via SVC (see
544/// [`rivet_svc_handler`] for why the MPU requires it).
545///
546/// The caller holds a critical section (PRIMASK=1). An `svc` issued with
547/// PRIMASK set runs at execution priority 0 — equal to the SVC's own
548/// default priority — which the architecture escalates to HardFault
549/// (QEMU's NVIC does exactly this). So PRIMASK is briefly cleared around
550/// the `svc`. This is safe: the SVC handler runs at priority 0, the
551/// highest configurable priority, so nothing (SysTick/PendSV at 0xFF) can
552/// preempt the frame write; the critical section's purpose — no task runs
553/// mid-initialization — is preserved.
554///
555/// (A real, if narrow, gap exists in the handful of instructions between
556/// `cpsie` and the `svc` actually being taken, and again between the
557/// `svc` returning and `cpsid` — during which SysTick/PendSV genuinely
558/// could preempt Thread-mode execution, even though nothing can preempt
559/// the SVC handler's own body once it's running. Tried closing it with
560/// `BASEPRI` — raise it to mask everything below SVC's priority before
561/// clearing `PRIMASK`, lower it back after — which is architecturally
562/// the right tool, but it reproducibly hard-faulted `cm3/demo` under
563/// QEMU's lm3s6965 model (root cause not yet isolated — possibly a
564/// `BASEPRI`-handling difference in QEMU's NVIC model, possibly a real
565/// interaction this crate's other assumptions don't account for). Reset
566/// back to the plain `PRIMASK` toggle rather than ship a fix that traded
567/// a rare real-hardware race for a reliable QEMU regression; revisit
568/// with `BASEPRI` again once the QEMU-specific failure is understood.)
569#[no_mangle]
570unsafe extern "Rust" fn __rivet_arch_init_task_stack(
571    stack_ptr: *mut u8,
572    stack_len: usize,
573    entry_fn: usize,
574    arg: usize,
575) -> usize {
576    let ptr = stack_ptr as usize;
577    let mut sp = 0usize;
578    // SAFETY: the SVC handler reads r0-r3 from the exception frame, builds
579    // the frame, and writes the new sp back into r0.
580    unsafe {
581        let mut primask: u32;
582        core::arch::asm!(
583            "mrs {0}, primask",
584            out(reg) primask,
585            options(nomem, nostack, preserves_flags),
586        );
587        if primask & 1 != 0 {
588            core::arch::asm!("cpsie i", options(nomem, nostack, preserves_flags));
589        }
590        core::arch::asm!(
591            "svc 0",
592            inout("r0") ptr => sp,
593            in("r1") stack_len,
594            in("r2") entry_fn,
595            in("r3") arg,
596            options(nomem, nostack, preserves_flags),
597        );
598        if primask & 1 != 0 {
599            core::arch::asm!("cpsid i", options(nomem, nostack, preserves_flags));
600        }
601    }
602    sp
603}
604
605/// Cortex-M system reset via SCB AIRCR SYSRESETREQ. A utility for BSPs'
606/// `__rivet_board_reset` implementation — architecturally universal, not
607/// board-specific.
608pub fn system_reset() -> ! {
609    // SAFETY: `0xE000ED0C` is the fixed SCB AIRCR register; writing
610    // VECTKEY=0x05FA | SYSRESETREQ=1 requests a system reset.
611    unsafe {
612        core::ptr::write_volatile(0xE000_ED0C as *mut u32, 0x05FA_0004);
613    }
614    loop {
615        core::hint::spin_loop();
616    }
617}