comodore-iec-emu/docs/kernel-notes.md
Christian Werner e942cedbfa docs(kernel): Update kernel notes with GPIO descriptor details and verification steps
Expand the documentation to include details on GPIO descriptor
lookup using chip label and hardware offset, replacing the deprecated
global GPIO number approach. Added instructions for verifying the module's
version magic to ensure compatibility with the running kernel. These
updates aim to prevent common initialization and deployment errors.
2026-06-19 00:24:38 +02:00

10 KiB
Raw Permalink Blame History

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 (~3040× faster than gpiod) iec_lines.h
GPIO access (init/exit) gpiod descriptor API; descriptors resolved by (chip, hwnum) via gpio_device_find_by_label("pinctrl-bcm2835") + gpio_device_get_desc(), not gpio_to_desc() (see "GPIO descriptor lookup" below) 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). See ../kernel/README.md for how to run it (kernel/selftest.sh), the result bitmask, and the bare-board caveat.

GPIO descriptor lookup (do not use gpio_to_desc())

iec_init resolves the four IEC line descriptors by chip label + hardware offset, not by the legacy global GPIO number:

iec_gdev = gpio_device_find_by_label("pinctrl-bcm2835");   /* ref held until exit */
gd_atn   = gpio_device_get_desc(iec_gdev, IEC_GPIO_ATN);   /* hwnum == BCM number */
...                                                        /* check with IS_ERR() */

Why — this bit us once (insmod: No such device / -ENODEV). On current Raspberry Pi OS kernels (6.12 here) the BCM2835 GPIO controller no longer starts at global number 0 — it's gpiochip512, base 512:

$ cat /sys/class/gpio/gpiochip*/base   # -> 512 (pinctrl-bcm2835), 566 (exp-gpio)

The old code used gpio_to_desc(2/3/17/18), i.e. the global numberspace assuming base 0. Those small numbers now fall outside the chip's range (512565), so every gpio_to_desc() returned NULL, the descriptor check tripped, and init bailed with -ENODEV. The (chip, hwnum) lookup passes the BCM number as the chip-relative offset (ATN=2, RESET=3, CLK=17, DATA=18), which is base-independent and survives kernel bumps / gpiochip renumbering.

Notes:

  • gpio_device_get_desc() returns an ERR_PTR on a bad offset (not NULL) — check with IS_ERR(), not !desc.
  • gpio_device_find_by_label() takes a reference; it's released with gpio_device_put(iec_gdev) in iec_exit and on the init error path.
  • Both symbols are EXPORT_SYMBOL_GPL (fine — the module is GPL) and need #include <linux/gpio/driver.h>.
  • If a future kernel renames the controller, update IEC_GPIO_CHIP_LABEL; confirm the live label with gpioinfo / the /sys/class/gpio/gpiochip*/label files on the Pi.

Build & deploy (on the Pi)

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):

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:

sudo apt-mark hold raspberrypi-kernel raspberrypi-kernel-headers raspberrypi-bootloader
# record the pinned version here once known:
# Pinned: raspberrypi-kernel <VERSION> (<DATE>)

Verify the module before loading it (vermagic)

insmod refuses a module whose vermagic doesn't match the running kernel (Invalid module format / version magic ... should be ... in dmesg). Always check it after copying a freshly built .ko to the Pi:

modinfo ~/iec_listener.ko | grep vermagic
uname -r          # what the running kernel expects

What correct looks like — the kernel-version, SMP, preempt, and arch tokens all match the running kernel (case differs: modinfo lowercases preempt, uname prints PREEMPT — that's fine):

vermagic:       6.12.93+rpt-rpi-v8 SMP preempt mod_unload modversions aarch64
                ^^^^^^^^^^^^^^^^^^^ ^^^ ^^^^^^^                    ^^^^^^^^
                = uname -r          |   |                          = arch (arm64)
                                    SMP  PREEMPT

The leading 6.12.93+rpt-rpi-v8 must equal uname -r exactly — that token is the only thing insmod hard-checks. modversions means symbol CRCs were built against the matching Module.symvers, so symbol resolution is consistent too. mod_unload just means rmmod is supported. All good → load it.

What wrong looks like — any difference in the version token, e.g.:

vermagic:       6.6.51+rpt-rpi-v8 SMP preempt mod_unload modversions aarch64
                ^^^^^^ kernel moved on; uname -r says 6.12.93 → insmod rejects it

or a wrong/blank arch (armv7l vs aarch64 → you built the 32-bit -v7/-v6 flavour by mistake), or a missing modversions (built against the wrong headers tree). The fix is always the same: rebuild against headers matching the current uname -r (re-run the one-liner above to get HEADERS_PKG / KERNEL_VERSION), or keep the Pi pinned so the kernel can't drift out from under the module.

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.

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
KERNEL_VERSION=1:6.6.51-1+rpt3 ./build-in-docker.sh

Find the exact values for your Pi — run this on the Pi (e.g. over SSH); it prints the two lines ready to copy into the build-in-docker.sh invocation:

pkg="linux-headers-$(uname -r | sed 's/.*+rpt-//')"; echo "HEADERS_PKG=$pkg KERNEL_VERSION=$(dpkg-query -W -f='${Version}' "$pkg")"

On the project's Pi Zero 2 W (chris@10.1.0.41, kernel 6.12.93+rpt-rpi-v8) this currently prints:

HEADERS_PKG=linux-headers-rpi-v8 KERNEL_VERSION=1:6.12.93-1+rpt1

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.