Portability
Lambda OS was written for a 64-bit x86 machine — page tables, multiple cores, ACPI, UEFI, ring 3. Every abstraction was allowed to assume that machine.
Then it went down: to riscv64, to aarch64, to a bare MMU-less RV32, and finally to an ARMv6-M Cortex-M0+ — a 32-bit part with no MMU, no FPU, no atomic instructions at all, and 264 KB of SRAM.
Landing on a part with no atomics breaks a kernel whose contracts were drawn in the wrong place: every assumption that leaked — a register layout, a lock, a page table, a compare-and-swap — becomes a rewrite at the bottom.
What is shared and what is swapped
The scheduler, the VM host and the network stack are the same source on all five targets.
Inside them are a handful of compile-time branches that size a table to the chip (task count, stack size, alignment — 264 KB is not 4 GiB). But no register layout, no lock and no page table crosses the line.
Arch-neutral code never imports an architecture's file directly. It always goes through the HAL selector.
The filesystem rides the block driver, so it is live on riscv64 and aarch64 only. The physical frame allocator is x86's.
The HAL contract
The selection happens at compile time. A target that has not implemented a module it needs fails at compile time, not at runtime.
Seven modules exist on all five architectures:
cpu · console · mmu · intc · trap · gdt · nic
Four more — smp, ioapic, pci, acpi — are x86-only, and the block driver blk exists on riscv64 and aarch64 only.
Each maps to that architecture's real hardware:
| Concern | x86_64 | riscv64 | aarch64 | RV32 | ARMv6-M |
|---|---|---|---|---|---|
| Interrupt controller | APIC / I/O APIC | CLINT | GICv2 | CLINT direct | NVIC |
| Memory protection | 4-level paging | Sv39 paging | TTBR tables | PMP regions | MPU regions |
| Second-core start | INIT-SIPI-SIPI | SBI HSM hart_start | PSCI CPU_ON | SIO FIFO | SIO FIFO |
| Timebase | TSC (PIT-calibrated) | rdtime | CNTVCT / CNTFRQ | CLINT mtime | µs TIMER |
| NIC | e1000 | virtio-net | virtio-net | CYW43439 | CYW43439 |
How the neutral core stays arch-blind
Selection alone is not enough — the neutral code has to genuinely not depend on any arch detail. That comes from narrowing each boundary contract to a shape that carries no arch specifics.
The context switch is the clearest case. Register layout is exactly where architectures differ most, so the trap boundary is narrowed to one function that never exposes it:
onTrap(sp: usize) usize
The arch stub stacks every register on the interrupted stack, hands the scheduler only the stack pointer, and the scheduler saves it and returns the next task's pointer. The stub then pops and returns (iretq / sret / eret / an exception return).
The scheduler manipulates opaque stack pointers and never sees a register frame. What a frame looks like stays entirely inside the arch layer. On Cortex-M the hardware does half the job — exception entry already stacks eight registers — so the handler saves the other eight and the same contract holds.
That is why the same scheduler runs on all five targets.
ARMv6-M: an instruction set sharing no ancestry
It is ARMv6-M. It is not the aarch64 port — that is ARMv8-A: 64-bit, MMU, EL0/EL1, a GIC. ARMv6-M shares nothing with it but the vendor name: Thumb-1 only, no MMU, no FPU, and no atomic instructions at all.
Yet the same arch-neutral scheduler and the same Lambda C VM ran on it. The scheduler's only concession was its size table. Its logic went down as-is.
An instruction set that shares no ancestry with the other three still slotted under the same contracts — so a new target is a bounded port (write the arch layer), not a fork of the OS.
The MMU-less MCU profile
The same arch-neutral scheduler, VM and actor layers run on a bare RISC-V (RV32 / RV64) in M-mode with:
- no OpenSBI — the kernel is the M-mode firmware, reset at the chip's entry address;
- no MMU — a flat, physical address space, with isolation enforced by PMP regions;
- a CLINT-direct timer — the periodic interrupt is armed by writing a hardware compare register, no firmware call.
Three arch modules are swapped for the profile: the trap and the CPU become M-mode variants, and the MMU becomes a no-op stub. Everything above the HAL is unchanged.
The RV32 path further spans a soft-float, FPU-less real chip. There, the M-mode trap is FP-presence-adaptive at compile time: it saves the float registers only when the target has an FPU, so one trap source serves both a hardware-FP target and a chip with no float registers at all.
Bytecode is a separable artifact
The application bytecode is not welded to the kernel image. The default image is baked in, but bytecode can also be delivered at boot:
- Over UART (an XMODEM-CRC transfer) — on x86_64, riscv64 and aarch64.
- Over the network (TFTP) — on x86 only, and gated on QEMU's
fw_cfgdevice, so it does not fire on real x86 hardware. - The MCU profiles (RV32, ARMv6-M) have no delivery channel yet. Bytecode is baked in.
Where delivery exists, a distinct application runs in place of the baked-in one. The priority is network-fetch, then UART, then baked-in; a plain boot pays nothing and stays byte-identical.
This is what lets the operator's software ship as an artifact separate from the OS image — the same separation of intent (bytecode) from mechanism (kernel) that runs through the whole design.