Boot and systemd
What happens between power-on and the first frame of nOSh content on the primary display: the Linux boot flow on the Pi Zero 2 W, the systemd unit graph for the nOSh runtime daemon and its peripheral siblings, ordering / dependency rules, restart policy, and log retention. Read this if you are adding a new systemd unit, debugging a slow boot, or wondering why nOSh refuses to start.
system-image-build.md— where these unit files get baked in (stage-kn86-runtime).device-tree-overlays.md— overlays that must be live before the dependent units start.kiosk-mode.md— getty/auto-login config that this graph assumes.coprocessor-firmware.md— Pico 2 firmware whose UART handshake gates the nOSh unit start.../../software/runtime/orchestration.md— what the nOSh process does once systemd hands it control.
Linux boot flow
Section titled “Linux boot flow”The Pi Zero 2 W boots through a five-stage chain. We do not touch the first stage; the first three stages are vendor-supplied and pi-gen-provisioned (system-image-build.md).
power-on -> VideoCore GPU bootloader (vendor firmware on the SoC, untouchable) -> reads /autoboot.txt from p1, picks active bootfs slot per ADR-0011 -> loads start.elf + kernel8.img + DTB + cmdline.txt + config.txt from p2 or p3 -> Linux kernel boots -> systemd starts as PID 1, target = multi-user.target (NOT graphical.target) -> systemd brings up the unit graph below -> kn86-nosh.service runs nOSh as the kiosk user -> nOSh opens SDL2, renders boot animation on the Elecrow primary displayThere is no display manager, no X server, no Wayland compositor. nOSh owns the framebuffer directly via SDL2’s KMSDRM backend (kiosk-mode.md).
Unit graph
Section titled “Unit graph”systemd-tmpfiles-setup.service | vsystemd-udevd.service ----+----> kn86-cartridge-mount.path (subscribes to USB-MSC udev) | +----> kn86-coprocessor.service (waits for /dev/serial0) | | +----> kn86-display-init.service (DPMS on, vt1 console clear) | +-----------------+ v kn86-nosh.service | +-- (nOSh runtime — see orchestration.md)Boot wall-clock target: kernel handoff to first nOSh frame in <8 s on a warm SD. ADR-0011’s USB-enumeration wait for the early-boot key gate adds ~2 s in the not-holding-the-combo case; that is included in the 8 s budget.
Unit file inventory
Section titled “Unit file inventory”All units live at /etc/systemd/system/kn86-*.service (or .path), installed by stage-kn86-runtime of the system-image build.
| Unit | Type | Job |
|---|---|---|
kn86-display-init.service | oneshot | Clears the kernel console on tty1, sets DPMS on, configures the framebuffer mode for the Elecrow per device-tree-overlays.md. |
kn86-coprocessor.service | simple | Owns /dev/serial0. Performs the HELLO + VERSION handshake with the Pico 2 per coprocessor-firmware.md and the coprocessor-protocol.md §5.3 bootstrap. Stays alive as the userspace daemon that nOSh talks to over a Unix socket at /run/kn86/coproc.sock. |
kn86-cartridge-mount.path | path | Watches /dev/disk/by-id/usb-*KN86CART* ; activates kn86-cartridge-mount@.service instances on insertion (../../software/runtime/cartridge-lifecycle.md). |
kn86-cartridge-mount@.service | template (instantiated per device) | Mounts the cart’s filesystem read-only at /mnt/cart and emits a sd_notify message that nOSh picks up. |
kn86-updater-gate.service | oneshot, before nOSh | ADR-0011 attention-gesture scan. Reads /dev/input/event* for ~2 s after USB HID enumerates; if SYS+LINK held, kexec into the updater image; otherwise exit 0. |
kn86-nosh.service | simple | Launches the nOSh binary as user nosh. Owns the primary display, the OLED via the coprocessor daemon, and the input event loop. |
Service ordering
Section titled “Service ordering”Ordering is expressed with After= / Requires= / Wants= as follows:
[Unit]Description=KN-86 Pico 2 coprocessor daemon (UART, audio, OLED bridge)After=systemd-udev-settle.serviceWants=systemd-udev-settle.serviceConditionPathExists=/dev/serial0
[Service]Type=simpleExecStart=/opt/nosh/bin/kn86-coproc-daemon /dev/serial0Restart=on-failureRestartSec=2sStartLimitIntervalSec=30sStartLimitBurst=5[Unit]Description=KN-86 nOSh runtimeAfter=kn86-display-init.service kn86-coprocessor.service kn86-updater-gate.serviceRequires=kn86-coprocessor.serviceWants=kn86-display-init.serviceConditionPathExists=/run/kn86/coproc.sock
[Service]Type=simpleUser=noshGroup=noshExecStart=/opt/nosh/bin/noshRestart=on-failureRestartSec=3sStartLimitIntervalSec=60sStartLimitBurst=3Requires=kn86-coprocessor.service means: if the Pico handshake fails repeatedly and kn86-coprocessor.service enters failed state, nOSh refuses to start at all. The user sees the failure on Row 24 of the primary display via the early kn86-display-init.service writing a fallback message to the Linux text console (which Row 0/24 of nOSh is not yet drawing over because nOSh hasn’t started). This is the operator-visible hard fail the coprocessor protocol §5.3 specifies.
ConditionPathExists=/dev/serial0 on the coprocessor daemon and /run/kn86/coproc.sock on nOSh prevents pointless restart loops when the device-tree overlay hasn’t applied (see device-tree-overlays.md for the UART0 overlay).
Restart policy
Section titled “Restart policy”| Unit | Restart= | RestartSec | Burst | Window | Rationale |
|---|---|---|---|---|---|
kn86-coprocessor.service | on-failure | 2 s | 5 | 30 s | Transient UART glitches recover on restart; persistent failure is a hardware problem and hammering doesn’t help. |
kn86-nosh.service | on-failure | 3 s | 3 | 60 s | nOSh segfault is rare; if it happens 3× in a minute, leave the service in failed and let the user power-cycle. |
kn86-display-init.service | no | — | — | — | Oneshot; if it fails, the console message from systemd is enough. |
kn86-updater-gate.service | no | — | — | — | Oneshot pre-nOSh gate; failure here is logged and boot proceeds (better to enter nOSh than refuse to boot). |
When the burst rate-limit fires, nOSh ends up in failed state and the framebuffer holds whatever the display-init unit drew. Recovery is a power-cycle. Production mode disables the SSH path, so systemctl restart from the bench rig is dev-mode only (kiosk-mode.md).
Wait conditions
Section titled “Wait conditions”The graph relies on three implicit wait gates:
/dev/serial0exists. Created by the UART0 device-tree overlay (device-tree-overlays.md); the coprocessor daemon’sConditionPathExists=polls until it appears.- USB HID enumeration. The updater-gate scan reads
/dev/input/event*. If the keyboard controller hasn’t enumerated through the internal USB hub yet (per ADR-0018), the scan window quietly returns “no key held” and boot continues — false negative is acceptable, false positive (entering updater on no key press) is not. /run/kn86/coproc.sockexists. The coprocessor daemon creates it after the Pico HELLO+VERSION handshake clears. nOSh’sConditionPathExists=waits on it; this is what produces the “nOSh blocks on Pico ready” behavior the coprocessor protocol §5.3 specifies.
Journald + log retention
Section titled “Journald + log retention”Per kiosk-mode.md, journald runs in volatile mode by default — logs live in /run/log/journal/ (tmpfs) and clear on reboot. This is intentional: the read-only-root kiosk filesystem has no good place to keep persistent logs, and a kiosk device leaking SD writes to a log is a worn-flash risk.
In developer mode (the /boot/kn86-mode.txt flag from ../hardware/build-specification.md §5), journald flips to persistent mode and writes to /var/log/journal/ on the rootfs. Log size capped at 100 MB total / 30 MB per service. Useful for journalctl -u kn86-nosh -f from an SSH session during a debug run.
A bench rig that needs to capture logs in production mode for a one-off bug should toggle to dev mode for the repro, capture, then toggle back — there is no in-place “make this prod-mode boot persistent” knob, and adding one would defeat the kiosk-write-discipline.