fix(raspi): the Pi 5's native path turns the MMU and caches on before any C; bounded mailbox; framebuffer address from dma-ranges

boards/BRINGUP.md R2/R3, FABRIC-3 SXXXIX.  The entry stub cleans the cache
over the image, builds an identity map (the image's GiB Normal, the rest
Device) and enables MMU and caches at EL2 (or EL1); rpi5_mmu.c makes the
devicetree's RAM Normal and the framebuffer uncached.  Mailbox waits give
up after one second; the buffer is cleaned around the exchange; the
framebuffer address goes through dma-ranges, not a Pi 4 mask.

New rpi5-rehearsal target runs the native image on QEMU virt at EL2.  It
found three more that would each have stopped the board: the PL011 shift
(mine, c9519173; the host test asserted it), the GIC address not carried
through /soc's ranges (new fdt_reg_address(), checked against the real
Pi 5 DTB), and no timer IRQ at EL2 (HCR_EL2.IMO/FMO/AMO).

Rehearsal: POST 1052 passed, same parity hash as UEFI.  Standard acceptance
on three ISAs: POST 1052, hash 0x82f4ae7cb2e2d424.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-14 12:58:35 -04:00
co-authored by Claude Opus 5.5
parent 4da370f492
commit 0e2c60dac3
23 changed files with 36835 additions and 85 deletions
+50
View File
@@ -1202,15 +1202,65 @@ RPI5_NATIVE_LINKER_SCRIPT := kernel/linker/starkernel-native-rpi5.ld
RPI5_NATIVE_ELF := $(BUILD_DIR)/starkernel_rpi5.elf
RPI5_NATIVE_IMG := $(BUILD_DIR)/kernel_2712.img
# RPI5_NATIVE_BASE: the image's load address. 0x80000 is the Pi 5's
# (boards/raspi/config.txt kernel_address). Change it only to rehearse the
# native path on QEMU's virt machine at EL2:
# make -f kernel/Makefile ARCH=aarch64 RPI5_NATIVE_BASE=0x40080000 rpi5-rehearsal
RPI5_NATIVE_BASE ?= 0x80000
$(RPI5_NATIVE_ELF): $(KERNEL_OBJS) $(RPI5_NATIVE_LINKER_SCRIPT) | $(BUILD_DIR)
@echo "LD (rpi5 native) $@"
@$(LD) -T $(RPI5_NATIVE_LINKER_SCRIPT) -nostdlib --build-id=none \
--defsym=RPI5_NATIVE_BASE=$(RPI5_NATIVE_BASE) \
-e rpi5_native_start $(KERNEL_OBJS) -o $@
$(RPI5_NATIVE_IMG): $(RPI5_NATIVE_ELF)
@echo "OBJCOPY (raw) $@"
@$(OBJCOPY) -O binary $< $@
# rpi5-rehearsal: the Pi 5's native path -- no UEFI, raw image, entered at
# EL2 with the MMU off and x0 = the devicetree -- rehearsed on QEMU's virt
# machine, which has no Pi 5 model but enters a raw image exactly that way
# with virtualization=on. Linked at virt's 0x40080000 instead of 0x80000;
# everything else is the Pi 5 image. One QEMU, foreground, serial to
# logs/<TS>/aarch64-rpi5/ (2026-10-14, FABRIC-3.md SXXXIX).
.PHONY: rpi5-rehearsal
rpi5-rehearsal:
@if [ "$(ARCH)" != "aarch64" ] || [ "$(RPI5_NATIVE_BASE)" != "0x40080000" ]; then \
echo "rpi5-rehearsal: ARCH=aarch64 RPI5_NATIVE_BASE=0x40080000"; exit 1; fi
@rm -f $(RPI5_NATIVE_ELF) $(RPI5_NATIVE_IMG)
@$(MAKE) -f kernel/Makefile ARCH=aarch64 RPI5_NATIVE_BASE=$(RPI5_NATIVE_BASE) $(RPI5_NATIVE_IMG)
@mkdir -p $(QEMU_LOG_DIR)
@TS=$$(date +%Y%m%d-%H%M%S); \
mkdir -p $(QEMU_LOG_DIR)/$$TS/aarch64-rpi5; \
LOG=$(QEMU_LOG_DIR)/$$TS/aarch64-rpi5/qemu-aarch64-rpi5-$$TS.log; \
SERIAL_SOCK=$(BUILD_DIR)/qemu-serial-aarch64-$$TS.sock; \
QMP_SOCK=$(BUILD_DIR)/qemu-qmp-aarch64-$$TS.sock; \
echo "=== Rehearsing the Pi 5 native path on QEMU virt at EL2 ==="; \
echo " Serial log: $$LOG"; \
touch $$LOG; \
qemu-system-aarch64 \
-machine virt,virtualization=on \
-cpu cortex-a76 \
-kernel $(RPI5_NATIVE_IMG) \
-drive id=artdisk,file=$(ARTDISK),format=raw,if=none \
-device virtio-blk-pci,drive=artdisk \
-device virtio-keyboard-pci,addr=0x3 \
-device qemu-xhci,id=xhci0,addr=0x4,p2=$(XHCI_PORTS),p3=$(XHCI_PORTS) \
$(ZUSEDISK_QEMU_ARGS) \
-object rng-random,id=rng0,filename=/dev/urandom \
-device virtio-rng-pci,rng=rng0,addr=0x5 \
-chardev socket,id=cserial,path=$$SERIAL_SOCK,server=on,wait=off,logfile=$$LOG \
-serial chardev:cserial \
-qmp unix:$$QMP_SOCK,server=on,wait=off \
$(QEMU_EXTRA) \
-display none \
-m 4096 \
-no-reboot \
-d guest_errors; \
rm -f $$SERIAL_SOCK $$QMP_SOCK; \
echo "=== Serial log: $$LOG ==="
.PHONY: boot_image
ifeq ($(strip $(BOARD)),)
boot_image:
+14
View File
@@ -200,4 +200,18 @@ const void* fdt_root_node(const void* fdt);
const void* fdt_find_child(const void* fdt, const void* parent,
const char* name, uint32_t name_len);
/**
* @brief The CPU's address for entry @p index of the `reg` of the first
* enabled node compatible with @p compatible (2026-10-14).
*
* The entry is read in its parent's #address-cells/#size-cells and carried
* up through every bus's `ranges` to the root -- an empty `ranges` keeps
* the address, a bus with no `ranges` has no way up. The Pi 5's GIC, for
* one, sits at 0x7fff9000 on /soc, which is 0x107fff9000 to the CPU.
*
* @return 1 and the address in @p out, or 0 if there is no such node,
* entry or way up.
*/
int fdt_reg_address(const void* fdt, const char* compatible, uint32_t index, uint64_t* out);
#endif /* STARKERNEL_FDT_H */
+5 -3
View File
@@ -58,9 +58,11 @@ typedef struct {
* driver has been designed against; others are not
* refused outright but are unverified).
* @param out Populated on success; untouched on failure.
* @return 0 on success, negative on failure (mailbox node not found
* in the DTB, VC firmware rejected the request, or a response
* tag came back with an unexpected size).
* @return 0 on success, negative on failure: -2 no mailbox node in the
* DTB, -3 the VC firmware rejected the request, -4 a response
* came back empty, -5 no answer within one second (every wait is
* bounded), -6 the devicetree's dma-ranges cannot place the
* framebuffer's bus address.
*/
int rpi5_mailbox_get_framebuffer(const void* dtb, uint32_t width, uint32_t height,
uint32_t bpp, Rpi5FramebufferInfo* out);
+26
View File
@@ -0,0 +1,26 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2026 Robert A. James. All rights reserved.
Licensed under the StarForth License, Version 1.0.
*/
/**
* rpi5_mmu.h - the Pi 5 native path's identity map, once C runs
* (rpi5_mmu.c; native_rpi5_entry.S turns the MMU on).
*/
#ifndef STARKERNEL_RPI5_MMU_H
#define STARKERNEL_RPI5_MMU_H
#include <stdint.h>
/* Makes every further GiB the devicetree's /memory covers entirely Normal
* write-back. Returns how many GiB entries changed, or -1 if /memory
* cannot be read. */
int rpi5_mmu_map_ram(const void *dtb);
/* Makes [addr, addr+size) Normal non-cacheable (2 MiB blocks in the first
* GiB, a whole GiB above it). Returns 0, or -1 if it cannot. */
int rpi5_mmu_uncached(uint64_t addr, uint64_t size);
#endif /* STARKERNEL_RPI5_MMU_H */
+4 -1
View File
@@ -24,7 +24,10 @@ ENTRY(rpi5_native_start)
SECTIONS
{
. = 0x80000;
/* RPI5_NATIVE_BASE comes from kernel/Makefile (--defsym): 0x80000 on
* the Pi 5; 0x40080000 for the QEMU virt rehearsal only, whose RAM
* starts at 0x40000000 and which loads a raw image 0x80000 into it. */
. = RPI5_NATIVE_BASE;
__kernel_start = .;
+20 -48
View File
@@ -126,42 +126,7 @@ static uint32_t s_timer_ppi = TIMER_PPI_EL1;
static uint64_t s_timer_period_tsc = 0;
static uint64_t s_counter_hz_apic = 0; /* CNTFRQ_EL0 (item 0.8/§26) */
/**
* @brief Read one big-endian 32-bit cell from a devicetree property blob.
*
* `fdt.c` keeps its own `be32()` helper file-local (freestanding, no shared
* byte-swap utility to reuse) -- duplicated here rather than exposing it,
* same "a few lines is simpler than a new shared dependency" precedent
* `rpi5_dtb.c`'s own `reg_first_cell_be32()` already set for this exact
* situation.
*/
static uint32_t be32_cell(const unsigned char *b)
{
return ((uint32_t) b[0] << 24) | ((uint32_t) b[1] << 16) |
((uint32_t) b[2] << 8) | (uint32_t) b[3];
}
/**
* @brief Read one two-cell (64-bit) big-endian address from a `reg` entry.
*
* BCM2712's `gicv2` node (see file header) uses two address cells and two
* size cells per `reg` entry -- confirmed directly against `bcm2712.dtsi`'s
* `axi`/`gicv2` nodes (`#address-cells = <2>; #size-cells = <2>;`), unlike
* `rpi5_dtb.c`'s UART/mailbox lookups which read a single 32-bit cell
* because their `soc` parent node declares `#address-cells = <1>`. Reads
* only the two address cells; the caller advances past the two size cells
* itself via @c GIC_REG_ENTRY_BYTES to reach the next entry.
*/
static uint64_t be64_addr_cell_pair(const unsigned char *b)
{
return ((uint64_t) be32_cell(b) << 32) | (uint64_t) be32_cell(b + 4);
}
/* One gicv2 `reg` entry is 4 cells (2 address + 2 size) * 4 bytes = 16
* bytes. Entry 0 = GICD (distributor), entry 1 = GICC (CPU interface 0) --
* confirmed against bcm2712.dtsi's own `reg` list order (GICD, GICC, GICH,
* GICV, the standard arm,gic-400 binding order), not assumed. */
#define GIC_REG_ENTRY_BYTES 16u
/**
* @brief Try to discover the real GICD/GICC base addresses from the DTB.
@@ -181,21 +146,28 @@ static uint64_t be64_addr_cell_pair(const unsigned char *b)
*/
static int gic_bases_from_dtb(const void *dtb, uintptr_t *gicd_out, uintptr_t *gicc_out)
{
const void *node;
const unsigned char *reg;
uint32_t len;
/* Entry 0 of the GIC's reg is the distributor, entry 1 the CPU
* interface, each carried through its buses' ranges to the CPU's
* address (fdt_reg_address(), 2026-10-14). Until then reg was read as
* two 64-bit cells with no translation: right only where the GIC is a
* direct child of the root, as on QEMU's virt machine. On the Pi 5 it
* is on /soc, one cell, 0x7fff9000 -- which is 0x107fff9000 to the
* CPU. "arm,cortex-a15-gic" is QEMU virt's GICv2, so the rehearsal of
* the Pi 5's native path finds its GIC the same way (FABRIC-3.md
* SXXXIX). */
static const char *const compatible[] = { "arm,gic-400", "arm,cortex-a15-gic" };
uint64_t d, c;
unsigned k;
if (!fdt_valid(dtb)) return 0;
node = fdt_find_node_by_compatible(dtb, "arm,gic-400");
if (!node) return 0;
reg = (const unsigned char *) fdt_find_prop_in_node(dtb, node, "reg", &len);
if (!reg || len < 2u * GIC_REG_ENTRY_BYTES) return 0;
*gicd_out = (uintptr_t) be64_addr_cell_pair(reg);
*gicc_out = (uintptr_t) be64_addr_cell_pair(reg + GIC_REG_ENTRY_BYTES);
return 1;
for (k = 0; k < sizeof compatible / sizeof compatible[0]; k++) {
if (fdt_reg_address(dtb, compatible[k], 0, &d) && fdt_reg_address(dtb, compatible[k], 1, &c)) {
*gicd_out = (uintptr_t) d;
*gicc_out = (uintptr_t) c;
return 1;
}
}
return 0;
}
/**
+147 -2
View File
@@ -2,8 +2,9 @@
* native_rpi5_entry.S (aarch64) - Raspberry Pi 5 native (non-UEFI) entry
* stub (FABRIC-3.md §IV.3 item 1, 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.
* Never run on the board yet. Rehearsed on QEMU's virt machine at EL2
* since 2026-10-14 (`make ... rpi5-rehearsal`, FABRIC-3.md §XXXIX): POST
* passes there, same parity hash as the UEFI build.
*
* Entered directly by Pi 5 firmware at the classic bare-metal load address
* 0x80000 (item 6, `config.txt`'s `os_check=0`, is still open -- that's
@@ -47,6 +48,18 @@
g_rpi5_dtb_ptr:
.space 8 /* uint64_t -- masked DTB pointer */
/* The early identity map (2026-10-14): one level-1 table of 512 1 GiB
* entries (39-bit VA, 4 KiB granule), and one level-2 table that splits
* the GiB the image is in into 2 MiB blocks, so that a framebuffer
* inside it can be made uncached alone (rpi5_mmu.c). */
.align 12
.global g_rpi5_l1
g_rpi5_l1:
.space 4096
.global g_rpi5_l2
g_rpi5_l2:
.space 4096
.align 4
g_rpi5_native_stack:
.space 0x200000 /* 2 MiB, same convention as the
@@ -63,6 +76,30 @@ rpi5_native_start:
mov x1, #0xffffffff
and x0, x0, x1
/* Clean and invalidate the data cache over the whole image, .bss and
* stack included, FIRST: whatever the firmware may have left in the
* cache for these addresses goes to RAM now, before anything below
* writes them with the cache off -- a dirty line cleaned later would
* land on top of that work. Line size from CTR_EL0.DminLine. */
mrs x6, ctr_el0
ubfx x6, x6, #16, #4
mov x7, #4
lsl x7, x7, x6 /* x7 = line size in bytes */
sub x8, x7, #1
adrp x4, __kernel_start
add x4, x4, :lo12:__kernel_start
bic x4, x4, x8
adrp x5, __kernel_end
add x5, x5, :lo12:__kernel_end
.Lcache_clean:
cmp x4, x5
b.hs .Lcache_done
dc civac, x4
add x4, x4, x7
b .Lcache_clean
.Lcache_done:
dsb sy
/* Zero .bss: [__bss_start, __bss_end), both 16-byte aligned by the
* native linker script. */
adrp x4, __bss_start
@@ -85,6 +122,114 @@ rpi5_native_start:
add x0, x0, :lo12:g_rpi5_native_stack_top
mov sp, x0
/* MMU and caches on, before any C (2026-10-14). The firmware enters
* with the MMU off, where every access is Device memory: an unaligned
* access or an exclusive faults, and nothing is cached. Identity map:
* the first GiB Normal write-back (every Pi 5 has at least that much
* RAM; rpi5_mmu.c makes the rest of the devicetree's RAM Normal once C
* runs), everything else Device-nGnRE, never executable. At
* whichever EL the firmware chose -- EL2 on the Pi 5, EL1 handled too.
* "The first GiB" is precisely the GiB the image is in: 0 on the Pi 5
* (0x80000), 1 on QEMU's virt machine, where it is rehearsed.
* MAIR: attr0 Normal WB RA/WA (0xFF), attr1 Device-nGnRE (0x04),
* attr2 Normal non-cacheable (0x44).
* block: AF (bit 10), SH inner (bits 9:8 = 3) for Normal, AttrIndx
* (bits 4:2), bit 54 XN/UXN for Device. */
adrp x1, g_rpi5_l1
add x1, x1, :lo12:g_rpi5_l1
adrp x2, g_rpi5_l2
add x2, x2, :lo12:g_rpi5_l2
adrp x10, __kernel_start
add x10, x10, :lo12:__kernel_start
lsr x10, x10, #30 /* x10 = the GiB the image is in */
lsl x11, x10, #30 /* level 2: that GiB, 512 x 2 MiB Normal */
mov x3, #0
mov x4, #0x701
.Ll2_fill:
lsl x5, x3, #21
add x5, x5, x11
orr x5, x5, x4
str x5, [x2, x3, lsl #3]
add x3, x3, #1
cmp x3, #512
b.lo .Ll2_fill
mov x3, #0 /* level 1: 1 GiB Device, but the image's GiB */
mov x4, #0x405
orr x4, x4, #(1 << 54)
.Ll1_fill:
lsl x5, x3, #30
orr x5, x5, x4
cmp x3, x10
b.ne .Ll1_store
orr x5, x2, #3 /* -> the level 2 table */
.Ll1_store:
str x5, [x1, x3, lsl #3]
add x3, x3, #1
cmp x3, #512
b.lo .Ll1_fill
dsb sy
movz x6, #0x04FF
movk x6, #0x0044, lsl #16 /* MAIR = 0x4404FF */
mov x7, #(1 << 0) | (1 << 2) | (1 << 12) /* SCTLR: M, C, I */
mrs x8, CurrentEL
ubfx x8, x8, #2, #2
cmp x8, #2
b.ne .Lmmu_el1
msr mair_el2, x6
movz x9, #0x3519 /* T0SZ 25, IRGN0/ORGN0 WB, SH0 inner, TG0 4K */
movk x9, #0x8082, lsl #16 /* PS 40-bit, RES1 bits 23 and 31 */
msr tcr_el2, x9
msr ttbr0_el2, x1
isb
tlbi alle2
dsb ish
isb
ic iallu
dsb ish
mrs x9, sctlr_el2
orr x9, x9, x7
bic x9, x9, #(1 << 1) /* A: no alignment checking */
bic x9, x9, #(1 << 19) /* WXN: the image is writable Normal memory */
msr sctlr_el2, x9
isb
/* Interrupts to EL2, where the kernel runs: with HCR_EL2.IMO/FMO/AMO
* clear (as a stub that hands Linux EL1 leaves them, and QEMU resets
* them) an IRQ targets EL1 and is never taken at EL2 -- the timer
* never ticked (FABRIC-3.md SXXXIX). */
mrs x9, hcr_el2
orr x9, x9, #(1 << 3) /* FMO */
orr x9, x9, #(1 << 4) /* IMO */
orr x9, x9, #(1 << 5) /* AMO */
msr hcr_el2, x9
isb
b .Lmmu_done
.Lmmu_el1:
msr mair_el1, x6
movz x9, #0x3519 /* T0SZ 25, IRGN0/ORGN0 WB, SH0 inner, TG0 4K */
movk x9, #0x0080, lsl #16 /* EPD1: no TTBR1 walks */
movk x9, #0x0002, lsl #32 /* IPS 40-bit */
msr tcr_el1, x9
msr ttbr0_el1, x1
isb
tlbi vmalle1
dsb ish
isb
ic iallu
dsb ish
mrs x9, sctlr_el1
orr x9, x9, x7
bic x9, x9, #(1 << 1)
bic x9, x9, #(1 << 19)
msr sctlr_el1, x9
isb
.Lmmu_done:
/* 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
+151 -22
View File
@@ -57,6 +57,7 @@
#include "starkernel/rpi5_mailbox.h"
#include "starkernel/rpi5_dtb.h"
#include "starkernel/fdt.h"
#define MBOX_OFF_READ 0x00u
#define MBOX_OFF_STATUS0 0x18u
@@ -108,6 +109,128 @@ static inline uint32_t mmio_read32(uint64_t addr)
return *(volatile uint32_t*) addr;
}
/* Every wait on the mailbox is bounded (2026-10-14): this runs before any
* console exists, so a VideoCore that never answers must cost a missing
* framebuffer, not a silent hang. One second, by the architectural
* counter -- there at any EL with no setup. */
static uint64_t counter_now(void)
{
uint64_t v;
__asm__ volatile("isb\n\tmrs %0, cntpct_el0" : "=r"(v));
return v;
}
static uint64_t counter_freq(void)
{
uint64_t v;
__asm__ volatile("mrs %0, cntfrq_el0" : "=r"(v));
return v ? v : 54000000u; /* the Pi 5's 54 MHz, if the firmware left it unset */
}
/* The VideoCore reads and writes RAM directly, not through the ARM's
* caches (the MMU and caches are on by now -- native_rpi5_entry.S): the
* request goes out to RAM before it is sent, and the cached copy is
* dropped before the answer is read. */
static void buffer_to_ram(void)
{
uint64_t a;
for (a = (uint64_t)(uintptr_t)g_mbox_buf; a < (uint64_t)(uintptr_t)(g_mbox_buf + 40); a += 16)
__asm__ volatile("dc civac, %0" :: "r"(a) : "memory");
__asm__ volatile("dsb sy" ::: "memory");
}
/* fdt helpers for the bus-address translation below. */
static uint32_t mb_be32(const unsigned char *p)
{
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3];
}
static uint64_t mb_cells(const unsigned char *p, uint32_t n)
{
return n == 2 ? ((uint64_t)mb_be32(p) << 32) | mb_be32(p + 4) : mb_be32(p);
}
static uint32_t mb_cell_prop(const void *dtb, const void *node, const char *name, uint32_t otherwise)
{
uint32_t len;
const void *v = fdt_find_prop_in_node(dtb, node, name, &len);
return (v && len == 4) ? mb_be32((const unsigned char *)v) : otherwise;
}
static int mb_compatible(const void *dtb, const void *node, const char *want)
{
uint32_t len, i = 0, wl = 0;
const char *c = (const char *)fdt_find_prop_in_node(dtb, node, "compatible", &len);
while (want[wl]) wl++;
while (c && i < len) {
uint32_t l = 0, k;
while (i + l < len && c[i + l]) l++;
if (l == wl) {
for (k = 0; k < l && c[i + k] == want[k]; k++) { }
if (k == l) return 1;
}
i += l + 1;
}
return 0;
}
/* The path from the root to the first node compatible with `want`:
* path[0] is the root, path[depth-1] the node. Depth-first. */
static int mb_find_path(const void *dtb, const void *node, const char *want,
const void **path, int depth, int max)
{
const void *child = (const void *)0;
if (depth >= max) return 0;
path[depth] = node;
if (mb_compatible(dtb, node, want)) return depth + 1;
while ((child = fdt_next_child_node(dtb, node, child)) != (const void *)0) {
int d = mb_find_path(dtb, child, want, path, depth + 1, max);
if (d) return d;
}
return 0;
}
/* The firmware's framebuffer address is an address on the firmware's own
* bus. It becomes an ARM physical address through the devicetree's
* dma-ranges, from the firmware node up to the root (2026-10-14): a node
* with none, or an empty one, maps one to one; otherwise the entry that
* holds the address moves it into the parent's space. On the Pi 5 the
* firmware node's is empty and /soc has none -- the address is already
* physical. (The 0x3FFFFFFF mask this replaced was the Pi 4's habit:
* there /soc's dma-ranges maps bus 0xC0000000 onto physical 0, which
* this handles as well.) Returns 0, or -1 if no entry holds it. */
static int bus_to_phys(const void *dtb, uint64_t bus, uint64_t *phys)
{
const void *path[12];
int depth, i;
const void *root = fdt_root_node(dtb);
if (!root) return -1;
depth = mb_find_path(dtb, root, "raspberrypi,bcm2835-firmware", path, 0, 12);
if (depth < 2) return -1;
for (i = depth - 1; i >= 1; i--) {
uint32_t len, ca, pa, sz, entry, k;
const unsigned char *r =
(const unsigned char *)fdt_find_prop_in_node(dtb, path[i], "dma-ranges", &len);
if (!r || len == 0) continue; /* one to one */
ca = mb_cell_prop(dtb, path[i], "#address-cells", 2);
sz = mb_cell_prop(dtb, path[i], "#size-cells", 1);
pa = mb_cell_prop(dtb, path[i - 1], "#address-cells", 2);
if (ca < 1 || ca > 2 || pa < 1 || pa > 2 || sz < 1 || sz > 2) return -1;
entry = (ca + pa + sz) * 4u;
for (k = 0; k + entry <= len; k += entry) {
uint64_t cb = mb_cells(r + k, ca);
uint64_t pb = mb_cells(r + k + ca * 4u, pa);
uint64_t n = mb_cells(r + k + (ca + pa) * 4u, sz);
if (bus >= cb && bus - cb < n) { bus = pb + (bus - cb); break; }
}
if (k + entry > len) return -1; /* no entry holds it */
}
*phys = bus;
return 0;
}
/* Appends one tag (id, value-buffer size in bytes, request size in
* bytes, then `nvalues` u32 value words) to g_mbox_buf starting at
* word index *idx, advancing *idx past it (including alignment
@@ -176,21 +299,31 @@ int rpi5_mailbox_get_framebuffer(const void* dtb, uint32_t width, uint32_t heigh
send_val = ((uint32_t) (uintptr_t) g_mbox_buf & ~0xFu) | MBOX_CH_PROPERTY;
while ((mmio_read32(base + MBOX_OFF_STATUS1) & MBOX_FULL) != 0)
{ /* spin until MBOX1 (send) has room */
}
mmio_write32(base + MBOX_OFF_WRITE, send_val);
for (;;)
buffer_to_ram();
{
while ((mmio_read32(base + MBOX_OFF_STATUS0) & MBOX_EMPTY) != 0)
{ /* spin until MBOX0 (receive) has a message */
uint64_t limit = counter_freq(); /* one second, for the whole exchange */
uint64_t t0 = counter_now();
while ((mmio_read32(base + MBOX_OFF_STATUS1) & MBOX_FULL) != 0)
{ /* MBOX1 (send) has no room yet */
if (counter_now() - t0 > limit) return -5;
}
mmio_write32(base + MBOX_OFF_WRITE, send_val);
for (;;)
{
while ((mmio_read32(base + MBOX_OFF_STATUS0) & MBOX_EMPTY) != 0)
{ /* MBOX0 (receive) has nothing yet */
if (counter_now() - t0 > limit) return -5;
}
if (mmio_read32(base + MBOX_OFF_READ) == send_val) break;
/* A message arrived, but not the one we sent (another channel's
* traffic) -- discard and keep waiting, per the wiki's own
* documented channel-matching convention. */
if (counter_now() - t0 > limit) return -5;
}
if (mmio_read32(base + MBOX_OFF_READ) == send_val) break;
/* A message arrived, but not the one we sent (another channel's
* traffic) -- discard and keep waiting, per the wiki's own
* documented channel-matching convention. */
}
buffer_to_ram(); /* drops the cached copy: what follows reads the answer */
if (g_mbox_buf[1] != MBOX_CODE_RESPONSE_OK) return -3; /* VC rejected the request */
@@ -215,16 +348,12 @@ int rpi5_mailbox_get_framebuffer(const void* dtb, uint32_t width, uint32_t heigh
if (fb_addr == 0 || fb_size == 0 || pitch_bytes == 0) return -4;
/* Allocate-buffer's own response address is a VC bus address,
* not necessarily the plain ARM-physical one -- masking the
* top bits to recover the ARM-physical alias is the documented
* convention on earlier Pi chips; kept here defensively even
* though the Pi-5-specific reference this file cites for the
* *send* side found no bus-alias bit in play there. Genuinely
* unverified which of these is correct for Pi 5's own
* allocate-buffer response specifically -- flagged, not
* guessed past this comment. */
out->base = (void*) (uintptr_t) (fb_addr & 0x3FFFFFFFu);
/* Allocate-buffer answers with an address on the firmware's bus:
* translated by the devicetree (bus_to_phys(), above), not masked
* by a rule from another chip. */
uint64_t fb_phys;
if (bus_to_phys(dtb, fb_addr, &fb_phys) != 0) return -6;
out->base = (void*) (uintptr_t) fb_phys;
out->size = fb_size;
out->width = width;
out->height = height;
+149
View File
@@ -0,0 +1,149 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2026 Robert A. James. All rights reserved.
Licensed under the StarForth License, Version 1.0.
*/
/**
* rpi5_mmu.c - the Pi 5 native path's identity map, once C runs
* (2026-10-14; boards/BRINGUP.md R2).
*
* native_rpi5_entry.S turns the MMU and caches on before any C: the GiB
* the image is in Normal write-back in 2 MiB blocks (g_rpi5_l2) -- GiB 0
* on the Pi 5 -- everything else Device, 1 GiB blocks (g_rpi5_l1). From
* here:
* - rpi5_mmu_map_ram() makes every further GiB the devicetree's /memory
* covers entirely Normal write-back, so RAM past the first GiB is
* cached and can hold unaligned data -- a GiB only partly RAM stays
* Device rather than let the CPU speculate into what is not RAM;
* - rpi5_mmu_uncached() makes a range Normal non-cacheable, for the
* framebuffer, which the display reads straight from RAM.
* Each change breaks before it makes (invalid entry, TLB flush, new
* entry), as the architecture requires for a change of memory type.
*
* Never run on real hardware yet. Rehearsed on QEMU's virt machine at EL2
* (FABRIC-3.md §XXXIX).
*/
#include "starkernel/rpi5_mmu.h"
#include "starkernel/fdt.h"
extern uint64_t g_rpi5_l1[512];
extern uint64_t g_rpi5_l2[512];
extern char __kernel_start[], __kernel_end[];
#define GIB (1ull << 30)
#define MIB2 (1ull << 21)
#define BLOCK_NORMAL_WB 0x701ull /* AF, SH inner, AttrIndx 0 */
#define BLOCK_NORMAL_NC 0x709ull /* AF, SH inner, AttrIndx 2 */
/* The GiB that level 2 splits: the one the image is in. */
static uint64_t l2_gib(void)
{
return (uint64_t)(uintptr_t)__kernel_start >> 30;
}
static uint32_t current_el(void)
{
uint64_t el;
__asm__ volatile("mrs %0, CurrentEL" : "=r"(el));
return (uint32_t)((el >> 2) & 3u);
}
static void tlb_flush_all(void)
{
__asm__ volatile("dsb ishst" ::: "memory");
if (current_el() == 2)
__asm__ volatile("tlbi alle2" ::: "memory");
else
__asm__ volatile("tlbi vmalle1" ::: "memory");
__asm__ volatile("dsb ish\n\tisb" ::: "memory");
}
/* Break before make: the old entry is made invalid and the TLB flushed
* before the new one is written. */
static void replace_entry(volatile uint64_t *entry, uint64_t value)
{
*entry = 0;
tlb_flush_all();
*entry = value;
tlb_flush_all();
}
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];
}
static uint64_t cells_at(const unsigned char *p, uint32_t n)
{
return n == 2 ? ((uint64_t)be32_at(p) << 32) | be32_at(p + 4) : be32_at(p);
}
int rpi5_mmu_map_ram(const void *dtb)
{
const void *mem, *reg;
uint32_t acells, scells, len, entry, i, made = 0;
const unsigned char *p;
if (!fdt_prop_u32(dtb, "#address-cells", &acells) ||
!fdt_prop_u32(dtb, "#size-cells", &scells) ||
(acells != 1 && acells != 2) || (scells != 1 && scells != 2))
return -1;
mem = fdt_find_node_by_device_type(dtb, "memory");
if (!mem) return -1;
reg = fdt_find_prop_in_node(dtb, mem, "reg", &len);
if (!reg) return -1;
entry = (acells + scells) * 4u;
p = (const unsigned char *)reg;
for (; len >= entry; len -= entry, p += entry) {
uint64_t start = cells_at(p, acells);
uint64_t end = start + cells_at(p + acells * 4u, scells);
uint64_t first = (start + GIB - 1) / GIB; /* whole GiBs only */
uint64_t last = end / GIB; /* exclusive */
for (i = (uint32_t)first; i < last && i < 512u; i++) {
uint64_t want = ((uint64_t)i << 30) | BLOCK_NORMAL_WB;
if (i == l2_gib()) continue; /* level 2's, already Normal */
if (g_rpi5_l1[i] != want) {
replace_entry(&g_rpi5_l1[i], want);
made++;
}
}
}
return (int)made;
}
int rpi5_mmu_uncached(uint64_t addr, uint64_t size)
{
uint64_t a, end = addr + size;
if (size == 0) return -1;
/* Never a block that holds the image: its entry goes invalid for a
* moment below, and the code doing it would lose itself. */
{
uint64_t ks = (uint64_t)(uintptr_t)__kernel_start & ~(MIB2 - 1);
uint64_t ke = ((uint64_t)(uintptr_t)__kernel_end + MIB2 - 1) & ~(MIB2 - 1);
if (addr < ke && end > ks) return -1;
}
/* Clean and invalidate the range first, so no cached copy of it
* outlives the change -- line by line, 64 bytes at a time at most. */
for (a = addr & ~63ull; a < end; a += 64)
__asm__ volatile("dc civac, %0" :: "r"(a) : "memory");
__asm__ volatile("dsb sy" ::: "memory");
if ((addr >> 30) != ((end - 1) >> 30))
return -1; /* straddles a GiB boundary: not mapped uncached */
if ((addr >> 30) == l2_gib()) {
for (a = addr & ~(MIB2 - 1); a < end; a += MIB2)
replace_entry(&g_rpi5_l2[(a >> 21) & 511u], (a & ~(MIB2 - 1)) | BLOCK_NORMAL_NC);
return 0;
}
{
/* In another GiB: the whole GiB goes uncached. The image is in
* level 2's GiB, so this never touches it. */
replace_entry(&g_rpi5_l1[addr >> 30], ((addr >> 30) << 30) | BLOCK_NORMAL_NC);
return 0;
}
}
@@ -56,6 +56,7 @@
#include "starkernel/rpi5_native_boot.h"
#include "starkernel/fdt.h"
#include "starkernel/rpi5_mailbox.h"
#include "starkernel/rpi5_mmu.h"
#include "starkernel/cmdline.h"
#include "starkernel/uefi.h"
@@ -355,6 +356,11 @@ void rpi5_native_boot(uint64_t dtb)
rpi5_native_halt();
}
/* The entry stub turned the MMU on with only the first GiB as RAM
* (Normal); the rest of the devicetree's RAM becomes Normal here,
* before anything allocates from it. */
(void)rpi5_mmu_map_ram(dtb_ptr);
memmap_count = build_memory_map(dtb_ptr);
if (memmap_count == 0)
{
@@ -398,9 +404,18 @@ void rpi5_native_boot(uint64_t dtb)
g_rpi5_boot_info.framebuffer.pixels_per_scanline = 0;
g_rpi5_boot_info.framebuffer.pixel_format = 0;
fb.base = (void *)0; /* untouched by a mailbox call that fails */
if (rpi5_mailbox_get_framebuffer(dtb_ptr, RPI5_FB_DEFAULT_WIDTH,
RPI5_FB_DEFAULT_HEIGHT,
RPI5_FB_DEFAULT_BPP, &fb) == 0)
{
/* The display reads the framebuffer straight from RAM: uncached,
* or what the console draws sits in the cache (rpi5_mmu.c). A
* framebuffer that cannot be made uncached is not used. */
if (rpi5_mmu_uncached((uint64_t)(uintptr_t)fb.base, fb.size) != 0)
fb.base = (void *)0;
}
if (fb.base)
{
g_rpi5_boot_info.framebuffer.base = fb.base;
g_rpi5_boot_info.framebuffer.size = fb.size;
+94
View File
@@ -654,3 +654,97 @@ const void* fdt_find_prop_in_node(const void* fdt, const void* node,
return (void*)0;
}
/* ---- a node's address, carried through its buses (2026-10-14) ---- */
#define FDT_PATH_MAX 12u
static uint32_t node_cells(const void* fdt, const void* node, const char* name, uint32_t otherwise)
{
uint32_t len;
const void* v = fdt_find_prop_in_node(fdt, node, name, &len);
return (v && len == 4) ? be32(v) : otherwise;
}
static uint64_t read_cells_at(const unsigned char* p, uint32_t n)
{
return n == 2 ? ((uint64_t)be32(p) << 32) | be32(p + 4) : (uint64_t)be32(p);
}
static int node_enabled(const void* fdt, const void* node)
{
uint32_t len;
const char* v = (const char*)fdt_find_prop_in_node(fdt, node, "status", &len);
return !v || compat_list_contains(v, len, "okay") || compat_list_contains(v, len, "ok");
}
/* Depth first: the path from the root to the first enabled node
* compatible with `want` -- path[0] the root, path[depth-1] the node. */
static uint32_t path_to(const void* fdt, const void* node, const char* want,
const void** path, uint32_t depth)
{
const void* child = (const void*)0;
uint32_t len;
const char* c;
if (depth >= FDT_PATH_MAX) return 0;
path[depth] = node;
c = (const char*)fdt_find_prop_in_node(fdt, node, "compatible", &len);
if (c && compat_list_contains(c, len, want) && node_enabled(fdt, node)) return depth + 1;
while ((child = fdt_next_child_node(fdt, node, child)) != (const void*)0)
{
uint32_t d = path_to(fdt, child, want, path, depth + 1);
if (d) return d;
}
return 0;
}
int fdt_reg_address(const void* fdt, const char* compatible, uint32_t index, uint64_t* out)
{
const void* path[FDT_PATH_MAX];
const unsigned char* reg;
const void* root;
uint32_t depth, len, ac, sc, i;
uint64_t addr;
if (!out || !compatible || !fdt_complete(fdt)) return 0;
root = fdt_root_node(fdt);
if (!root) return 0;
depth = path_to(fdt, root, compatible, path, 0);
if (depth < 2) return 0;
/* entry `index` of its reg, in its parent's cells */
ac = node_cells(fdt, path[depth - 2], "#address-cells", 2);
sc = node_cells(fdt, path[depth - 2], "#size-cells", 1);
if (ac < 1 || ac > 2 || sc > 2) return 0;
reg = (const unsigned char*)fdt_find_prop_in_node(fdt, path[depth - 1], "reg", &len);
if (!reg || len < 4u * (ac + sc) * (index + 1u)) return 0;
addr = read_cells_at(reg + 4u * (ac + sc) * index, ac);
/* up through each bus to the CPU's addresses: an empty ranges is the
* same addresses above, no ranges at all is no way up */
for (i = depth - 2; i >= 1; i--)
{
uint32_t cac = node_cells(fdt, path[i], "#address-cells", 2);
uint32_t csc = node_cells(fdt, path[i], "#size-cells", 1);
uint32_t pac = node_cells(fdt, path[i - 1], "#address-cells", 2);
const unsigned char* r = (const unsigned char*)fdt_find_prop_in_node(fdt, path[i], "ranges", &len);
uint32_t entry, at;
int carried = 0;
if (!r) return 0;
if (len == 0) continue;
if (cac < 1 || cac > 2 || pac < 1 || pac > 2 || csc < 1 || csc > 2) return 0;
entry = 4u * (cac + pac + csc);
for (at = 0; at + entry <= len; at += entry)
{
uint64_t from = read_cells_at(r + at, cac);
uint64_t to = read_cells_at(r + at + 4u * cac, pac);
uint64_t size = read_cells_at(r + at + 4u * (cac + pac), csc);
if (addr >= from && addr - from < size) { addr = to + (addr - from); carried = 1; break; }
}
if (!carried) return 0;
}
*out = addr;
return 1;
}
+5 -1
View File
@@ -146,7 +146,11 @@ int fdt_console_uart(const void* fdt, fdt_uart_t* out)
if (!v) return 0;
if (compatible_has(v, len, "arm,pl011") || compatible_has(v, len, "arm,pl011-axi"))
{
out->reg_shift = 0;
/* A PL011's registers are 32-bit words: register n is at n << 2.
* (0 until 2026-10-14, which put the flag register at +6 instead
* of +0x18 -- an alignment fault on Device memory, found
* rehearsing the Pi 5's native path; FABRIC-3.md SXXXIX.) */
out->reg_shift = 2;
out->io_width = 4;
out->base = addr;
out->kind = FDT_UART_PL011;