← Back · part of NextBSD Research · sub-plan of the Pi 500+ native-boot plan
Phase 0 is done — a serial console is live on the board. This page answers the three questions that came out of it: what kernel8.img actually is, why this machine can never boot an ISO, and which repo owns a Pi 500+ image. Everything in §1 was measured on the hardware on 2026-08-22; everything reasoned rather than measured is marked as such.
Phase 0 complete A serial console is working on the real board. Debug Probe on the 40-pin header (pins 6/8/10), 115200 8N1, firmware handoff → kernel messages → login: → a root shell running commands. This was the gate on every other phase: the built-in keyboard sits five layers deep behind PCIe→RP1→dwc3→xHCI→HID, none of which exists yet, so serial is the only input channel for the entire bring-up.
Measured Read off the running machine, not a spec sheet.
Disklabel type: dos <- MBR, not GPT
Device Start End Sectors Size Id Type
/dev/nvme0n1p1 16384 1064959 1048576 512M c W95 FAT32 (LBA)
/dev/nvme0n1p2 1064960 500118191 499053232 238G 83 Linux
The boot partition holds 87 MB of files, but nearly all of it is other boards’ DTBs and overlays — Raspberry Pi OS ships one universal image covering every Pi from the 2 B up. This board’s own boot path needs four files:
| File | Role | Source |
|---|---|---|
config.txt | firmware configuration; names the kernel and DTB | written by the image builder |
cmdline.txt | boot args, behind the FreeBSD: guard | written by the image builder |
kernel8.img | the kernel, booti-wrapped (§2) | nextbsd-kernel — does not exist yet |
bcm2712-rpi-500.dtb | device tree; no 500+-specific DTB exists, the 500 DTS covers both | Raspberry Pi firmware, 78,721 bytes, sha256 1cce1d35… |
Reasoned bootcode.bin, start*.elf and fixup*.dat are present on that partition but are not in this board’s path — on the Pi 5 family the second-stage bootloader lives in SPI EEPROM. This is high-confidence rather than measured, and it is conveniently self-proving: if a four-file stick boots, it is settled.
Measured BOOT_ORDER was 0xf461, which reads LSB-first as SD → NVMe → USB. On a machine with a populated internal NVMe that means a USB stick never gets a look. Changed to 0xf614 — USB → SD → NVMe — so the rotation is physical: stick in, you boot NextBSD; stick out, you fall through to the Raspberry Pi OS install that is still on the NVMe.
Side effect worth recording rpi-eeprom-config --apply does not patch the running EEPROM image — it rebuilds from the newest bootloader image on disk. So the same command also upgraded the bootloader 2025-06-13 → 2025-11-05. The parent plan wanted that update anyway (“update available and probably wanted”), and it verified clean, but it means two variables moved at once. If the next boot misbehaves, that is the first place to look.
kernel8.img isIt is not a Raspberry Pi format. It is the FreeBSD kernel, ELF-stripped, with a 64-byte arm64 Image header prepended — the same header Linux uses. The 8 means ARMv8/64-bit.
The bootloader reads config.txt, loads the file named by kernel=, parses that header for the load offset and image size, copies the payload to 0x200000, and jumps to it at EL2 with x0 holding the physical address of the DTB.
FreeBSD already emits exactly this. Every piece was verified against freebsd-src main in the parent plan:
| Piece | Location | What it does |
|---|---|---|
kernel.bin target | sys/conf/Makefile.arm64:87 | strips the ELF, prepends the boot header blob |
write_v8booti() | sys/tools/arm_kernel_boothdr.awk:150 | emits a real arm64 Image_header, magic 0x644d5241 |
LINUX_BOOT_ABI | sys/arm64/conf/std.arm64:70 | included by GENERIC; enables the booti entry path |
| FDT pointer | sys/arm64/arm64/locore.S | x0 != 0 && x0 < KERNBASE → treat as physical FDT |
| boot args | sys/arm64/arm64/machdep_boot.c:62 | CMDLINE_GUARD "FreeBSD:" — why cmdline.txt must start with FreeBSD: |
So kernel8.img is cp kernel.bin kernel8.img. The work is not inventing a format.
The actual gap Measured nextbsd-kernel is an overlay repo — patches/, src-overlay/, config/, ci/, no full source tree. Its CI builds arm64 with KERNCONF=NEXTBSD and publishes nextbsd-kernel-arm64, but nothing anywhere invokes the kernel.bin target. That one missing make target is the entire distance between this project and a FreeBSD banner on real hardware.
The BCM2712 bootloader looks for a FAT partition containing config.txt, and from there loads a kernel and a device tree. An ISO 9660 image has neither a FAT partition nor a config.txt, so the firmware never looks inside it. This is not a NextBSD limitation and no amount of image work changes it — it is true of every Raspberry Pi.
The consequence for build.sh is narrow but real: its .iso.zip output lane is meaningless for this target. A Pi 500+ lane produces a .img and nothing else.
“Live USB” still works — it is a .img written to the stick, with a FAT partition first. That is precisely what FreeBSD’s own RPI.img board images are, which is the precedent to model.
Measured Two image builders exist and they are not peers:
nextbsd/build.sh | nextbsd-userland/ci/assemble-image.sh | |
|---|---|---|
| Its own description | “assemble bootable NextBSD images by INSTALLING the packages” | “Phase 1: assemble a minimal, CI-only bootable NextBSD .img… ports build.sh’s makefs/mkimg recipe” |
| Produces | NextBSD-<arch>-<date>.img.zip + .iso.zip | a throwaway disk.img for qemu boot-testing |
| Rootfs from | pkg install NextBSD-everything + pkglist.txt | the four cross-built CI artifacts |
| arm64 today | yes — ARCH=arm64, ABIARCH=aarch64, cross-installs FreeBSD:15:aarch64 | yes, but UEFI-only |
The Pi 500+ target belongs in nextbsd. That is where shipping images are assembled from packages; the userland one is a CI smoke-test that explicitly describes itself as a port of the same recipe.
rpi500 target addsThe root half needs no change — pkg install into a root, makefs -t ffs, done, and the aarch64 lane already does this. Only the boot half is new:
| aarch64 lane today | Pi 500+ | |
|---|---|---|
| partition scheme | mkimg -s gpt | MBR / dos label |
| FAT partition | ESP, EFI/BOOT/BOOTAA64.EFI | plain FAT32 type 0x0c, not an ESP |
| boot payload | loader.efi | config.txt + cmdline.txt + kernel8.img + DTB |
| ISO lane | .iso.zip | none — §3 |
Reasoned MBR is chosen because it is the shape Raspberry Pi OS actually ships and therefore the proven one. The BCM2712 bootloader is believed to handle GPT as well; nobody has tested that here, and phase 1 is not the place to find out.
Measured The convention is two lines of build.sh:
: "${IMG_DATE:=$(date -u +%Y%m%d-%H%M%S)}" # :32 the SINGLE source of truth for the version
IMG_NAME="NextBSD-${ARCH}-${IMG_DATE}.img" # :379
That same $IMG_DATE also becomes PRETTY_NAME, the os-release VERSION and nextbsd-version, so the artifact name and the running system always agree. The published pair today:
out/NextBSD-arm64-20260822-231455.img.zip
out/NextBSD-arm64-20260822-231455.iso.zip
out/NextBSD-rpi500-arm64-20260822-231455.img.zip
out/NextBSD-rpi500-arm64-20260822-231455.img.zip.sha256
No .iso member — §3. Board token first, arch retained, same $IMG_DATE stamp so the version identity rule is untouched.
Why rpi500 and not bcm2712. It is what the machine calls itself: /proc/device-tree/model reads Raspberry Pi 500 Rev 1.0 on a 500+, the firmware appends the revision at runtime, and no 500+ DTB exists on any branch — the 500 DTS covers both. So the token is the board's own identifier rather than a filename compromise, and the + never has to appear (it is legal in FAT32 and on GitHub, but URL-encodes as a space and gets mangled by tools).
Coverage caveat The silicon is shared across the whole Pi 5 family — Pi 5 B, Pi 500, Pi 500+ and the CM5 variants are all BCM2712 + RP1 — so the kernel work is not board-specific. Two config lines are: device_tree=bcm2712-rpi-500.dtb is hardcoded (omit it and the firmware selects by board revision, which is how Raspberry Pi OS ships one image for every model), and hw.uart.console forces the console onto RP1 UART0 on the 40-pin header, overriding the FDT — wrong for a Pi 5 B owner using the 3-pin debug connector that board exposes and the 500+ does not.
Only the 500+ has ever been booted. The C1-stepping early Pi 5 Bs are untested and §2 of the parent plan is entirely about D0-vs-C1 differences biting elsewhere. rpi500 is therefore the honest scope statement: it names what has been proven. If a Pi 5 B is ever tested, broaden the token then rather than over-claiming now — renaming after something reaches continuous is the expensive direction.
NextBSD-arm64-rpi500-…The obvious name is the arch-first one. It cannot be used, and the reason is mechanical rather than aesthetic.
Measured .github/workflows/build.yml globs on the NextBSD-<arch>- prefix in four places:
| Line | Code | Effect of an arch-first Pi name |
|---|---|---|
:276 | IMG=$(ls out/NextBSD-${{ matrix.arch }}-*.img.zip) | breaks — a bare ls into a scalar; two matches yields a two-line value |
:256 | --pattern 'NextBSD-${{ matrix.arch }}-*.img.zip' | ambiguous match |
:244, :315 | pattern: NextBSD-${{ matrix.arch }}-* | pulls the Pi image into lanes that cannot use it |
The trap An arch-first Pi name would not merely fail to work — it would break the existing arm64 lane, because :276 would start matching two files where it has always matched one. A board image that cannot boot in qemu would be handed to the qemu boot-test. NextBSD-rpi500-arm64-… sidesteps all four globs without touching a line of the workflow.
continuous only once provenMeasured The existing CI already enforces this, and needs no change to do it. The release job is guarded:
release:
if: github.ref == 'refs/heads/main'
&& (push || repository_dispatch || workflow_dispatch)
&& needs.build.result == 'success'
It therefore cannot run on a pull request. That gives two stages for free:
| Stage | What runs | Where the image goes |
|---|---|---|
| 1. PR | build only — release is skipped by the ref/event guard | an ordinary workflow artifact, fetched with gh run download. Written to a USB stick and booted on the board. Nothing reaches continuous. |
| 2. Merge | push to main → release | Measured The board image is deleted before the release glob sees it. out/NextBSD-*.img.zip would otherwise match NextBSD-rpi500-arm64-*.img.zip as happily as the generic ones, so the release job now runs rm -fv out/NextBSD-rpi500-* first. Generic images publish; Pi images do not. |
Policy The Pi image is never published from a PR, and is only published to continuous after it has booted the real board. This is the same rule the kext work already follows — green CI is not evidence for this class of change, because nothing in CI can boot a Pi 500+. The board is the test.
continuousPolicy Booting is necessary and no longer sufficient. The bar for publishing is a machine somebody can sit down at: a fully functional desktop. Serial is a bring-up instrument, not a way to use a published image — an image whose only console is a cable into a connector inside the case has no audience.
This is a deliberate raise. Everything below is a real requirement, not a wish-list, and each is either blocked on a named ticket or already done.
| Requirement | Where the hardware lives | Blocked on |
|---|---|---|
| Video — console, then a desktop on it | VideoCore mailbox + fb, inside the SoC. Not RP1. | kernel#96 |
Keyboard and mouse — USB HID behind xhci | usb@200000, usb@300000 — RP1 BAR1 +0x200000, +0x300000 | kernel#95 |
| Ethernet | 1f00100000.ethernet — RP1 BAR1 +0x100000 | kernel#95, then driver work |
| Boot, NVMe, root filesystem | — | Done |
Measured One bug blocks three of the four. Ethernet and both USB controllers are all RP1 children reached through the same ranges translation, and that translation is broken in the same way for all of them — RP1’s parent is a PCI node, so its ranges names the parent in three-cell PCI form and simplebus_fill_ranges() passes the child address through untouched. Fixing kernel#95 is the critical path for keyboard, mouse and networking at once.
Measured The nodes FreeBSD already binds to are present on BCM2712 under the names it already knows — /soc@107c000000/fb is brcm,bcm2708-fb and mailbox@7c013880 is brcm,bcm2835-mbox, which is exactly what bcm2835_fbd.c and bcm2835_mbox.c match. The firmware is already driving HDMI before the kernel starts. That path is switched off only because bcm283x_armc_to_vcbus() applies an ARM→VideoCore offset that is right for the Pi 3/4 and wrong here.
Reasoned A framebuffer console gets vt(4) a screen. A desktop then needs X on top of it, unaccelerated, the way FreeBSD runs X on ARM boards without KMS. An accelerated desktop is a different order of work entirely: brcm,bcm2712-hdmi0 would need a real KMS driver (HVS, HDMI PHY, V3D), and none exists here. The bar above is functional, deliberately — treating “accelerated” as the gate would defer this image indefinitely.
| Deferred | Why |
|---|---|
| Wi-Fi | Measured A BCM4345/6 (BCM43455) on SDIO at mmc@1100000 (brcm,bcm2712-sdhci), inside the SoC — so it does not depend on RP1 and is independent of everything in the table above. Deferred past milestone 1 because Ethernet already gives a usable machine, not because it is hard. Measured The driver is already in the tree: sys/contrib/dev/broadcom/brcm80211/brcmfmac/sdio.c, SPDX-License-Identifier: ISC, LinuxKPI-based — and sys/modules/brcm80211/brcmfmac/Makefile simply ships the SDIO half switched off (BRCMFMAC_SDIO= 0, BRCMFMAC_OF= 0). The work is net80211 on arm64 (kernel#53), an sdhci_fdt attachment for brcm,bcm2712-sdhci (kernel#100), flipping those flags, and the firmware blob — enabling an existing driver rather than porting one. Tracked in kernel#99. |
| Bluetooth | NextBSD has no Bluetooth stack on any architecture, so this board is not where that starts. |
Note the board-first name earns its keep twice here: it keeps the image out of img-test’s NextBSD-<arch>-* matrix at stage 1, and still matches the deliberately broader NextBSD-* release glob at stage 2.
Still required The naming choice keeps the Pi image out of the qemu boot-test by accident rather than by intent. A rpi500 lane should skip img-boot-test.sh explicitly — qemu cannot emulate a Pi 500+, and the only real boot test for this artifact is the board itself. That is the same “prove it on the hardware” rule the rest of this project already follows.
Reasoned FreeBSD’s own board images put the board last — FreeBSD-14.3-RELEASE-arm64-aarch64-RPI.img.xz. The inversion here is deliberate and is forced by the glob shapes above; it is worth a sentence in the PR so it does not read as an oversight.
This is the part that makes a Pi image more than a partition-layout variant, and it deserves to be settled before any PR.
Route (a) — firmware loads kernel8.img directly — means there is no loader. No loader.conf, no boot menu, no boot environments, and critically no .ko is loaded at boot.
But build.sh assembles the rootfs by installing NextBSD-everything, which lays down the kexts. On a Pi 500+ image those kexts would be present on disk, correctly installed and correctly owned — and never loaded by anything. Everything boot-critical must instead be compiled into the kernel.
That is the parent plan’s §5 open decision — “boot-minimum compiled in, rest kexts / everything a kext / defer the split” — arriving as a hard constraint rather than a preference. A Pi 500+ target therefore implies a board kernel config with the boot-critical tier built in, not just a new mkimg invocation. Any image PR that does not also answer this ships a rootfs the kernel cannot use.
Worth stating plainly, because it is easy to over-build here. Phase 1’s success criterion is a banner on the serial console — not a mounted root, not packages, not an installer. A hand-made FAT stick with four files answers it:
# config.txt
kernel=kernel8.img
device_tree=bcm2712-rpi-500.dtb
enable_rp1_uart=1
pciex4_reset=0
enable_uart=1
dtparam=uart0_console
# cmdline.txt
FreeBSD: -v
The image work in §4 becomes necessary at phase 2, where mountroot needs a root filesystem to find. Designing the image before phase 1 has run means designing against a hand-off nobody has observed on this silicon — and phase 1 has already produced one correction to the plan, below.
The parent plan says to hard-code EARLY_PRINTF at 0x1F_0003_0000. Linux on the running board agrees:
rp1 0002:01:00.0: bar1 len 0x400000, start 0x1f00000000
1f00030000.serial: ttyAMA0 at MMIO 0x1f00030000 is a PL011 AXI
serial0 = /axi/pcie@1000120000/rp1/serial@30000
But the firmware, which prints to serial because BOOT_UART=1 is set, reported RP1_UART 0000001c00030000 — 0x1C_0003_0000.
Settled on hardware · 2026-08-23 Confirmed: the firmware window is the live one, and the plan's address was wrong.
Booting Raspberry Pi OS with earlycon=pl011,0x1c00030000 put Linux's own earliest output on the serial cable —
earlycon: pl11 at MMIO 0x0000001c00030000, printk: legacy bootconsole [pl11] enabled, Booting Linux on physical CPU —
and later in that same boot 1f00030000.serial: ttyAMA0 appears. Both windows, in sequence. So the firmware's mapping survives the hand-off,
a kernel writing there at entry is heard, and SOCDEV_PA=0x1c00030000 is correct for route (a).
Superseded reasoning, kept for the record The likeliest explanation is that these are the same register block at two different times: the firmware maps RP1 at 0x1C_…, and Linux later reprograms the PCIe outbound window to 0x1F_…. If so, the plan’s EARLY_PRINTF address is wrong for route (a) — a kernel loaded directly by the firmware runs before any PCIe driver exists and inherits the firmware’s window, so it must poke 0x1C_0003_0000. Getting this wrong produces a working kernel that looks completely dead, which is a punishing first bug. /proc/device-tree/chosen/log yielded no text, so the provenance of the firmware value is not yet confirmed.
Measured Overnight 2026-08-22/23, driven over a tryboot harness (below). Four kernels were built and booted on the board.
The firmware loads and enters a FreeBSD kernel.bin with no new boot code, exactly as the parent plan predicted:
Read /tryboot.txt bytes 269
Loading 'bcm2712-rpi-500.dtb' to 0x00000000 offset 0x100
Read /nextbsd-cmdline.txt bytes 12
'FreeBSD: -v'
Loading 'kernel8.img' to 0x00000000 offset 0x200000
Read kernel8.img bytes 15715704
Device tree loaded to 0x2efec600
Temporary markers in sys/arm64/arm64/locore.S, guarded on SOCDEV_PA, storing straight to the physical PL011 (the MMU is off for the first two):
| Marker | Where | Result |
|---|---|---|
A | _start, MMU off | seen the firmware genuinely transfers control |
B | page tables built | seen create_pagetables survives this DTB |
C | virtdone, MMU on, via socdev_va | seen running in the kernel VA space |
So route (a) is not a theory any more. The remaining problem is not booting — it is being heard.
Two things compound. First, cninit() releases the early console unconditionally:
cnselect(best_cn);
#ifdef EARLY_PRINTF
early_putc = NULL; /* <- unconditional */
#endif
The console it just selected is the one the FDT advertises — RP1 UART0, behind PCIe, undrivable until phase 3. So the kernel falls mute at exactly the moment it would start talking, and on the wire that is identical to a kernel that died. That ambiguity cost most of the night.
Second, simply keeping early_putc does not work. sys/arm64/include/machdep.h says of socdev_va: “Only valid while the early pagetables are valid.” Measured: with early_putc retained, the board stopped resetting and started hanging — the first cnputc() after cninit() faults on the stale mapping, and faulting inside the console path means the panic handler's own output faults again.
Next The supported mechanism rather than another hack: hw.uart.console="mm:0x1c00030000,br:115200". uart_subr.c parses mm:, br: and dt: tags, and that path builds a real uart console with a durable mapping instead of borrowing the early page tables. With no loader (route (a)) there is no loader.conf to set it in — but a kernel config can embed a static environment with the env directive, which is precisely what loader-less boots are for. Open: whether the default uart class on arm64 resolves to uart_pl011_class, or whether dt: must name it explicitly — an ns8250 pointed at a PL011 would produce garbage rather than silence.
Booted 2026-08-23 NextBSD runs on the Raspberry Pi 500+. With hw.uart.console giving a real console and the wrong-SoC Broadcom drivers switched off, the kernel boots to an interactive mountroot> prompt — the parent plan's phase 1 and phase 2 success criteria, on silicon no BSD had run on before.
---<<BOOT>>---
NextBSD Kernel Version 20260823-150236; ... NEXTBSD-RPI500 arm64
mach services loaded - mach system calls available
real memory = 17175670784 (16379 MB)
Starting CPU 1 (100) / CPU 2 (200) / CPU 3 (300)
FreeBSD/SMP: Multiprocessor System Detected: 4 CPUs
gic0: pn 0x2, arch 0x2, rev 0x1, implementer 0x43b irqs 320
generic_timer0: Timecounter "ARM MPCore Timecounter" frequency 54000000 Hz
uart0: <PrimeCell UART (PL011)> mem 0x7d001000-0x7d0011ff irq 19
CPU 0: ARM Cortex-A76 r4p1 affinity: 0 0
Release APs...done
Trying to mount root from ufs:/dev/ufs/ROOTFS []...
Mounting from ufs:/dev/ufs/ROOTFS failed with error 19.
mountroot>
| Claim | Status |
|---|---|
| Route (a) boots FreeBSD with zero new boot code | confirmed firmware → kernel.bin → EL2 → FDT in x0 |
| 16 GB sized correctly | confirmed real memory = 16379 MB |
| SMP over PSCI on all four A76s | confirmed 4 CPUs, Release APs...done |
| MPIDR Aff1 risk (plan §8, med) | closed (100)/(200)/(300) are the DT reg values; FreeBSD read them correctly, no bug |
mountroot reached | confirmed error 19 = ENODEV, correct — no root fs on the boot partition |
| Everything useful is behind PCIe | confirmed ? at the prompt lists zero GEOM disks |
Two board-specific config changes got it there, both measured rather than guessed:
hw.uart.console="mm:0x1c00030000,br:115200,dt:pl011,rs:2,rw:4,xo:0" in a compiled-in env. dt: is required because uart_cpu_getdev() defaults to uart_ns8250_class; rs:/rw: are required because uart_getenv() hardcodes regshft=0, regiowidth=1 and does not inherit them from the class.nooptions SOC_BRCM_BCM2837 / BCM2838. std.broadcom enables the Pi 3 and Pi 4 SoCs; the Pi 500 DTB matches bcm2835_fbd anyway, which then panics in bcm283x_armc_to_vcbus — a VideoCore bus offset that is 2837/2838-specific. files.arm64 gates both files on exactly those options.Next is phase 3, unchanged from the parent plan and now with hardware evidence behind it: SOC_BRCM_BCM2712 plus bcm2712-pcie and bcm2712-mip ported from OpenBSD. Nothing on this board — keyboard, NVMe, ethernet — exists until PCIe does.
Worth reusing. tryboot is one-shot: reboot '0 tryboot' boots from tryboot.txt instead of config.txt, and any failure reverts on the next power cycle. With BOOT_ORDER=0xf461 (NVMe ahead of USB) the machine's default is always Raspberry Pi OS, so a dead kernel costs one reboot and ssh comes back by itself. That made an unattended build→deploy→boot→read loop possible with no USB stick and nothing to unplug.
Its one limit is real: a kernel that hangs rather than resets takes the board with it, and only a physical power cycle recovers. That is how the night ended.
| Question | What would settle it | |
|---|---|---|
| high | Which RP1 UART base is live at kernel entry — 0x1C or 0x1F? | Capture the full firmware boot log over serial and confirm where RP1_UART is printed; or simply try 0x1C first in phase 1, since it costs one rebuild. |
| high | Which drivers are compiled into the board kernel, given no loader can load a kext? | §6. Blocks any image PR, not just this target. |
| med | Does nextbsd-kernel CI gain a kernel.bin step, or is the wrap done in nextbsd/build.sh from the published artifact? | Cheaper in the kernel CI (the boothdr awk script and objcopy are already there); cleaner in build.sh if other boards follow. |
| med | MBR or GPT for the Pi image? | MBR is proven by Raspberry Pi OS. Test GPT only after a stick boots. |
| low | Do bootcode.bin / start*.elf / fixup*.dat need shipping? | Believed no on the Pi 5 family. A four-file stick that boots settles it. |
Measured on root@pi — Raspberry Pi 500+ rev e04190, Debian 13 trixie, kernel 6.12.47+rpt-rpi-2712, root on the built-in NVMe — over ssh and over the serial console, 2026-08-22. Note that this same board is currently the rollback host for the Quassel core; its /var/lib/quassel should be copied off before the machine is repurposed.