rpi5_native_boot.c: carve /reserved-memory out of the Pi 5 memory map
Build / build-amd64-iso (push) Waiting to run
Build / build-aarch64-iso (push) Waiting to run
Build / build-riscv64-img (push) Waiting to run

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
This commit is contained in:
Robert Allan James
2026-09-04 22:24:57 -04:00
co-authored by Claude Sonnet 5
parent 67e3fb7459
commit 7187d68082
12 changed files with 28050 additions and 50 deletions
+238 -43
View File
@@ -22,27 +22,35 @@
* 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:
*
* - **`/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.
* - **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"
@@ -51,16 +59,33 @@
#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
#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) |
@@ -92,31 +117,20 @@ static void rpi5_native_halt(void)
}
/**
* @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).
* @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 entries written (0 on any failure).
* @return Number of ranges written (0 on any failure).
*/
static uint32_t build_memory_map(const void *dtb)
static uint32_t collect_ram_regions(const void *dtb, uint32_t acells, uint32_t scells,
rpi5_range_t *out, uint32_t max)
{
uint32_t acells, scells;
const void *mem_node;
const void *reg;
uint32_t reg_len;
uint32_t entry_bytes, count, i;
uint32_t reg_len, 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;
@@ -127,26 +141,207 @@ static uint32_t build_memory_map(const void *dtb)
if (entry_bytes == 0) return 0;
count = reg_len / entry_bytes;
if (count > RPI5_MAX_MEMMAP_ENTRIES) count = RPI5_MAX_MEMMAP_ENTRIES;
if (count > max) count = max;
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;
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;