comodore-iec-emu/docs/kernel-notes.md
Christian Werner b2e4f9710d
All checks were successful
Build kernel module / build (1:6.12.93-1+rpt1, bookworm) (pull_request) Successful in 1m1s
Build kernel module / build (1:6.18.34-1+rpt1, trixie) (pull_request) Successful in 1m8s
Build kernel module / package (pull_request) Successful in 29s
Build kernel module / release (pull_request) Has been skipped
refactor(kernel): align GPIO pin layout with the reference listener
Adopt the confirmed-working reference's pin assignment so the module runs
on that proven wiring: DATA=BCM2 (pin 3), CLK=BCM3 (pin 5), ATN=BCM4
(pin 7). RESET is kept and relocated to BCM17 (pin 11), the pin freed by
moving CLK. All five GPIOs stay in bank 0, so the direct-register hot
path and ATN IRQ are unchanged -- only the IEC_GPIO_* defines, the
device-tree overlay, and the docs/self-test move.

DATA/CLK now sit on the ARM I2C pins (GPIO2/3) with the SoC's fixed
~1.8k pull-ups; keep dtparam=i2c_arm off. The bare-board self-test
expectation changes from 0x15 to 0x0b accordingly.

Generated by Clanker
2026-06-20 16:09:10 +02:00

238 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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` |
| Sense-line reads | Debounced (glitch-filtered): a level change is believed only after it holds `IEC_DEBOUNCE_US` (5 µs); shorter pulses are rejected as noise. Mirrors a confirmed-working reference listener. Fast path is a single register read, so tight CLK polls stay cheap. ATN ISR + self-test stay **raw** | `iec_read_stable`, `db_*_asserted` |
| 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`](../kernel/README.md#self-test) 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:
```c
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/4/17)`, 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=4, RESET=17, CLK=3, DATA=2),
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)
```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 <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:
```bash
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.
```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 |
|-----|---------|---------|
| `SUITE` | `bookworm` | Debian/RPi OS suite; **must match the kernel**`bookworm` ⇒ 6.12.x, `trixie` ⇒ 6.18.x |
| `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
SUITE=trixie KERNEL_VERSION=1:6.18.34-1+rpt1 ./build-in-docker.sh
```
**`SUITE` must match `KERNEL_VERSION`.** Each raspberrypi archive suite carries
only its own latest kernel, so a trixie-era version against a `bookworm` base
fails with `Version '…' was not found`. Find the Pi's suite with
`. /etc/os-release; echo "$VERSION_CODENAME"` (or `lsb_release -cs`).
**trixie SHA1 caveat.** On trixie, apt verifies signatures with Sequoia (`sqv`),
whose crypto policy rejects SHA1 since 2026-02-01. The raspberrypi archive key's
binding self-signature is SHA1, so trixie's apt rejects the repo as *"not
signed"* (`Policy rejected … SHA1 is not considered secure …`). The Dockerfile
works around this by marking the raspberrypi repo `trusted=yes` **only when
`DEBIAN_SUITE=trixie`**; bookworm (gpgv) keeps full signature verification.
**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:
```bash
pkg="linux-headers-$(uname -r | sed 's/.*+rpt-//')"; echo "HEADERS_PKG=$pkg KERNEL_VERSION=$(dpkg-query -W -f='${Version}' "$pkg")"
```
On the Pi at `chris@10.1.0.41` (a Pi 3B+, kernel `6.12.93+rpt-rpi-v8`; same
arm64/`-v8` headers as the Zero 2 W) this currently prints:
```bash
HEADERS_PKG=linux-headers-rpi-v8 KERNEL_VERSION=1:6.12.93-1+rpt1
```
**vermagic caveat:** each raspberrypi apt archive **suite** normally serves only
the *latest* kernel in its pool (`bookworm` ⇒ 6.12.x, `trixie` ⇒ 6.18.x), so the
container's `SUITE` must match the Pi's release, and pinning `KERNEL_VERSION` to
an old release within a suite may not be downloadable. The reliable strategy is
to keep the Pi current (`sudo apt full-upgrade`) and build with the matching
`SUITE` + default (latest) version — 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),
`IEC_CLK_TIMEOUT_US` (frame errors under load), and `IEC_DEBOUNCE_US` (the
sense-line glitch-filter window — raise it if a noisy bus still yields bit
errors, lower it if it smears fast edges).
## 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.