feat(kernel): Add GPIO descriptor-based initialization and self-test script
Replace legacy gpio_to_desc() with GPIO descriptor resolution by chip label and hardware number, ensuring compatibility with kernels using non-zero gpiochip bases. Added `selftest.sh` for non-persistent module testing, which performs a hardware self-test and verifies wiring before connecting the real bus. Included a detailed README.md documenting the self-test process.
This commit is contained in:
parent
e4cab679b5
commit
e0a78871ca
78
kernel/README.md
Normal file
78
kernel/README.md
Normal file
@ -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*.
|
||||
@ -25,6 +25,7 @@
|
||||
#include <linux/delay.h>
|
||||
#include <linux/fs.h>
|
||||
#include <linux/gpio/consumer.h>
|
||||
#include <linux/gpio/driver.h>
|
||||
#include <linux/interrupt.h>
|
||||
#include <linux/kfifo.h>
|
||||
#include <linux/kthread.h>
|
||||
@ -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");
|
||||
}
|
||||
|
||||
143
kernel/selftest.sh
Executable file
143
kernel/selftest.sh
Executable file
@ -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"
|
||||
Loading…
x
Reference in New Issue
Block a user