← Back · part of NextBSD Research · sub-plan of the Pi 500+ native-boot plan

The Pi 500+ boot partition, and where the image gets built

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.

2026-08-22. Written after phase 0 was proven on the hardware. Measurements taken over ssh and over the serial console from the board itself; repo claims read directly out of nextbsd/build.sh, nextbsd-userland/ci/assemble-image.sh and nextbsd-kernel/.github/workflows/build.yml rather than from memory. Boot-header citations carried over from the parent plan, which verified them against freebsd/freebsd-src main.

Contents

  1. What the board actually requires
  2. What kernel8.img is
  3. Why an ISO can never boot here
  4. Which repo owns the image
  5. What the published image is called
  6. The loader-less consequence
  7. Phase 1 needs no image at all
  8. Unresolved: the RP1 UART address
  9. First boot on the hardware
  10. Open questions

1. What the board actually requires

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:

FileRoleSource
config.txtfirmware configuration; names the kernel and DTBwritten by the image builder
cmdline.txtboot args, behind the FreeBSD: guardwritten by the image builder
kernel8.imgthe kernel, booti-wrapped (§2)nextbsd-kerneldoes not exist yet
bcm2712-rpi-500.dtbdevice tree; no 500+-specific DTB exists, the 500 DTS covers bothRaspberry 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.

EEPROM state, as of this session

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 0xf614USB → 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.

2. What kernel8.img is

It 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:

PieceLocationWhat it does
kernel.bin targetsys/conf/Makefile.arm64:87strips the ELF, prepends the boot header blob
write_v8booti()sys/tools/arm_kernel_boothdr.awk:150emits a real arm64 Image_header, magic 0x644d5241
LINUX_BOOT_ABIsys/arm64/conf/std.arm64:70included by GENERIC; enables the booti entry path
FDT pointersys/arm64/arm64/locore.Sx0 != 0 && x0 < KERNBASE → treat as physical FDT
boot argssys/arm64/arm64/machdep_boot.c:62CMDLINE_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.

3. Why an ISO can never boot here

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.

4. Which repo owns the image

Measured Two image builders exist and they are not peers:

nextbsd/build.shnextbsd-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”
ProducesNextBSD-<arch>-<date>.img.zip + .iso.zipa throwaway disk.img for qemu boot-testing
Rootfs frompkg install NextBSD-everything + pkglist.txtthe four cross-built CI artifacts
arm64 todayyes — ARCH=arm64, ABIARCH=aarch64, cross-installs FreeBSD:15:aarch64yes, 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.

What a rpi500 target adds

The root half needs no changepkg install into a root, makefs -t ffs, done, and the aarch64 lane already does this. Only the boot half is new:

aarch64 lane todayPi 500+
partition schememkimg -s gptMBR / dos label
FAT partitionESP, EFI/BOOT/BOOTAA64.EFIplain FAT32 type 0x0c, not an ESP
boot payloadloader.eficonfig.txt + cmdline.txt + kernel8.img + DTB
ISO lane.iso.zipnone — §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.

5. What the published image is called

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

Proposed, for this board

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.

Why board-first, and not 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:

LineCodeEffect of an arch-first Pi name
:276IMG=$(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, :315pattern: 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.

How it gets delivered: PR first, continuous only once proven

Measured 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:

StageWhat runsWhere the image goes
1. PRbuild only — release is skipped by the ref/event guardan ordinary workflow artifact, fetched with gh run download. Written to a USB stick and booted on the board. Nothing reaches continuous.
2. Mergepush to mainreleaseMeasured 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.

What must work before continuous

Policy 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.

RequirementWhere the hardware livesBlocked on
Video — console, then a desktop on itVideoCore mailbox + fb, inside the SoC. Not RP1.kernel#96
Keyboard and mouse — USB HID behind xhciusb@200000, usb@300000 — RP1 BAR1 +0x200000, +0x300000kernel#95
Ethernet1f00100000.ethernet — RP1 BAR1 +0x100000kernel#95, then driver work
Boot, NVMe, root filesystemDone

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.

Video is two steps, and only the first is small

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, with reasons

DeferredWhy
Wi-FiMeasured 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.
BluetoothNextBSD 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.

6. The loader-less consequence

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.

7. Phase 1 needs no image at all

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.

8. Unresolved: the RP1 UART address

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 0000001c000300000x1C_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.

9. First boot on the hardware — what happened

Measured Overnight 2026-08-22/23, driven over a tryboot harness (below). Four kernels were built and booted on the board.

The boot chain works

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

And the kernel really runs

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):

MarkerWhereResult
A_start, MMU offseen the firmware genuinely transfers control
Bpage tables builtseen create_pagetables survives this DTB
Cvirtdone, MMU on, via socdev_vaseen 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.

The console is the wall

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.

Result: phases 1 and 2 complete

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> 
ClaimStatus
Route (a) boots FreeBSD with zero new boot codeconfirmed firmware → kernel.bin → EL2 → FDT in x0
16 GB sized correctlyconfirmed real memory = 16379 MB
SMP over PSCI on all four A76sconfirmed 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 reachedconfirmed error 19 = ENODEV, correct — no root fs on the boot partition
Everything useful is behind PCIeconfirmed ? at the prompt lists zero GEOM disks

Two board-specific config changes got it there, both measured rather than guessed:

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.

The tryboot harness

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.

10. Open questions

QuestionWhat would settle it
highWhich 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.
highWhich drivers are compiled into the board kernel, given no loader can load a kext?§6. Blocks any image PR, not just this target.
medDoes 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.
medMBR or GPT for the Pi image?MBR is proven by Raspberry Pi OS. Test GPT only after a stick boots.
lowDo 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.

← Back · NextBSD Research · Pi 500+ native-boot plan