Kernel
A single arch-neutral scheduler runs on all five targets. It is fixed-priority, preemptive and per-core, implementing the BMP model: every task is bound to one core and never migrates.
A frame-free context switch
The scheduler never sees a register frame. The trap boundary is narrowed to a single function:
onTrap(sp: usize) usize
The arch trap stub pushes all registers onto the interrupted stack, calls onTrap with the interrupted stack pointer, and the scheduler saves that SP on the current task and returns the next task's SP. The stub then pops and returns — iretq on x86, sret on riscv, eret on aarch64, an exception return on Cortex-M.
This one narrowing is what lets the same scheduler run on five targets. The register layout never leaks into neutral code.
Per-core state, pinned tasks
Run-state — the current task, the round-robin cursor, the tick count — is indexed by a dense logical core id. Real hardware can hand out non-contiguous APIC ids, so a topology table maps physical ↔ logical. Each task records the core it belongs to and never moves between cores.
Multi-core bring-up
All five targets bring up their secondary cores.
| Arch | Second-core start | Then |
|---|---|---|
| x86_64 | INIT-SIPI-SIPI to a trampoline at physical 0x8000 | real → protected → long mode; up to four cores from the ACPI MADT |
| ARMv6-M (RP2040) | the inter-core SIO FIFO launch handshake | a second Cortex-M0+, indexing its per-core state by the core-id register |
| RV32 (RP2350) | the SIO FIFO launch handshake | a second Hazard3 core joins the scheduler, verified on real silicon |
| riscv64 | SBI HSM hart_start to the M-mode firmware | secondary harts enter S-mode and join the scheduler |
| aarch64 | PSCI CPU_ON to the firmware | secondary cores enter EL1 and join the scheduler |
On x86 the number of cores is discovered, not assumed: a core that never answers is reported as a real failure, and offline cores have their pinned tasks re-homed to live cores before the scheduler goes live.
A new actor spawned at runtime is placed on the least-loaded core — on x86_64, riscv64 and aarch64. The MCU flight builds pin their tasks to cores explicitly.
Memory model
The kernel owns its own page tables. They differ per architecture.
- x86 — an identity map of
[0, 4 GiB)in 2 MiB huge pages, with NX and write-protect enabled. - riscv64 — the 39-bit space identity-mapped in 1 GiB megapages.
- aarch64 — identity-mapped in 1 GiB blocks.
The physical frame allocator is x86's. It manages the largest free RAM region the firmware memory map reports.
- O(1) allocation. A bump allocator plus an intrusive free-list — a freed frame stores the next-free pointer in its own first word, so there is zero metadata overhead.
- On-demand fine mapping. A huge page is split down to 4 KiB and the user or uncached bit set on the target leaf. MMIO regions — a NIC's BAR, the I/O APIC — are mapped uncached.
riscv64 and aarch64 draw their intermediate page tables from a small fixed pool instead.
On the MMU-less MCU profiles there is no MMU at all: the address space is flat and physical (VA == PA), the MMU module is a no-op stub, and isolation is enforced by PMP or MPU regions instead of page tables.
The VM as the kernel's payload
The kernel's whole reason to exist is to host Lambda C VM instances as scheduled, isolated actors.
Each task is a self-contained VM with its own stack, global area and heap; the scheduler saves and restores the VM's process-wide run-state on every context switch, so a VM frame is fully preemptible with no interrupt-disabled section around it.
The details of that contract are on the Actors page. How each actor is confined to its own memory is on the Isolation page. The hard-real-time primitives the scheduler carries are on the Real-Time page.