diff --git a/kernel/README.md b/kernel/README.md new file mode 100644 index 0000000..0eece06 --- /dev/null +++ b/kernel/README.md @@ -0,0 +1,78 @@ +# `iec_listener` kernel module + +Commodore IEC **listener** (printer, default address 4) for the Raspberry Pi +Zero 2 W. The timing-critical IEC handshake runs in this kernel module; received +bytes and bus events are pushed to userspace through `/dev/iec0`. + +Build/deploy, pinning, GPIO-descriptor and timing details live in +[`../docs/kernel-notes.md`](../docs/kernel-notes.md). This README documents the +**self-test**, which is the first thing to run after loading the module on real +hardware. + +## Self-test + +The module exposes a hardware self-test via the `IEC_IOC_SELFTEST` ioctl on +`/dev/iec0`. It drives the **DATA** line (the only line the Pi asserts) low and +reads it back, releases it, then samples **ATN / CLK / RESET**, returning a +bitmask of line states. Run it **with the C64 disconnected** to check the wiring +before connecting the real bus (see PLAN.md §10). + +### Running it + +`selftest.sh` is non-persistent (nothing is installed into `/lib/modules`, no +autoload): it checks vermagic, loads the module, runs the self-test, and always +unloads it again. + +```bash +sudo ./selftest.sh # auto-finds ./ or ~/iec_listener.ko, address 4 +sudo ./selftest.sh ~/iec_listener.ko --address 5 # runs with ~/iec_listener.ko, address 5 +``` + +It exits non-zero unless the result is a full pass (`0x1F`), so it is usable in +scripts/CI. + +### Result bitmask + +The ioctl returns a `u32`; a full pass is **`0x1F`** (all five bits set). + +| Bit | Value | Meaning | Source | +|-----|-------|---------|--------| +| `DATA_ASSERT_OK` | `0x01` | DATA read **low** while the module drives it low | Pi drive + sense path | +| `DATA_FLOAT_OK` | `0x02` | DATA read **high** after release (Hi-Z) | external pull-up on DATA | +| `ATN_RELEASED` | `0x04` | ATN high (idle) | line state | +| `CLK_RELEASED` | `0x08` | CLK high (idle) | line state | +| `RESET_RELEASED` | `0x10` | RESET high (idle) | line state | + +Constants are defined in [`iec_listener.h`](iec_listener.h); the ioctl number is +`_IOR('I', 3, __u32)` = `0x80044903`. + +### Interpreting failures + +- **`DATA_ASSERT_OK` missing** → driving DATA low doesn't read back low: level + shifter wired backwards/inverting, wrong pin, or the sense path is broken. + This is the only bit that is fully internal to the Pi — if it fails, suspect + the module/build or the DATA wiring, not pull-ups. +- **`DATA_FLOAT_OK` missing** → DATA stays low after release: missing/weak + pull-up on DATA, or the pin didn't return to input. +- **`CLK_RELEASED` missing** → CLK reads low at idle: no pull-up on CLK, short, + or a swapped signal. +- **`ATN_RELEASED` / `RESET_RELEASED` missing** → that line reads low at idle: + short, missing pull-up, or swapped wiring. + +### ⚠️ Bare-board caveat + +With **nothing connected**, the expected result is **`0x15`**, *not* a fault: + +- `DATA_ASSERT_OK` (`0x01`) passes — it's internal to the Pi. +- `ATN_RELEASED` (`0x04`) and `RESET_RELEASED` (`0x10`) pass **only because + GPIO2/GPIO3 have fixed ~1.8 kΩ pull-ups built into the BCM2710 SoC** (they are + the I²C0 pins; the pull-ups can't be disabled). They read high even with + nothing wired, so on a bare board these two bits prove **nothing** about your + wiring. +- `DATA_FLOAT_OK` (`0x02`) and `CLK_RELEASED` (`0x08`) read low because GPIO17/18 + have no such built-in pull-up and nothing is attached. + +So on a bare board the only meaningful signal is `DATA_ASSERT_OK`. A full `0x1F` +is only reachable once the level shifter (with its CLK/DATA pull-ups) is wired +and powered. To make ATN/RESET meaningful, briefly ground each at the connector +and confirm the corresponding bit *drops*. diff --git a/kernel/iec_listener.c b/kernel/iec_listener.c index 0e68a97..810ae36 100644 --- a/kernel/iec_listener.c +++ b/kernel/iec_listener.c @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -51,9 +52,18 @@ MODULE_PARM_DESC(address, "primary IEC device address (default 4)"); /* --- globals ------------------------------------------------------------- */ u32 __iomem *iec_gpio; /* defined extern in iec_lines.h; ioremap'd in init */ +static struct gpio_device *iec_gdev; /* BCM2835 GPIO chip, ref held for the module lifetime */ static struct gpio_desc *gd_atn, *gd_clk, *gd_reset, *gd_data; static int irq_atn; +/* + * Label of the Pi's main 40-pin GPIO controller. We resolve descriptors by + * (chip, hwnum) instead of the legacy global gpio_to_desc() numberspace, whose + * base is no longer 0 on current kernels (gpiochip base = 512 on 6.12) — the + * old flat numbers fell outside the chip and returned NULL -> -ENODEV. + */ +#define IEC_GPIO_CHIP_LABEL "pinctrl-bcm2835" + #define IEC_FIFO_SIZE 256 /* records; power of two */ static DECLARE_KFIFO(iec_fifo, struct iec_record, IEC_FIFO_SIZE); static DECLARE_WAIT_QUEUE_HEAD(iec_read_wq); @@ -398,15 +408,26 @@ static int __init iec_init(void) if (!iec_gpio) return -ENOMEM; - /* claim GPIOs via descriptor API (init/exit only) */ - gd_atn = gpio_to_desc(IEC_GPIO_ATN); - gd_clk = gpio_to_desc(IEC_GPIO_CLK); - gd_reset = gpio_to_desc(IEC_GPIO_RESET); - gd_data = gpio_to_desc(IEC_GPIO_DATA); - if (!gd_atn || !gd_clk || !gd_reset || !gd_data) { + /* + * Resolve GPIO descriptors by (chip, hwnum) — hwnum is the BCM number, + * which is base-independent (see IEC_GPIO_CHIP_LABEL above). Used in + * init/exit only; the hot path uses direct register access. + */ + iec_gdev = gpio_device_find_by_label(IEC_GPIO_CHIP_LABEL); + if (!iec_gdev) { + pr_err("iec: GPIO chip '%s' not found\n", IEC_GPIO_CHIP_LABEL); ret = -ENODEV; goto err_unmap; } + gd_atn = gpio_device_get_desc(iec_gdev, IEC_GPIO_ATN); + gd_clk = gpio_device_get_desc(iec_gdev, IEC_GPIO_CLK); + gd_reset = gpio_device_get_desc(iec_gdev, IEC_GPIO_RESET); + gd_data = gpio_device_get_desc(iec_gdev, IEC_GPIO_DATA); + if (IS_ERR(gd_atn) || IS_ERR(gd_clk) || IS_ERR(gd_reset) || IS_ERR(gd_data)) { + pr_err("iec: failed to get GPIO descriptors on '%s'\n", IEC_GPIO_CHIP_LABEL); + ret = -ENODEV; + goto err_put_gdev; + } gpiod_direction_input(gd_atn); gpiod_direction_input(gd_clk); gpiod_direction_input(gd_reset); @@ -457,6 +478,8 @@ err_cdev: cdev_del(&iec_cdev); err_region: unregister_chrdev_region(iec_devno, 1); +err_put_gdev: + gpio_device_put(iec_gdev); err_unmap: iounmap(iec_gpio); return ret; @@ -475,6 +498,7 @@ static void __exit iec_exit(void) class_destroy(iec_class); cdev_del(&iec_cdev); unregister_chrdev_region(iec_devno, 1); + gpio_device_put(iec_gdev); iounmap(iec_gpio); pr_info("iec: unloaded, DATA released\n"); } diff --git a/kernel/selftest.sh b/kernel/selftest.sh new file mode 100755 index 0000000..ed4b336 --- /dev/null +++ b/kernel/selftest.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# +# selftest.sh - non-persistent self-test for the IEC listener kernel module. +# +# This is NOT a full install: nothing is copied into /lib/modules and no +# autoload is configured. It only loads the module, tests it, and unloads it. +# +# Entry point: the built iec_listener.ko is already on the Pi. This script: +# 1. checks the module is compatible with the running kernel (vermagic), +# 2. loads it (insmod) - non-persistent, no reboot needed, +# 3. runs the self-test ioctl on /dev/iec0, +# 4. unloads it again (rmmod) so the Pi is left exactly as before. +# +# The module is never left loaded by this script; it always unloads, even if +# the self-test fails or the script is interrupted. +# +# Usage: +# sudo ./selftest.sh [path/to/iec_listener.ko] [--address N] +# +# Defaults: ./iec_listener.ko (or ~/iec_listener.ko), address=4 (printer). + +set -euo pipefail + +MODULE="iec_listener" +ADDRESS=4 +KO="" +LOADED=0 + +# ---- pretty output (color only on a tty) -------------------------------- +if [ -t 1 ]; then + C_OK=$'\033[32m'; C_ERR=$'\033[31m'; C_INFO=$'\033[36m'; C_RST=$'\033[0m' +else + C_OK=""; C_ERR=""; C_INFO=""; C_RST="" +fi +ok() { printf '%s OK %s %s\n' "$C_OK" "$C_RST" "$*"; } +info() { printf '%s ==>%s %s\n' "$C_INFO" "$C_RST" "$*"; } +die() { printf '%sFAIL%s %s\n' "$C_ERR" "$C_RST" "$*" >&2; exit 1; } + +# ---- always unload on the way out --------------------------------------- +cleanup() { + if [ "$LOADED" = 1 ] && lsmod | grep -q "^${MODULE}\b"; then + info "rmmod ${MODULE} (releases DATA, removes /dev/iec0)" + rmmod "$MODULE" 2>/dev/null && ok "unloaded - Pi left as before" \ + || info "rmmod failed; unload manually: sudo rmmod ${MODULE}" + fi +} +trap cleanup EXIT + +# ---- args --------------------------------------------------------------- +while [ $# -gt 0 ]; do + case "$1" in + --address) ADDRESS="${2:-}"; shift 2 ;; + --address=*) ADDRESS="${1#*=}"; shift ;; + -h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + -*) die "unknown option: $1" ;; + *) KO="$1"; shift ;; + esac +done + +# ---- preconditions ------------------------------------------------------ +[ "$(id -u)" -eq 0 ] || die "must run as root (use: sudo $0 ...)" +case "$ADDRESS" in ''|*[!0-9]*) die "address must be 0-30 (got '$ADDRESS')" ;; esac +{ [ "$ADDRESS" -ge 0 ] && [ "$ADDRESS" -le 30 ]; } || die "address out of range 0-30: $ADDRESS" + +if [ -z "$KO" ]; then + for cand in "./${MODULE}.ko" "$HOME/${MODULE}.ko" "$(dirname "$0")/${MODULE}.ko"; do + [ -f "$cand" ] && { KO="$cand"; break; } + done +fi +[ -n "$KO" ] && [ -f "$KO" ] || die "module not found; pass the path, e.g. sudo $0 ~/${MODULE}.ko" +command -v modinfo >/dev/null || die "modinfo not found (install kmod)" + +info "module: $KO" +info "address: $ADDRESS" + +# ---- 1. compatibility check (vermagic vs running kernel) ---------------- +RUNNING="$(uname -r)" +ARCH="$(uname -m)" +VERMAGIC="$(modinfo -F vermagic "$KO")" || die "cannot read vermagic from $KO" +MOD_KVER="${VERMAGIC%% *}" # first token of vermagic = kernel version + +info "running kernel: $RUNNING ($ARCH)" +info "module vermagic: $VERMAGIC" + +[ "$MOD_KVER" = "$RUNNING" ] || die \ +"kernel mismatch: module built for '$MOD_KVER' but running '$RUNNING'. + Rebuild against the current headers (see docs/kernel-notes.md): + pkg=\"linux-headers-\$(uname -r | sed 's/.*+rpt-//')\" + echo \"HEADERS_PKG=\$pkg KERNEL_VERSION=\$(dpkg-query -W -f='\${Version}' \"\$pkg\")\"" + +case "$VERMAGIC" in + *"$ARCH"*) ;; + *) die "arch mismatch: module vermagic has no '$ARCH' (wrong flavour built?)" ;; +esac +ok "compatible with running kernel" + +# ---- 2. load (reload if a stale copy is already in) --------------------- +if lsmod | grep -q "^${MODULE}\b"; then + info "a copy is already loaded; removing it first" + rmmod "$MODULE" || die "rmmod failed (is /dev/iec0 in use?)" +fi +info "insmod ${MODULE}.ko address=${ADDRESS}" +insmod "$KO" address="$ADDRESS" || die "insmod failed; check: dmesg | tail" +LOADED=1 +[ -c /dev/iec0 ] || die "/dev/iec0 was not created" +ok "loaded; /dev/iec0 present" + +# ---- 3. self-test ioctl ------------------------------------------------- +info "running self-test (IEC_IOC_SELFTEST) - run with the C64 disconnected" +set +e +python3 - <<'PY' +import array, fcntl, sys +IEC_IOC_SELFTEST = 0x80044903 # _IOR('I', 3, __u32) +BITS = [ + ("DATA_ASSERT_OK", 0x01), # DATA read low while driven low + ("DATA_FLOAT_OK", 0x02), # DATA read high after release (Hi-Z) + ("ATN_RELEASED", 0x04), # ATN high (idle) + ("CLK_RELEASED", 0x08), # CLK high (idle) + ("RESET_RELEASED", 0x10), # RESET high (idle) +] +buf = array.array("I", [0]) +with open("/dev/iec0", "rb", buffering=0) as f: + fcntl.ioctl(f, IEC_IOC_SELFTEST, buf, True) +r = buf[0] +print(" selftest = 0x%02x (%s)" % (r, "PASS" if r == 0x1F else "FAIL")) +for name, bit in BITS: + print(" %-15s %s" % (name, "ok" if r & bit else "MISSING")) +sys.exit(0 if r == 0x1F else 1) +PY +ST=$? +set -e + +# ---- 4. report (rmmod happens in the EXIT trap) ------------------------- +echo +if [ "$ST" -eq 0 ]; then + ok "self-test PASSED (0x1F) - all four lines wired correctly" +else + info "self-test did not fully pass." + info "On a BARE board 0x15 is expected and fine: only DATA_ASSERT_OK is" + info "meaningful; ATN/RESET pass via the SoC's fixed GPIO2/3 pull-ups." + info "Re-run once the level shifter is wired + powered; you want 0x1F." +fi +exit "$ST"