# Kernel module notes Phase-0.5 decisions, resolved in `_research/pi-kernel-module-rt-gpio-2026-06-18.md` (read that for the full evidence and source-level analysis). This file is the short, actionable summary the code in `kernel/` is built on. ## Decisions baked into the code | Question | Decision | Where | |----------|----------|-------| | CLK: interrupt vs. busy-poll | ATN falling-edge hardirq enters the state machine; bytes are busy-polled with `local_irq_save()` held for one byte (ninepin pattern) | `atn_isr`, `receive_byte` | | IRQ discipline | `local_irq_save()` around each byte only (~200 µs normal, ≤ 8 ms abort cap), **not** the whole ATN phase | `receive_byte` | | GPIO access (hot path) | Direct BCM register access via `ioremap` (~30–40× faster than gpiod) | `iec_lines.h` | | GPIO access (init/exit) | gpiod descriptor API (`gpio_to_desc`, `gpiod_to_irq`) | `iec_init`/`iec_exit` | | Kernel↔userspace | Character device `/dev/iec0` + `kfifo` + wait queue (IEC ≤ 1000 B/s; relayfs not justified) | `iec_read`, `emit_record` | | `udelay` vs. poll | Poll-with-timeout for CLK transitions; `udelay` only for fixed delays (EOI ack 80 µs, EOI detect 250 µs) | `iec_timing.h`, `wait_clk` | | isolcpus / nohz_full | **Not** in the Phase-1 baseline. Add `isolcpus=3 nohz_full=3 rcu_nocbs=3 irqaffinity=0-2` only if Phase-2 bit-error rate > 1% | (boot cmdline) | | PREEMPT_RT | Stock kernel sufficient; RT is a last resort | — | | Module signing | Not required on stock RPi OS Bookworm | — | | Peripheral base | `0x3F000000` (BCM2710A1 / Pi Zero 2 W; **not** ninepin's `0x20000000`) | `iec_lines.h` | ## Level-shifter / DATA-sensing note (deviation from the research doc) The research doc (§4.3) analysed a **7406 inverting** drive + separate resistor divider sense, and flagged a "DATA sensing gap" (a 7406 output pin can't read the bus back). The **PLAN** instead specifies a **non-inverting bidirectional level shifter** (BSS138, sd2iec-style single DATA pin, §3.1). That choice: - makes the logic **non-inverting**: bus low (asserted) ⇒ Pi reads LOW; - **closes the sensing gap** — when the DATA pin is switched to *input* (Hi-Z), reading it back through the bidirectional shifter returns the real bus state the C64 is driving. This is exactly what `receive_byte` relies on for bit sampling. So `iec_lines.h` is non-inverting; there is no 7406 inversion to track. Confirm with the `IEC_IOC_SELFTEST` ioctl (drive low → read low; release → read high) before connecting the C64 (PLAN.md §10 risk row). ## Build & deploy (on the Pi) ```bash sudo apt install raspberrypi-kernel-headers cd kernel make # iec_listener.ko make overlay # dts/iec-overlay.dtbo (optional pin reservation) sudo insmod iec_listener.ko address=4 ls -l /dev/iec0 # ... talk to the C64 ... sudo rmmod iec_listener # releases DATA on the way out ``` Optional pin-reservation overlay (Bookworm paths — note the `/boot/firmware/` prefix; the legacy `/boot/` paths no longer apply on 64-bit Bookworm): ```bash sudo cp dts/iec-overlay.dtbo /boot/firmware/overlays/ echo "dtoverlay=iec-overlay" | sudo tee -a /boot/firmware/config.txt sudo reboot # after reboot, verify it loaded: dtoverlay -l ``` **Pin the kernel *before* the first `apt full-upgrade`** — any kernel bump breaks the module via a vermagic mismatch (`Invalid module format`), so hold the kernel packages up front rather than after the fact: ```bash sudo apt-mark hold raspberrypi-kernel raspberrypi-kernel-headers raspberrypi-bootloader # record the pinned version here once known: # Pinned: raspberrypi-kernel () ``` ## Building off the Pi (emulated arm64 Docker) You can compile the module on a non-Pi (x86) host with `kernel/build-in-docker.sh` (or `make docker-build`). It runs an **emulated arm64** Raspberry Pi OS container, installs the raspberrypi kernel headers via apt, and builds natively so the module's *vermagic* matches the Pi. ```bash cd kernel ./build-in-docker.sh # -> iec_listener.ko (arm64) in this dir ./build-in-docker.sh clean ``` Configurable via env vars (kernel version is configurable as requested): | Var | Default | Purpose | |-----|---------|---------| | `HEADERS_PKG` | `linux-headers-rpi-v8` | headers package; use `-v7`/`-v6` for 32-bit, or `raspberrypi-kernel-headers` | | `KERNEL_VERSION` | *(latest)* | exact version pin, e.g. `1:6.6.51-1+rpt3` | | `IMAGE` | `iec-kbuild` | builder image tag | ```bash KERNEL_VERSION=1:6.6.51-1+rpt3 ./build-in-docker.sh ``` **vermagic caveat:** the raspberrypi apt archive normally serves only the *latest* kernel in its pool, so pinning `KERNEL_VERSION` to an old release may not be downloadable. The reliable strategy is to keep the Pi current (`sudo apt full-upgrade`) and build with the default (latest) — then the Pi and the container agree. If you must target an older/specific kernel, copy the Pi's `/lib/modules/$(uname -r)/build` tree into the container instead of using apt. `uname -r` is **not** used inside the container (it reports the host kernel under emulation); the entrypoint derives `KDIR` from the installed headers under `/lib/modules/`. The host needs qemu binfmt for arm64; the script registers it once via `tonistiigi/binfmt --install arm64` (a one-time privileged container). ## Starting timing constants See `kernel/iec_timing.h`. Tune in Phase 2 against a real C64 / logic analyser. The likely first knobs: `IEC_EOI_DETECT_US` (EOI false positives/negatives) and `IEC_CLK_TIMEOUT_US` (frame errors under load). ## Open items to verify on hardware - GPIO IRQ latency on BCM2710A1 under representative load (target ATN ack ≪ 1 ms). - Direct-register read latency inside the IRQ-off loop on the A53 (budget vs. the 20 µs C64 bit window). - Whether `isolcpus` is needed once Phase 2 runs under WiFi/SD load. - `IEC_GPIO_DATA` direction-flip latency (`GPFSEL` write) — should be tens of ns.