Skip to content

Device Tree Overlays

The device-tree configuration baked into the system image: config.txt snippets, the GPIO pin reservation table, and the validation steps that confirm a freshly-flashed SD has its peripherals wired up correctly. Read this if you are adding a new SPI / I2C / GPIO peripheral or if a peripheral is silently failing to enumerate.


The KN-86 image ships with a config.txt derived from pi-gen’s Pi OS Lite default plus the project-specific overlays below. The full file lives in tools/sd-provision/pi-gen-stages/stage-kn86-firmware/files/boot/config.txt. The relevant additions:

# --- KN-86 device-tree overlays ---
# Primary display: HDMI to Elecrow 7" IPS @ 1024x600
hdmi_force_hotplug=1
hdmi_group=2
hdmi_mode=87
hdmi_cvt=1024 600 60 6 0 0 0
disable_overscan=1
# UART0 to Pi Pico 2 coprocessor (ADR-0017)
# GPIO14 = TXD0 -> Pico RX (GPIO1)
# GPIO15 = RXD0 <- Pico TX (GPIO0)
enable_uart=1
dtoverlay=disable-bt # release UART0 from BT module to /dev/serial0
init_uart_baud=1000000
# GPIO22 = Pico BOOTSEL control, GPIO23 = Pico RESET control (ADR-0017 §6)
# Driven by the coprocessor daemon during firmware-update flow; reserved here.
gpio=22=op,dh
gpio=23=op,dh
# Audio — BENCH PHASE (GWP-698): Waveshare WM8960 Audio HAT, Pi-driven
# I2S (GPIO18-21) + I2C1 codec config (GPIO2/3, addr 0x1a). Canonical
# topology moves the whole chain to the Pico 2 (ADR-0017/ADR-0054), at
# which point both lines go away. See "Pi-side audio" below.
dtparam=i2c_arm=on
dtoverlay=wm8960-soundcard
# SPI0 is FREE (post-ADR-0017). Pre-ADR-0017 the OLED was on Pi SPI0; the
# OLED is now driven from the Pico's SPI bus. SPI0 is disabled here so any
# kernel SPI driver doesn't claim the pins.
dtparam=spi=off
# Hardware watchdog enable (GWP-352). The closed-lid Pelican kiosk has no
# accessible reset button; the BCM2837 watchdog is the only hang-recovery
# path. systemd consumes this via /etc/systemd/system.conf.d/10-kn86-watchdog.conf
# (RuntimeWatchdogSec=15, RebootWatchdogSec=120). See
# docs/device/os/boot-and-systemd.md "Hardware watchdog".
dtparam=watchdog=on
# USB peripheral mode for the updater image (ADR-0011)
# Used only by the kexec'd updater rootfs, not by the normal nOSh boot.
# config.txt loads dwc2 with peripheral mode; cmdline.txt in the updater
# bootfs slot carries `modules-load=dwc2,g_mass_storage`.
dtoverlay=dwc2,dr_mode=peripheral

config.txt lives on p1 (the common boot region per ../../adr/ADR-0011-device-firmware-update-system.md). Each bootfs slot (p2, p3) carries its own cmdline.txt. Slot-shared firmware-level config goes in config.txt; slot-specific kernel command-line lives in the per-slot cmdline.txt.

enable_uart=1
dtoverlay=disable-bt
init_uart_baud=1000000

dtoverlay=disable-bt re-routes the BCM43438 Bluetooth module off UART0 onto the lower-quality mini-uart (UART1). UART0 (PL011) is the full-feature UART with hardware flow control and stable timing — exactly what the 1 Mbps Pi↔Pico link requires. Bluetooth is unused on the KN-86, so this trade is free.

After boot, /dev/serial0 is a symlink to /dev/ttyAMA0. The coprocessor daemon (boot-and-systemd.md) opens /dev/serial0 at 1,000,000 baud, 8N1, and runs the §5.3 bootstrap from coprocessor-protocol.md.

The Pi Zero 2 W’s OTG port fans out through an internal USB 2.0 hub IC (TUSB2036, FE1.1s, or equivalent — exact part TBD per ADR-0018 KBD-03) to two downstream devices: the QMK-compatible keyboard controller, and the USB-to-SD card reader bridge IC for cartridges (ADR-0019). This is not a device-tree-overlay matter on the Pi side — USB hubs and USB-MSC bridges are class-compliant and surface through the kernel’s standard USB stack with no DTB hint required. The only Pi-side config.txt interaction is dtoverlay=dwc2,dr_mode=peripheral for the updater image’s USB-MSC mode (above); the host-mode that the normal boot uses is the default and does not need an explicit overlay.

Keyboard enumeration appears as /dev/input/event* once the udev pipeline settles. Cartridge insertion appears as a /dev/sd* block device subscribed by the udev rule shipped in stage-kn86-runtime (boot-and-systemd.md, ../../software/runtime/cartridge-lifecycle.md).

Stable USB device names (GWP-355): the udev rule /etc/udev/rules.d/90-kn86-usb.rules (also shipped in stage-kn86-runtime) maps the keyboard and the cart-MSC bridge to /dev/kn86-keyboard and /dev/kn86-cart respectively, so nOSh and the cartridge-mount path unit do not depend on hub-port enumeration order. The keyboard rule matches on idVendor=239a (Adafruit) — the QMK firmware’s idProduct is TBD pending the descriptor pin per ADR-0024 F2. The cart-bridge rule matches on the USB-MSC interface class (08:06:50) instead of VID/PID, which is durable across the candidate bridge IC families in ADR-0019 §Open Questions §1. Both VID/PID matches will be tightened in the same PR that pins the bring-up choices.

Pi-side audio: bench-phase WM8960 HAT overlay (GWP-698, amends GWP-358)

Section titled “Pi-side audio: bench-phase WM8960 HAT overlay (GWP-698, amends GWP-358)”

Bench phase (current): the Waveshare WM8960 Audio HAT sits on the Pi’s 40-pin header, and the Pi drives it directly — dtoverlay=wm8960-soundcard (the in-tree simple-audio-card overlay; no vendor DKMS module) plus dtparam=i2c_arm=on for the codec’s register config (I2C1, addr 0x1a). This validates the ADR-0054 codec in HAT form ahead of the Pico wiring. Three things have to agree or the card sits in deferred probe (asoc-simple-card: parse error, observed on deckline 2026-07-08):

  1. The overlay + I2C lines in config.txt (this doc’s listing above).
  2. snd_soc_bcm2835_i2s not blacklisted — GWP-698 amends the GWP-358 blacklist (/etc/modprobe.d/kn86-blacklist.conf) to allow the Pi I2S DAI to autoload; snd_bcm2835 (legacy PWM/HDMI ALSA) stays blocked.
  3. ALSA default routing by card name in /etc/asound.conf (hw:wm8960soundcard) — vc4-hdmi enumerates first and owns card 0, so anything defaulting by index plays out the HDMI panel instead.

The audio-wm8960 converge role delivers 3 and the codec mixer state (DAC → output-mixer routing, which powers up disconnected, i.e. silent).

Canonical topology (end state): audio is owned end-to-end by the Pico 2 coprocessor (YM2149 PSG → I2S → WM8960 codec, ADR-0017/ADR-0054); the Pi never produces a sample. When the chain moves to the Pico, the overlay lines, /etc/asound.conf, and the audio-wm8960 role are removed together and the snd_soc_bcm2835_i2s blacklist returns.

SSD1322 OLED — Pi side has no overlay (ADR-0015 + ADR-0017)

Section titled “SSD1322 OLED — Pi side has no overlay (ADR-0015 + ADR-0017)”

Pre-ADR-0017, the SSD1322 CIPHER-LINE OLED was driven from Pi SPI0 with a dtoverlay=spi0-2cs plus a custom SSD1322 driver. That entire path is obsolete. The OLED is now driven from the Pico 2’s SPI bus, commanded by the Pi over UART using OLED frame types from coprocessor-protocol.md §4.

Concretely on the Pi:

  • dtparam=spi=off is set in config.txt so Linux does not claim SPI0/SPI1.
  • No SSD1322 driver is shipped in the kernel module set; the OLED never appears as a Linux character device.
  • The Pico-side SPI pin assignment to the SSD1322 is finalised at the Pico firmware F2 bring-up (coprocessor-firmware.md) and is not visible to Linux at all.

If a future hardware revision moves the OLED back to the Pi, the original Pi-side overlay would re-enter — but that is not in scope under ADR-0017 + ADR-0019.

The Pi Zero 2 W has 26 usable GPIO pins on the 40-pin header. The KN-86 v1 claims a small subset; the rest are explicitly unclaimed to leave headroom for future peripherals.

Pin (BCM)HeaderClaimed byFunction
GPIO 14pin 8UART0 (this doc)TXD0 → Pico GPIO1 (RX)
GPIO 15pin 10UART0 (this doc)RXD0 ← Pico GPIO0 (TX)
GPIO 22pin 15Coprocessor daemon (ADR-0017 §6)Pico BOOTSEL control. TBD pending bring-up — see ADR-0017 KU#9 if a conflict surfaces.
GPIO 23pin 16Coprocessor daemon (ADR-0017 §6)Pico RESET control. TBD pending bring-up — see ADR-0017 KU#9.
GPIO 2 / 3pin 3 / 5WM8960 HAT I2C1 (GWP-698, bench phase)Codec register config, addr 0x1a. Frees back up when audio moves to the Pico per ADR-0054; the battery fuel gauge candidate can share the bus meanwhile (I2C is multi-drop).
GPIO 7–11pin 24/26/19/21/23(free — reserved SPI0)Available; not claimed because OLED moved to Pico per ADR-0017.
GPIO 18–21pin 12 / 35 / 38 / 40WM8960 HAT I2S (GWP-698, bench phase)BCLK / LRCLK / ADC / DAC. Frees back up when audio moves to the Pico per ADR-0017/ADR-0054.
All others(free)No project claim.

Conflict-avoidance rule: any new peripheral that wants a GPIO must update this table in the same PR that lands the overlay change, and must not collide with an existing claim. canonical-spec.md Spec Hygiene Rule 3 applies — the table is the source of truth, not a hint.

After flashing a fresh SD and first-boot, run these from an SSH session in dev mode (or from the bench-rig serial console — see ../hardware/build-specification.md §5):

Terminal window
# 1. Confirm UART0 is live and routed to /dev/serial0:
ls -l /dev/serial0
# expected: lrwxrwxrwx /dev/serial0 -> ttyAMA0
# 2. Confirm the BT module is OFF UART0:
dmesg | grep -i bluetooth
# expected: nothing on the PL011, "Bluetooth: disabled" or "no UART"
# 3. Confirm GPIO claims match the table above:
raspi-gpio get
# Spot-check rows for GPIO 14, 15, 22, 23 — should show the assignments above.
# GPIO 7-11 and 18/19/21 should be inputs (free).
# 4. Confirm SPI is OFF:
ls /dev/spidev* 2>/dev/null && echo "SPI is ON (BAD)" || echo "SPI is OFF (good)"
# 5. Confirm the USB hub + downstream devices enumerated:
lsusb -t
# Expected tree: Pi root hub -> internal hub IC -> {keyboard controller, SD card reader bridge}
# (Cartridge bridge appears even with no SD inserted; the SD card itself appears under it on insertion.)
# 6. Confirm the keyboard:
ls /dev/input/by-id/ | grep -i kn86
# expected: a /dev/input/by-id/usb-KN86_*-event-kbd symlink

If any of these fail, the most likely cause is a stale config.txt — verify the file on p1 matches stage-kn86-firmware/files/boot/config.txt from the source tree. If the file is correct and the validation still fails, the overlay snippet may be syntactically valid but blocked by a Pi firmware version mismatch (see system-image-build.md “Base Debian release pinning”) — capture /boot/config.txt, dmesg | head -100, and vcgencmd version, and escalate.