Files
LithosAnanake/src/starkernel/arch/aarch64/rpi5_native_boot.c
T
Robert Allan JamesandClaude Sonnet 5 7187d68082
Build / build-amd64-iso (push) Canceled after 0s
Build / build-aarch64-iso (push) Canceled after 0s
Build / build-riscv64-img (push) Canceled after 0s
rpi5_native_boot.c: carve /reserved-memory out of the Pi 5 memory map
Previously deferred (rpi5_native_boot.c's own header comment flagged this
as needing interval-splitting logic written blind against hardware not
yet in hand) -- revisited by fetching bcm2712-ds.dtsi directly rather than
assuming reserved-memory was empty or absent. It has one static child
(atf@0, ARM Trusted Firmware's own region) and one dynamic child
(linux,cma, size/alloc-ranges only, no fixed reg) -- the dynamic one is
skipped, nothing fixed to carve and no allocator this early to service it
against anyway.

Adds two fdt.c primitives: fdt_find_node_by_name() (reserved-memory has
neither compatible nor device_type per DT spec) and fdt_next_child_node()
-- one exported symbol, not the two-primitive general sibling-walker
originally sketched, collapsed after review since the only real use here
is "iterate one node's direct children."

collect_reserved_ranges() reads 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, no libc qsort in freestanding).
RPI5_MAX_MEMMAP_ENTRIES is the exact worst-case count, recomputed rather
than estimated -- the rpi5_mailbox.c buffer-size bug is the standing
lesson for this pattern.

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
2026-09-04 22:24:57 -04:00

423 lines
16 KiB
C
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.
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 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.
*
* `/memory`'s own `reg` (root `#address-cells`/`#size-cells`, confirmed
* against `bcm2712.dtsi`'s actual root node -- `<2>`/`<2>` -- not assumed)
* is turned into `EfiConventionalMemory` descriptors, with `/reserved-memory`
* children that carry a static `reg` (`brcm,bcm2712-ds.dtsi`'s real
* `reserved-memory` node has one, `atf@0`, confirmed by fetching that file
* rather than assumed) carved out as `EfiReservedMemoryType` descriptors
* instead of being silently left inside a conventional range. Children
* with no `reg` (`linux,cma`, which uses `size`/`alloc-ranges` -- a
* dynamic reservation with no fixed address, confirmed against the same
* file) have nothing fixed to carve and are skipped -- there is no
* allocator this early to service a dynamic reservation against anyway.
* `no-map`/`reusable` flags are not distinguished: every static
* reservation is excluded from `EfiConventionalMemory` regardless of
* which flag it carries, since this pass has no different treatment for
* either. `/reserved-memory` itself may declare its own
* `#address-cells`/`#size-cells` different from the root's (confirmed:
* `bcm2712-ds.dtsi`'s does -- `<2>`/`<1>`, not `<2>`/`<2>`) -- read from
* the node itself, falling back to root's only if absent, per DT spec
* §3.5.4's own inheritance rule.
*
* What this deliberately does NOT do yet, named honestly rather than
* silently skipped:
*
* - **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 and `/reserved-memory` `#address-cells`/`#size-cells` must
* each be exactly 1 or 2** -- 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_FB_DEFAULT_WIDTH 1920u
#define RPI5_FB_DEFAULT_HEIGHT 1080u
#define RPI5_FB_DEFAULT_BPP 32u
#define RPI5_MAX_RAM_REGIONS 4
#define RPI5_MAX_RESERVED_RANGES 16
/* Worst case per RAM region: one EfiConventionalMemory gap before, plus
* one EfiReservedMemoryType segment for, each reserved range that
* intersects it, plus one trailing conventional segment after the last
* one = 2*RESERVED_RANGES + 1. Total worst case across all regions:
* RAM_REGIONS * (2*RESERVED_RANGES + 1). Recomputed exactly, not
* estimated -- rpi5_mailbox.c's own buffer-size bug is the lesson here.
* Real DTBs need far fewer: bcm2712-ds.dtsi's own reserved-memory node
* has exactly one static reservation (atf@0) against one RAM region. */
#define RPI5_MAX_MEMMAP_ENTRIES (RPI5_MAX_RAM_REGIONS * (2 * RPI5_MAX_RESERVED_RANGES + 1))
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];
typedef struct
{
uint64_t addr;
uint64_t size;
} rpi5_range_t;
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 raw (addr, size) ranges, 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 ranges written (0 on any failure).
*/
static uint32_t collect_ram_regions(const void *dtb, uint32_t acells, uint32_t scells,
rpi5_range_t *out, uint32_t max)
{
const void *mem_node;
const void *reg;
uint32_t reg_len, entry_bytes, count, i;
const unsigned char *p;
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 > max) count = max;
p = (const unsigned char *)reg;
for (i = 0; i < count; i++)
{
out[i].addr = read_cells(p, acells);
out[i].size = read_cells(p + acells * 4u, scells);
p += entry_bytes;
}
return count;
}
/**
* @brief Read one node-scoped `#address-cells`/`#size-cells`-style u32
* property, falling back to @p fallback if the node doesn't declare its
* own (DT spec §3.5.4's own inheritance rule for `/reserved-memory`).
*/
static uint32_t node_cells_or_fallback(const void *dtb, const void *node,
const char *prop_name, uint32_t fallback)
{
uint32_t len;
const void *v = fdt_find_prop_in_node(dtb, node, prop_name, &len);
if (!v || len != 4) return fallback;
return be32_at((const unsigned char *)v);
}
/**
* @brief Collect every `/reserved-memory` child's static `reg` range(s).
* Children with no `reg` (dynamic reservations -- `size`/`alloc-ranges`
* only) are skipped: there is no allocator this early to service a
* dynamic reservation against, and nothing fixed to carve either way.
*
* @return Number of ranges written (0 if `/reserved-memory` is absent or
* empty of static reservations -- not an error, just nothing to carve).
*/
static uint32_t collect_reserved_ranges(const void *dtb, uint32_t root_acells,
uint32_t root_scells,
rpi5_range_t *out, uint32_t max)
{
const void *rsvmem;
const void *child;
uint32_t acells, scells, entry_bytes, count;
rsvmem = fdt_find_node_by_name(dtb, "reserved-memory");
if (!rsvmem) return 0;
acells = node_cells_or_fallback(dtb, rsvmem, "#address-cells", root_acells);
scells = node_cells_or_fallback(dtb, rsvmem, "#size-cells", root_scells);
if ((acells != 1 && acells != 2) || (scells != 1 && scells != 2)) return 0;
entry_bytes = (acells + scells) * 4u;
count = 0;
child = (const void *)0;
while (count < max &&
(child = fdt_next_child_node(dtb, rsvmem, child)) != (const void *)0)
{
uint32_t reg_len, n, i;
const unsigned char *p;
const void *reg = fdt_find_prop_in_node(dtb, child, "reg", &reg_len);
if (!reg) continue;
n = reg_len / entry_bytes;
p = (const unsigned char *)reg;
for (i = 0; i < n && count < max; i++)
{
out[count].addr = read_cells(p, acells);
out[count].size = read_cells(p + acells * 4u, scells);
count++;
p += entry_bytes;
}
}
return count;
}
/** @brief Insertion sort -- small, fixed N; no libc qsort in freestanding. */
static void sort_ranges_by_addr(rpi5_range_t *arr, uint32_t n)
{
uint32_t i, j;
for (i = 1; i < n; i++)
{
rpi5_range_t key = arr[i];
j = i;
while (j > 0 && arr[j - 1].addr > key.addr)
{
arr[j] = arr[j - 1];
j--;
}
arr[j] = key;
}
}
static void add_memmap_entry(uint32_t *idx, uint32_t max_entries, uint32_t type,
uint64_t addr, uint64_t size)
{
if (*idx >= max_entries) return;
g_rpi5_memmap[*idx].Type = type;
g_rpi5_memmap[*idx].PhysicalStart = addr;
g_rpi5_memmap[*idx].VirtualStart = addr;
/* Floor to whole pages either way: any leftover fractional page is
* simply covered by no descriptor at all, which is safe in both
* directions -- PMM's bitmap starts fully reserved and only clears
* pages an EfiConventionalMemory descriptor explicitly lists
* (memory/pmm.c's Pass 3), so an uncovered fractional page just
* stays reserved by default, same as if it were never mentioned. */
g_rpi5_memmap[*idx].NumberOfPages = size / 4096u;
g_rpi5_memmap[*idx].Attribute = 0;
(*idx)++;
}
/**
* @brief Emit one RAM region as alternating conventional/reserved
* descriptors, clipping @p reserved (already sorted by address) to the
* region's own bounds.
*/
static void emit_region_with_carveouts(uint64_t ram_addr, uint64_t ram_size,
const rpi5_range_t *reserved, uint32_t reserved_count,
uint32_t *idx, uint32_t max_entries)
{
uint64_t cursor = ram_addr;
uint64_t ram_end = ram_addr + ram_size;
uint32_t i;
for (i = 0; i < reserved_count && *idx < max_entries; i++)
{
uint64_t r_start = reserved[i].addr;
uint64_t r_end = r_start + reserved[i].size;
uint64_t clip_start, clip_end;
if (r_end <= cursor) continue; /* fully before the cursor already */
if (r_start >= ram_end) break; /* sorted -- no more overlaps possible */
if (r_start > cursor)
{
add_memmap_entry(idx, max_entries, EfiConventionalMemory,
cursor, r_start - cursor);
}
clip_start = r_start > cursor ? r_start : cursor;
clip_end = r_end < ram_end ? r_end : ram_end;
if (clip_end > clip_start)
{
add_memmap_entry(idx, max_entries, EfiReservedMemoryType,
clip_start, clip_end - clip_start);
cursor = clip_end;
}
}
if (cursor < ram_end)
{
add_memmap_entry(idx, max_entries, EfiConventionalMemory, cursor, ram_end - cursor);
}
}
/**
* @brief Build `g_rpi5_memmap` from `/memory` with `/reserved-memory`'s
* static children carved out. See this file's own top doc comment for
* exactly what is and is not handled.
*
* @return Number of entries written (0 on any failure -- absence of
* `/reserved-memory` itself is not a failure, just zero carve-outs).
*/
static uint32_t build_memory_map(const void *dtb)
{
uint32_t acells, scells;
rpi5_range_t ram_regions[RPI5_MAX_RAM_REGIONS];
rpi5_range_t reserved[RPI5_MAX_RESERVED_RANGES];
uint32_t ram_count, reserved_count, idx, i;
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;
}
ram_count = collect_ram_regions(dtb, acells, scells, ram_regions, RPI5_MAX_RAM_REGIONS);
if (ram_count == 0) return 0;
reserved_count = collect_reserved_ranges(dtb, acells, scells,
reserved, RPI5_MAX_RESERVED_RANGES);
sort_ranges_by_addr(reserved, reserved_count);
idx = 0;
for (i = 0; i < ram_count; i++)
{
emit_region_with_carveouts(ram_regions[i].addr, ram_regions[i].size,
reserved, reserved_count,
&idx, RPI5_MAX_MEMMAP_ENTRIES);
}
return idx;
}
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();
}