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
423 lines
16 KiB
C
423 lines
16 KiB
C
/*
|
||
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", ®_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", ®_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();
|
||
}
|