Skip to content

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.


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 display

There is no display manager, no X server, no Wayland compositor. nOSh owns the framebuffer directly via SDL2’s KMSDRM backend (kiosk-mode.md).

systemd-tmpfiles-setup.service
|
v
systemd-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.

All units live at /etc/systemd/system/kn86-*.service (or .path), installed by stage-kn86-runtime of the system-image build.

UnitTypeJob
kn86-display-init.serviceoneshotClears the kernel console on tty1, sets DPMS on, configures the framebuffer mode for the Elecrow per device-tree-overlays.md.
kn86-coprocessor.servicesimpleOwns /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.pathpathWatches /dev/disk/by-id/usb-*KN86CART* ; activates kn86-cartridge-mount@.service instances on insertion (../../software/runtime/cartridge-lifecycle.md).
kn86-cartridge-mount@.servicetemplate (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.serviceoneshot, before nOShADR-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.servicesimpleLaunches the nOSh binary as user nosh. Owns the primary display, the OLED via the coprocessor daemon, and the input event loop.

Ordering is expressed with After= / Requires= / Wants= as follows:

/etc/systemd/system/kn86-coprocessor.service
[Unit]
Description=KN-86 Pico 2 coprocessor daemon (UART, audio, OLED bridge)
After=systemd-udev-settle.service
Wants=systemd-udev-settle.service
ConditionPathExists=/dev/serial0
[Service]
Type=simple
ExecStart=/opt/nosh/bin/kn86-coproc-daemon /dev/serial0
Restart=on-failure
RestartSec=2s
StartLimitIntervalSec=30s
StartLimitBurst=5
/etc/systemd/system/kn86-nosh.service
[Unit]
Description=KN-86 nOSh runtime
After=kn86-display-init.service kn86-coprocessor.service kn86-updater-gate.service
Requires=kn86-coprocessor.service
Wants=kn86-display-init.service
ConditionPathExists=/run/kn86/coproc.sock
[Service]
Type=simple
User=nosh
Group=nosh
ExecStart=/opt/nosh/bin/nosh
Restart=on-failure
RestartSec=3s
StartLimitIntervalSec=60s
StartLimitBurst=3

Requires=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).

UnitRestart=RestartSecBurstWindowRationale
kn86-coprocessor.serviceon-failure2 s530 sTransient UART glitches recover on restart; persistent failure is a hardware problem and hammering doesn’t help.
kn86-nosh.serviceon-failure3 s360 snOSh segfault is rare; if it happens 3× in a minute, leave the service in failed and let the user power-cycle.
kn86-display-init.servicenoOneshot; if it fails, the console message from systemd is enough.
kn86-updater-gate.servicenoOneshot 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).

The graph relies on three implicit wait gates:

  1. /dev/serial0 exists. Created by the UART0 device-tree overlay (device-tree-overlays.md); the coprocessor daemon’s ConditionPathExists= polls until it appears.
  2. 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.
  3. /run/kn86/coproc.sock exists. The coprocessor daemon creates it after the Pico HELLO+VERSION handshake clears. nOSh’s ConditionPathExists= waits on it; this is what produces the “nOSh blocks on Pico ready” behavior the coprocessor protocol §5.3 specifies.

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.