rivet-arch-cortex-m 0.2.0

Rivet RTOS: ARM Cortex-M ISA port (context switch, PendSV, MPU) — no board/MMIO knowledge
Documentation
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
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
//! Rivet RTOS — ARM Cortex-M ISA port.
//!
//! Implements the Group A (`rivet::port::arch`) symbol contract for
//! Cortex-M targets: PendSV context switch, MemManage fault handling, MPU
//! programming, SysTick tick source. Contains **no board/MMIO knowledge**
//! beyond what's genuinely part of the Cortex-M architecture (SCB, MPU,
//! SysTick, and their fixed System Control Space addresses — identical on
//! every Cortex-M3/4/7/33). A board's clock rate, console, and exit/reset
//! path are supplied separately by a `rivet-bsp-*` crate.
//!
//! # Preemptive context switch
//!
//! Tasks run in Thread mode using PSP (Process Stack Pointer); exceptions
//! (SysTick, PendSV, everything else) always run in Handler mode using MSP
//! (Main Stack Pointer) — automatic Cortex-M behavior. That split matters:
//! a PendSV handler's own nested Rust calls (the scheduler, atomics, etc.)
//! run on MSP, never touching a task's PSP-based stack — no RISC-V-style
//! risk of the scheduler's own call chain competing for space with
//! whatever a task had reserved for itself.
//!
//! Following ARM's recommended pattern: SysTick only *requests* a
//! reschedule (`SCB.ICSR.PENDSVSET`); the actual register save/restore and
//! scheduling decision happen in PendSV, which — being the lowest-priority
//! exception — never preempts a higher-priority ISR mid-flight.

#![no_std]

// This crate's context-switch/SVC asm assumes a soft-float ABI
// throughout: `rivet_svc_handler`'s EXC_RETURN decode checks the fixed
// byte pattern for "no FP frame" (`0xFD`/`0xF9`) rather than testing
// EXC_RETURN bit 2 (the FP-context-active flag), and neither it nor the
// `PendSV` handler save/restore `s16-s31`/`FPSCR`. Both are silently
// correct only because a soft-float target (`thumbv7em-none-eabi`, not
// `-eabihf`) never sets the FPU's context-active state (`FPCA`) in the
// first place — no VFP instruction is ever emitted, so there's no FP
// context to lose. Catch the unsupported configuration at compile time
// instead of producing a corrupted stack frame (garbage SP from
// `rivet_svc_handler` reading the wrong exception-frame location) the
// first time a task is spawned from inside another task on a hardfloat
// build.
#[cfg(target_feature = "vfp2")]
compile_error!(
    "rivet-arch-cortex-m assumes a soft-float ABI (build for e.g. \
     thumbv7em-none-eabi, not -eabihf) — see this crate's own module \
     docs for what a hardfloat port would additionally need"
);

pub mod dwt;
pub mod mpu;
#[cfg(feature = "nvic")]
pub mod nvic;
pub mod semihosting;
#[cfg(feature = "systick")]
pub mod systick;

/// Minimum task stack: the PendSV frame (32 bytes r4-r11 + 32 bytes
/// hardware-stacked r0-r3/r12/lr/pc/xPSR) plus slack for the entry
/// trampoline.
pub const MIN_TASK_STACK: usize = 64 + 64;

#[no_mangle]
extern "Rust" fn __rivet_arch_init() {
    // SCB.VTOR's reset value is architecturally 0x00000000 — correct for
    // every board in this workspace that happens to load its flash at
    // address 0 (QEMU's `lm3s6965evb`/`mps2-an385`), a no-op write here,
    // but *wrong* for a board whose vector table lives somewhere else
    // (a real chip's actual flash base, e.g. the STM32F401RE's
    // 0x08000000): without this, exceptions vector through whatever
    // VTOR defaults to instead of the board's real table, which reads
    // as "boot works, then total silence the instant the first
    // interrupt (SysTick) would otherwise fire" — no fault, no crash,
    // because the hardware is faithfully doing exactly what it's told
    // to, just not with the table this kernel actually built. Confirmed
    // by bisection on real STM32F401RE hardware (see
    // rivet-bsp-stm32f401re/link-stm32f401re.ld's own doc for the full
    // story). `__vector_table` is provided by every Cortex-M board's
    // linker script in this workspace, so this is unconditional, not
    // feature-gated.
    unsafe extern "C" {
        static __vector_table: u32;
    }
    // SAFETY: `SCB::PTR` is the statically-known System Control Block
    // base; `__vector_table` is a linker-defined symbol (its address,
    // not its value, is what VTOR needs — matches every other `la
    // __symbol`-style linker-script constant this workspace uses).
    unsafe {
        (*cortex_m::peripheral::SCB::PTR)
            .vtor
            .write(core::ptr::addr_of!(__vector_table) as u32);
    }

    mpu::init();
    dwt::init();

    // PendSV must run at the lowest possible priority so it never preempts
    // a higher-priority ISR mid-flight — it only runs once everything else
    // has finished, which is what makes it safe to do the actual stack
    // switch there. Set SHPR3.PRI_14 (PendSV) and SHPR3.PRI_15 (SysTick)
    // to the lowest priority (0xFF, all implemented priority bits set).
    //
    // SAFETY: `SCB::PTR` is the statically-known System Control Block
    // base, valid on every Cortex-M; these SHPR/SHCSR writes are volatile
    // MMIO accesses and the SCB is exclusively owned by this module.
    unsafe {
        let scb = &*cortex_m::peripheral::SCB::PTR;
        scb.shpr[10].write(0xFF); // PendSV priority (SHPR3 byte 2)
        scb.shpr[11].write(0xFF); // SysTick priority (SHPR3 byte 3)
                                  // Enable the dedicated Bus/Usage/MemManage fault handlers; without
                                  // this they escalate straight to HardFault, hiding the real cause.
        scb.shcsr.write(
            (1 << 16) // MEMFAULTENA
            | (1 << 17) // BUSFAULTENA
            | (1 << 18), // USGFAULTENA
        );
    }

    // Every external NVIC IRQ resets to priority 0 — the *highest*
    // configurable priority, strictly above PendSV/SysTick's 0xFF. Left
    // alone, any future peripheral IRQ that touches kernel state
    // (`rivet::irq::dispatch` calling `unblock`/a waker, or anything
    // else that ends up in `sched`/`timer`) could preempt PendSV or
    // SysTick *mid-reschedule* — the two-separate-atomics
    // `READY_BITMAP`/`QUEUES` update `sched::ready_add`/`ready_remove`
    // do is exactly the kind of thing that isn't safe to interrupt.
    // Floor every implemented IRQ to PendSV/SysTick's own 0xFF so
    // nothing outranks the scheduler unless a board *deliberately*
    // raises one (every board that registers its own IRQ already does,
    // explicitly, via `rivet::irq::set_priority` — matching this floor,
    // not fighting it). `NVIC::PTR.ipr` covers the architectural maximum
    // (240 IRQs); writing entries a given chip doesn't implement is
    // architecturally safe (unimplemented IPR bits/registers are
    // fixed/ignored, never a fault).
    //
    // SAFETY: `NVIC::PTR` is the statically-known NVIC base, valid on
    // every Cortex-M; IPR is byte-addressable, plain volatile MMIO,
    // and this runs once, before any IRQ is enabled.
    unsafe {
        let nvic = &*cortex_m::peripheral::NVIC::PTR;
        for ipr in nvic.ipr.iter() {
            ipr.write(0xFF);
        }
    }
}

#[no_mangle]
extern "Rust" fn __rivet_arch_idle() {
    cortex_m::asm::wfi();
}

#[no_mangle]
extern "Rust" fn __rivet_arch_min_task_stack() -> usize {
    MIN_TASK_STACK
}

/// No hardware minimum: the CM3 MPU denies the whole task-stack pool with
/// one region rather than a per-stack guard (see this crate's own
/// `on_switch_to`), so this value is unused for actual protection —
/// still a real power of two, matching the historical guard size, since
/// `rivet::preempt::stack_pool`'s layout math needs *some* value.
#[no_mangle]
extern "Rust" fn __rivet_arch_min_guard_size() -> usize {
    64
}

#[no_mangle]
extern "Rust" fn __rivet_arch_cycle_count() -> u64 {
    dwt::cycle_count()
}

/// plan.md Phase 13: these three are hard-required by the port contract
/// (every existing binary must still link even if it never enables the
/// `nvic` feature), so they're defined unconditionally here rather than
/// only inside `nvic.rs` — a board that doesn't enable `nvic` gets a
/// harmless no-op instead of a link error naming a symbol it doesn't need.
#[no_mangle]
extern "Rust" fn __rivet_arch_irq_enable(_irq_num: u32) {
    #[cfg(feature = "nvic")]
    nvic::enable(_irq_num);
}

#[no_mangle]
extern "Rust" fn __rivet_arch_irq_disable(_irq_num: u32) {
    #[cfg(feature = "nvic")]
    nvic::disable(_irq_num);
}

#[no_mangle]
extern "Rust" fn __rivet_arch_irq_set_priority(_irq_num: u32, _priority: u8) {
    #[cfg(feature = "nvic")]
    nvic::set_priority(_irq_num, _priority);
}

/// plan.md Phase 19: every Cortex-M board this workspace targets is
/// QEMU-modeled strictly single-core (confirmed empirically — `-smp 4`
/// is rejected outright by both `lm3s6965evb` and `mps2-an385`), so this
/// is always hart 0.
#[no_mangle]
extern "Rust" fn __rivet_arch_hart_id() -> usize {
    0
}

/// plan.md Phase 19: never called with `hart != 0` on a single-core
/// board (see `__rivet_arch_hart_id`'s docs above) — aliasing straight to
/// the self-reschedule path keeps the contract satisfiable without a
/// separate no-op that would silently swallow a real bug if it ever were
/// called with a nonzero hart.
#[no_mangle]
extern "Rust" fn __rivet_arch_request_reschedule_on(hart: usize) {
    debug_assert_eq!(hart, 0, "rivet-arch-cortex-m: single-core, hart must be 0");
    __rivet_arch_request_reschedule();
}

/// plan.md Phase 12: cycle stamp at the moment a reschedule was
/// requested, consumed by `rivet_pendsv_rust` to record `IrqEntry`
/// latency — the single trigger point below covers both the tick-driven
/// and voluntary-yield paths uniformly (unlike RISC-V, Cortex-M has no
/// separate "just entered the handler" asm hook that's safe to touch
/// without risking the hand-tuned PendSV register-save sequence).
#[cfg(feature = "latency-histograms")]
static RESCHEDULE_REQUESTED_AT: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);

/// Set PendSV pending. Single trigger for every context switch, whether
/// tick-driven or a voluntary yield.
#[no_mangle]
extern "Rust" fn __rivet_arch_request_reschedule() {
    #[cfg(feature = "latency-histograms")]
    RESCHEDULE_REQUESTED_AT.store(dwt::cycle_count() as u32, core::sync::atomic::Ordering::Relaxed);
    // SAFETY: `SCB::PTR` is the statically-known System Control Block
    // base, valid on every Cortex-M; `ICSR` write is a volatile MMIO
    // access.
    unsafe {
        let scb = &*cortex_m::peripheral::SCB::PTR;
        scb.icsr.write(1 << 28); // PENDSVSET
    }
}

#[no_mangle]
extern "Rust" fn __rivet_arch_irq_save() -> usize {
    // `Primask::is_active()` means "exceptions are active", i.e.
    // interrupts are currently *enabled* (PRIMASK bit clear) — no
    // negation here, unlike a naive reading of the name might suggest.
    let was_enabled = cortex_m::register::primask::read().is_active();
    cortex_m::interrupt::disable();
    was_enabled as usize
}

#[no_mangle]
extern "Rust" fn __rivet_arch_irq_restore(token: usize) {
    if token != 0 {
        // SAFETY: re-enabling interrupts only if they were enabled at the
        // matching `__rivet_arch_irq_save` call.
        unsafe { cortex_m::interrupt::enable() };
    }
}

#[no_mangle]
extern "Rust" fn __rivet_arch_on_switch_to(stack_base: usize, stack_size: usize) {
    mpu::set_current_stack(stack_base, stack_size);
}

#[no_mangle]
extern "Rust" fn __rivet_arch_guard_register(_guard_base: usize, _slot: usize) {
    // No per-task locked guard on Cortex-M: the two-region MPU design
    // (whole-pool deny + current-stack allow) already gives full mutual
    // stack isolation without per-task PMP-style entries.
}

#[no_mangle]
extern "Rust" fn __rivet_arch_scratch_open(base: usize, size: usize) {
    mpu::allow_scratch(base, size);
}

#[no_mangle]
extern "Rust" fn __rivet_arch_scratch_close() {
    mpu::clear_scratch();
}

// ── Preemptive tier: PendSV context switch ────────────────────────

/// Rust-side PendSV logic. Called from the asm handler with `interrupted_sp`
/// (the interrupted task's PSP, pointing at its saved r4-r11 frame). Saves
/// the interrupted task's registers (already on the stack), asks the
/// scheduler what to run next, and returns the stack pointer to resume.
#[no_mangle]
unsafe extern "C" fn rivet_pendsv_rust(interrupted_sp: usize) -> usize {
    #[cfg(feature = "latency-histograms")]
    {
        let requested_at = RESCHEDULE_REQUESTED_AT.load(core::sync::atomic::Ordering::Relaxed);
        let now = dwt::cycle_count() as u32;
        rivet::latency::record(
            rivet::latency::Kind::IrqEntry,
            now.wrapping_sub(requested_at) as u64,
        );
    }
    rivet::preempt::on_tick(interrupted_sp)
}

core::arch::global_asm!(
    ".section .text.rivet_task_exit",
    ".global rivet_task_exit",
    ".thumb_func",
    "rivet_task_exit:",
    "  bl   rivet_task_exit_core", // r0/r1 carry the return value
    "1:",
    "  b    1b",
);

core::arch::global_asm!(
    ".section .text.PendSV",
    ".global PendSV",
    ".thumb_func",
    "PendSV:",
    // A lone `push {{lr}}` (one word) leaves MSP 4-mod-8 across the `bl`
    // below, an AAPCS 8-byte-alignment violation — harmless on M3 (no
    // LDRD/VLDR here), latent on M4F/M7. Fixed with an explicit `sub sp,
    // #4` instead of padding the push list with a second register: r4-r11
    // are semantically live across this function (r4 in particular gets
    // overwritten by `ldmia` below with the *new* task's value, so
    // pushing/popping it here would restore the *old* task's stale r4
    // right before returning — a real bug caught in review, not shipped).
    "  push {{lr}}",
    "  sub  sp, sp, #4",
    "  mrs  r0, psp",
    "  subs r0, r0, #32",
    "  stmia r0, {{r4-r11}}",
    "  bl   rivet_pendsv_rust",
    "  ldmia r0, {{r4-r11}}",
    "  adds r0, r0, #32",
    "  msr  psp, r0",
    "  add  sp, sp, #4",
    "  pop  {{lr}}",
    // Symbol for the GDB context-switch verification script (tests/gdb):
    // r4-r11 have been restored from the frame; frame base = psp - 32.
    ".global rivet_pendsv_resume",
    "rivet_pendsv_resume:",
    "  bx   lr",
);

// ── First task start / initial stack frame ────────────────────────

/// Set up the initial stack frame for a new task, then start the first
/// task's execution. Called once, from `preempt::start`, with the first
/// task's already-built stack frame.
#[no_mangle]
unsafe extern "Rust" fn __rivet_arch_start_first_task(sp: usize) -> ! {
    // SAFETY: `sp` is the freshly-built initial frame of the first task;
    // PSP is set exactly once here, before any interrupt can fire.
    let frame = sp as *const u32;
    let arg = unsafe { core::ptr::read(frame.add(8)) };
    let entry_fn = unsafe { core::ptr::read(frame.add(14)) };

    unsafe {
        core::arch::asm!(
            "msr psp, {sp}",
            "movs r2, #2",
            "msr control, r2", // SPSEL=1 (use PSP in Thread mode), stay privileged
            "isb",
            sp = in(reg) sp,
            out("r2") _,
        );
    }

    // PSP is valid now — safe to let SysTick/PendSV start firing.
    #[cfg(feature = "systick")]
    systick::enable();

    // Root cause (plan.md Phase 24), found via a real regression on real
    // Cortex-M hardware: `rivet::preempt::start()` now wraps its call
    // into this function in `port::arch::critical_section` (masking
    // interrupts for the whole gap between the scheduling decision and
    // this function actually consuming the picked task's state — closes
    // a real race found on Xtensa dual-core, plan.md Phase 24's own
    // section has the full story). That wrapper's own interrupt-restore
    // never runs, because its closure diverges into this `-> !`
    // function — every arch's `start_first_task` is now responsible for
    // re-enabling interrupts itself as part of dispatch. RISC-V's
    // `mret`-based resume already does this implicitly (the fabricated
    // context's own `mstatus` carries `MIE = 1`); this port had no
    // equivalent, so the very first task's interrupts silently never
    // came back — the whole system froze the instant any code past this
    // point needed a tick or exception. `cortex_m::interrupt::enable()`
    // is the same primitive `__rivet_arch_irq_restore` above already
    // uses.
    unsafe {
        cortex_m::interrupt::enable();
    }

    unsafe {
        core::arch::asm!(
            "mov r0, {arg}",
            "bx {entry}",
            arg = in(reg) arg,
            entry = in(reg) entry_fn,
            options(noreturn)
        );
    }
}

/// Frame layout (aligned to 8 bytes, 64 bytes total):
/// ```text
/// [sp+0]  r4
/// [sp+4]  r5
/// [sp+8]  r6
/// [sp+12] r7
/// [sp+16] r8
/// [sp+20] r9
/// [sp+24] r10
/// [sp+28] r11
/// [sp+32] r0   <- arg
/// [sp+36] r1
/// [sp+40] r2
/// [sp+44] r3
/// [sp+48] r12
/// [sp+52] lr   <- entry_fn (with Thumb bit set)
/// [sp+56] pc   <- entry_fn (with Thumb bit set)
/// [sp+60] xPSR <- 0x01000000 (Thumb mode)
/// ```
/// The PendSV handler restores r4-r11 from the first 32 bytes; the
/// hardware un-stacks the remaining 32 bytes on exception return, resuming
/// at `entry_fn` with `r0 = arg`.
unsafe fn init_task_stack_impl(stack: &mut [u8], entry_fn: usize, arg: usize) -> usize {
    const FRAME_WORDS: usize = 16; // 8 (r4-r11) + 8 (hw frame)
    const STACK_ALIGN: usize = 16;

    // SAFETY: `stack` is a valid mutable slice of at least MIN_TASK_STACK
    // bytes (the caller guarantees this); the writes below initialize the
    // frame INSIDE the slice (at the top, aligned down).
    unsafe {
        let base = stack.as_mut_ptr() as usize;
        let top = base + stack.len();
        let frame_start = (top - FRAME_WORDS * 4) & !(STACK_ALIGN - 1);
        let frame = frame_start as *mut u32;

        for i in 0..FRAME_WORDS {
            core::ptr::write(frame.add(i), 0);
        }
        core::ptr::write(frame.add(8), arg as u32); // r0
                                                    // r1,r2,r3,r12 (words 9-12) stay 0
        extern "C" {
            fn rivet_task_exit();
        }
        core::ptr::write(frame.add(13), rivet_task_exit as *const () as usize as u32); // lr
        core::ptr::write(frame.add(14), entry_fn as u32); // pc
        core::ptr::write(frame.add(15), 0x0100_0000); // xPSR: Thumb bit (T=1) set

        frame_start
    }
}

/// SVC-vectored kernel call: builds a new task's initial stack frame from
/// *Handler* mode, where the MPU does not apply the way it does in Thread
/// mode. Thread-mode code cannot write another task's stack: MPU region 6
/// denies the whole `.task_stacks` pool and region 7 only permits the
/// *current* task's stack — a spawner faulting on the new task's stack is
/// exactly what an unprivileged `init_task_stack` would hit.
///
/// Naked (no prologue): the exception frame base must be read from `sp`
/// *before* the compiler pushes anything, and the exception return value
/// in `lr` must be preserved across the `bl rivet_svc_core` call so the
/// handler returns with `bx lr` (EXC_RETURN), not a normal branch.
///
/// Preserves `lr` with a real `push`/`pop` on this handler's own stack
/// (MSP — Handler mode always uses it), the same shape `PendSV`'s own
/// asm below already uses for the identical alignment reason, rather
/// than stashing it in a register across the call. An earlier version
/// used `mov r4, lr` instead: `r4` is AAPCS callee-saved, but this naked
/// handler has no prologue to actually save/restore the *caller's*
/// (i.e. the interrupted code's) live `r4` — so it silently clobbered
/// whatever value the compiler's register allocator happened to be
/// keeping there, live across the SVC boundary, the instant it made
/// that choice (confirmed by disassembling a real build where it did
/// exactly that). `r12` isn't a fix either: it's AAPCS *caller*-saved,
/// so `bl rivet_svc_core` is free to clobber it too. `r1` here is dead
/// (only used for the EXC_RETURN low-byte check, already done) and just
/// rides along as the push/pop's 8-byte-alignment partner.
///
/// # Safety
/// Exception entry point; installed via the board's vector table
/// (`rivet-rt`); never called directly.
#[unsafe(naked)]
#[no_mangle]
unsafe extern "C" fn rivet_svc_handler() {
    // SAFETY: naked handler with no stack frame; the register-level
    // protocol with `rivet_svc_core` is documented in the doc comment.
    core::arch::naked_asm!(
        "uxtb r1, lr",    // EXC_RETURN 0xFFFFFFFD = taken from thread
        "cmp  r1, #0xfd", // mode with PSP (spawn from a running task);
        "bne  1f",        // 0xF9 = thread mode with MSP (boot context)
        "mrs  r0, psp",   // frame on PSP
        "b    2f",
        "1:",
        "mov  r0, sp", // frame on MSP — computed before the push below
                       // touches *this* handler's own (MSP) stack
        "2:",
        "push {{r1, lr}}", // preserve EXC_RETURN across the call
        "bl   rivet_svc_core",
        "pop  {{r1, lr}}",
        "bx   lr", // exception return
    );
}

/// Rust half of [`rivet_svc_handler`]: `frame` is the exception stack
/// frame ({r0,r1,r2,r3,r12,lr,pc,xPSR}) pushed by the `svc 0` issued from
/// `__rivet_arch_init_task_stack`.
#[no_mangle]
fn rivet_svc_core(frame: *mut u32) {
    // SAFETY: the caller guarantees `frame` points at the live exception
    // stack frame ({r0,r1,r2,r3,...}) pushed by the `svc 0`; all four
    // slots are valid, word-aligned reads.
    let (stack_ptr, stack_len, entry, arg) = unsafe {
        (
            *frame.add(0) as *mut u8,
            *frame.add(1) as usize,
            *frame.add(2) as usize,
            *frame.add(3) as usize,
        )
    };

    // SAFETY: the caller passed a valid `&mut [u8]` slice split across
    // r0/r1 (as_mut_ptr / len).
    let sp = unsafe {
        // The ARMv7-M MPU applies in Handler mode too, so the write into
        // the denied `.task_stacks` pool would fault even here. Disable
        // the MPU for the duration of the frame write (real RTOSes do the
        // same); the SVC handler runs at the highest configurable priority
        // so nothing can preempt us mid-window.
        let saved = mpu::disable_for_scope();
        let sp = init_task_stack_impl(
            core::slice::from_raw_parts_mut(stack_ptr, stack_len),
            entry,
            arg,
        );
        mpu::restore_after_scope(saved);
        sp
    };
    // Deliver the result via the exception frame's saved r0.
    // SAFETY: `frame` points at the live exception stack frame on MSP.
    unsafe {
        core::ptr::write_volatile(frame, sp as u32);
    }
}

/// Issue `init_task_stack_impl` from Handler mode via SVC (see
/// [`rivet_svc_handler`] for why the MPU requires it).
///
/// The caller holds a critical section (PRIMASK=1). An `svc` issued with
/// PRIMASK set runs at execution priority 0 — equal to the SVC's own
/// default priority — which the architecture escalates to HardFault
/// (QEMU's NVIC does exactly this). So PRIMASK is briefly cleared around
/// the `svc`. This is safe: the SVC handler runs at priority 0, the
/// highest configurable priority, so nothing (SysTick/PendSV at 0xFF) can
/// preempt the frame write; the critical section's purpose — no task runs
/// mid-initialization — is preserved.
///
/// (A real, if narrow, gap exists in the handful of instructions between
/// `cpsie` and the `svc` actually being taken, and again between the
/// `svc` returning and `cpsid` — during which SysTick/PendSV genuinely
/// could preempt Thread-mode execution, even though nothing can preempt
/// the SVC handler's own body once it's running. Tried closing it with
/// `BASEPRI` — raise it to mask everything below SVC's priority before
/// clearing `PRIMASK`, lower it back after — which is architecturally
/// the right tool, but it reproducibly hard-faulted `cm3/demo` under
/// QEMU's lm3s6965 model (root cause not yet isolated — possibly a
/// `BASEPRI`-handling difference in QEMU's NVIC model, possibly a real
/// interaction this crate's other assumptions don't account for). Reset
/// back to the plain `PRIMASK` toggle rather than ship a fix that traded
/// a rare real-hardware race for a reliable QEMU regression; revisit
/// with `BASEPRI` again once the QEMU-specific failure is understood.)
#[no_mangle]
unsafe extern "Rust" fn __rivet_arch_init_task_stack(
    stack_ptr: *mut u8,
    stack_len: usize,
    entry_fn: usize,
    arg: usize,
) -> usize {
    let ptr = stack_ptr as usize;
    let mut sp = 0usize;
    // SAFETY: the SVC handler reads r0-r3 from the exception frame, builds
    // the frame, and writes the new sp back into r0.
    unsafe {
        let mut primask: u32;
        core::arch::asm!(
            "mrs {0}, primask",
            out(reg) primask,
            options(nomem, nostack, preserves_flags),
        );
        if primask & 1 != 0 {
            core::arch::asm!("cpsie i", options(nomem, nostack, preserves_flags));
        }
        core::arch::asm!(
            "svc 0",
            inout("r0") ptr => sp,
            in("r1") stack_len,
            in("r2") entry_fn,
            in("r3") arg,
            options(nomem, nostack, preserves_flags),
        );
        if primask & 1 != 0 {
            core::arch::asm!("cpsid i", options(nomem, nostack, preserves_flags));
        }
    }
    sp
}

/// Cortex-M system reset via SCB AIRCR SYSRESETREQ. A utility for BSPs'
/// `__rivet_board_reset` implementation — architecturally universal, not
/// board-specific.
pub fn system_reset() -> ! {
    // SAFETY: `0xE000ED0C` is the fixed SCB AIRCR register; writing
    // VECTKEY=0x05FA | SYSRESETREQ=1 requests a system reset.
    unsafe {
        core::ptr::write_volatile(0xE000_ED0C as *mut u32, 0x05FA_0004);
    }
    loop {
        core::hint::spin_loop();
    }
}