Disclaimer: This README is AI-generated but was thoroughly reviewed and edited. The actual source code of this project is purely hand-written.
A small bare-metal AArch64 operating system, written in Rust. It boots two ways:
- On Raspberry Pi hardware (BCM2712/BCM2711) via the Pi's own firmware
boot path, driving the display through the VideoCore mailbox — no
bootloader beyond the Pi firmware, no host OS, no
std. - As a generic UEFI application (
BOOTAA64.EFI) on anything with AArch64 UEFI firmware — QEMU'svirtmachine, VirtualBox's ARM64 guests, and in principle real non-Pi hardware — driving the display through UEFI's Graphics Output Protocol (GOP) instead of the mailbox.
Both backends feed the same board-agnostic FrameConfig/DisplayBuffer in
motherboards/display.rs, so the drawing code, font, and console are written
once and shared across every target.
- Raspberry Pi path — custom AArch64 boot path (
_start) linked at the Pi firmware's load address (0x80000) via a hand-written linker script. Boot-core selection parks the three secondary cores inwfeand runs the kernel on core 0 only. Zeroes BSS, sets up the boot stack, enters Rust. - UEFI path — built for the
aarch64-unknown-uefitarget using theueficrate's#[entry]macro. Firmware loads and relocates the PE/COFF binary itself, so there's no hand-written boot assembly or linker script for this variant. - Panic handler halts the CPU (
wfe); no console output on panic yet.
Two backends, each producing the same FrameConfig (base address, size,
width, height, stride, bytes-per-pixel, BGR/RGB order):
- VideoCore mailbox driver (Pi) — property-tags channel (channel 8)
request/response with polling. HDMI framebuffer setup — physical + virtual
size, depth, pixel order, framebuffer allocation, pitch — all read back
from the firmware at runtime. Runtime
bpp = pitch / widthhandles the Pi 5's actual 16-bit RGB565 framebuffer and QEMUraspi4b's 32-bit framebuffer from the same code path. - UEFI GOP driver (generic) — locates the
GraphicsOutputprotocol, reads the current mode's resolution/stride/pixel format, and takes the linear framebuffer pointer directly. Only direct-write pixel formats (PixelFormat::Rgb/Bgr) are supported today;Bitmask/BltOnlyGOP implementations (e.g. QEMU'svirtio-gpu-pci) are rejected with a diagnostic over UEFI's stdout rather than hanging silently — see Running on QEMU (UEFI) for why the choice of display device matters here.
Once either backend produces a FrameConfig, the shared code in
motherboards/display.rs takes over — it doesn't know or care which board
built the config:
- Drawing primitives (all
i32coordinates with on-screen clipping; off-screen vertices are silently dropped):clear(color),draw_pixel,draw_rect(auto-clipped, order-agnostic),draw_triangle(half-space rasterizer, both windings). - Color — RGBA
Colorwith explicit packing methods (rgba/bgrafor 32bpp,rgb565/bgr565for 16bpp).write_colorpicks the right one at write time from the activeFrameConfig'sbppandbgrfields, so the Pi 5's real 16-bit hardware framebuffer and every 32-bit framebuffer (QEMU, UEFI GOP) go through the same drawing code without either corrupting the other.
- 8×8 bitmap font covering uppercase
A–Z, digits0–9, and:. - Scalable glyph rendering (
draw_char) that paints white glyph / black cell so re-drawing self-clears. - Framebuffer-backed
println!console (motherboards::console::GenericWriter) — cursor state in the module, handles\n/\r, folds lowercase to uppercase, wraps at screen width, wraps back to the top when it runs out of vertical space. Shared by both backends: text is drawn as pixels onto whicheverFrameConfigis active, not routed through a separate UART/serial console.
- Cargo features select the target board (see Feature flags).
cargo maketasks:compile-raspi5/compile-qemu-raspi4— objcopy the ELF into a flatkernel_2712.img.sync-raspi5— copy the image to a mounted SD card at/Volumes/bootfs.qemu-raspi4— build for QEMU'sraspi4bmachine and launch it.compile-uefi— build theaarch64-unknown-uefibinary.qemu-uefi— build the UEFI binary, package it asBOOTAA64.EFIon a FAT boot disk, and launch QEMU'svirtmachine with OVMF firmware.vbox-disk— build the UEFI binary and produce a VirtualBox-ready VDI disk image (see Running on VirtualBox).
kluster uses Cargo features to select the target board.
| Feature | What it selects |
|---|---|
raspi |
Base marker enabling the motherboards::raspberrypi module. |
rpi5 |
BCM2712 addresses: mailbox at 0x10_7C01_3880, A76 tuning. Implies raspi + device. |
rpi4 |
BCM2711 addresses: mailbox at 0xFE00_B880, A72 tuning. Implies raspi. |
device |
Running on real Pi hardware (framebuffer resolution/defaults). |
emulator |
Running under QEMU raspi4b. |
generic-uefi |
Build for aarch64-unknown-uefi; drive the display through UEFI GOP instead of the mailbox. Pulls in the uefi crate. Default. |
| Where I'm running | Cargo flags |
|---|---|
| Raspberry Pi 5 hardware | --no-default-features --features rpi5 |
QEMU raspi4b |
--no-default-features --features emulator,rpi4 |
QEMU virt / VirtualBox |
(default) — same as --features generic-uefi |
cargo make tasks bake the right combination in, so in day-to-day use you
don't pass features by hand.
Bringing up another board means adding a new motherboards/<board>/ module
whose job is just to produce a FrameConfig (see get_raspi_frame_config /
get_uefi_frame_config) — the drawing, font, and console code in
motherboards/display.rs and motherboards/console.rs is already shared and
doesn't need touching. Wire the new module into motherboards::mod's
#[cfg] gates and display::set_display_buffer(), and add a matching
cargo make task with the right target/RUSTFLAGS/features.
These are on the roadmap and in various states of design — not shipping yet.
A BlockDevice trait with a RAM-backed impl first (embedded disk image via
include_bytes!), then the same trait implemented by a real SD driver. Lets
the kernel open and read files by path (/HELLO.TXT) and print their
contents through the console. Short 8.3 names first; LFN later.
Install a VBAR_EL1 table so CPU faults (data aborts, unaligned access,
instruction faults) print a diagnostic instead of silently hanging.
Set up page tables, enable the MMU, and mark RAM as Normal cacheable memory.
Bring up the ARM generic timer and the GIC-400 on the Pi 5. Enable IRQs. Foundation for any preemptive scheduling later.
Wake secondary cores via PSCI (smc #0 with PSCI_CPU_ON) — the method
both the Pi 5 device tree and generic UEFI/ACPI platforms specify, so this
lands once and benefits both backends. Per-core stacks and a per-core entry.
BCM2712 SDHCI initialization sequence (CMD0 → ACMD41 → …), CSD/CID
parsing, high-speed switch, and DMA-based block reads. Drops into the same
BlockDevice trait FAT32 targets.
Extend the 8×8 glyph table to cover lowercase and common punctuation, so
println! can render real messages without the current auto-uppercase fold.
A small bump / linked-list allocator so the kernel can use Vec, Box,
String after early boot. Enables ergonomic data structures for the
filesystem, scheduler, etc.
Refactor the drawing primitives to target a &mut [u8] + geometry struct so
the rasterizer runs unchanged inside a minifb window on macOS/Linux for
tight visual iteration — no SD-card swap loop required. Also the natural
path to real cargo test coverage: splitting the board-agnostic logic
(color.rs, geometry.rs, the rasterizer math in display.rs) into a
lib.rs marked #![cfg_attr(not(test), no_std)] would let those pieces run
as ordinary host-native tests on macOS, with no QEMU or custom test runner
involved — only the boot entry and hardware-specific drivers in main.rs
would remain untestable this way, which is inherent to what they are.
config.txt tuning (hdmi_cvt, max_framebuffer_*, framebuffer_depth=32)
to get the Pi 5's HDMI to actually output the panel's native resolution
instead of clamping to 1080p. Requires experimenting with BCM2712 firmware
quirks.
The UEFI backend currently only supports direct-write GOP framebuffers
(PixelFormat::Rgb/Bgr). Devices that report BltOnly (QEMU's
virtio-gpu-pci, notably) have no CPU-writable linear buffer at all — pixels
have to be assembled in a RAM-side backing buffer and submitted via
GraphicsOutput::blt() once per frame instead. Worth doing eventually since
it's the more general path (and unlocks resolutions bigger than ramfb's
fixed mode list), but real UEFI firmware overwhelmingly supports direct-write
GOP already, so it isn't blocking anything today.
Prereqs (macOS):
# rustup + nightly + AArch64 targets
rustup toolchain install nightly
rustup target add aarch64-unknown-none-softfloat --toolchain nightly
rustup target add aarch64-unknown-uefi --toolchain nightly
rustup component add llvm-tools-preview --toolchain nightly
cargo install cargo-binutils cargo-make
brew install qemucargo make qemu-raspi4cargo make qemu-uefiThis builds the aarch64-unknown-uefi binary, copies it to
target/uefi_disk/EFI/BOOT/bootaa64.efi, and boots QEMU's virt machine
with OVMF firmware plus a ramfb display device. ramfb matters here —
it's what gives UEFI a genuine direct-write linear framebuffer. QEMU's other
common display option, virtio-gpu-pci, reports a BltOnly pixel format
instead, which this kernel doesn't support yet (see
🎨 Blt-based GOP fallback) — swapping devices in
Makefile.toml will silently regress this to a blank screen.
Requires VirtualBox 7.x with ARM64 guest support (Apple Silicon hosts).
bash setup_vbox_vm.shThis tears down any existing kluster VM, builds the UEFI binary and a FAT
boot disk (create_vbox_disk.sh — a real disk image file, since VirtualBox
can't synthesize a FAT filesystem from a host directory the way QEMU's
fat: driver can), creates an ARM64 VM (--chipset=armv8virtual --firmware=efi64), and attaches the disk. VirtualBox's default vboxvga
graphics controller crashes outright on the ARM chipset
(VERR_PGM_RAM_CONFLICT) — the script sets --graphicscontroller=qemuramfb,
VirtualBox's own direct-write framebuffer device and the ARM-side analogue of
QEMU's ramfb.
VBoxManage startvm kluster --type=guiSerial diagnostics (GOP handle/pixel-format failures, resolution/stride once
it succeeds) are written to /tmp/vbox_serial.log.
Updating after a code change — the VM itself doesn't need to be recreated, only the disk image:
VBoxManage controlvm kluster poweroff # only if currently running
cargo make vbox-disk
VBoxManage startvm kluster --type=guicreate_vbox_disk.sh stamps the rebuilt VDI with a fixed UUID
(sethduuid), so VirtualBox's media registry never drifts out of sync
between rebuilds and the VM doesn't need to be recreated.
SD card mounted at /Volumes/bootfs:
cargo make sync-raspi5src/
├── main.rs // crate root, module declarations
├── kernel.rs // kernel::main — the platform-agnostic entry
├── color.rs // Color: RGBA storage + component access
├── geometry.rs // Point + basic geometry types
├── panic_cfg.rs // #[panic_handler] + eh_personality
├── print.rs // print! / println! macros
├── types.rs // shared type aliases (KernelError/KernelResult)
├── processors/
│ └── aarch64/ // arch-specific: boot.S + Pi entry, UEFI entry, cpu helpers
└── motherboards/
├── display.rs // FrameConfig + DisplayBuffer — shared drawing, font; board-agnostic
├── console.rs // GenericWriter — framebuffer-backed println! console, shared
├── raspberrypi/ // Pi-specific: mailbox, HDMI mode setup, kernel.ld linker script
└── generic_uefi/ // UEFI-specific: GOP framebuffer discovery
kernel.rs talks to motherboards::display; each board module's only job is
producing a FrameConfig — all the pixel-pushing, font rendering, and
console logic lives once in motherboards/display.rs and
motherboards/console.rs, shared across every target. Same shape for the
CPU: processors::aarch64 for arch code, split by feature between the
hand-written Pi boot path and the UEFI entry point.