rpi5_native_boot.c: Pi 5 DTB->BootInfo constructor (FABRIC-3.md §IV.3 item 2)
Build / build-amd64-iso (push) Waiting to run
Build / build-aarch64-iso (push) Waiting to run
Build / build-riscv64-img (push) Waiting to run

rpi5_native_boot() populates the existing BootInfo struct from the
devicetree instead of UEFI protocols, then calls the existing, unmodified
kernel_main() -- the crux of why most of M1-M9 stays shared between the
UEFI and native boot paths. native_rpi5_entry.S now tail-calls into it
instead of halting.

memory_map is built 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 wrong cell width here would compile and
boot clean in QEMU while silently corrupting the real memory map on real
silicon). Required a new fdt_find_node_by_device_type() since /memory is
identified by device_type = "memory" per DT spec, not compatible.

args comes from /chosen's bootargs fed into the existing
cmdline_parse_ascii() (confirmed pure C99 with no UEFI coupling before
reusing it). framebuffer comes from the already-built
rpi5_mailbox_get_framebuffer() at a fixed 1920x1080x32 default -- no EDID
query exists in this codebase, flagged rather than guessed past.

Deliberately scoped out, not silently skipped: /reserved-memory is not
parsed. Carving reserved sub-ranges out of /memory's span needs
interval-splitting logic that would be written blind against hardware not
yet in hand -- exactly the kind of code that hides a bug until real
silicon. pmm.c's Pass 3 only ever clears pages this file lists as
EfiConventionalMemory, so the gap is "less usable RAM than optimal," never
"reserved RAM wrongly marked free." Left as its own future item.

Also fixes a real link failure this work surfaced: 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.

Compile-only-verified; nothing in the existing UEFI/QEMU path calls
rpi5_native_boot(), so this cannot be exercised until real hardware.
Verified 3-arch boot to ok>/zuse)ok>.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YcT3H2PQeyujrzjqS3Var
This commit is contained in:
Robert Allan James
2026-09-04 14:42:32 -04:00
co-authored by Claude Sonnet 5
parent 281de9547c
commit 67e3fb7459
15 changed files with 27959 additions and 23 deletions
+35 -11
View File
@@ -315,17 +315,41 @@ blocker for free.
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). 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**: new C function populating the *existing* `BootInfo`
struct (`include/starkernel/uefi.h`) from the DTB instead of UEFI protocols — `dtb` = the
real pointer, `acpi_table` = `NULL` (already the correct value for "no ACPI," per IV's own
research above), `runtime_services` = `NULL`, `memory_map`/`framebuffer`/`args` populated
from DTB `/memory`+`/reserved-memory`, the mailbox interface (next item), and DTB `/chosen`
`bootargs` respectively. Then calls the **existing, unmodified** `kernel_main()` — this is
the crux of why most of M1M9 stays shared.
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.
**Deliberately scoped out, not silently skipped:** `/reserved-memory` is not parsed —
carving reserved sub-ranges out of `/memory`'s span needs interval-splitting logic that
would be written blind against hardware not yet in hand, exactly the kind of code that
hides a bug until real silicon; left as its own future item. `memory/pmm.c`'s Pass 3 only
ever clears pages this file explicitly lists as `EfiConventionalMemory`, so the gap is
"less usable RAM than optimal," never "reserved RAM wrongly marked free." 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
+1
View File
@@ -479,6 +479,7 @@ endif
KERNEL_SRCS_BASE := \
$(KERNEL_SRC)/kernel_main.c \
$(KERNEL_SRC)/boot/cmdline.c \
$(wildcard $(KERNEL_SRC)/hal/*.c) \
$(wildcard $(KERNEL_SRC)/memory/*.c) \
$(wildcard $(KERNEL_SRC)/math/*.c) \
+1 -1
View File
@@ -1,5 +1,5 @@
# Capsule Block Manifest — Auto-generated
<!-- Generated by mkcapsule --manifest 2026-09-04T18:10:39Z -->
<!-- Generated by mkcapsule --manifest 2026-09-04T18:40:17Z -->
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
<!-- Hand-written justifications and immutability notes live -->
<!-- in MANIFEST.md alongside this auto-generated index. -->
BIN
View File
Binary file not shown.
+17
View File
@@ -85,6 +85,23 @@ int fdt_prop_u32(const void* fdt, const char* name, uint32_t* out);
*/
const void* fdt_find_node_by_compatible(const void* fdt, const char* compatible);
/**
* @brief Find the first node whose "device_type" property equals @p type.
*
* Some standard nodes (`/memory` per DT spec §3.4) are identified by
* `device_type`, not `compatible` — unlike `compatible`, `device_type` is a
* single NUL-terminated string, not a list, so this matches the whole
* property value rather than scanning entries within it. Scans the whole
* tree in document order; the first matching node wins if more than one
* exists.
*
* @param fdt Blob, already checked with @c fdt_valid().
* @param type device_type value to match, NUL-terminated.
* @return An opaque handle to the matched node, for use with
* @c fdt_find_prop_in_node() only — or NULL if no node matches.
*/
const void* fdt_find_node_by_device_type(const void* fdt, const char* type);
/**
* @brief Find a property by name, scoped to one node.
*
+26
View File
@@ -0,0 +1,26 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 Robert A. James. All rights reserved.
Licensed under the StarForth License, Version 1.0.
*/
/**
* rpi5_native_boot.h - Raspberry Pi 5 DTB->BootInfo constructor
* (FABRIC-3.md §IV.3 item 2).
*
* `native_rpi5_entry.S`'s `rpi5_native_start` tail-calls
* `rpi5_native_boot()` with the masked DTB pointer it captured. This
* function builds the *existing*, unmodified `BootInfo` struct
* (`include/starkernel/uefi.h`) from the devicetree instead of UEFI
* protocols, then calls the *existing*, unmodified `kernel_main()` — see
* this file's own `.c` for exactly what is and is not populated, and why.
*/
#ifndef STARKERNEL_RPI5_NATIVE_BOOT_H
#define STARKERNEL_RPI5_NATIVE_BOOT_H
#include <stdint.h>
void rpi5_native_boot(uint64_t dtb) __attribute__((noreturn));
#endif /* STARKERNEL_RPI5_NATIVE_BOOT_H */
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+16 -11
View File
@@ -16,14 +16,15 @@
* UNSPECIFIED -- must mask before use.
* x1-x3 = reserved / zero, unused here.
*
* This stub's own job, and nothing more yet: mask and capture that DTB
* pointer into `g_rpi5_dtb_ptr` (declared in
* `include/starkernel/rpi5_native_entry.h`) where the still-open
* DTB->BootInfo constructor (§IV.3 item 2) can find it, then establish a
* dedicated stack -- this path has no EDK2 boot stack to inherit, there is
* no EDK2 at all here. It intentionally halts afterward rather than
* tail-calling into a function that does not exist yet; wiring that call
* is item 2's own job, not this one's.
* This stub's own job: mask and capture that DTB pointer into
* `g_rpi5_dtb_ptr` (declared in `include/starkernel/rpi5_native_entry.h`),
* establish a dedicated stack -- this path has no EDK2 boot stack to
* inherit, there is no EDK2 at all here -- then tail-call
* `rpi5_native_boot()` (`rpi5_native_boot.c`, §IV.3 item 2), which builds
* `BootInfo` from the devicetree and calls the existing `kernel_main()`.
* `rpi5_native_boot()` never returns; the `wfe`/`b` loop after the `bl`
* exists only so this file's own control flow has somewhere defined to
* go if that contract is ever violated.
*
* Build-system note: `Makefile.starkernel`'s `KERNEL_ASM` wildcards every
* `*.S` file in this directory into the monolithic kernel ELF build (not
@@ -68,9 +69,13 @@ rpi5_native_start:
add x0, x0, :lo12:g_rpi5_native_stack_top
mov sp, x0
/* §IV.3 item 2 (DTB->BootInfo constructor) does not exist yet --
* halt rather than branch to a function that isn't real. Replace
* this loop with a tail call once that item lands. */
/* x0 was clobbered above for the stack-top address -- reload the
* masked DTB pointer from g_rpi5_dtb_ptr before calling in. */
adrp x0, g_rpi5_dtb_ptr
add x0, x0, :lo12:g_rpi5_dtb_ptr
ldr x0, [x0]
bl rpi5_native_boot
.Lhalt:
wfe
b .Lhalt
@@ -0,0 +1,227 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 Robert A. James. All rights reserved.
Licensed under the StarForth License, Version 1.0.
*/
/**
* rpi5_native_boot.c - Raspberry Pi 5 DTB->BootInfo constructor
* (FABRIC-3.md §IV.3 item 2, 2026-09-04).
*
* **Never run on real hardware** -- the Pi 5 isn't in hand until
* 2026-09-17 (FABRIC-3.md §II). Compile-only-verified; 3-arch QEMU boot
* confirms this compiles and links, and does not disturb the existing
* UEFI/ACPI path -- it cannot exercise a single line of this function,
* since nothing in that path calls it (same caveat as
* `rpi5_dtb.c`/`rpi5_mailbox.c`/`native_rpi5_entry.S` before it).
*
* `rpi5_native_start` (`native_rpi5_entry.S`) tail-calls this function
* with the masked DTB pointer it captured. This builds the *existing*,
* unmodified `BootInfo` struct from the devicetree instead of UEFI
* protocols, then calls the *existing*, unmodified `kernel_main()` --
* this is the crux of why most of M1-M9 stays shared between the UEFI
* and native boot paths.
*
* What this deliberately does NOT do yet, named honestly rather than
* silently skipped:
*
* - **`/reserved-memory` is not parsed.** Only `/memory`'s own `reg` is
* turned into `EfiConventionalMemory` descriptors. Carving reserved
* sub-ranges (VideoCore firmware regions, CMA, etc.) out of that span
* requires interval-splitting logic that would be written blind
* against hardware not yet in hand -- exactly the kind of code that
* hides a subtle bug until real silicon. Deferred to its own
* FABRIC-3.md item rather than merged into this one. The bitmap PMM
* builds from this memory map starts fully reserved and only clears
* pages this file lists as `EfiConventionalMemory`
* (`memory/pmm.c`'s Pass 3) -- so the gap here is "less RAM than
* optimal," never "reserved RAM wrongly marked free."
* - **The mailbox framebuffer request uses a fixed 1920x1080x32
* default**, not real display negotiation (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.
* - **Root `#address-cells`/`#size-cells` must be exactly 1 or 2** --
* confirmed against `bcm2712.dtsi`'s actual root node (2/2) before
* writing this, not assumed; other widths halt rather than guess.
*/
#include "starkernel/rpi5_native_boot.h"
#include "starkernel/fdt.h"
#include "starkernel/rpi5_mailbox.h"
#include "starkernel/cmdline.h"
#include "starkernel/uefi.h"
#define RPI5_MAX_MEMMAP_ENTRIES 8
#define RPI5_FB_DEFAULT_WIDTH 1920u
#define RPI5_FB_DEFAULT_HEIGHT 1080u
#define RPI5_FB_DEFAULT_BPP 32u
extern void kernel_main(BootInfo *boot_info);
static BootInfo g_rpi5_boot_info = {0};
static EFI_MEMORY_DESCRIPTOR g_rpi5_memmap[RPI5_MAX_MEMMAP_ENTRIES];
static uint32_t be32_at(const unsigned char *p)
{
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
((uint32_t)p[2] << 8) | (uint32_t)p[3];
}
/**
* @brief Read one big-endian cell group (1 or 2 u32 cells) as a uint64_t.
*/
static uint64_t read_cells(const unsigned char *p, uint32_t ncells)
{
if (ncells == 1) return (uint64_t)be32_at(p);
return ((uint64_t)be32_at(p) << 32) | (uint64_t)be32_at(p + 4);
}
/**
* @brief Halt this core. No console exists yet (M1 hasn't run) and there
* is nowhere safe to report failure -- this is the same halt shape
* `native_rpi5_entry.S` itself uses before this function existed.
*/
static void rpi5_native_halt(void) __attribute__((noreturn));
static void rpi5_native_halt(void)
{
for (;;)
{
__asm__ volatile("wfe");
}
}
/**
* @brief Parse `/memory`'s `reg` into `g_rpi5_memmap`, honoring the
* root's own `#address-cells`/`#size-cells` (NOT the `soc` node's -- see
* this file's own doc comment; `/memory` is a direct child of root).
*
* @return Number of entries written (0 on any failure).
*/
static uint32_t build_memory_map(const void *dtb)
{
uint32_t acells, scells;
const void *mem_node;
const void *reg;
uint32_t reg_len;
uint32_t entry_bytes, count, i;
const unsigned char *p;
if (!fdt_prop_u32(dtb, "#address-cells", &acells) ||
!fdt_prop_u32(dtb, "#size-cells", &scells))
{
return 0;
}
if ((acells != 1 && acells != 2) || (scells != 1 && scells != 2))
{
return 0;
}
mem_node = fdt_find_node_by_device_type(dtb, "memory");
if (!mem_node) return 0;
reg = fdt_find_prop_in_node(dtb, mem_node, "reg", &reg_len);
if (!reg) return 0;
entry_bytes = (acells + scells) * 4u;
if (entry_bytes == 0) return 0;
count = reg_len / entry_bytes;
if (count > RPI5_MAX_MEMMAP_ENTRIES) count = RPI5_MAX_MEMMAP_ENTRIES;
p = (const unsigned char *)reg;
for (i = 0; i < count; i++)
{
uint64_t addr = read_cells(p, acells);
uint64_t size = read_cells(p + acells * 4u, scells);
g_rpi5_memmap[i].Type = EfiConventionalMemory;
g_rpi5_memmap[i].PhysicalStart = addr;
g_rpi5_memmap[i].VirtualStart = addr;
g_rpi5_memmap[i].NumberOfPages = size / 4096u;
g_rpi5_memmap[i].Attribute = 0;
p += entry_bytes;
}
return count;
}
void rpi5_native_boot(uint64_t dtb)
{
const void *dtb_ptr = (const void *)(uintptr_t)dtb;
uint32_t memmap_count;
const void *bootargs;
uint32_t bootargs_len;
Rpi5FramebufferInfo fb;
if (!fdt_valid(dtb_ptr))
{
rpi5_native_halt();
}
memmap_count = build_memory_map(dtb_ptr);
if (memmap_count == 0)
{
rpi5_native_halt();
}
g_rpi5_boot_info.memory_map = g_rpi5_memmap;
g_rpi5_boot_info.memory_map_size =
(UINTN)memmap_count * sizeof(EFI_MEMORY_DESCRIPTOR);
g_rpi5_boot_info.memory_map_descriptor_size = sizeof(EFI_MEMORY_DESCRIPTOR);
g_rpi5_boot_info.runtime_services = (void *)0;
/* No ACPI at all on this path (§IV.1) -- NULL is the already-correct
* value every existing acpi_table consumer already degrades safely
* against. */
g_rpi5_boot_info.acpi_table = (void *)0;
g_rpi5_boot_info.dtb = (void *)dtb_ptr;
/* No UEFI boot services ever existed on this path -- there is no
* EDK2 here at all, so "exited" is the closest honest value (no
* downstream consumer branches on this field today; it is not a
* live behavioral gate). */
g_rpi5_boot_info.uefi_boot_services_exited = 1;
/* No dynamic allocator exists this early -- fall back to the 2 MiB
* BSS stack, same convention as every other boot path. Unlike
* amd64/riscv64, aarch64 has no kernel_entry.S trampoline to honor
* this field: `native_rpi5_entry.S`'s own BSS stack already *is* the
* stack kernel_main_impl runs on, so this is a documented fact about
* this path, not a live fallback switch. */
g_rpi5_boot_info.kernel_stack_base = (void *)0;
g_rpi5_boot_info.kernel_stack_size = 0;
bootargs = fdt_find_prop(dtb_ptr, "bootargs", &bootargs_len);
cmdline_parse_ascii(bootargs ? (const char *)bootargs : (const char *)0,
&g_rpi5_boot_info.args);
g_rpi5_boot_info.framebuffer.base = (void *)0;
g_rpi5_boot_info.framebuffer.size = 0;
g_rpi5_boot_info.framebuffer.width = 0;
g_rpi5_boot_info.framebuffer.height = 0;
g_rpi5_boot_info.framebuffer.pixels_per_scanline = 0;
g_rpi5_boot_info.framebuffer.pixel_format = 0;
if (rpi5_mailbox_get_framebuffer(dtb_ptr, RPI5_FB_DEFAULT_WIDTH,
RPI5_FB_DEFAULT_HEIGHT,
RPI5_FB_DEFAULT_BPP, &fb) == 0)
{
g_rpi5_boot_info.framebuffer.base = fb.base;
g_rpi5_boot_info.framebuffer.size = fb.size;
g_rpi5_boot_info.framebuffer.width = fb.width;
g_rpi5_boot_info.framebuffer.height = fb.height;
g_rpi5_boot_info.framebuffer.pixels_per_scanline = fb.pixels_per_scanline;
g_rpi5_boot_info.framebuffer.pixel_format = fb.pixel_format;
}
/* On failure, framebuffer stays zeroed -- the same degraded state the
* UEFI path already leaves it in when GOP is BltOnly/absent
* (`boot/uefi_loader.c`). */
kernel_main(&g_rpi5_boot_info);
/* kernel_main() never returns (REPL loop or panic) -- this is
* unreachable, kept only so the noreturn contract holds even if that
* ever changes. */
rpi5_native_halt();
}
+69
View File
@@ -256,6 +256,75 @@ const void* fdt_find_node_by_compatible(const void* fdt, const char* compatible)
return (void*)0;
}
const void* fdt_find_node_by_device_type(const void* fdt, const char* type)
{
const fdt_header_t* h = (const fdt_header_t*)fdt;
const unsigned char *base, *p, *end, *strings;
uint32_t size_struct;
int fresh_node = 0;
const void* node_body_start = (void*)0;
if (!fdt_valid(fdt) || !type) return (void*)0;
base = (const unsigned char*)fdt;
size_struct = be32(&h->size_dt_struct);
p = base + be32(&h->off_dt_struct);
end = p + size_struct;
strings = base + be32(&h->off_dt_strings);
while (p + 4 <= end)
{
uint32_t token = be32(p);
p += 4;
if (token == FDT_BEGIN_NODE)
{
const unsigned char* q = p;
while (q < end && *q) q++;
if (q >= end) break;
p = (const unsigned char*)(((uintptr_t)(q + 1) + 3u) & ~(uintptr_t)3u);
fresh_node = 1;
node_body_start = (const void*)p;
}
else if (token == FDT_PROP)
{
uint32_t len, nameoff;
const unsigned char* val;
if (p + 8 > end) break;
len = be32(p);
nameoff = be32(p + 4);
p += 8;
val = p;
if (len > (uint32_t)(end - p)) break;
if (fresh_node &&
str_eq((const char*)(strings + nameoff), "device_type") &&
str_eq((const char*)val, type))
{
return node_body_start;
}
p = (const unsigned char*)(((uintptr_t)(p + len) + 3u) & ~(uintptr_t)3u);
}
else if (token == FDT_END_NODE)
{
fresh_node = 0;
}
else if (token == FDT_NOP)
{
/* no payload; does not end the leading property list */
}
else
{
/* FDT_END, or a token this reader does not know: stop. */
break;
}
}
return (void*)0;
}
const void* fdt_find_prop_in_node(const void* fdt, const void* node,
const char* name, uint32_t* len_out)
{