Skip to content

Instantly share code, notes, and snippets.

@kdmukai
Created July 24, 2026 16:14
Show Gist options
  • Select an option

  • Save kdmukai/a99be3905c88d2404d012ef85a2d9086 to your computer and use it in GitHub Desktop.

Select an option

Save kdmukai/a99be3905c88d2404d012ef85a2d9086 to your computer and use it in GitHub Desktop.
Combine all SeedSigner Raspi releases into one image

Unified multi-board release image

Goal

Ship one downloadable .img that boots correctly on every supported Raspberry Pi, instead of asking users to pick the right per-board image. Each board must load only its own OS into RAM — never the whole file — so the per-boot RAM footprint is identical to today's single-board images.

This document is a build-test-PR proposal for upstream SeedSigner OS.

Background: how images are built today

Each supported board is a separate Buildroot target under opt/<board>/ producing its own image. The four release targets differ almost entirely in CPU architecture, set at compile time:

Target -march DTS built (BR2_LINUX_KERNEL_INTREE_DTS_NAME) Boards
pi0 ARMv6 arm1176jzf_s bcm2708-rpi-b b-plus zero-w zero Pi 1 B/B+, Zero, Zero W
pi2 ARMv7 cortex_a7 +NEON bcm2709-rpi-2-b Pi 2 B
pi02w ARMv8 cortex_a53 +NEON bcm2710-rpi-zero-2-w 3-b 3-b-plus / bcm2837-rpi-3-b Zero 2 W, Pi 3 B/B+
pi4 ARMv8 cortex_a72 +NEON bcm2711-rpi-4-b Pi 4 B

Two facts about the current design make this proposal simple and safe:

  1. Within one architecture, one image already spans several boards via multiple device trees. pi0 already carries the Zero, Zero W, and Pi 1 DTBs; the GPU bootloader selects the matching .dtb by board revision. This proposal is the same mechanism applied across architectures.

  2. The rootfs is RAM-resident, embedded in the kernel. Every board sets CONFIG_INITRAMFS_SOURCE="${BR_BINARIES_DIR}/rootfs.cpio" (see opt/<board>/board/kernel.config), so the entire rootfs is compiled into zImage. There is no rootfs partition and no root=; the whole shipped image is just the FAT boot partition. At boot the firmware loads one zImage and the kernel unpacks its embedded initramfs into RAM — which is what lets the card be removed after boot.

    This initramfs is large relative to the smallest board: per opt/pi0-dev/board/boot_config.txt, the image "unpacks a ~230MB initramfs into RAM on a 512MB board." Loading more than one board's rootfs into RAM is therefore not an option — the design must load exactly one.

Enabling mechanism: the firmware selects per model at boot

The Raspberry Pi GPU bootloader reads config.txt from the boot partition, evaluates conditional model filters, and loads only the files the matched section names. Two documented behaviors carry the design (see References):

  • Model filter tokens select a section by board: [pi0] (Zero), [pi0w] (Zero W), [pi02] (Zero 2 W), [pi1], [pi2], [pi3], [pi4], [all].
  • os_prefix prepends a subdirectory to operating-system files, defined by the docs as "kernels, initramfs, cmdline.txt, .dtbs and overlays." So a single os_prefix=pi4/ routes the kernel, DTBs, and overlays for that board into their own namespace.

Because the firmware only reads the files named by the active section, the other boards' zImages are never touched — they sit inert on the FAT card.

Design

One FAT boot partition, one config.txt

# --- board selection: route each model to its build's subdirectory ---
[pi1]
os_prefix=armv6/
[pi0]
os_prefix=armv6/
[pi0w]
os_prefix=armv6/
[pi02]
os_prefix=pi02w/
[pi3]
os_prefix=pi02w/
[pi2]
os_prefix=pi2/
[pi4]
os_prefix=pi4/

# --- shared settings ---
[all]
kernel=zImage
dtparam=spi=on
dtoverlay=ov5647        # OV5647 (v1). imx219 for v2 modules; both .dtbo ship per subdir.
gpu_mem=32
disable_splash=1
boot_delay=0

Per-board settings that differ today migrate into the matching model section rather than [all], e.g.:

  • pi0force_turbo=1, start_file=start.elf / fixup_file=fixup.dat (full firmware; the cut-down start_cd.elf lacks the camera ISP service).
  • pi4start_file=start4.elf / fixup_file=fixup4.dat, dtoverlay=disable-wifi, dtoverlay=disable-bt.

Partition layout

Everything lives on the single FAT boot partition — there are no rootfs partitions, so the MBR four-primary-partition limit is a non-issue.

/config.txt
/bootcode.bin   /start.elf   /fixup.dat        # ARMv6/v7/A53 boards
/start4.elf     /fixup4.dat                     # Pi 4 (coexist; each board uses its own)
/armv6/   zImage  bcm2708-rpi-{zero,zero-w,b,b-plus}.dtb   overlays/{ov5647,imx219}.dtbo …
/pi02w/   zImage  bcm2710-rpi-{zero-2-w,3-b,3-b-plus}.dtb  overlays/…
/pi2/     zImage  bcm2709-rpi-2-b.dtb                       overlays/…
/pi4/     zImage  bcm2711-rpi-4-b.dtb                       overlays/…

The GPU firmware blobs live at the FAT root, outside os_prefix (it only affects OS files, not firmware). start.elf and start4.elf coexist on one partition — this is exactly how stock Raspberry Pi OS ships.

Model → build routing

Filter token(s) os_prefix Build
[pi1], [pi0], [pi0w] armv6/ pi0
[pi2] pi2/ pi2
[pi02], [pi3] pi02w/ pi02w
[pi4] pi4/ pi4

The RAM guarantee

At boot the firmware:

  1. reads the board revision;
  2. matches exactly one model section and its os_prefix;
  3. loads only <os_prefix>zImage (+ that board's DTB and overlays) into RAM;
  4. never reads the other three zImages.

Peak RAM = one board's kernel + its unpacked ~230MB rootfs — identical to the current single-board image. Bundling the other three builds costs FAT storage on the card, not RAM.

Side benefit: removes the overlay/DTB version hazard

opt/<board>/board/post-image-seedsigner.sh currently rebuilds the camera overlays from the board's own kernel tree to override the rpi-firmware copies, because an overlay whose symbols don't resolve against the running .dtb is silently dropped and libcamera then enumerates no cameras. Namespacing each build's kernel, DTBs, and overlays under its own os_prefix subdirectory makes cross-version contamination structurally impossible — each board only ever sees files built together.

Build changes

The per-board Buildroot targets stay byte-for-byte unchanged (same arch, same NEON, same rootfs). Add a top-level "combine" step that runs after the four release builds:

  1. Create one FAT boot partition sized for four kernel+embedded-rootfs sets plus firmware (≈ 4 × current per-board boot payload; see Size below).
  2. For each release target, copy its zImage, .dtbs, and overlays/ into <os_prefix>/ on that partition.
  3. Copy the shared GPU firmware blobs (bootcode.bin, start.elf/fixup.dat, start4.elf/fixup4.dat) to the FAT root.
  4. Emit the merged config.txt (model sections + [all]).
  5. Name the artifact e.g. seedsigner_os_multiboard.img.

This reuses each target's existing post-image-seedsigner.sh output; the combine step is purely an assembly pass over already-built binaries. Dev targets (*-dev) stay separate.

Size

One FAT partition ≈ 4 × (kernel-with-embedded-rootfs + DTBs) + firmware ≈ ~200 MB. One download replaces four. Well within FAT32 limits and an acceptable trade for dropping the board-selection step.

Test plan

On-hardware boot is the acceptance gate — one physical board per bucket:

Board Expected os_prefix Verify
Pi Zero or Zero W armv6/ boots, display + camera enumerate
Pi Zero 2 W pi02w/ boots on A53 build (NEON path), camera enumerate
Pi 2 B pi2/ boots, display + camera
Pi 4 B pi4/ boots from start4.elf, wifi/bt disabled, camera

For each: confirm the app starts and a scan works, and confirm the other three zImages were not loaded (free RAM matches the single-board baseline for that board).

Risks and edge cases

  • os_prefix fails silently. If a section's os_prefix points at a path whose kernel the firmware can't find, it is ignored rather than erroring. Paths must be exact; a typo shows up only as a wrong/failed boot. This is the main reason per-bucket hardware testing is mandatory, not optional.
  • Pi 2 v1.2 ships a BCM2837 (A53) and enumerates like a Pi 3, so [pi2] may not catch it; it would land in the pi02w bucket instead. Confirm the intended routing on that revision, or add an explicit filter.
  • Pi 400 / CM4 are BCM2711 like the Pi 4. If they are in scope, add [pi400] / [cm4]pi4/; otherwise they are intentionally unsupported.
  • [pi3] vs [pi3+] — verify the Pi 3 B+ matches the token used; fold into pi02w/.
  • Boot partition size must be set from the summed payload, not the current per-board size.

Upstream PR notes

  • Keep the four per-board builds untouched; introduce the combine step as additive, so the existing single-board images remain buildable and reproducible.
  • Preserve reproducibility: the combine step must stage files deterministically (sorted globbing, fixed timestamps) the same way the current post-image-seedsigner.sh does.
  • Frame the user-facing win: one image, board auto-selected at boot, no per-board download decision.

References

  • Raspberry Pi config.txt — conditional filters and os_prefix: https://www.raspberrypi.com/documentation/computers/config_txt.html ("Any value in os_prefix is prepended to the name of any operating system files loaded by the firmware, where 'operating system files' is defined to mean kernels, initramfs, cmdline.txt, .dtbs and overlays.")
  • In-repo: opt/<board>/configs/<board>_defconfig (arch + DTS lists), opt/<board>/board/kernel.config (CONFIG_INITRAMFS_SOURCE — rootfs embedded in kernel), opt/<board>/board/boot_config.txt (per-board firmware/overlay settings; ~230MB-into-RAM note in the -dev variants), opt/<board>/board/post-image-seedsigner.sh (boot-partition assembly and the overlay/DTB-match workaround).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment