diff --git a/crates/hal/src/arch/aarch64/boot.rs b/crates/hal/src/arch/aarch64/boot.rs index 29d715a..3689903 100644 --- a/crates/hal/src/arch/aarch64/boot.rs +++ b/crates/hal/src/arch/aarch64/boot.rs @@ -60,13 +60,48 @@ core::arch::global_asm!( " ldr x0, =0x000000000080351bULL", " msr tcr_el1, x0", " isb", - // ── MMU: left OFF for initial bring-up ─────────────────────────────── - // The MAIR/TCR values above are staged, but enabling SCTLR_EL1.M - // without a valid TTBR0_EL1 translation table faults the very next - // instruction fetch (level-1 translation abort). Until an identity - // page table is installed in TTBR0_EL1, run with the MMU off (flat - // physical addressing), which is sufficient to reach the serial - // console and the per-arch init path on the QEMU `virt` machine. + // ── Build a minimal identity page table in TTBR0_EL1 ───────────────── + // With TCR T0SZ=27 (37-bit VA) and a 4 KiB granule, the top level is + // L1 where each entry maps a 1 GiB block. We identity-map two 1 GiB + // blocks, which covers everything the QEMU `virt` machine needs: + // entry 0: VA/PA 0x0000_0000..0x4000_0000 — device MMIO + // (GICD 0x0800_0000, GICR 0x080A_0000, PL011 0x0900_0000), + // MAIR index 1 (Device-nGnRnE). + // entry 1: VA/PA 0x4000_0000..0x8000_0000 — RAM (kernel loads at + // 0x4008_0000), MAIR index 0 (Normal WB/WA). + // Block descriptor bits: bit0=1 (valid), bit1=0 (block, not table), + // AttrIndx = bits[4:2], NS=bit5, AP=0b00 bits[7:6] (EL1 RW), SH=0b11 + // bits[9:8] (inner shareable), AF=bit10 (access flag) = 1. + // Normal block flags = AF|SH_inner|AttrIdx0|valid = (1<<10)|(3<<8)|(0<<2)|1 + // Device block flags = AF|AttrIdx1|valid = (1<<10)|(1<<2)|1 + // (device memory is outer-shareable implicitly; SH is ignored for it.) + " adrp x0, __ttbr0_l1", + " add x0, x0, :lo12:__ttbr0_l1", + // entry 0 → device block at PA 0 + " mov x1, #0x0", + " movz x2, #0x0405", // (1<<10)|(1<<2)|1 = 0x405 + " orr x1, x1, x2", + " str x1, [x0]", + // entry 1 → normal block at PA 0x4000_0000 + " movz x1, #0x4000, lsl #16", // PA 0x4000_0000 + " movz x2, #0x0701", // (1<<10)|(3<<8)|1 = 0x701 + " orr x1, x1, x2", + " str x1, [x0, #8]", + // TTBR0_EL1 = &__ttbr0_l1 + " msr ttbr0_el1, x0", + " isb", + // Invalidate TLB and ensure page-table writes are visible. + " dsb ish", + " tlbi vmalle1", + " dsb ish", + " isb", + // ── SCTLR_EL1: enable MMU (M=1), D-cache (C=1), I-cache (I=1) ──────── + " mrs x0, sctlr_el1", + " orr x0, x0, #(1 << 0)", // M — MMU enable + " orr x0, x0, #(1 << 2)", // C — D-cache enable + " orr x0, x0, #(1 << 12)", // I — I-cache enable + " msr sctlr_el1, x0", + " isb", // ── Enable FP/SIMD access at EL0/EL1 ───────────────────────────────── // Rust codegen for aarch64 emits Advanced SIMD/NEON (memcpy, slice ops, // formatting). Without CPACR_EL1.FPEN = 0b11 those instructions trap @@ -118,4 +153,12 @@ core::arch::global_asm!( " .space 65536", ".global __stack_top", "__stack_top:", + // ── TTBR0_EL1 level-1 identity page table (4 KiB, 4 KiB-aligned) ───── + // 512 × 8-byte descriptors; only entries 0 (device) and 1 (RAM) are + // populated by _start, the rest stay zero (invalid). BSS-resident so + // it is zeroed by the boot BSS-clear before use. + ".section .bss.pagetable", + ".align 12", + "__ttbr0_l1:", + " .space 4096", ); diff --git a/crates/kernel/src/arch/aarch64/kthread.rs b/crates/kernel/src/arch/aarch64/kthread.rs new file mode 100644 index 0000000..3dff470 --- /dev/null +++ b/crates/kernel/src/arch/aarch64/kthread.rs @@ -0,0 +1,253 @@ +// Copyright 2026 ONCRIX Contributors +// SPDX-License-Identifier: Apache-2.0 + +//! AArch64 kernel thread creation and management. +//! +//! Mirror of [`crate::arch::x86_64::kthread`] for the aarch64 port. +//! Provides the ability to spawn kernel threads with their own stacks +//! and CPU contexts. Kernel threads run at EL1 (Ring 0) and are used +//! for background tasks (idle loop, init, housekeeping). +//! +//! The distinguishing aarch64 detail is the **seeded switch frame**: +//! [`spawn_kthread`] writes a 96-byte frame at the top of the new +//! thread's stack laid out exactly the way +//! [`switch_context`](super::context::switch_context) pops it, so the +//! very first switch *into* the thread restores zeroed callee-saved +//! registers and `ret`s straight to the entry point. + +use super::context::CpuContext; +use oncrix_process::pid::{Pid, Tid, alloc_tid}; +use oncrix_process::thread::{Priority, Thread}; + +/// Kernel thread stack size (16 KiB per thread). +/// +/// Matches the x86_64 port: 8 KiB is too small for threads that nest +/// function calls, 16 KiB matches the Linux kernel default. +const KTHREAD_STACK_SIZE: usize = 16384; + +/// Maximum number of kernel threads. +const MAX_KTHREADS: usize = 32; + +/// Size of the seeded [`switch_context`](super::context::switch_context) +/// restore frame, in bytes (12 × 8 = the `x19`–`x30` callee-saved set +/// plus the frame pointer). A multiple of 16, so subtracting it from a +/// 16-aligned stack top keeps `SP` 16-aligned as AArch64 requires. +const SWITCH_FRAME_SIZE: u64 = 96; + +/// 16-byte aligned stack buffer. +/// +/// The AAPCS64 procedure call standard requires `SP` to be 16-byte +/// aligned at a public interface. Wrapping the stack array ensures the +/// base address (and therefore every 16-aligned offset) is compliant. +#[repr(C, align(16))] +#[derive(Clone, Copy)] +struct AlignedStack([u8; KTHREAD_STACK_SIZE]); + +impl AlignedStack { + const fn zero() -> Self { + Self([0; KTHREAD_STACK_SIZE]) + } +} + +/// Static pool of kernel thread stacks. +/// +/// Each thread gets a dedicated 16 KiB, 16-byte-aligned stack. Placed +/// in BSS so it does not bloat the kernel image. +static mut KTHREAD_STACKS: [AlignedStack; MAX_KTHREADS] = [AlignedStack::zero(); MAX_KTHREADS]; + +/// CPU contexts for kernel threads (parallel array with stacks). +static mut KTHREAD_CONTEXTS: [CpuContext; MAX_KTHREADS] = + [const { CpuContext::empty() }; MAX_KTHREADS]; + +/// Allocation bitmap: true = slot in use. +static mut KTHREAD_USED: [bool; MAX_KTHREADS] = [false; MAX_KTHREADS]; + +/// Execute a closure with interrupts disabled, restoring the previous +/// `DAIF` interrupt-mask state on return. +/// +/// AArch64 analogue of the x86_64 `pushfq`/`cli`/`sti` helper: the +/// current `DAIF` field is captured with `mrs`, all four masks +/// (`D`, `A`, `I`, `F`) are asserted with `msr daifset`, and the saved +/// state is written back verbatim with `msr daif` — so a caller that +/// was already masked stays masked. +/// +/// # Safety +/// +/// The closure must not enable interrupts itself. +#[inline] +unsafe fn with_interrupts_disabled(f: F) -> R +where + F: FnOnce() -> R, +{ + let daif: u64; + // SAFETY: Critical-section entry — read the current DAIF mask into + // `daif`, then mask all interrupts. `mrs`/`msr daifset` have no + // memory operands and do not touch the stack. + unsafe { + core::arch::asm!( + "mrs {0}, daif", + "msr daifset, #0b1111", + out(reg) daif, + options(nomem, nostack), + ); + } + let result = f(); + // SAFETY: Restore the exact DAIF state captured above. `msr daif` + // writes only the interrupt-mask PSTATE field. + unsafe { + core::arch::asm!("msr daif, {0}", in(reg) daif, options(nomem, nostack)); + } + result +} + +/// Kernel thread descriptor returned after spawning. +#[derive(Debug, Clone, Copy)] +pub struct KernelThread { + /// Thread ID. + pub tid: Tid, + /// Slot index in the static pool. + pub slot: usize, +} + +/// Spawn a new kernel thread. +/// +/// `entry` is the function the thread will execute. It must be +/// `extern "C" fn() -> !` (never returns). +/// +/// A 96-byte [`switch_context`](super::context::switch_context) restore +/// frame is seeded at the top of the thread's stack so that the first +/// switch *into* the thread pops zeroed callee-saved registers and +/// `ret`s to `entry`. The parallel [`CpuContext`] in the static pool is +/// initialised to point `SP` at that frame; callers copy it into the +/// scheduler-owned [`Thread`] via [`kthread_context`]. +/// +/// Returns a [`KernelThread`] descriptor and a [`Thread`] for the +/// scheduler. +/// +/// # Safety +/// +/// `entry` must point to a valid kernel function that never returns. +pub unsafe fn spawn_kthread( + entry: extern "C" fn() -> !, + priority: Priority, +) -> oncrix_lib::Result<(KernelThread, Thread)> { + // All access to the static arrays is wrapped in a critical section + // (DAIF-masked) to prevent races with interrupt handlers that might + // also inspect thread state. + + // Find a free slot. + // SAFETY: Interrupts are disabled for the duration of the search, + // preventing concurrent modification of the pool bitmap. + let slot = unsafe { + with_interrupts_disabled(|| { + let used_ptr = &raw mut KTHREAD_USED; + let mut found = None; + for (i, used) in (*used_ptr).iter_mut().enumerate() { + if !*used { + *used = true; + found = Some(i); + break; + } + } + found + }) + } + .ok_or(oncrix_lib::Error::OutOfMemory)?; + + // Compute the stack top. AArch64 stacks grow downward; the buffer is + // 16-byte aligned via `AlignedStack` and `KTHREAD_STACK_SIZE` is a + // multiple of 16, so `stack_top` is 16-aligned. + // SAFETY: Interrupts disabled; raw pointer to our own pool. + let stack_top = unsafe { + with_interrupts_disabled(|| { + let stacks_ptr = &raw const KTHREAD_STACKS; + let base = (*stacks_ptr)[slot].0.as_ptr(); + base as u64 + KTHREAD_STACK_SIZE as u64 + }) + }; + + // Seed the switch-context restore frame and the parallel CpuContext. + // + // `switch_context` restores the new thread with: + // ldp x19, x20, [sp, #80] + // ldp x21, x22, [sp, #64] + // ldp x23, x24, [sp, #48] + // ldp x25, x26, [sp, #32] + // ldp x27, x28, [sp, #16] + // ldp x29, x30, [sp], #96 ; x29 <- [sp+0], x30 <- [sp+8], sp += 96 + // ret ; branch to x30 + // + // So the only slot that matters is byte offset +8 (x30 = the address + // `ret` jumps to). Every other slot is a callee-saved GPR / frame + // pointer we simply zero. + // + // SAFETY: Interrupts disabled; `frame_base .. frame_base + 96` lies + // inside this slot's 16 KiB stack buffer, and we write our own + // context array. + let frame_base = stack_top - SWITCH_FRAME_SIZE; + unsafe { + with_interrupts_disabled(|| { + // Zero all twelve 8-byte slots of the frame. + for i in 0..12u64 { + let slot_ptr = (frame_base + i * 8) as *mut u64; + *slot_ptr = 0; + } + // x30 (link register / return address) lives at offset +8. + let x30_ptr = (frame_base + 8) as *mut u64; + *x30_ptr = entry as *const () as u64; + + let ctx_ptr = &raw mut KTHREAD_CONTEXTS; + let ctx = &mut (*ctx_ptr)[slot]; + *ctx = CpuContext::empty(); + // CpuContext.sp is byte offset 0 — the value switch_context + // loads into SP before popping the seeded frame. + ctx.sp = frame_base; + ctx.pc = entry as *const () as u64; + }); + } + + let tid = alloc_tid(); + let thread = Thread::new(tid, Pid::KERNEL, priority); + + let kt = KernelThread { tid, slot }; + + Ok((kt, thread)) +} + +/// Get a pointer to a kernel thread's CPU context. +/// +/// Callers copy the pointed-to [`CpuContext`] into the scheduler-owned +/// [`Thread`] (via [`Thread::set_cpu_context`]) so the scheduler's +/// context-switch datapath reads the seeded `SP`/`PC`. +/// +/// # Safety +/// +/// The slot must be a valid, in-use kernel thread slot. +pub unsafe fn kthread_context(slot: usize) -> *mut CpuContext { + // SAFETY: Interrupts disabled to prevent concurrent modification. + // The caller guarantees `slot` is valid. + unsafe { + with_interrupts_disabled(|| { + let ctx_ptr = &raw mut KTHREAD_CONTEXTS; + &raw mut (*ctx_ptr)[slot] + }) + } +} + +/// Free a kernel thread slot. +/// +/// # Safety +/// +/// The thread must no longer be scheduled or running. +pub unsafe fn free_kthread(slot: usize) { + if slot < MAX_KTHREADS { + // SAFETY: Interrupts disabled to prevent concurrent access; + // `slot` is bounds-checked above. + unsafe { + with_interrupts_disabled(|| { + let used_ptr = &raw mut KTHREAD_USED; + (*used_ptr)[slot] = false; + }); + } + } +} diff --git a/crates/kernel/src/arch/aarch64/mod.rs b/crates/kernel/src/arch/aarch64/mod.rs index 29db55b..6c48ec2 100644 --- a/crates/kernel/src/arch/aarch64/mod.rs +++ b/crates/kernel/src/arch/aarch64/mod.rs @@ -8,15 +8,20 @@ //! via VBAR_EL1 in the HAL boot stub (`crates/hal/src/arch/aarch64/boot.rs`). //! This module provides the Rust-side init functions called from `kernel_main`. //! -//! The `clone`, `context`, `init`, `init_embed`, `sched_glue`, and -//! `syscall_entry` submodules are aarch64 build stubs that mirror the -//! x86_64 public API so architecture-neutral kernel code type-checks; they -//! are not yet functional. +//! The `context`, `kthread`, and `sched_glue` submodules are functional +//! for **kernel-thread cooperative scheduling** (the [`switch_context`] +//! primitive plus a seeded-stack spawn path). The `clone`, `init_embed`, +//! and `syscall_entry` submodules remain aarch64 build stubs that mirror +//! the x86_64 public API so architecture-neutral kernel code type-checks; +//! the userspace/EL0 transition they need is not written yet. +//! +//! [`switch_context`]: context::switch_context pub mod clone; pub mod context; pub mod init; pub mod init_embed; +pub mod kthread; pub mod sched_glue; pub mod syscall_entry; diff --git a/crates/kernel/src/arch/aarch64/sched_glue.rs b/crates/kernel/src/arch/aarch64/sched_glue.rs index f0165ca..deb9520 100644 --- a/crates/kernel/src/arch/aarch64/sched_glue.rs +++ b/crates/kernel/src/arch/aarch64/sched_glue.rs @@ -3,37 +3,79 @@ //! AArch64 scheduler plumbing (context-switch driver). //! -//! aarch64 build stub — not yet functional. +//! Kernel-thread cooperative scheduling for the aarch64 bring-up port. +//! Unlike the x86_64 [`sched_glue`](crate::arch::x86_64::sched_glue), +//! this path deliberately does **no** userspace/TSS/CR3/syscall-mirror +//! work: aarch64 kernel threads all run at EL1 on the identity-mapped +//! address space installed by the boot stub (`TTBR0_EL1` is already +//! programmed), so a switch is just the callee-saved register swap +//! performed by [`switch_context`](super::context::switch_context). //! -//! Mirrors the public API of [`crate::arch::x86_64::sched_glue`] so the -//! architecture-neutral `current`/fork glue type-checks on aarch64. Real -//! preemption requires the aarch64 [`switch_context`](super::context) -//! implementation, which is not written yet. +//! Per-process address spaces and the ring 0 → EL0 transition needed for +//! *user* threads are still unimplemented; those hooks live in +//! [`crate::arch::aarch64::init`] as no-ops. use oncrix_process::context::Cr3Frame; use oncrix_process::scheduler::RoundRobinScheduler; -/// Attempt one round-robin preemption. +/// Attempt one round-robin, kernel-thread-only cooperative switch. /// -/// aarch64 build stub — not yet functional; always reports "no switch -/// happened" so callers fall through without touching CPU state. +/// Asks the scheduler for the next runnable thread via +/// [`RoundRobinScheduler::prepare_switch`]; if one exists, saves the +/// outgoing thread's callee-saved state and restores the incoming +/// thread's via [`switch_context`](super::context::switch_context). +/// +/// Returns `true` if a switch actually happened, `false` if no other +/// ready thread was available (or the scheduler handed back a null +/// context pointer). /// /// # Safety /// -/// Must be called with interrupts disabled. Currently a no-op that returns -/// `false`. -pub unsafe fn sched_yield_once(_sched: &mut RoundRobinScheduler) -> bool { - false +/// * Must be called with interrupts (`DAIF`) masked. +/// * `sched` must be the live kernel scheduler whose threads own the +/// kernel stacks referenced by the saved contexts. +/// * The outgoing thread must be reachable again via its own +/// [`CpuContext`](oncrix_process::context::CpuContext) once the switch +/// completes. +pub unsafe fn sched_yield_once(sched: &mut RoundRobinScheduler) -> bool { + let Some(t) = sched.prepare_switch() else { + return false; + }; + if t.prev_ctx.is_null() || t.next_ctx.is_null() { + return false; + } + // Kernel threads share the identity-mapped address space (TTBR0_EL1 + // is already installed), so there is no CR3/TSS or per-process + // page-table work to do around the switch — just swap register state. + // + // SAFETY: `prev_ctx`/`next_ctx` are non-null `CpuContext` pointers + // owned by threads in `sched` (checked above); interrupts are masked + // per this function's contract; `switch_context` upholds the AAPCS64 + // saved-frame invariant documented on it. + unsafe { + super::context::switch_context(t.prev_ctx, t.next_ctx); + } + true } -/// Read the active address-space root. +/// Read the active address-space root (`TTBR0_EL1`). /// -/// aarch64 build stub — not yet functional; returns [`Cr3Frame::NONE`]. On -/// aarch64 the real implementation would read `TTBR0_EL1`. +/// The aarch64 analogue of reading `CR3`. Consumed by the +/// architecture-neutral fork dispatch; the aarch64 kernel-thread +/// scheduler does not otherwise need it (kernel threads keep the boot +/// `TTBR0_EL1`). /// /// # Safety /// -/// Safe to call from any ring-0 context. Currently returns a placeholder. +/// Safe to call from any ring-0 (EL1) context; `mrs , ttbr0_el1` +/// is a privileged but side-effect-free system-register read. pub unsafe fn read_cr3() -> Cr3Frame { - Cr3Frame::NONE + let ttbr0: u64; + // SAFETY: Reading `TTBR0_EL1` is privileged but has no side effects; + // the ring-0 caller is legitimate by construction (kernel-only + // module). No memory operand, no stack use. + unsafe { + core::arch::asm!("mrs {0}, ttbr0_el1", out(reg) ttbr0, options(nomem, nostack)); + } + Cr3Frame::new(ttbr0) } diff --git a/crates/kernel/src/main.rs b/crates/kernel/src/main.rs index a30c634..3ce9058 100644 --- a/crates/kernel/src/main.rs +++ b/crates/kernel/src/main.rs @@ -43,6 +43,48 @@ core::arch::global_asm!(include_str!("arch/x86_64/boot.S"), options(att_syntax)) // // No additional global_asm! is needed here; the HAL's boot.rs provides _start. +/// AArch64 cooperative-scheduler bring-up demo: kernel thread A. +/// +/// Prints a marker proving it executed, then cooperatively yields back +/// to the scheduler. Never returns — parks in `wfi` if it is ever +/// resumed a second time. +#[cfg(target_arch = "aarch64")] +extern "C" fn demo_thread_a() -> ! { + let mut serial = Pl011::new(PL011_BASE); + let _ = serial.write_str("[ONCRIX/aarch64] cooperative scheduler: thread A ran\n"); + loop { + // SAFETY: interrupts are masked for the whole demo and no + // scheduler borrow is held across this cooperative yield. + unsafe { + let _ = oncrix_kernel::current::yield_now(); + } + // SAFETY: `wfi` parks the CPU until a wakeup event; it is + // harmless at EL1 and does not corrupt architectural state. + unsafe { + core::arch::asm!("wfi", options(nomem, nostack)); + } + } +} + +/// AArch64 cooperative-scheduler bring-up demo: kernel thread B. +/// +/// Counterpart to [`demo_thread_a`]; prints its own marker then yields. +#[cfg(target_arch = "aarch64")] +extern "C" fn demo_thread_b() -> ! { + let mut serial = Pl011::new(PL011_BASE); + let _ = serial.write_str("[ONCRIX/aarch64] cooperative scheduler: thread B ran\n"); + loop { + // SAFETY: see `demo_thread_a`. + unsafe { + let _ = oncrix_kernel::current::yield_now(); + } + // SAFETY: see `demo_thread_a`. + unsafe { + core::arch::asm!("wfi", options(nomem, nostack)); + } + } +} + /// Main kernel initialization sequence. /// /// Called from the 64-bit trampoline in `boot.S` after the 32-bit stub @@ -256,6 +298,78 @@ pub extern "C" fn kernel_main() -> ! { } let _ = serial.write_str("[ONCRIX/aarch64] All early initialization complete.\n"); + + // ─── Cooperative kernel-thread scheduling bring-up demo ─── + // + // Proves the aarch64 `switch_context` (#139) actually schedules + // real kernel threads at runtime. Sequence: + // 1. Mask DAIF — `switch_context`/`sched_yield_once` require + // interrupts masked, and a cooperative hand-off must not be + // preempted by the generic timer. + // 2. Register the running boot context as a schedulable thread + // and promote it to Running, so `prepare_switch` has a slot + // to save the outgoing state into. + // 3. Spawn two kernel threads. `spawn_kthread` seeds each + // stack's 96-byte switch frame (x30 = entry) plus a parallel + // `CpuContext`; copy that context into the scheduler-owned + // `Thread` before registering it. + // 4. Yield. Control flows boot → A → B → boot (round-robin), so + // both thread bodies print before we return here and halt. + { + use oncrix_kernel::arch::aarch64::kthread::{kthread_context, spawn_kthread}; + use oncrix_kernel::arch::init::SCHEDULER; + use oncrix_kernel::current::{spawn_thread, yield_now}; + use oncrix_process::pid::{Pid, alloc_tid}; + use oncrix_process::thread::{Priority, Thread}; + + // Step 1: mask all interrupts for the whole sequence. + // SAFETY: ring-0 (EL1) boot context; masking interrupts is the + // documented precondition of the switch/yield primitives. + unsafe { + core::arch::asm!("msr daifset, #0b1111", options(nomem, nostack)); + } + + let _ = + serial.write_str("[ONCRIX/aarch64] cooperative scheduler: bring-up demo start\n"); + + // Step 2: register + promote the boot thread. + // SAFETY: single-threaded boot; SCHEDULER is not aliased here, + // and each access below is a distinct, non-overlapping borrow. + unsafe { + let boot = Thread::new(alloc_tid(), Pid::KERNEL, Priority::NORMAL); + let _ = spawn_thread(boot); + let sched = &raw mut SCHEDULER; + let _ = (*sched).schedule(); + } + + // Step 3: spawn the two demo kernel threads and copy each + // seeded context into the scheduler-owned `Thread`. + // SAFETY: entry fns are valid `extern "C" fn() -> !`; single + // CPU with interrupts masked keeps the pool + scheduler + // exclusive to this code. + unsafe { + if let Ok((kt, mut thread)) = spawn_kthread(demo_thread_a, Priority::NORMAL) { + thread.set_cpu_context(*kthread_context(kt.slot)); + let _ = spawn_thread(thread); + } + if let Ok((kt, mut thread)) = spawn_kthread(demo_thread_b, Priority::NORMAL) { + thread.set_cpu_context(*kthread_context(kt.slot)); + let _ = spawn_thread(thread); + } + } + + // Step 4: hand the CPU to the demo threads; returns once both + // A and B have run and yielded back to the boot thread. + // SAFETY: interrupts masked above; no scheduler borrow held. + unsafe { + let _ = yield_now(); + } + + let _ = serial.write_str( + "[ONCRIX/aarch64] cooperative scheduler: thread A/B ran, back on boot thread\n", + ); + } + let _ = serial.write_str("[ONCRIX/aarch64] Entering halt loop.\n"); }