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}