Introduce a conditional `trusted=yes` setting for the Raspberry Pi repository in the Dockerfile when `DEBIAN_SUITE=trixie`. This bypasses SHA1 signature rejection by trixie's apt system using Sequoia. Ensure that bookworm maintains full signature verification. Updated documentation to explain the trixie-specific caveat.
235 lines
11 KiB
Markdown
235 lines
11 KiB
Markdown
# 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; 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`](../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/17/18)`, i.e. the *global* numberspace
|
||
assuming base 0. Those small numbers now fall outside the chip's range
|
||
(512–565), 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)
|
||
|
||
```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) 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.
|