Skip to content

Local Test Workflow

How Josh validates a freshly-built kn86-os-vDEV.img on macOS without bringing up the full prototype every time. Two paths: Path A writes the image to a real microSD and boots a real Pi Zero 2 W — this is the sole live-boot validation path. Path B is static rootfs inspection via debugfs / mount-loopback on macOS — it validates filesystem layout, unit files, and config content without booting anything. Live-boot validation that cannot run on real hardware belongs to Stage 1c bring-up (see below).

Related:

  • system-image-build.md — how the .img is built. Internals; this doc does not duplicate them.
  • kiosk-mode.md — what a “successful kiosk boot” actually means (auto-login, no display manager, read-only root).
  • power-idle.mdkn86-cpufreq.service, schedutil governor, the items the smoke test checks.
  • ../../../CLAUDE.md — Canonical Hardware Specification (Pi Zero 2 W, Elecrow display, etc.). This doc does not restate spec values.

Terminal window
# For Path B static inspection (ext4 rootfs via debugfs):
brew install e2fsprogs # provides debugfs; hdiutil is macOS built-in
# Optional: fuse-ext2 for read-only FUSE mount of the ext4 rootfs
brew install --cask macfuse && brew install fuse-ext2

For Path A only:

  • Raspberry Pi Imager — download from https://www.raspberrypi.com/software/.
  • microSD card, ≥8 GB — any reputable A1-rated card.
  • microSD reader — built-in or USB.
  • The image artifactkn86-os-vDEV.img produced by system-image-build.md. The build script writes it to tools/sd-provision/build/ (path TBD until the build pipeline lands; check the build-script output line).

Path A — Real hardware (Pi Zero 2 W) — sole live-boot path

Section titled “Path A — Real hardware (Pi Zero 2 W) — sole live-boot path”

This is the only path that exercises a real boot. Use it when you need to verify anything that requires the system to actually run: systemd ordering, runtime service behavior, GPIO, the SSD1322 OLED, the Pico 2 over UART, USB-MSC cartridge mounting, or the Elecrow display. If the prototype hardware is not available, use Path B for static inspection and defer live-boot validation to Stage 1c.

  1. Open Raspberry Pi Imager.
  2. Click CHOOSE OS → Use custom, navigate to kn86-os-vDEV.img, select it.
  3. Click CHOOSE STORAGE, pick the microSD card. Double-check this — Pi Imager will overwrite whatever you point it at.
  4. Click the gear icon ⚙ for advanced options. Disable “Set hostname”, “Enable SSH”, “Set username and password”, and “Configure wireless LAN”. The KN-86 image is self-contained — Pi Imager’s customization layer interferes with the kiosk autologin chain and the read-only root.
  5. Click WRITE. Pi Imager flashes, then verifies. Roughly 3–5 minutes for an 8 GB card on a USB 3 reader.
  6. When verification passes, eject the card. Pi Imager prompts you to.
  1. Insert the microSD into the Pi Zero 2 W.
  2. Power up via USB (a 5V/2.5A supply or a powered USB hub).
  3. What to expect, in order:
    • The Pi’s onboard green LED blinks during initial boot (kernel handoff, fstab mount).
    • The Elecrow shows kernel messages briefly, then clears.
    • The kn86-nosh service starts. If a Pico 2 + cartridge slot are present, you should see the boot sequence (mission board, Bare Deck HUD per ../../software/runtime/bare-deck-terminal.md).
    • Total time: kernel-handoff → nOSh first frame is <8 s target per kiosk-mode.md.

If you don’t have the prototype assembled past the keyboard / display stage, the runtime will likely fail its preconditions (no Pico, no /run/kn86/coproc.sock). That’s expected — proceed to the smoke-test checklist with whatever subsystems you have wired.

Run the smoke-test checklist at the bottom of this doc.


Path B — Static rootfs inspection (debugfs / mount-loopback)

Section titled “Path B — Static rootfs inspection (debugfs / mount-loopback)”

Use this path when you’re iterating on systemd unit files, config.txt / cmdline.txt content, kiosk-mode plumbing, the nosh user setup, or filesystem layout — anything that can be verified by reading files rather than running them. No QEMU, no emulation, no hardware needed. Inspection is instant.

The pi-gen image is built for the Pi vendor firmware boot chain (VideoCore GPU bootloader + Pi-flavored kernel). Two QEMU approaches were evaluated and both fail:

  • -M virt with kernel + initrd extracted from the image: Pi OS Lite’s kernel is built without VIRTIO_BLK or VIRTIO_NET driver support — the kernel boots into initramfs and the local-block script in the initramfs hangs waiting for /dev/vda (the virtio-blk device) to appear. The device never appears because the kernel has no driver for it. Boot does not proceed past initramfs.
  • -M raspi3b: The raspi3b machine type expects the Pi GPU firmware boot chain (bootcode.bin, start.elf, GPU mailbox handshake). A standard pi-gen trixie arm64 kernel skips the GPU mailbox on the assumption that the firmware already ran; under QEMU there is no firmware, so the kernel produces no serial output at all — the console is silent.

Decision: Shipping a custom kernel per slot with VIRTIO_BLK + VIRTIO_NET compiled in would unblock the -M virt path, but adds ~10 MB per slot to the image, requires rebuilding whenever the pi-gen kernel pin advances, and creates a separate kernel artifact to maintain in CI. The cost-to-value ratio is poor given that Stage 1c real-Pi bring-up is imminent and provides the runtime coverage QEMU would approximate — and approximation is all QEMU can offer, since it cannot model the Pi’s GPU boot chain, the Pico 2 UART link, or the SSD1322 OLED SPI bus.

QEMU is therefore not a supported path for live-boot validation of this image. Use Path A (real hardware) or Stage 1c.

VirtualBox is the wrong tool here, and it gets asked about often enough to address head-on:

  1. The image is arm64. VirtualBox is x86_64-guest only; it has no arm64 emulation backend. Even on an Apple Silicon host, VirtualBox runs an x86_64 hypervisor and cannot boot an aarch64 kernel.
  2. Apple Silicon hosts can’t run x86_64 VirtualBox guests anyway. VirtualBox for Apple Silicon is in beta, and even on x86 Macs the project is shedding maintenance rather than gaining it.

If anyone (Josh included, future-self) wonders “couldn’t I just run this in VirtualBox” — no.

Mount the image partitions read-only on macOS using hdiutil and inspect the filesystem contents directly:

Terminal window
IMG=tools/sd-provision/build/kn86-os-vDEV.img
# Attach all partitions read-only, no auto-mount.
hdiutil attach -nomount -readonly "$IMG"
# hdiutil prints device nodes; the rootfs slot A is p4.
# Example: /dev/disk5s4 Linux (p4 - rootfs slot A)
# Mount the ext4 rootfs (macOS cannot mount ext4 natively; use fuse-ext2 or
# inspect via debugfs — debugfs is available via 'brew install e2fsprogs').
fuse-ext2 /dev/disk5s4 /tmp/kn86-rootfs -o ro,allow_other
# --- or, without FUSE, use debugfs directly ---
debugfs /dev/disk5s4
# Inside debugfs: ls, cat, stat, etc.
# Example: debugfs> cat /etc/systemd/system/kn86-cpufreq.service
# Mount the boot FAT32 partition (macOS mounts FAT32 natively).
mkdir -p /tmp/kn86-bootfs
sudo mount -t msdos -o ro /dev/disk5s2 /tmp/kn86-bootfs

Things Path B can reliably check:

  • Filesystem layout — partition table matches ADR-0011 (6 partitions, correct labels, correct sizes).
  • File presence — unit files exist at expected paths (/etc/systemd/system/kn86-*.service, /opt/nosh/bin/kn86-nosh, etc.).
  • Systemd unit contentExecStart=, ConditionPathExists=, WantedBy=, dependency ordering directives are correct. Read the unit files; you do not need to run them to verify the content.
  • config.txt and cmdline.txt — device-tree overlays are listed, quiet / ro / overlayroot flags are present or absent as expected.
  • Kiosk-mode masking — symlinks in /etc/systemd/system/*.service/dev/null confirm masked units.
  • User and group entries/etc/passwd and /etc/group show the nosh:nosh kiosk user.
  • Build-id/etc/kn86-build-id contains the expected version string from the build.

Things Path B cannot validate:

  • Runtime behavior of any systemd unit (whether it actually starts, ordering races, ExecStartPost= side effects).
  • systemctl status / journalctl output — the system is not running.
  • Auto-login on tty1 (requires a running getty + PAM + autologin config to all cooperate).
  • kn86-cpufreq.service successfully writing the schedutil governor (the write target /sys/devices/system/cpu/... does not exist in a mounted image).
  • Read-only root behavior at runtime (overlayroot is a mount-time mechanism; you can verify cmdline.txt contains overlayroot=..., but not that it works).
  • GPU init, Elecrow display output, SSD1322 OLED, Pico 2 UART link, USB-MSC cartridge mounting.
  • Any behavior that depends on the Pi firmware boot chain, device tree application, or hardware peripheral enumeration.

What replaces Path B for live-boot validation

Section titled “What replaces Path B for live-boot validation”

Stage 1c — real Pi Zero 2 W bring-up (see ../hardware/build-specification.md §4 Stage 1c). Stage 1c is the correct gate for all runtime validation: it runs the actual image on the actual hardware, exercises the full boot chain, and catches timing and ordering issues that no emulator can surface. Path A in this doc is Stage 1c’s local equivalent once the prototype is on hand.


Run after a fresh boot on Path A (real hardware). You’ll need a USB keyboard or a serial console hooked up. Sign in is automatic — you should already be at a nosh@…$ prompt without typing anything.

Terminal window
# 1. Auto-login fired on tty1 — already true if you got a prompt without typing.
who # expect: nosh tty1 ...
tty # expect: /dev/tty1
# 2. cpufreq service ran clean (one-shot).
systemctl status kn86-cpufreq.service
# Expect: "Active: inactive (dead)" with "ExecStart= ... status=0/SUCCESS"
# (one-shot services go inactive after success — that's correct)
# 3. schedutil governor is in effect.
cat /sys/devices/system/cpu/cpufreq/policy0/scaling_governor
# Expect: schedutil
# 4. No display manager loaded.
systemctl list-units --type=service | grep -iE 'lightdm|gdm|sddm'
# Expect: empty output
# 5. tty2..tty6 silent (no getty).
systemctl list-units 'getty@tty*.service'
# Expect: only getty@tty1.service is active; tty2-tty6 do not appear or show as masked
# 6. Read-only root.
touch /etc/foo
# Expect: touch: cannot touch '/etc/foo': Read-only file system

All six green = the kiosk + power-idle plumbing is intact. If any fail, see the next section.


start.elf not found / Pi never boots (Path A)

Section titled “start.elf not found / Pi never boots (Path A)”

Pi Imager’s “advanced options” customization can corrupt or unmount the boot partition. Re-flash with the gear-icon options all disabled per Path A step 1. If it still fails, try a different microSD card — older or knock-off cards are a frequent culprit on the Pi Zero 2 W.

kn86-nosh.service stuck in “activating” (Path A)

Section titled “kn86-nosh.service stuck in “activating” (Path A)”

Expected when prototype hardware is partially assembled. The service waits on /run/kn86/coproc.sock, which only exists when a real Pico 2 is connected over UART. If the Pico is not yet wired, either ignore it (the rest of the boot is fine) or temporarily mask it:

Terminal window
sudo systemctl mask kn86-nosh.service

Do not commit the masked unit back into the image. This is a debug-session-only override.

See “Why not VirtualBox?” in the Path B section above. Short version: arm64 image, x86_64-only hypervisor, doesn’t even start.

No. See the QEMU limitation explanation at the top of Path B. The short version: Pi OS Lite’s kernel ships without VIRTIO drivers (blocks -M virt), and -M raspi3b requires the Pi GPU firmware boot chain that QEMU doesn’t provide. Use Path A for live-boot validation.