Files
LithosAnanake/FABRIC-3.md
T
Robert Allan JamesandClaude Sonnet 5 350287850f
Build / build-amd64-iso (push) Waiting to run
Build / build-aarch64-iso (push) Waiting to run
Build / build-riscv64-img (push) Waiting to run
arch/aarch64: fix VBAR_EL2 stale comment + hardcode HVC PSCI conduit
FABRIC-3.md §IV.3 item 7's code audit found two more findings; fixed both
per direct instruction naming them specifically.

Comments only, no behavior change:
interrupts.c's arch_interrupts_init() doc comment (and its matching
runtime EL2 console message) claimed VBAR_EL2 was "a known gap... not yet
wired." Checked against isr.S's aarch64_install_vectors() and found that
claim stale: the assembly already branches on aarch64_current_el() and
writes vbar_el2/vbar_el1 correctly, threading the same answer through
el2_mode_flag for the IRQ trampoline's ELR/SPSR selection too. There was
no gap to close -- only the comment was wrong. Also fixed a same-vintage
one-word staleness in arch.c's aarch64_install_vectors extern comment
("installs VBAR_EL1" -> EL-aware), caught while touching this.

Real behavior change, gated to preserve existing QEMU behavior:
arch_cold_reset() hardcoded the PSCI conduit to HVC, a documented
QEMU-specific workaround (QEMU's AAVMF has no genuine EL3 to answer SMC).
Real Pi 5 hardware's ATF means EL3 exists there; confirmed further this
pass that bcm2712.dtsi's own /psci node declares method="smc" directly,
not just inferred from ATF's presence.

New aarch64_psci_conduit_init(dtb) in arch.c: fdt_valid(dtb) ->
fdt_find_node_by_compatible(dtb, "arm,psci-1.0") ->
fdt_find_prop_in_node(..., "method", ...) -- sets a module-static
s_psci_use_smc flag to 1 only when method reads exactly "smc"; every
other outcome (no DTB, no PSCI node, method="hvc", property absent)
leaves it at its default 0/HVC, preserving this system's existing QEMU
behavior exactly. arch_cold_reset() now branches on that flag between
smc #0/hvc #0. Called once from apic_init() (arch/aarch64/apic.c), the
one point in boot with boot_info->dtb already in hand -- collocated with
its consumer in arch.c rather than defined in apic.c, since
arch_cold_reset() has no boot_info of its own by the time it runs (called
from deep in VM execution, via BYE).

Three-arch QEMU acceptance run exercised both new guard branches: the
aarch64 boot log shows "PSCI: no DTB -- using HVC (QEMU virt-machine
default)" immediately before the existing GIC discovery lines, then
boots clean to zuse)ok> -- confirming the QEMU/UEFI path is unchanged.
The SMC success branch itself stays unverified until real Pi 5 hardware
runs BYE.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YcT3H2PQeyujrzjqS3Var
2026-09-05 00:06:41 -04:00

904 lines
66 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# FABRIC-3.md — bare metal boot
**Status:** Living working document, opened 2026-09-04 as the successor to `FABRIC-2.md`
(now closed/archival — see its own header). Topic for this document, per direct instruction:
**bare metal boot** — getting LithosAnanke to actually boot on real hardware, not just QEMU.
`FABRIC-2.md` §I.6 (Milestone 8) already named this as the one item that pass couldn't close
from a coding session at all, for exactly this reason — it needs a real machine and a human
physically present. This document is where that work, and everything downstream of it, gets
tracked.
**How to use this document going forward.** New findings, new punch-list items, and new
decisions for bare-metal-boot work get added here, not to `FABRIC-2.md`. Same discipline every
prior document in this series used: write the decision and its reasoning down before building,
close items with a dated note citing real evidence, never silently drop a stale claim.
---
## I.1 — Task 1: merge `v2.0.1` into `master`, verify build/function equivalence
**Written up before executing**, per direct instruction and this series' own standing
discipline.
**Why this is task 1.** `FABRIC-2.md`'s entire 7-step closure pass (§I.1–§I.5, §I.7, plus
today's FABRIC-series rename) happened on the `v2.0.1` branch, not `master`. Before any real
bare-metal-boot work starts, that work needs to land where `.claude/CLAUDE.md` says the
project's sole production line actually lives: `master`. Doing this first, cleanly, before
starting new work avoids ever having two divergent lines to reconcile later.
**Investigated before writing this up, not assumed:**
- `git merge-base --is-ancestor master v2.0.1`**true**. `master` (local HEAD `d2a0305`) is
a strict ancestor of `v2.0.1` (HEAD `b031b80`) — `v2.0.1` is exactly `master` plus 47 commits
forward, no divergent history on either side. This means the "merge" is a pure **fast-forward**,
not a real three-way merge — nothing to resolve, no conflict possible.
- `origin/master` carries exactly one commit beyond local `master` (`58c59e8`, "Initial
commit") that local `master` hadn't fetched yet — confirmed already contained in `v2.0.1`'s
own history (`git merge-base --is-ancestor 58c59e8 v2.0.1` — true), so it introduces no
discrepancy either.
- `master`'s own tree still has the *old* `FABRIC.md`/`FABRIC-2.md`/`FABRIC-3.md` naming
(unrenamed) — expected, since today's rename commit (`b031b80`) only exists on `v2.0.1` so
far. The fast-forward brings the rename to `master` along with everything else; nothing
separate needs doing for it.
**Plan:**
1. Fast-forward `master` to `v2.0.1`'s tip (`git checkout master && git merge --ff-only v2.0.1`)
— refuses loudly instead of silently doing a real merge if the ancestor relationship somehow
isn't what the investigation above found, so this step re-verifies its own precondition.
2. Push `master` to `origin`.
3. **Verify build/function equivalence on a genuinely clean tree**, not by inference: `git clean`
(after confirming nothing untracked-but-wanted is present), then the full acceptance sequence
`.claude/CLAUDE.md` already mandates for any kernel change — `clean qemu` on all three
architectures, in the foreground, one at a time, each reaching `ok>` and shutting down
cleanly. Since the tree is byte-identical to `v2.0.1`'s post-fast-forward, this is expected
to reproduce exactly what `v2.0.1`'s own last acceptance pass already showed — the point of
re-running it here is to confirm that expectation holds on `master` itself, not to assume it
from the fast-forward alone.
4. Return to `v2.0.1` as the working branch afterward (`.claude/CLAUDE.md`'s own rule: always
return to the correct working branch after any out-of-branch work), unless told otherwise.
**DONE 2026-09-04, exactly as planned:**
1. Committed the write-up above on `v2.0.1` first (`72c14cb`), pushed. This became `v2.0.1`'s
new tip.
2. `git checkout master && git merge --ff-only v2.0.1`**Fast-forward**, `d2a0305..72c14cb`,
confirming the investigated ancestor relationship held exactly as expected; no conflict, no
merge commit.
3. `git push origin master``origin/master` moved `58c59e8..72c14cb`.
4. **Verified on a genuinely clean `master` tree**, not inferred from the fast-forward:
- Hosted build (`make clean && make`): clean compile, zero warnings, same as `v2.0.1`.
- Full 3-arch kernel acceptance (`clean qemu`, amd64/aarch64/riscv64, each in the foreground):
all three reached `(zuse) ok>`/`ok>` and shut down cleanly, zero build errors, zero
unexpected warnings — identical outcome to `v2.0.1`'s own last acceptance pass, confirmed
directly rather than assumed. Logs: `logs/20260904-113208/amd64/`,
`logs/20260904-113320/aarch64/`, `logs/20260904-113552/riscv64/`.
5. `master` and `v2.0.1` are now identical (`72c14cb` on both, `origin` and local). Returned to
`v2.0.1` as the working branch per plan step 4.
**Task 1 closed.** `master` genuinely is the production line again, current through today's
FABRIC-series rename and the full `FABRIC-2.md` §I closure. Bare-metal-boot work (this
document's actual topic) starts from here.
## I.2 — Task 2: version correction — the `v2.0.1` bump and `v2.0.0` tag were premature
**Direct instruction, 2026-09-04**: the `LITHOS_VERSION` bump to `2.0.1` (and the branch name
that followed it) got ahead of the real state — per `Makefile.starkernel`'s own versioning
policy (`v2.0.0` = QEMU release, even major/LTS; `v2.0.1` = the SER5 hardware-track *line*,
RDRAND backend + thumbdrive image goal), claiming `2.0.1` implies hardware-track progress that
was never actually verified on real hardware — that verification is precisely `FABRIC-3.md`'s
whole open topic (§I.6 in the closed `FABRIC-2.md`). The current `master` HEAD is, correctly,
still a `v2.0.0`-class QEMU-only release. "Nothing harmful" — a version-label correction, not a
functional rollback.
**Found and fixed while correcting this, not left half-done:**
- A real gap in the FABRIC-series rename from earlier today: `Makefile.starkernel`,
`Kconfig.kernel`, `scripts/bleach_zuse_img.sh`, four `proof/*.thy` files, and
`src/starkernel/arch/amd64/isr.S` all still had stale `FABRIC.md`/`FABRIC-2.md`/`FABRIC-3.md`
citations — the original sweep's file-list only matched `--include=*.md/*.c/*.h/*.4th`, which
silently skipped every file without one of those four extensions. Found by re-grepping with
the extensions excluded instead of included. Fixed with the same safe placeholder-substitution
technique the original rename used (each file, one pass, ordered `FABRIC-3→2→1→0` placeholders
then resolved) — verified no double-shifted or broken references remained afterward.
`.claude/settings.local.json`'s own historical Bash-permission-grant log (literal past command
strings naming the file as it was called *at the time*) was deliberately left alone — rewriting
it would falsify an audit trail, not fix a stale citation.
- `ClaudeEXPORT/memories.json`/`conversations.json` also still reference the old names — left
untouched on purpose, same reasoning as the memory note on that archive: it's a frozen export,
mining material, not live documentation to keep in sync.
**Changes:**
1. `Makefile.starkernel`: `LITHOS_VERSION ?= 2.0.1``2.0.0`.
2. The rename-gap fix above (7 files).
3. Verified 3-arch boot (`clean qemu`, amd64/aarch64/riscv64, each in the foreground): all three
show `LithosAnanke v2.0.0` in the boot banner (confirmed directly in each serial log, not
assumed from the Makefile edit alone), zero build errors, zero unexpected warnings, clean
shutdown.
4. Moved the existing `v2.0.0` git tag (previously at `2efd7fe`, the original QEMU-release
milestone commit — that commit and its own message stay fully intact in history, only the
tag pointer moves) to the current `master`/`v2.0.1`-branch HEAD, per explicit instruction —
the prior tag placement was itself part of the same "got ahead of myself" correction, not a
separate decision. No remote tag existed yet (`git ls-remote --tags origin` was empty for
`v2.0.0`), so no destructive remote operation was needed, only a local move-and-push.
5. **Follow-up, same day**: `v2.0.1` (the working branch this and Task 1 happened on) deleted,
local and `origin` — confirmed a strict ancestor of `master`'s new HEAD first, so nothing
was lost. `master` is the repo's only branch from here on.
---
## II. Three architectures, three different hardware scopes
Per direct instruction, 2026-09-04. The real-hardware targets are **not** symmetric across
architectures — each gets its own section below because the actual scope of "done" is
different for each:
- **amd64 — genericity is the goal, not just the SER5.** The Beelink SER5 is the machine in
hand and the development/reference target, but the real requirement is broader: this needs
to boot on *any* x86_64 machine — laptop, desktop, tower, or mini PC — not just one vendor's
quirks. SER5-only success is necessary but not sufficient; anything that works only because
of an SER5-specific assumption (a particular ACPI table shape, a specific UEFI
implementation's quirks) is a bug against this goal, not a deferred nice-to-have.
- **aarch64 — Raspberry Pi 5, and only the Raspberry Pi 5.** No genericity requirement across
aarch64 boards — this is the one and only target for this architecture.
- **riscv64 — Milk-V Mars, and only the Milk-V Mars.** Same as aarch64: one specific board,
not a generic riscv64-SBC goal.
**How to use sections IIIV below.** Same discipline as everything else in this series: plan
before building, one section at a time, iterating — not all three architectures in parallel,
and not front-loading a complete plan before any real hardware is in front of us. Each section
starts with what's already true (existing repo infrastructure, already-decided policy) and
what's still genuinely unknown, not assumed.
## III. amd64 — generic x86_64 bare metal (reference hardware: Beelink SER5)
**Already true, not to be re-derived:**
- `ROADMAP.md`'s "Board-by-board hardware rollout" already names this `v2.2.0`'s gate: the
generic GPT/FAT32 thumbdrive image (`make -f Makefile.starkernel ARCH=amd64 thumbdrive`,
already built — `Makefile.starkernel:1018`) flashes to and boots on the real SER5 via its
real UEFI, reaching POST + `ok>`, with the amd64 RDRAND entropy backend
(`src/starkernel/rng/rng.c`, already built and part of `master`) serving live entropy.
- `iso-usb` (`Makefile.starkernel:1060`) is the alternate, novice-friendly path (UEFI
isohybrid ISO for tools like GNOME Disks "Restore Disk Image...") — same underlying image,
different flashing UX.
- `FABRIC-2.md` §I.6's own 8-step physical-boot sequence (build ISO, identify the target
device, flash it, physically boot, decide an observation method, confirm POST, confirm
`ok>`, document) is the closest thing to an existing plan — but it predates the genericity
requirement and was written with no hardware in hand yet.
**Decided in conversation, 2026-09-04:**
- **Observation: HDMI (interactive) + serial (logged transcript), both.** The kernel's own
VT100 framebuffer console (`console.c`/`vt100.c`/`framebuffer.c`) already gives a real
interactive display over HDMI — no new code needed there. Serial capture, if the SER5
exposes a UART header, uses the Raspberry Pi's own GPIO UART as the USB-serial bridge
(already available hardware, not a purchase blocker) — this needs the SER5's own UART pins
physically identified first (not yet confirmed it has an accessible header at all).
- **Genericity is verified by standards-compliance, not a second machine** — no second x86_64
box is available right now. The bar is: nothing in the boot path may depend on an
SER5-specific assumption (a particular ACPI table shape, a specific UEFI implementation's
quirk) — argued by code audit against real UEFI/ACPI standards, not by testing on a second
board, until one becomes available. This is a real constraint on the punch list below (item
6), not a deferred nice-to-have.
- **Secure Boot: already disabled on this SER5.** No signed-loader work needed for this pass —
"Secure Boot disabled in firmware setup" is the supported path, documented as such rather
than built around.
**Punch list, this cadence's actual next steps:**
1. Build the generic thumbdrive image: `make -f Makefile.starkernel ARCH=amd64 thumbdrive`.
2. Flash it to a USB stick (`dd`, per the target's own existing usage message).
3. Physically inspect the SER5 for an exposed UART header/pins; if present, wire the
Raspberry Pi's GPIO UART to it as the serial bridge. If absent, HDMI-only for this pass —
not a blocker, just a scope note for step 7's log.
4. Connect HDMI + keyboard to the SER5.
5. Boot the SER5 from the flashed stick (firmware boot-order menu as needed — Secure Boot
already disabled, confirmed above, so no signing prompt expected).
6. **Code audit pass — DONE, 2026-09-04.** Reviewed `src/starkernel/boot/uefi_loader.c` and
every file in `arch/amd64/` (`arch.c`, `apic.c`, `ioapic.c`, `interrupts.c`, `i8042.c`,
`timer.c`) for anything that assumes SER5-specific hardware rather than standard UEFI/ACPI.
Report only, per this project's own "identify, don't fix unless asked" rule — nothing
below was changed.
**Clean (verified standards-correct, not just "worked in QEMU"):**
- `uefi_loader.c`: COM1 presence is probed (scratch-register readback + bounded THRE spin)
before any code trusts it, so a board with no legacy serial decode — the documented SER5
case — degrades to silently dropping bytes rather than hanging. The ACPI table search
does two explicit passes (2.0 GUID first, 1.0 only as fallback) rather than a single
first-match loop, avoiding a real bug class (silently preferring the RSDT-only 1.0
pointer). `GetMemoryMap`/`ExitBootServices` follows the UEFI "golden path" (nothing
between the two calls) with the required `EFI_INVALID_PARAMETER` retry loop. GOP
framebuffer degrades to `PixelBltOnly` cleanly when no linear framebuffer exists.
- `apic.c`: LAPIC presence, x2APIC-vs-xAPIC mode, and the APIC timer's own tick rate are
all determined at runtime (MSR read, empirical TSC-timed calibration) — no hardcoded
bus-frequency assumption anywhere in the timer path.
- `ioapic.c`: I/O APIC base and GSI base come from the ACPI MADT, not a hardcoded address.
- `timer.c`'s PM_TMR_BLK port: already fixed correctly (`fadt_find_pm_tmr_port()`,
2026-08-11 per this file's own comment) — discovered from the FADT, not assumed from a
single machine type's legacy port.
**Findings (real, not fixed — reported per policy):**
1. **HPET base address hardcoded, same bug class as the already-fixed PM_TMR port.**
`timer.c`'s `HPET_PHYS_BASE 0xFED00000` is never cross-checked against the ACPI `"HPET"`
table (confirmed absent — no `"HPET"` signature lookup exists anywhere in the tree,
unlike the FADT/`"FACP"` walk `fadt_find_pm_tmr_port()` already does and could extend to
reach it with the same RSDP→XSDT→table-by-signature walk). 0xFED00000 is the
near-universal legacy convention, not a guaranteed one — this file's own comment already
documents the *identical* failure mode for the PM Timer port and how it was fixed;
the same reasoning was never applied to HPET.
2. **LAPIC relocation: MADT data already parsed one file over, never plumbed through.**
`apic_init()`'s own doc comment self-flags `boot_info` as unused, "reserved for future
use (e.g., reading the ACPI MADT for the physical LAPIC base address)" — always uses
`LAPIC_DEFAULT_PHYS = 0xFEE00000`. Narrower than "unimplemented": `ioapic.c`'s
`MadtHeader.local_apic_address` field is already read into a parsed struct by
`parse_madt()`, just never passed to `apic.c`. `parse_madt()` also only handles MADT
entry types 1 (I/O APIC) and 2 (Interrupt Source Override) — type 5 (Local APIC Address
Override), the entry that matters on a system that actually relocates it, isn't parsed
at all.
3. **i8042 PS/2 controller poked unconditionally, no presence check.** `i8042_init()` and
`i8042_drain_stale()` read/write ports 0x60/0x64 with no probe of any kind — contrast
with this same codebase's own `raw_serial_init()`, which explicitly probes COM1's
scratch register before trusting it exists. The ACPI FADT carries an IA-PC
boot-architecture flags field that reports 8042 presence; it is never consulted. Same
root gap in `pic_disable()` (`interrupts.c`): unconditionally masks the legacy 8259 PIC
at ports 0x21/0xA1 with no corresponding FADT check.
4. **`arch_cold_reset()` uses the legacy i8042 reset-pulse hack instead of the ACPI Reset
Register.** Confirmed live, not dormant: called directly from `BYE`
(`mama_forth_words.c:1437`, Hera-only cold restart). Pulses port 0x64 with 0xFE — the
pre-ACPI convention that assumes the same i8042 controller finding #3 already flags —
instead of the FADT's `RESET_REG`/`RESET_VALUE` fields, which are the actual
standards-defined generic reset mechanism (confirmed absent from the tree — no
`RESET_REG` reference exists anywhere) and don't depend on i8042 existing at all.
Degrades gracefully (falls through to a `cli;hlt` spin loop, doesn't hang) if the
pulse has no effect — the honest risk here is a silent no-op reset on an i8042-less
board, not a hang.
None of these four are certain to actually bite on the real SER5 — the near-universal
conventions they assume (0xFED00000, 0xFEE00000, i8042-at-0x60/0x64) hold on the large
majority of real PC-compatible x86_64 systems including consumer mini-PCs, and only
finding 4 has a confirmed live call path. Recorded here because the decision above
requires arguing genericity from standards compliance, not from "it booted" — these are
the concrete gaps between what the code assumes and what UEFI/ACPI actually guarantees.
7. Capture the boot: confirm POST reaches the same `1012/0/0` result QEMU shows, confirm
`ok>`/`zuse)ok>`, confirm `rng: backend = rdrand` (live entropy, not the QEMU-only
`virtio-rng` path), save the serial transcript (if wired) the same way `logs/` already
keeps QEMU's.
8. Mint a Zuse identity on a second thumbdrive on the real SER5, confirm it re-attaches —
the same real-hardware round-trip `ROADMAP.md`'s `v2.2.0` gate already names.
9. Update this section with results — pass/fail per step, any SER5-specific or genuinely
generic-UEFI finding either way, before moving to aarch64.
## IV. aarch64 — Raspberry Pi 5
**Already true:** `ROADMAP.md` names this `v2.4.0`'s gate: boots on the real board, aarch64
peripheral-RNG backend live, Zuse mint/attach on real media. The peripheral-RNG backend itself
is **not yet built** — today's `rng_get_bytes()` (`src/starkernel/rng/rng.c`) only has a
`virtio-rng` path, real on QEMU, meaningless on real Pi 5 hardware (no virtio device there).
**Decided in conversation, 2026-09-04:**
- **Observation: HDMI-only for this board's own bring-up.** No second Pi, no dedicated
USB-serial adapter available. The Milk-V Mars could in principle serve as a GPIO-UART
bridge once it arrives (same 40-pin-header shape as the SER5 plan), but using it to observe
the Pi 5 before the Mars has been independently validated itself would be a chicken-and-egg
dependency, not a real plan. Revisit serial capture later if genuinely needed, once at least
one board is proven working — not a blocker for this pass.
- **Both boards (Pi 5, Milk-V Mars) arrive 2026-09-17.** Real runway exists to finish the
design/code work below *before* any hardware is in hand — "plan well before doing," per
direct instruction.
**Still genuinely open, not yet decided:**
- **A pinned GPIO VM, theory-stage** — see `FABRIC-4.md` §2. Raised in conversation, not yet
scoped; downstream of §IV.1's own native-boot-flow work (a GPIO VM needs GPIO addresses
from the DTB the same way the rest of this bring-up does).
### IV.1 — Boot-chain decision: UEFI vs. native, researched 2026-09-04
**Researched, not assumed** (web search, current as of this session):
**UEFI option investigated and found weak.** A real UEFI+ACPI firmware for Pi 5 exists —
[`rpi5-uefi`](https://github.com/worproject/rpi5-uefi) (TF-A + EDK2, SBBR-compliant). But:
it's **archived as of 2025-02-04**, support ended because newer Pi EEPROM firmware broke
compatibility with it; its own README says ACPI support is "under development and limited to
a few devices"; RP1 Ethernet/GPIO/PWM/EEPROM don't work under it. This kernel's whole
aarch64 boot path (`boot/uefi_loader.c`, `BootInfo->acpi_table`) assumes UEFI+ACPI the same
way amd64 and the QEMU aarch64 target do — but that assumption may not hold on a real,
current-firmware Pi 5 at all.
**Native boot flow — the real alternative, researched concretely:**
- Boot partition needs `bcm2712-rpi-5-b.dtb`, `config.txt`, and the kernel image itself —
Pi 5 firmware defaults to loading `kernel_2712.img`, falling back to `kernel8.img` if that's
absent.
- `config.txt` needs `os_check=0` for a non-Linux image, or the firmware assumes Linux and
loads from `0x200000` instead of the classic Pi bare-metal load address `0x80000`.
- Entry protocol: `x0` = 32-bit DTB pointer (upper 32 bits of the 64-bit register
unspecified — must mask before use), `x1``x3` reserved/zero. **No UEFI PE loader, no ACPI
at all** — a completely different entry shape from `boot/uefi_loader.c`.
- Framebuffer: the VideoCore **mailbox property interface** (channel 8) — a real, different
mechanism from UEFI GOP, no precedent anywhere in this codebase today.
**Decision, per direct instruction 2026-09-04: native boot flow.** Not UEFI. The archived,
partially-working UEFI project is too fragile a foundation to build a real-hardware release
on top of.
**What this actually means for the codebase, named honestly rather than estimated small:**
- A **new, non-UEFI entry path** for aarch64 real hardware — this kernel's boot sequence
currently assumes `uefi_loader.c`'s PE-loader shape unconditionally on aarch64; a Pi 5
native boot needs its own entry point (linked at `0x80000`, receiving `x0` = DTB pointer
directly, no `BootInfo` from UEFI at all).
- A **DTB-driven `BootInfo` equivalent** replacing ACPI-sourced data for this path — memory
map, peripheral addresses (UART, etc.) all come from the devicetree instead.
- **One real, genuine piece of reusable groundwork**: `starkernel/hal/fdt.c`/`fdt.h`, the
minimal FDT reader already built for riscv64's `timebase-frequency` lookup
(`arch/riscv64/timer.c`), is directly extensible for this — parsing `bcm2712-rpi-5-b.dtb`
for peripheral addresses is the same kind of lookup, not a new mechanism.
- A **new mailbox-property-interface framebuffer driver** — genuinely new code, no existing
precedent in this codebase, needed before the VT100 console framework
(`console.c`/`vt100.c`/`framebuffer.c`) has anything to draw onto for this board.
- This is a real architectural fork for aarch64, not a small per-board addition — QEMU
aarch64 keeps its existing UEFI+ACPI path unchanged; Pi 5 real hardware gets a second,
parallel entry path. **Not yet scoped into a punch list** — that's the next step, once this
fork's own shape (how much of `kernel_main.c`'s post-entry sequence can stay shared between
the two paths vs. needs its own branch) is thought through.
### IV.2 — Peripheral RNG: unresolved, not just under-researched
`ROADMAP.md` names an "aarch64 peripheral-RNG backend" as part of `v2.4.0`'s gate. Researched
directly rather than assumed still-TODO: Broadcom's `iproc-rng200` block (real, on Pi 4/BCM2711
as `brcm,bcm2711-rng200`) has **no `bcm2712` compatible-string entry anywhere in current
mainline Linux** (checked the actual driver's `of_device_id` table directly). The RP1
companion chip's own published peripheral list (GPIO/USB/Ethernet/DMA/ADC/PLLs/SRAM/
UARTs/SPIs) doesn't mention an RNG either. Two real possibilities, not yet distinguished:
BCM2712 still has the RNG200 block but Linux hasn't wired it into a devicetree binding yet, or
it genuinely isn't exposed to the ARM cores this generation. No public register address exists
to target right now — this needs either a Broadcom datasheet (if one becomes available) or
direct hardware probing once the board is in hand (scan the known BCM2711 RNG200 offset region
on the BCM2712 memory map and see if anything responds — risky without a datasheet confirming
it's safe to touch, so likely a "board in hand, careful probe" task, not a today task).
Deliberately **not a blocker for the first native boot** — reaching `ok>` doesn't require a
live entropy backend; `rng_get_bytes()` already has a "no entropy backend available" WARNING
path (`rng.c`) rather than a hard failure, so this can land after boot succeeds.
### IV.3 — Punch list: design/code work, no hardware needed (before 2026-09-17)
Traced against real code before writing this, not estimated: `boot_info->acpi_table`'s only
aarch64-relevant consumers today are `pci_init()` (`kernel_main.c:589`, unconditional, not
amd64-gated — relevant because RP1 is PCIe-attached on real Pi 5 hardware) and this session's
own `running_under_hypervisor()` (`arch/aarch64/timer.c`, already degrades safely to "not a
hypervisor" when `acpi_table` is `NULL` — no fix needed there).
`ioapic_init()`/`i8042_init()` are `#ifdef ARCH_AMD64`-gated, irrelevant here.
`arch/aarch64/apic.c` (GIC init) already only ever tries `boot_info->dtb`, never
`acpi_table` — its own doc comment already anticipated DTB-based discovery, just blocked
until now because QEMU's own UEFI firmware never publishes one; Pi 5 native boot removes that
blocker for free.
1. **Entry stub — DONE 2026-09-04.** New
`src/starkernel/arch/aarch64/native_rpi5_entry.S` / `include/starkernel/rpi5_native_entry.h`:
`rpi5_native_start` masks `x0` down to the documented 32-bit DTB-pointer range (§IV.1: the
firmware leaves the upper 32 bits of the register unspecified), stores it into
`g_rpi5_dtb_ptr` for item 2's still-open constructor to read, then switches `sp` to a
dedicated 2 MiB BSS stack (this path has no EDK2 boot stack to inherit — there is no EDK2 at
all here, unlike every other entry path this codebase has). Intentionally halts (`wfe`/`b`
loop) afterward rather than tail-calling into item 2's constructor, which doesn't exist yet.
**Not yet linked at `0x80000`** — that needs its own linker script/build target (item 6's own
`config.txt` work is the sibling piece; the separate-image build itself is not scoped into
this item). Now tail-calls item 2's constructor (below) instead of halting — updated
2026-09-04 when that item landed. Verified 3-arch boot to `ok>`/`zuse)ok>`
`Makefile.starkernel`'s `KERNEL_ASM` wildcards every `*.S` in `arch/aarch64/`, so this file
compiles and links into the existing QEMU/UEFI acceptance build as dead code (unreferenced
symbol, nothing there ever branches to it), same as `rpi5_dtb.c`/`rpi5_mailbox.c` before it.
2. **DTB → `BootInfo` constructor — DONE 2026-09-04.** New
`include/starkernel/rpi5_native_boot.h` / `src/starkernel/arch/aarch64/rpi5_native_boot.c`:
`rpi5_native_boot()` populates the *existing* `BootInfo` struct from the devicetree instead
of UEFI protocols (`dtb` = the real pointer, `acpi_table`/`runtime_services` = `NULL`,
`kernel_stack_base` = `NULL`/BSS-fallback — aarch64 has no `kernel_entry.S` trampoline at
all, so item 1's own BSS stack already *is* the stack `kernel_main_impl` runs on),
`args` via `/chosen`'s `bootargs` fed straight into the *existing*
`cmdline_parse_ascii()` (pure C99, no UEFI coupling — confirmed before reusing it, not
assumed), `framebuffer` via item 3's `rpi5_mailbox_get_framebuffer()` at a fixed
1920x1080x32 default (no EDID query exists in this codebase — flagged, not guessed past
this comment, revisit once real hardware and a real attached display are in hand), then
calls the **existing, unmodified** `kernel_main()` — this is the crux of why most of M1M9
stays shared. `memory_map` comes from `/memory`'s own `reg`, honoring the *root* node's
`#address-cells`/`#size-cells` (confirmed against `bcm2712.dtsi`'s actual root node — `<2>`/
`<2>` — not assumed; a hardcoded-wrong cell width here would compile clean and boot clean in
QEMU while silently corrupting the real memory map on real silicon, so this was verified
from the source rather than recalled). Required a new `fdt_find_node_by_device_type()`
(`fdt.c`/`fdt.h`) since `/memory` is identified by `device_type = "memory"` per DT spec
§3.4, not `compatible`. Required a Makefile fix: `boot/cmdline.c` was only in `LOADER_SRCS_BASE`
(the `.efi` target), not `KERNEL_SRCS_BASE` (the separate `.elf` target `arch/aarch64/*.c`
also wildcards into) — added it there too, a real link failure caught before it could ship.
**`/reserved-memory` carving — DONE 2026-09-04.** Originally deferred here as needing
interval-splitting logic written blind against hardware not yet in hand — revisited once an
actual reservation was confirmed to exist rather than assumed either way: fetched
`bcm2712-ds.dtsi` directly and found a real `reserved-memory` node with one static child
(`atf@0`, `reg`-addressed, ARM Trusted Firmware's own region) and one dynamic child
(`linux,cma`, `size`/`alloc-ranges` only, no fixed address — skipped, nothing fixed to carve
and no allocator this early to service it against anyway). `collect_reserved_ranges()`
walks `reserved-memory`'s children via two new `fdt.c` primitives —
`fdt_find_node_by_name()` (needed since `/reserved-memory` has neither `compatible` nor
`device_type` per DT spec §3.5.4) and `fdt_next_child_node()` (one exported symbol, not the
two-primitive general sibling-walker originally sketched — collapsed after review, since
this codebase's only real use is "iterate one node's direct children," not general tree
navigation) — reading each child's own `#address-cells`/`#size-cells` with a fallback to
root's only if absent (confirmed necessary, not just defensive:
`reserved-memory`'s own declared `<2>`/`<1>` genuinely differs from root's `<2>`/`<2>`).
`emit_region_with_carveouts()` clips a sorted reserved-range list against each RAM region,
emitting alternating `EfiConventionalMemory` gaps and `EfiReservedMemoryType` carve-outs
(insertion sort, not `qsort` — freestanding, no libc). `no-map`/`reusable` flags are not
distinguished; every static reservation is excluded from `EfiConventionalMemory` regardless.
`RPI5_MAX_MEMMAP_ENTRIES` is the exact worst-case count
(`RAM_REGIONS * (2*RESERVED_RANGES + 1)`), recomputed rather than estimated — the
`rpi5_mailbox.c` buffer-size bug is the standing lesson for this pattern. Verified 3-arch
boot to `ok>`/`zuse)ok>` — compile-only, same caveat as every item in this list: nothing in
the existing UEFI/QEMU path calls `rpi5_native_boot()`, so this cannot be exercised until
real hardware.
3. **Mailbox-property-interface framebuffer driver — DONE 2026-09-04.** New
`include/starkernel/rpi5_mailbox.h` / `src/starkernel/arch/aarch64/rpi5_mailbox.c`:
`rpi5_mailbox_get_framebuffer()` builds and sends one property-tag buffer (phys size, virt
size, depth, pixel order, virtual offset, allocate-buffer, get-pitch), populating an
`Rpi5FramebufferInfo` kept in exact field-for-field sync with `uefi.h`'s `FramebufferInfo`
so `console.c`/`vt100.c`/`framebuffer.c` need no changes downstream. Register layout
(`+0x00`/`+0x18` MBOX0 read/status, `+0x20`/`+0x38` MBOX1 write/status) confirmed against a
Pi-5-specific bare-metal reference (main.lv), independently cross-checked against this
codebase's own `rpi5_dtb.c` translated base address — two independent sources agreeing. A
real buffer-overflow bug was found and fixed before compiling (the static request buffer
was sized 32 words against an actual 35-word requirement, recomputed exactly rather than
re-estimated; resized to 40 words for margin). **Two things flagged, not guessed, as
genuinely unverified against real hardware:** the `TAG_ALLOCATE_BUFFER` tag's request-size
field value (set to the response size, matching common practice across surveyed reference
implementations, not a single spec-quoted number); and whether the allocate-buffer
response address needs the classic `& 0x3FFFFFFF` bus-alias masking on Pi 5 specifically —
kept defensively even though the same Pi-5-specific send-side reference found no bus-alias
bit in play there. Verified 3-arch boot to `ok>`/`zuse)ok>` (compile-only — no caller yet;
that's the entry-stub/DTB-constructor items above, still open).
4. **`fdt.c`/`fdt.h` extension — DONE 2026-09-04.** Added `fdt_find_node_by_compatible()`
(matches any entry in a node's NUL-separated `compatible` list, first match in document
order) and `fdt_find_prop_in_node()` (scoped to that one node's own direct properties only
— stops at the first child node or the node's own end, never descends or continues into a
sibling). Same minimal, non-tree-building style as the existing reader — no new state, no
allocation, one linear scan per call. Verified 3-arch boot to `ok>`.
**4a. UART + mailbox address lookup — DONE 2026-09-04.** New
`include/starkernel/rpi5_dtb.h` / `src/starkernel/arch/aarch64/rpi5_dtb.c`:
`rpi5_uart_base()`/`rpi5_mailbox_base()`, each `fdt_find_node_by_compatible()` (`"arm,pl011"`
/ `"brcm,bcm2835-mbox"`) → `fdt_find_prop_in_node(..., "reg", ...)`. **A real translation
gap found and fixed before this could have been silently wrong**: confirmed directly
against `bcm2712.dtsi` (raspberrypi/linux) that both peripherals live under one `soc`
simple-bus node whose `ranges` property adds a fixed `0x10_0000_0000` offset to every
child `reg` value — `fdt.c`'s reader deliberately does not apply `ranges` translation
generally (not a general devicetree library), so this file applies that one, fixed,
SoC-wide offset explicitly by name (`BCM2712_SOC_RANGES_OFFSET`), documented with the exact
devicetree excerpt that confirmed it. Verified 3-arch boot to `ok>` (compile-only — these
two functions have no caller yet; that's the entry-stub/framebuffer-driver items above,
still open).
5. **`pci_init()` DTB path — RE-SCOPED 2026-09-04, this item's own original text was wrong.**
Investigated before writing any code (per this document's own discipline) rather than
building the originally-assumed "swap the ACPI MCFG lookup for a DTB one, same flat-ECAM
access underneath" version — that assumption does not hold, confirmed against three primary
sources, not guessed:
- `bcm2712-rpi-5-b.dts` (real board file): RP1 is attached under `pcie2`
(`pcie2: pcie@1000120000`, `compatible = "brcm,bcm2712-pcie"`) via
`rp1_target: &pcie2 { status = "okay"; };`.
- `pcie2`'s own `reg` is `<0x10 0x00120000 0x00 0x9310>` — a ~37 KB window, far too small to
be a flat 256 MB ECAM region (256 buses × 1 MB) the way `pci.c`'s existing `ecam_*`
functions assume.
- The real Linux driver for `"brcm,bcm2712-pcie"` (`drivers/pci/controller/pcie-brcmstb.c`)
confirms why: `brcm_pcie_map_bus()` computes a standard ECAM-shaped offset
(`PCIE_ECAM_OFFSET(bus, devfn, 0)`) but does **not** read/write it directly — it writes
that offset to an `IDX_ADDR` index register, then accesses the config data through a small
windowed `DATA_ADDR` region. Root-complex-local accesses (`devfn == 0` on the root bus)
skip the indirection and hit `base + PCIE_ECAM_REG(where)` directly; every downstream
device access (RP1 included) goes through the index/data window.
- This is a real **third config-access mechanism**, not a bigger version of "find the ECAM
base in the DTB instead of ACPI." `pci.c`'s `cfg_read32`/`cfg_write32` family
(`pci.c:188232`) today dispatches exactly two ways, selected by `#ifdef ARCH_AMD64` vs.
everything else (flat `ecam_*`) — and that `#ifdef` selection is itself now wrong for this
case: QEMU aarch64 (flat ECAM via ACPI MCFG, real and working) and Pi 5 aarch64
(Broadcom indirect windowing) are the **same build** (`ARCH=aarch64`), so the dispatch
must become a runtime choice, not a compile-time one — a real change to a shared file all
three architectures currently boot clean through, not a small addition.
- Secondary finding, not yet acted on: `pci_init()`'s `vmm_map_range()` call for the ECAM
window is inside `#ifdef ARCH_AMD64` only (`pci.c:306318`) — aarch64/riscv64 rely on
UEFI's own identity map instead (per this file's own header comment). Native boot has no
UEFI identity map at all, so **any** PCIe work on this path — regardless of access
mechanism — needs its own explicit `vmm_map_range()` call, not just a new config-access
branch.
- **Not implemented this pass.** The re-scoped shape (add a third, runtime-selected
Broadcom-indirect access mode to `pci.c`, plus explicit VMM mapping for the native path)
is a materially larger, higher-blast-radius change than items 14 — it touches a file all
three architectures share and boot through today. Recording the real shape here is this
pass's own deliverable; building it is future work.
6. **`config.txt` contents — DONE, 2026-09-04.** Written to `boot_media/rpi5/config.txt`
(new directory — `configs/` is Kconfig defconfigs, `img/` is banners/docs, neither fits).
Researched against the official current `config.txt` reference
(raspberrypi.com/documentation/computers/config_txt.html) and one real, working Pi 5
bare-metal project's own checked-in `config.txt` (`leopoldch/BatMetal`), not assumed from
general Pi knowledge. Final contents, with the reasoning as comments in the file itself:
- `kernel=kernel_2712.img` — the real Pi-5-specific default filename firmware looks for
first (falls back to `kernel8.img` if absent); naming our image this exactly removes
ambiguity, so the item's own `kernel8.img`/`os_check=0` fallback alternative wasn't
needed.
- `os_check=0` — official docs name "bare-metal development" as the explicit use case for
this flag. Kept despite BatMetal's own config.txt *not* setting it and still booting —
recorded as an open discrepancy rather than silently resolved; no hardware in hand yet to
confirm which behavior is actually gated.
- `device_tree=bcm2712-rpi-5-b.dtb` — pins the DTB explicitly rather than relying on
board-revision auto-selection (confirmed via WebSearch of `config.txt` syntax docs).
- `enable_uart=1` — **correction to this item's own original text, which didn't have
this.** Added per review: costs nothing when nothing is listening, and is the only
diagnostic channel that survives a framebuffer failure on the first real boot (the
mailbox framebuffer path, `rpi5_native_boot.c`, has two documented-unverified details
and has never run on silicon). Enables the Pi 5's dedicated JST debug UART, not the
GPIO 14/15 header (`dtoverlay=uart0`, deliberately left out — needs a USB-serial
adapter this HDMI-only bring-up doesn't have). `rpi5_uart_base()` already exists
and is currently uncalled — this is the hook a UART fallback console would use.
- `arm_64bit=1`**dropped, correction to this item's own original text.** Official
docs: "Models that only support a 64-bit kernel ignore this flag" — confirmed inert on
Pi 5, not wrong to include, just meaningless.
- `dtparam=pciex1` — researched (BatMetal's own comment: "Required for RP1 access"), not
included. Community-reported only, not confirmed against official docs, and surprising
if true (RP1 is normally always-on). Not needed yet — item 5's real RP1/PCIe work is
still re-scoped, not implemented. Revisit if that work needs it.
- `boot_media/rpi5/README.md` also added, noting the one remaining gap this item's
`config.txt` exposes but doesn't itself close: `kernel_2712.img` names a file nothing
currently builds — item 1's separate-image build target (linking at `0x80000`) is still
outstanding future work, same gap that item's own text already flagged.
7. **Code audit pass — DONE, 2026-09-04.** Reviewed `arch/aarch64/apic.c`, `arch.c`,
`timer.c`, `interrupts.c` for the same class of QEMU-virt-vs-real-hardware assumption
§III item 6's amd64 audit looked for. Report-only by default, per this project's own
"identify, don't fix unless asked" rule — **all three findings below were fixed in code
across two follow-up rounds, each named specifically by direct instruction after this
audit landed**: the GIC base address (severe), the stale VBAR_EL2 doc comment (doc-only),
and the hardcoded HVC PSCI conduit. §V.3's own parallel audit found a fourth finding of
this same class (riscv64's `satp`-clear reasoning, lowest severity of the whole set) that
remains report-only, not asked for. See each finding's own
own text for what changed.
**Finding — severe, confirmed live, blocked reaching `ok>` on real hardware as the code
stood — FIXED 2026-09-04, per direct instruction (this item's own findings are normally
report-only; this one was explicitly asked for by name).** `apic.c`'s own file header
already self-documented `GICD_BASE_PA`/`GICC_BASE_PA` (`0x08000000`/`0x08010000`) as
"QEMU-virt-machine constants... a deliberate, recorded exception," reasoned correctly at
the time it was written: no DTB was ever available to discover them from, because QEMU's
own aarch64 UEFI firmware doesn't forward one. That premise no longer held — the native
boot path (item 2, DONE) receives a real DTB directly and calls the same, unmodified
`kernel_main()` M4 sequence, which calls `apic_init(boot_info)` unconditionally
(confirmed: `kernel_main.c:413`, no arch- or boot-path gating) — `apic_init()` itself
ignored `boot_info` entirely and always programmed the QEMU addresses. The real BCM2712
GIC-400 is at `0x10_7fff9000` (distributor) / `0x10_7fffa000` (CPU interface 0) —
confirmed directly against `bcm2712.dtsi`'s own `axi`/`gicv2` nodes
(`compatible = "arm,gic-400"`, `#address-cells = <2>`/`#size-cells = <2>`, `axi`'s own
`ranges` identity-mapping this region — no offset needed, unlike `rpi5_dtb.c`'s
1-cell/offset `soc` peripherals), not recalled.
**Fix**: `apic.c` now tries `gic_bases_from_dtb()` first — `fdt_valid(dtb)`
`fdt_find_node_by_compatible(dtb, "arm,gic-400")``fdt_find_prop_in_node(..., "reg",
...)`, reading the first two 2-address-cell/2-size-cell entries (GICD, then GICC — the
standard arm,gic-400 binding order) — and falls back to the QEMU constants only when no
DTB or no matching node is found, so the existing QEMU/UEFI path's behaviour (verified via
the mandatory 3-arch boot) is unchanged. `GICD_BASE_PA`/`GICC_BASE_PA` became
`s_gicd_base`/`s_gicc_base` (module-static `uintptr_t`, not `#define`s, since they're no
longer compile-time constants on this path) — every MMIO call site (`apic_init()`,
`apic_spi_enable()`, `apic_read_iar()`, `apic_eoi_intid()`) now reads through them.
The 3-arch acceptance run exercised the *guard*, not just compiled it: `apic_init()`'s
`gic_bases_from_dtb()` call runs unconditionally on every aarch64 boot, including this
one, and its `fdt_valid()` check correctly returned 0 on this system's QEMU/UEFI firmware
— confirmed via the boot log's own `GICv2: no DTB GIC node -- using QEMU virt-machine
defaults` line, followed by the unchanged `distributor+CPU interface enabled, PPI 30`.
What remains genuinely unexercised is the *success* branch (a real DTB with a matching
`"arm,gic-400"` node) — that needs a devicetree this build never has, so it stays
unverified until real Pi 5 hardware, same caveat as every other native-path item in this
list.
**Finding — doc-only, not a functional gap — FIXED (comment corrected) 2026-09-04, per
direct instruction naming it specifically.** `arch_interrupts_init()`'s own doc comment
in `interrupts.c` claimed "`VBAR_EL1` is written unconditionally regardless of the
detected level... if EL2, this is a known gap" — checked against `isr.S`
(`aarch64_install_vectors`, lines 124146) and found **stale**: the actual implementation
already branches on `aarch64_current_el()` and writes `vbar_el2`/`vbar_el1` correctly,
with `el2_mode_flag` threading the same answer through the IRQ trampoline for
`ELR_EL1`/`SPSR_EL1` vs `ELR_EL2`/`SPSR_EL2` selection. Rewrote the doc comment and the
matching runtime console message (which made the identical stale claim — "not yet
wired, see item 0.5/0.7") to describe what `isr.S` actually does, and fixed a
same-vintage one-word staleness in `arch.c`'s own `aarch64_install_vectors` extern
comment ("installs `VBAR_EL1`" → EL-aware) caught while touching this. No behavior
changed anywhere — this was purely three comments correcting themselves to match code
that was already right. Which EL the Pi 5's ATF hands the kernel off at under the native
path is itself still unconfirmed (the `atf@0` reserved-memory region found during item
2's work only establishes that EL3 firmware exists, not which EL the kernel lands at) —
moot for this specific fix now that both paths are correctly wired regardless of which EL
is chosen, but worth knowing for other EL-dependent code.
**Finding — same defect class as amd64's `arch_cold_reset()` finding, on the QEMU side of
this arch specifically — FIXED 2026-09-04, per direct instruction naming it
specifically.** `arch/aarch64/arch.c`'s `arch_cold_reset()` issued PSCI `SYSTEM_RESET` via
`HVC` unconditionally — a fix already documented in-file as a QEMU-specific workaround,
because QEMU's AAVMF firmware has no genuine EL3/TrustZone secure monitor to answer an
`SMC`. The Pi 5's real ATF (confirmed present via the `atf@0` reservation) is exactly the
kind of genuine EL3 firmware PSCI's `SMC` conduit assumes — confirmed further this pass:
`bcm2712.dtsi`'s own `/psci` node declares `method = "smc"` directly, not inferred from
ATF's mere presence. **Fix**: new `aarch64_psci_conduit_init(dtb)` (`arch.c`) —
`fdt_valid(dtb)``fdt_find_node_by_compatible(dtb, "arm,psci-1.0")`
`fdt_find_prop_in_node(..., "method", ...)` — sets a module-static `s_psci_use_smc` flag
to 1 only when the DTB's own `method` property reads exactly `"smc"`; every other
outcome (no DTB, no PSCI node, `method="hvc"`, property absent) leaves it at its default
0, preserving the exact HVC behaviour this system's QEMU boot already relies on.
`arch_cold_reset()` now branches on that flag between the `smc #0`/`hvc #0` trap
instructions; the function ID and calling convention are identical either way, matching
the file's own existing comment about that. Called once from `apic_init()`
(`arch/aarch64/apic.c`), the one point in boot with `boot_info->dtb` already in hand —
collocated in `arch.c`, not `apic.c`, because `arch_cold_reset()` is the actual consumer
and has no `boot_info` of its own by the time it runs (called from deep in VM execution,
via `BYE`). Riscv64's equivalent (`arch/riscv64/arch.c`, SBI SRST) remains the one cold-
reset implementation across all three architectures that never needed this kind of fix —
still worth naming as the standard the other two moved toward, one now fixed, amd64's
i8042-pulse version still open. 3-arch acceptance exercised the guard (this system's QEMU
AAVMF forwards no DTB, so `aarch64_psci_conduit_init()` hits its own "no DTB" branch and
`arch_cold_reset()` keeps using HVC, unchanged) but not the `SMC` success branch itself —
that stays unverified until real Pi 5 hardware runs `BYE`.
**Hardware-dependent, after 2026-09-17 (not started until then):**
8. Build the boot media (SD card: `config.txt`, `bcm2712-rpi-5-b.dtb`, kernel image).
9. Connect HDMI + keyboard (observation decision above).
10. Boot; confirm `ok>`/`zuse)ok>` reached.
11. Mint a Zuse identity on real media, confirm re-attach — the `v2.4.0` gate's own
requirement, same shape as amd64's.
12. Update this section with results before moving to riscv64's own hardware-dependent steps.
## V. riscv64 — Milk-V Mars
**Already true:** `ROADMAP.md` names this (generically, "Milk-V") as part of `v2.5.0`'s gate:
boots on the real board, the Zkr (RNDR) entropy backend live. Same gap as aarch64:
`rng_get_bytes()` has no riscv64 hardware-RNG path today, only `virtio-rng`.
### V.1 — Boot chain: resolved, researched 2026-09-04
**Resolved, not left open.** The Mars is a documented mainline U-Boot board target in its own
right ([U-Boot docs — Milk-V Mars](https://docs.u-boot.org/en/latest/board/starfive/milk-v_mars.html)),
and it uses **the exact same U-Boot binaries as the StarFive VisionFive 2** — same SoC
(StarFive JH7110), board identity detected at SPL time, devicetree patched accordingly, no
separate Mars-specific firmware. This directly answers §V's own previously-open question:
**U-Boot + OpenSBI + devicetree, not UEFI** — same fork this kernel already decided for
aarch64 (§IV.1), now confirmed for riscv64 too.
**Boot chain, concretely:**
1. BootROM (ZSBL), StarFive's on-chip loader at `0x2A000000`, selects boot media by GPIO pins.
2. U-Boot SPL (FSBL) — initializes DRAM, configures PLLs.
3. OpenSBI (`fw_dynamic.bin`) — M-mode runtime services.
4. U-Boot main, S-mode, depends on OpenSBI.
5. Boot media: QSPI flash (recommended) or UART XMODEM (recovery). SD/eMMC boot modes are
deprecated in current U-Boot.
**Entry protocol, from real VisionFive 2 bare-metal work (same SoC, directly applicable per
§VI's own cross-reference):**
- Entry point `0x40000000`.
- Core identification via the `mhartid` CSR — the SiFive S7 monitor core is hart 0, the four
U74 application cores are harts 14 (matches the QEMU riscv64 target's own hart numbering
convention already assumed elsewhere in this codebase — worth double-checking, not
assuming, once real hardware is in hand).
- UART at `0x10000000`, 115200 baud, already initialized by firmware before handoff.
- Custom bare-metal images package via `vf2-imager` (invokes U-Boot's `mkimage`) into a FIT
image — same tooling should apply to the Mars, unconfirmed until tried.
- **Not yet found**: what registers carry the DTB pointer/hart ID at the actual kernel entry
point under this specific chain (the source consulted covered the image-packaging tooling,
not the OpenSBI→kernel handoff register convention). **Resolved 2026-09-04**: standard
RISC-V SBI boot protocol, confirmed via OpenSBI's own docs — `a0`=hart ID, `a1`=DTB pointer,
S-mode entry. Not chain-specific guesswork; this is the universal convention OpenSBI's
`FW_DYNAMIC` firmware type uses regardless of vendor, so it applies to this chain directly.
**What this means for the codebase** — same shape of fork as aarch64 (§IV.1): a non-UEFI
entry path, a DTB-driven `BootInfo` equivalent (the existing `starkernel/hal/fdt.c` reader
extends here too, same as for the Pi 5), no ACPI.
**Decided in conversation, 2026-09-04: observation is HDMI-only**, same reasoning and same
constraint as the Pi 5 (§IV) — no bridge hardware available for this board's own first
bring-up either; the Mars has its own HDMI 2.0 output (§VI).
### V.2 — Peripheral RNG and Zkr: still genuinely open
- Zkr/RNDR instruction availability on the Mars's actual CPU (riscv64 Scalar Crypto extension
support varies by implementation) — not yet confirmed; the VisionFive 2 bare-metal research
above didn't surface this either, would need its own targeted look (or a real-hardware
probe of `misa`/the Zkr extension discovery mechanism). Deliberately **not a blocker for
first boot**, same reasoning as §IV.2's aarch64 RNG gap — `rng_get_bytes()` already
WARNs rather than hard-fails with no backend.
- **Whether the Mars needs the same pinned-GPIO-VM treatment as the Pi 5** — explicitly
**not decided either way**, per direct instruction ("same for Milk-V (? not sure here)").
See `FABRIC-4.md` §2. The Mars does have its own 40-pin GPIO header (§VI), so the open
question is the VM architecture around it, not whether the hardware exists.
### V.3 — Punch list: design/code work, no hardware needed (before 2026-09-17)
Traced against real code before writing this, same discipline as §IV.3: `pci_init()`
(`kernel_main.c:589`, unconditional) is the one real `acpi_table` consumer relevant here too
— the Mars's M.2 E-Key slot (§VI) is PCIe-attached, same shape of gap as the Pi 5's RP1.
`riscv64/timer.c` is **already** fully DTB-driven (both `timebase-frequency` and this
session's own hypervisor-detection check) — no further work needed there; it was built DTB-
first from the start, unlike aarch64's timer which needed a new ACPI-based check today.
**One real, already-flagged risk found while tracing this**: `arch/riscv64/apic.c`'s own doc
comment says the PLIC base address is "a constant, not discovered from `boot_info->dtb`" —
and `arch/riscv64/plic.c`'s own doc comment (predating this document) already warned
`PLIC_BASE`/`PLIC_CONTEXT_S` are "QEMU-virt-specific... not assumed stable across" other
configurations. That warning becomes concrete now: the JH7110's real PLIC address on the Mars
is not confirmed to match QEMU-virt's, and the interrupt controller will not work correctly if
it doesn't. This is a real punch-list item, not a hypothetical.
1. **Entry stub**: new native riscv64 entry point at `0x40000000` (§V.1), receiving `a0`=hart
ID, `a1`=DTB pointer directly (now-confirmed SBI convention) — no UEFI, no PE loader.
2. **DTB → `BootInfo` constructor**: same shape as aarch64's (§IV.3 item 2) — `dtb`=real
pointer, `acpi_table`=`NULL`, memory map from DTB `/memory`+`/reserved-memory`, `args` from
`/chosen`/`bootargs`.
3. **PLIC base address: make it DTB-discovered**, not the current QEMU-virt-specific
constant — the one concrete, already-flagged risk above. `fdt.c`'s node-scoped lookup
extension (§IV.3 item 4, **DONE 2026-09-04**, shared with the Pi 5's UART/mailbox
addresses) is the primitive this calls
(`fdt_find_node_by_compatible(fdt, "sifive,plic-1.0.0")``fdt_find_prop_in_node(..., "reg", ...)`,
plausible compatible string, not yet confirmed against the Mars's real DTB) — the actual
PLIC-init call site update is still open, only the primitive it needs now exists.
4. **Framebuffer for HDMI output**: JH7110's display path is genuinely unresearched this
pass — unlike the Pi 5's mailbox interface (well-documented, reused across many Pi bare-
metal projects), no equivalent research done yet for JH7110's own display controller.
Flagged here rather than assumed simple.
5. **`pci_init()` DTB path — the "shares §IV.3 item 5's code" framing is now wrong, corrected
2026-09-04.** §IV.3 item 5's own investigation found the Pi 5's RP1 sits behind a
Broadcom-specific indirect config-access window (`"brcm,bcm2712-pcie"`, confirmed against
`pcie-brcmstb.c`), not flat ECAM — a SoC-specific mechanism with no reason to assume it
matches JH7110's own PCIe controller. The "one implementation, two consumers" plan this line
originally stated no longer holds; the Mars's M.2 E-Key slot needs its **own** primary-source
check of JH7110's actual PCIe controller (compatible string, `reg` window size, and whether
its Linux driver uses flat ECAM or its own indirect scheme) before any implementation
decision, same discipline §IV.3 item 5 itself just applied. Not yet done.
6. **Boot image packaging**: `vf2-imager`/`mkimage`-based FIT image (§V.1) — confirm this
tooling's actual invocation once building the first real image, not just cited from
VisionFive 2 research.
7. **Code audit pass — DONE, 2026-09-04.** Reviewed `arch/riscv64/apic.c`, `plic.c`,
`arch.c`, `interrupts.c`, `timer.c` for the same class of QEMU-virt-vs-real-hardware
assumption §III item 6's amd64 audit and item 7 above's aarch64 audit looked for. Report
only — nothing below was changed.
**Clean, and the most portable pattern of all three architectures**: the SBI timer path
(`apic.c`) is genuinely hardware-independent — it probes for the TIME extension at
runtime and reports loudly rather than assuming it's present, so it depends on nothing
but OpenSBI itself, present on both QEMU and (per §V.1's own research) the Mars's real
U-Boot+OpenSBI chain. `arch_cold_reset()` (`arch.c`) uses the SBI SRST extension — a real
standards-defined mechanism, not a board-specific hack — making it the one of the three
architectures' cold-reset implementations that does **not** need a finding here (contrast
amd64's i8042-pulse hack and aarch64's HVC-hardcoded PSCI call, both flagged above/in
§III). `timer.c` is confirmed DTB-first as this section's own preamble already stated:
`timebase-frequency` is read via `fdt_prop_u32()` with a *named* QEMU-only fallback
(`RISCV_TIMEBASE_HZ_FALLBACK`) used only when the DTB is absent or invalid.
**Finding — already tracked, this pass confirms rather than discovers it**: `plic.c`'s
`PLIC_BASE`/`PLIC_CONTEXT_S` hardcoded-to-QEMU-virt situation is exactly item 3 above,
already flagged as "a real punch-list item, not a hypothetical" before this audit ran.
Nothing new to add beyond confirming the file's own header comment is accurate and the
risk is real, not overstated.
**Finding — minor, reasoning doesn't transfer to the native path, action likely still
safe**: `arch_early_init()`'s explicit `satp` clear (Bare-mode switch) is justified in
its own comment entirely by behavior *observed under QEMU's EDK2 RISC-V firmware*
(confirmed `satp.MODE=10`/Sv57 live, kernel identity-mapped within it). Under the native
boot path (U-Boot+OpenSBI, no UEFI/EDK2 at all per §V.1) that specific observation cannot
apply — OpenSBI's S-mode handoff conventionally already leaves `satp=0` (Bare mode), most
likely making this a no-op there rather than a hazard, but the *stated justification* for
the switch (identity-mapped, safe to clear) was derived from a firmware stack the native
path doesn't use, and hasn't been re-confirmed for OpenSBI's actual handoff state. Lowest
severity of any finding across both audits — flagged for completeness, not urgency.
**Hardware-dependent, after 2026-09-17:**
8. Build and flash the boot image to QSPI flash (or attempt UART XMODEM recovery boot if QSPI
flashing isn't set up yet — both are real supported paths per §V.1).
9. Connect HDMI + keyboard.
10. Boot; confirm `ok>`/`zuse)ok>` reached.
11. Mint a Zuse identity on real media, confirm re-attach — the `v2.5.0` gate's own
requirement.
12. Update this section with results.
---
## VI. Hardware identification reference
Per-board SoC/CPU facts, consolidated here so later sections don't have to re-derive them.
Researched 2026-09-04 (web search, sources cited); anything not directly confirmed against
the actual unit in hand is flagged as such rather than assumed.
### amd64 — Beelink SER5 (reference/development machine)
- **CPU: AMD Ryzen 7 family.** Beelink has shipped the "SER5" name with several different
Ryzen 7 SKUs over its product life (5700U, 5800H, 7735HS all confirmed to exist under this
branding) — **exact SKU on this unit not yet confirmed**; check `dmesg`/BIOS/the physical
unit when convenient (`cat /proc/cpuinfo` or the BIOS splash screen under Linux/before
LithosAnanke boots, since LithosAnanke itself has no CPU-identification word yet). Not
load-bearing for this document's own genericity requirement (§III) — the boot path must not
depend on which SKU this is, by design — but worth pinning down for this reference's own
accuracy.
- **Architecture generation**: Zen2 (5700U/5800H) or Zen3 (7735HS) depending on the SKU above
— matters for any future CPU-feature-detection work (e.g. RDRAND is present on all of
these; that part's already confirmed live via `rng: backend = rdrand`, §III).
- Sources: [Gentoo wiki — SER5 5560U](https://wiki.gentoo.org/wiki/Beelink_SER5_AMD_Ryzen_5_5560U_Mini_PC),
[Starry Hope — SER5](https://www.starryhope.com/minipcs/models/beelink-ser5-mini-pc/),
[Starry Hope — SER5 Pro](https://www.starryhope.com/minipcs/models/beelink-ser5-pro-mini-pc/),
[Minixpc — SER5 Max](https://minixpc.com/blogs/news/beelink-ser5-max-review-powered-by-amd-ryzen7-5800h-processor).
### aarch64 — Raspberry Pi 5 (sole target)
- **SoC: Broadcom BCM2712.**
- **CPU**: quad-core 64-bit Arm Cortex-A76, 2.4 GHz, 512 KB per-core L2 cache, 2 MB shared L3.
- **GPU**: VideoCore VII, 12-core, 800 MHz, OpenGL ES 3.1 + Vulkan 1.2 (not relevant to
LithosAnanke's own framebuffer work — that goes through the mailbox property interface,
§IV.1 — but recorded here for completeness).
- **RAM**: LPDDR4X-4267, board variants at 1/2/4/8/16 GB, 32-bit memory interface, ~17 GB/s
bandwidth.
- **I/O**: RP1 companion chip (PCIe 2.0 x4-attached) handles GPIO, USB 2.0/3.0, Gigabit
Ethernet, CSI/DSI, analog video — confirmed separately (§IV.2) to have no RNG peripheral in
its own published peripheral list.
- **Cortex-A76 and `FEAT_RNG` (ARMv8.5 `RNDR`/`RNDRRS`)**: not confirmed present — A76 is not
among the cores that typically implement this feature (more common on newer cores like
Cortex-X2/A710); if this matters for any future entropy-source decision, verify via `ID_AA64ISAR0_EL1`
directly on the real board rather than assuming either way.
- Sources: [CNX Software — Pi 5 launch](https://www.cnx-software.com/2023/09/28/raspberry-pi-5-sbc-broadcom-bcm2712-quad-core-cortex-a76-soc/),
[Raspberry Pi — Processors doc](https://www.raspberrypi.com/documentation/computers/processors.html),
[sbcwiki — BCM2712](https://sbcwiki.com/docs/soc-manufacturers/broadcom/bcm2712/boards/rasperrypi-5/).
### riscv64 — Milk-V Mars (sole target)
- **SoC: StarFive JH7110**, 28 nm.
- **CPU**: 4× SiFive U74-MC application cores (RV64GC) + 1× SiFive S7 monitor core, up to
1.5 GHz.
- **RAM**: up to 8 GB LPDDR4; storage via eMMC slot + microSD slot.
- **I/O**: 3× USB 3.0, 1× USB 2.0, HDMI 2.0 (4K), Gigabit Ethernet with PoE support, M.2 E-Key
(WiFi/BT), 4-lane + 2-lane MIPI CSI, 40-pin GPIO header.
- **Physical**: designed to Raspberry Pi 3B dimensions — cases/heatsinks/fans for that form
factor are compatible.
- **Multimedia**: H.264/H.265 4K@60fps decode, H.265 1080p@30fps encode (not relevant to
LithosAnanke's own bring-up, recorded for completeness).
- Same JH7110 SoC as the StarFive VisionFive 2 — any VisionFive 2 bring-up material found
while researching §V's own boot-chain question is likely directly applicable here too, worth
checking first before assuming Mars-specific research is needed from scratch.
- Sources: [milkv.io — Mars overview](https://milkv.io/docs/mars/overview),
[milkv.io — Mars product page](https://milkv.io/mars),
[TinyComputers.io — Mars review](https://tinycomputers.io/posts/milk-v-mars-review.html).
### Noted for later, not yet in scope — BeagleBone Black
Added to this reference per direct instruction 2026-09-04, **recorded only — no work scoped
around it yet.** Genuinely different from the three targets above: the BeagleBone Black's
SoC is a **32-bit ARM** part, not aarch64 — a fourth architecture this kernel has no support
for at all today (amd64/aarch64/riscv64 only), not another board under an existing one.
- **SoC: TI Sitara AM335x.**
- **CPU**: single-core ARM Cortex-A8, 1 GHz, armv7-a (32-bit) — up to ~2000 MIPS.
- **RAM**: 512 MB DDR3L. Storage: 4 GB eMMC (default boot source) + microSD (secondary/
overridable to primary).
- **Other on-die units**: PowerVR SGX530 3D GPU; 2× PRU (Programmable Realtime Unit) 32-bit
200 MHz microcontrollers — real-time I/O coprocessors, no equivalent on any of the three
boards above; crypto accelerators.
- **Boot modes**: eMMC, microSD, serial, USB.
- Sources: [element14 — BBB product page](https://www.element14.com/community/docs/DOC-84108/l/beaglebone-black-development-board-with-1ghz-am335x-arm%C3%A3%C3%A2-cortex-a8-processor),
[TI.com — BEAGL-BONE-BLACK](https://www.ti.com/tool/BEAGL-BONE-BLACK).
### Noted for later, not yet in scope — Zynq-7000 (Puzhi PZ7010/PZ7020 "StarLite")
Added per direct instruction 2026-09-04, **recorded only — no work scoped around it yet.**
Unlike BeagleBone Black above, this one isn't a random addition: `ROADMAP.md` already names
**Zynq FPGA as the next big milestone beyond v2.5.0** — "the step where the battle-tested
amd64/aarch64/riscv64 story rides on configurable silicon," and the three-product split
decided alongside it names "hardware steady-state machinery with sealed executions,
HOL-proven" as the FPGA-native product this board would ultimately serve. This entry just
puts a concrete, purchasable board under that already-named milestone.
- **Board**: Puzhi PZ7010-StarLite (XC7Z010) or PZ7020-StarLite (XC7Z020) — same board design,
two SoC variants. 90×60mm, black PCB, immersion gold finish.
- **SoC: Xilinx/AMD Zynq-7000**, combining a **Processing System (PS)** — dual-core ARM
Cortex-A9, up to 667 MHz (`-1` speed grade) or 800 MHz (`-2`, XC7Z020 only) — with
**Programmable Logic (PL)**, 28 nm Artix-7/Kintex-7-based FPGA fabric. Genuinely a fifth
architecture class in this reference: ARMv7-A again (like BeagleBone Black), but a
different core (Cortex-A9 vs. A8) *and* an FPGA fabric with no equivalent on any board
above — this is the "configurable silicon" milestone `ROADMAP.md` already flagged as
reshaping the hardware story (soft/hard CPU cores, PL fabric, non-standard memory map,
custom peripherals), not a small per-board addition even in concept.
- **PS details** (identical between both variants): 256 KB on-chip memory, DDR3 controller,
32 KB I-cache + 32 KB D-cache per core, 512 KB shared L2.
- **PL resources (the actual XC7Z010 vs. XC7Z020 difference)**: XC7Z010 — 4,400 logic slices,
17,600 6-input LUTs, 35,200 flip-flops, 270 KB block RAM, 80 DSP slices. XC7Z020 — 13,300
logic slices, 53,200 LUTs, 106,400 flip-flops, 630 KB block RAM, 220 DSP slices.
- **RAM/storage**: 512 MB/1 GB DDR3, QSPI flash, EEPROM, SD boot.
- **I/O**: JTAG, UART, HDMI out, Gigabit Ethernet, USB 2.0 host, 40-pin expansion; MIPI CSI on
the 7020 variant only.
- Sources: [Puzhi — PZ7010-StarLite](https://www.en.puzhi.com/Product/AMD-FPGA-Development-Board/Zynq-7000-SoC/PZ7010-StarLite),
[Puzhi — PZ7020-StarLite](https://www.en.puzhi.com/Product/AMD-FPGA-Development-Board/Zynq-7000-SoC/PZ7020-StarLite),
[Xilinx/AMD — Zynq-7000 SoC Data Sheet (DS190)](https://www.mouser.com/datasheet/2/903/ds190-Zynq-7000-Overview-1595492.pdf),
[PCBSync — XC7Z010 vs XC7Z020 comparison](https://pcbsync.com/xilinx-xc7z010/).