Compare commits

..

18 Commits

Author SHA1 Message Date
a79a469aac Merge pull request 'fix-build' (#4) from fix-build into master
All checks were successful
Build kernel module / build (1:6.12.93-1+rpt1, bookworm) (push) Successful in 1m9s
Build kernel module / build (1:6.18.34-1+rpt1, trixie) (push) Successful in 1m8s
Build kernel module / package (push) Successful in 33s
Build kernel module / release (push) Successful in 20s
Reviewed-on: #4
2026-06-20 05:51:52 +02:00
27ff299f43 feat(kernel): Upload package tree instead of pre-zipped artifact
All checks were successful
Build kernel module / build (1:6.12.93-1+rpt1, bookworm) (pull_request) Successful in 49s
Build kernel module / build (1:6.18.34-1+rpt1, trixie) (pull_request) Successful in 54s
Build kernel module / package (pull_request) Successful in 23s
Build kernel module / release (pull_request) Has been skipped
Switch to uploading the assembled package directory as the artifact, allowing the platform to encapsulate it in a single-layer zip. This avoids nested zips and ensures the release process reuses the same archive structure for consistency.
2026-06-20 05:42:55 +02:00
a5406f503c feat(kernel): Add PR packaging and artifact upload for testing
All checks were successful
Build kernel module / build (1:6.18.34-1+rpt1, trixie) (pull_request) Successful in 49s
Build kernel module / build (1:6.12.93-1+rpt1, bookworm) (pull_request) Successful in 1m9s
Build kernel module / package (pull_request) Successful in 18s
Build kernel module / release (pull_request) Has been skipped
Enable packaging and artifact upload for pull requests to facilitate
testing. Package job generates a zip archive identical to release
artifacts, allowing smoke tests before final releases. Adjusted naming
for zip files to include PR reference or SHA for traceability.
2026-06-20 05:36:06 +02:00
dafcdfbd7b feat(kernel): Add launch.sh for streamlined module loading and frontend execution
All checks were successful
Build kernel module / build (1:6.18.34-1+rpt1, trixie) (pull_request) Successful in 1m13s
Build kernel module / build (1:6.12.93-1+rpt1, bookworm) (pull_request) Successful in 1m16s
Build kernel module / release (pull_request) Has been skipped
Introduce `launch.sh` as a single execution entry point for the IEC listener module and Python frontend. Automates kernel module selection, virtual environment setup, and cleanup to simplify usage. Ensures consistency by unloading modules on script exit.
2026-06-20 05:31:21 +02:00
b3d63664a0 docs(readme): Add instructions for running frontend on Pi
Document the process for setting up and running the Python frontend on
a Raspberry Pi using release packages. Include details on wiring
self-tests, module selection, and using `launch.sh` for streamlined
execution. Update kernel README to cross-reference top-level changes.
2026-06-20 05:31:11 +02:00
65ff323d50 feat(kernel): Include helper scripts and Python frontend in releases
Add `launch.sh` and `iecpoc` Python frontend to the release package for streamlined execution on the target device. Ensure accompanying files like `pyproject.toml` and `README.md` are included, while cleaning unnecessary caches.
2026-06-20 05:29:32 +02:00
3135a2502b fix(kernel): Handle unreadable module files in selftest.sh
Improve error handling in module discovery by identifying unreadable
`.ko` files and providing detailed error messages. Suggest actions like
unzip or rebuild to resolve issues.
2026-06-20 05:13:48 +02:00
2881fae5bf fix(kernel): Correct Docker tag sanitization for kernel versions
All checks were successful
Build kernel module / build (1:6.18.34-1+rpt1, trixie) (pull_request) Successful in 52s
Build kernel module / build (1:6.12.93-1+rpt1, bookworm) (pull_request) Successful in 1m23s
Build kernel module / release (pull_request) Has been skipped
Ensure Docker image tags map unsupported characters `:`, `/`, and `+`
to `-` for compatibility. This prevents invalid tags in Docker builds
when kernel version strings include these characters.
2026-06-20 04:57:26 +02:00
f91b06da9d fix(kernel): Use per-version tags in Docker build to isolate jobs
Some checks failed
Build kernel module / build (1:6.12.93-1+rpt1, bookworm) (pull_request) Failing after 41s
Build kernel module / build (1:6.18.34-1+rpt1, trixie) (pull_request) Failing after 42s
Build kernel module / release (pull_request) Has been skipped
Prevent race conditions in concurrent job builds by introducing unique
Docker image tags per kernel version. This ensures isolated builds with
correct verm
2026-06-20 04:53:45 +02:00
2aa5104812 feat(kernel): Improve selftest.sh module discovery and error handling
Enhance module selection process by validating vermagic against the running kernel. Add detailed error messages with suggestions for manual path overrides or rebuilding for the current kernel. Ensure robust fallback behavior and improve user guidance.
2026-06-20 04:53:40 +02:00
6e5ec9b84f docs(kernel): Update README with module selection details
Expand documentation on selftest.sh behavior, module matching, and kernel compatibility. Clarify the auto-selection process based on `vermagic` and provide examples to guide users through fallback handling and manual overrides.
2026-06-20 04:53:31 +02:00
160f6bb799 Merge pull request 'fix(kernel): allow command-phase byte reception while ATN is asserted' (#3) from fix/command-phase-atn-abort into master
All checks were successful
Build kernel module / build (1:6.18.34-1+rpt1, trixie) (push) Successful in 1m22s
Build kernel module / build (1:6.12.93-1+rpt1, bookworm) (push) Successful in 1m23s
Build kernel module / release (push) Successful in 34s
Reviewed-on: #3
2026-06-20 03:36:35 +02:00
c9cfebafda fix(kernel): allow command-phase byte reception while ATN is asserted
All checks were successful
Build kernel module / build (1:6.12.93-1+rpt1, bookworm) (pull_request) Successful in 1m18s
Build kernel module / build (1:6.18.34-1+rpt1, trixie) (pull_request) Successful in 1m13s
Build kernel module / release (pull_request) Has been skipped
wait_clk() aborted on any asserted ATN, but ATN is held low for the
entire command phase by design. Reception therefore aborted on the
first poll before clocking a single bit, so the listener never decoded
the LISTEN command and always fell through to "not addressed -> IDLE".

Make the ATN abort phase-relative via IEC_RX_ATN meaning "ATN changed":
in the data phase abort when ATN asserts (C64 interrupts with a new
command); in the command phase abort when ATN releases (command
sequence finished). The check collapses to
`iec_atn_asserted() == data_phase`.

Closes #2

Generated by Clanker

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 03:11:51 +02:00
62dff1264d Merge pull request 'Add 1:6.18.34-1+rpt1 kernel support' (#1) from chris-patch-1 into master
All checks were successful
Build kernel module / build (1:6.12.93-1+rpt1, bookworm) (push) Successful in 1m1s
Build kernel module / build (1:6.18.34-1+rpt1, trixie) (push) Successful in 1m13s
Build kernel module / release (push) Successful in 18s
Reviewed-on: #1
2026-06-19 21:17:49 +02:00
de2fb56a59 feat(kernel): Add trixie SHA1 signature workaround to Dockerfile
All checks were successful
Build kernel module / build (1:6.12.93-1+rpt1, bookworm) (pull_request) Successful in 8m4s
Build kernel module / build (1:6.18.34-1+rpt1, trixie) (pull_request) Successful in 5m25s
Build kernel module / release (pull_request) Has been skipped
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.
2026-06-19 21:03:18 +02:00
914c92bdea feat(kernel): Add support for Debian suite configuration
Some checks failed
Build kernel module / build (1:6.12.93-1+rpt1, bookworm) (pull_request) Successful in 8m4s
Build kernel module / build (1:6.18.34-1+rpt1, trixie) (pull_request) Failing after 2m31s
Build kernel module / release (pull_request) Has been skipped
Introduce `DEBIAN_SUITE` as a configurable argument in the Dockerfile, build
script, and CI workflow to align kernel builds with the target Raspberry Pi
OS release. Updated documentation to clarify the relationship between
suite versions and kernel compatibility.
2026-06-19 20:45:15 +02:00
aeb5113366 Add 1:6.18.34-1+rpt1 kernal support
Some checks failed
Build kernel module / build (1:6.12.93-1+rpt1) (pull_request) Successful in 1m24s
Build kernel module / build (1:6.18.34-1+rpt1) (pull_request) Failing after 57s
Build kernel module / release (pull_request) Has been skipped
2026-06-19 19:28:45 +02:00
3aecb25a61 fix(ci): Update artifact download action to use direct repository link
All checks were successful
Build kernel module / build (1:6.12.93-1+rpt1) (push) Successful in 1m24s
Build kernel module / release (push) Successful in 22s
Replaced `actions/download-artifact@v4` with a direct link to
`gitea-download-artifact` repository. This ensures the workflow uses
the latest version of the custom action and improves maintainability.
2026-06-19 04:13:57 +02:00
9 changed files with 434 additions and 46 deletions

View File

@ -27,8 +27,14 @@ jobs:
# Don't cancel the other versions if one fails to build.
fail-fast: false
matrix:
kernel_version:
- "1:6.12.93-1+rpt1"
# Each raspberrypi archive suite only carries its own latest kernel, so
# the Debian base suite is paired with the kernel version it can install:
# bookworm -> 6.12.x, trixie -> 6.18.x.
include:
- kernel_version: "1:6.12.93-1+rpt1"
suite: bookworm
- kernel_version: "1:6.18.34-1+rpt1"
suite: trixie
steps:
- name: Checkout
uses: actions/checkout@v4
@ -38,28 +44,40 @@ jobs:
env:
HEADERS_PKG: ${{ env.HEADERS_PKG }}
KERNEL_VERSION: ${{ matrix.kernel_version }}
DEBIAN_SUITE: ${{ matrix.suite }}
run: |
set -eux
# arm64 emulation for the builder image (no-op if already registered).
if [ ! -e /proc/sys/fs/binfmt_misc/qemu-aarch64 ]; then
docker run --privileged --rm tonistiigi/binfmt --install arm64
fi
# Per-version image tag. The matrix entries can run concurrently on a
# single runner sharing one Docker daemon; a fixed tag (e.g. iec-kbuild)
# is then a shared mutable name and the builds race — whichever `docker
# build` finishes last wins the tag, so both `docker create` calls
# resolve to the same image and every job emits the same vermagic.
# A unique tag per kernel version isolates them. Docker tags allow only
# [a-zA-Z0-9._-], so map ':' '/' '+' (all present in 1:6.12.93-1+rpt1)
# to '-'; the tag just needs to be unique and valid, not reversible.
img="iec-kbuild:${KERNEL_VERSION//[:\/+]/-}"
# Builder image: toolchain + matching raspberrypi kernel headers.
docker build --platform linux/arm64 \
--build-arg DEBIAN_SUITE="$DEBIAN_SUITE" \
--build-arg HEADERS_PKG="$HEADERS_PKG" \
--build-arg KERNEL_VERSION="$KERNEL_VERSION" \
-t iec-kbuild .
-t "$img" .
# Compile inside the container. We use `docker cp` instead of the bind
# mount that build-in-docker.sh uses for local builds: under the runner's
# docker-in-docker, /workspace is a volume the host daemon can't see, so
# `-v "$PWD:/build"` mounts an empty dir and make finds no Makefile.
cid=$(docker create --platform linux/arm64 --entrypoint sleep iec-kbuild infinity)
cid=$(docker create --platform linux/arm64 --entrypoint sleep "$img" infinity)
docker start "$cid"
docker cp ./. "$cid:/build"
docker exec "$cid" /usr/local/bin/docker-entrypoint.sh clean
docker exec "$cid" /usr/local/bin/docker-entrypoint.sh
docker cp "$cid:/build/iec_listener.ko" ./iec_listener.ko
docker rm -f "$cid"
docker rmi "$img" || true
- name: Stage build output (modules/iec_listener_<kernel_version>.ko)
working-directory: kernel
@ -81,36 +99,89 @@ jobs:
path: kernel/out
if-no-files-found: error
# Release flow only: merges all built modules into one modules/ folder, wraps it
# in a single repo-named top folder alongside selftest.sh, zips it and attaches
# it to the Gitea release for the pushed tag. Skipped entirely on pull requests.
release:
# Merge all built modules into one modules/ folder, wrap it in a single
# repo-named top folder alongside the helper scripts (selftest.sh, launch.sh)
# and the iecpoc Python frontend (so launch.sh can pip-install it on the Pi).
# Runs on BOTH triggers and uploads the assembled FOLDER as an artifact: on a PR
# this lets you download and smoke-test the exact tree a release would ship; on a
# tag the release job below zips this same tree and attaches it.
#
# We intentionally upload the folder, not a pre-made .zip: artifacts are always
# transported as a zip by the platform, so uploading a .zip would nest one zip
# inside another. Uploading the tree makes the platform's wrapper zip *be* the
# package archive (a single, clean layer).
package:
needs: build
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download all build output
uses: actions/download-artifact@v4
uses: https://github.com/christopherHX/gitea-download-artifact@v4
with:
path: dist
# Merge every artifact back into the shared modules/ folder.
# Merge every per-kernel artifact back into the shared modules/ folder.
merge-multiple: true
- name: Assemble release package
- name: Determine version label
id: ver
run: |
# Tags ship under the tag name; PR builds get a traceable pr<n>-<sha>.
case "${{ github.ref }}" in
refs/tags/*) v="${{ github.ref_name }}" ;;
*) v="pr${{ github.event.pull_request.number }}-$(echo "${{ github.sha }}" | cut -c1-7)" ;;
esac
echo "ver=$v" >> "$GITHUB_OUTPUT"
- name: Assemble package tree
run: |
repo="${{ github.event.repository.name }}"
root="package/$repo"
mkdir -p "$root"
cp -r dist/modules "$root"/ # modules/iec_listener_<kernel_version>.ko
cp kernel/selftest.sh "$root"/ # selftest.sh only at the package root
(cd package && zip -r "../${repo}-${{ github.ref_name }}.zip" "$repo")
cp kernel/selftest.sh "$root"/ # one-shot hardware self-test
cp launch.sh "$root"/ # load module -> run frontend -> unload
# iecpoc Python frontend + its packaging metadata. launch.sh pip-installs
# this on the Pi; the build reads pyproject.toml's readme = "README.md",
# so README.md must travel with it. __pycache__ is stripped to stay clean.
cp pyproject.toml README.md "$root"/
cp -r iecpoc "$root"/
find "$root/iecpoc" -name __pycache__ -type d -prune -exec rm -rf {} +
- name: Upload package
uses: https://github.com/christopherHX/gitea-upload-artifact@v4
with:
name: ${{ github.event.repository.name }}-${{ steps.ver.outputs.ver }}
# Upload the contents of package/, so the artifact zip contains the
# single top folder package/<repo>/ -> <repo>/...
path: package
if-no-files-found: error
# Release flow only: download the assembled tree the package job uploaded, zip it
# into the named release asset and attach it to the Gitea release for the pushed
# tag. No re-assembly here (the tree is taken as-is). Skipped on PRs.
release:
needs: package
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- name: Download package
uses: https://github.com/christopherHX/gitea-download-artifact@v4
with:
# The single artifact the package job produced for this tag; extracts to
# dist/<repo>/...
name: ${{ github.event.repository.name }}-${{ github.ref_name }}
path: dist
- name: Zip release asset
run: |
repo="${{ github.event.repository.name }}"
(cd dist && zip -r "../${repo}-${{ github.ref_name }}.zip" "$repo")
- name: Publish to Gitea release
uses: https://gitea.com/actions/gitea-release-action@v1.3.6
with:
api_key: ${{ secrets.GITEA_TOKEN }}
files: |-
*.zip
${{ github.event.repository.name }}-${{ github.ref_name }}.zip

View File

@ -71,6 +71,39 @@ See [`docs/wiring.md`](docs/wiring.md) for the level-shifter circuit and the
pre-C64 bring-up checklist, and [`docs/kernel-notes.md`](docs/kernel-notes.md)
for the resolved kernel real-time decisions.
## Running the frontend on the Pi
A tagged build publishes a release zip that bundles everything needed to run on
a Pi without building anything:
```
comodore-iec-emu/
selftest.sh one-shot wiring self-test (no C64 connected)
launch.sh load module → run iecpoc → unload
pyproject.toml iecpoc packaging metadata
README.md
iecpoc/ the Python frontend
modules/ iec_listener_<kernel_version>.ko, one per supported kernel
```
Unzip it on the Pi and run **`launch.sh`**. It installs the frontend into a local
`.venv`, detects the running kernel and picks the matching `.ko` (by *vermagic*,
not filename), `insmod`s it, runs `iecpoc`, and **always `rmmod`s on exit** — so
the Pi is left exactly as before:
```bash
unzip comodore-iec-emu-*.zip && cd comodore-iec-emu
sudo ./selftest.sh # first: check the wiring (want 0x1F)
sudo ./launch.sh # then: load + run the frontend (address 4)
sudo ./launch.sh --address 8 # listen as device 8
sudo ./launch.sh -- --raw # forward extra args (after --) to iecpoc
```
The module is picked automatically; pass `--ko PATH` only to force a specific
file. `launch.sh` needs `python3` plus `python3-venv` (and network on first run
for the build backend). See [`kernel/README.md`](kernel/README.md) for the
self-test details and the per-kernel module selection.
## Status
- **Phase 0 (userspace + tests):** complete, runs on any host.

View File

@ -168,14 +168,27 @@ 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
KERNEL_VERSION=1:6.6.51-1+rpt3 ./build-in-docker.sh
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:
@ -183,19 +196,21 @@ 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:
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:** 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.
**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

View File

@ -9,18 +9,38 @@
# docker run --privileged --rm tonistiigi/binfmt --install arm64
#
# Kernel version is configurable via build args:
# --build-arg DEBIAN_SUITE=trixie # Debian/RPi OS suite (see notes)
# --build-arg HEADERS_PKG=linux-headers-rpi-v8 # 64-bit Pi Zero 2 W (default)
# --build-arg KERNEL_VERSION=1:6.6.51-1+rpt3 # optional exact pin (see notes)
#
# DEBIAN_SUITE must match the kernel you target: each raspberrypi archive suite
# only carries its own latest kernel (bookworm -> 6.12.x, trixie -> 6.18.x), so
# pinning a trixie-era KERNEL_VERSION against a bookworm base will fail with
# "Version ... was not found". Keep this in sync with the Pi's release.
#
# Build context is this kernel/ directory.
FROM --platform=linux/arm64 debian:bookworm
ARG DEBIAN_SUITE=bookworm
FROM --platform=linux/arm64 debian:${DEBIAN_SUITE}
# Re-declare after FROM so it is in scope for the RUN below (ARGs before FROM
# are only visible to the FROM line itself).
ARG DEBIAN_SUITE=bookworm
# The raspberrypi kernel/headers live in the raspberrypi.com archive, not Debian.
#
# 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 refuses the repo as "not signed"
# (bookworm's gpgv doesn't enforce this). We mark the repo trusted=yes *only on
# trixie* to bypass that check; bookworm keeps full signature verification. The
# key is still fetched over HTTPS and pinned via signed-by where it is honoured.
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl gnupg \
&& curl -fsSL https://archive.raspberrypi.com/debian/raspberrypi.gpg.key \
| gpg --dearmor -o /usr/share/keyrings/raspberrypi-archive-keyring.gpg \
&& echo "deb [signed-by=/usr/share/keyrings/raspberrypi-archive-keyring.gpg] http://archive.raspberrypi.com/debian/ bookworm main" \
&& opts="signed-by=/usr/share/keyrings/raspberrypi-archive-keyring.gpg" \
&& if [ "$DEBIAN_SUITE" = "trixie" ]; then opts="$opts trusted=yes"; fi \
&& echo "deb [$opts] http://archive.raspberrypi.com/debian/ ${DEBIAN_SUITE} main" \
> /etc/apt/sources.list.d/raspi.list \
&& rm -rf /var/lib/apt/lists/*

View File

@ -20,14 +20,45 @@ 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.
autoload): it detects the running kernel, picks the matching module, loads it,
runs the self-test, and always unloads it again.
A released package ships several kernel builds side by side, plus the launcher
and the Python frontend:
```
comodore-iec-emu/
├── selftest.sh # one-shot wiring self-test (this document)
├── launch.sh # load module → run the iecpoc frontend → unload
├── pyproject.toml # iecpoc packaging metadata (launch.sh pip-installs it)
├── README.md
├── iecpoc/ # the Python userspace decoder/trace
└── modules/
├── iec_listener_1-6.12.93-1+rpt1.ko # built for 6.12.x (bookworm)
└── iec_listener_1-6.18.34-1+rpt1.ko # built for 6.18.x (trixie)
```
`selftest.sh` only verifies the wiring (it loads and immediately unloads). To
actually capture and decode C64 traffic, use **`launch.sh`**, which installs the
frontend, loads the matching module, runs `iecpoc`, and unloads on exit — see the
[top-level README](../README.md#running-the-frontend-on-the-pi).
You don't pick the file yourself: the script reads `uname -r`, then scans
`modules/` (and a few fallback locations) and selects the `.ko` whose **vermagic**
matches the running kernel. Matching is by vermagic rather than filename because
the packaged name carries a Debian epoch/revision (`1:6.12.93-1+rpt1`) that
`uname -r` (`6.12.93+rpt-rpi-v8`) does not.
```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
sudo ./selftest.sh # auto-selects the matching module, address 4
sudo ./selftest.sh --address 5 # same, address 5
sudo ./selftest.sh ~/iec_listener.ko --address 5 # force a specific module file
```
If no module matches the running kernel, the script lists the modules it found
(with the kernel each was built for) and exits — rebuild for the current kernel,
or pass a path explicitly.
It exits non-zero unless the result is a full pass (`0x1F`), so it is usable in
scripts/CI.

View File

@ -9,19 +9,22 @@
# ./build-in-docker.sh clean # clean build artifacts
#
# Configurable via environment variables:
# SUITE Debian/RPi OS suite (default: bookworm; use trixie for 6.18.x)
# HEADERS_PKG headers package (default: linux-headers-rpi-v8, 64-bit Zero 2 W)
# e.g. linux-headers-rpi-v7 / -v6 for 32-bit, raspberrypi-kernel-headers
# KERNEL_VERSION exact version to pin, e.g. 1:6.6.51-1+rpt3 (default: latest in repo)
# IMAGE builder image tag (default: iec-kbuild)
#
# NOTE: the raspberrypi apt archive generally serves only the *latest* kernel in
# its pool, so pinning KERNEL_VERSION to an old release may fail to download. The
# NOTE: each raspberrypi apt archive suite serves only the *latest* kernel in its
# pool (bookworm -> 6.12.x, trixie -> 6.18.x), so SUITE must match the kernel you
# target and pinning KERNEL_VERSION to an old release may fail to download. The
# reliable match strategy is to keep the Pi current (`sudo apt full-upgrade`) and
# build with the default (latest). See docs/kernel-notes.md.
# build with the matching SUITE + default (latest) version. See docs/kernel-notes.md.
set -euo pipefail
cd "$(dirname "$0")"
IMAGE="${IMAGE:-iec-kbuild}"
SUITE="${SUITE:-bookworm}"
HEADERS_PKG="${HEADERS_PKG:-linux-headers-rpi-v8}"
KERNEL_VERSION="${KERNEL_VERSION:-}"
@ -32,8 +35,9 @@ if [ ! -e /proc/sys/fs/binfmt_misc/qemu-aarch64 ]; then
fi
# 2. Build the builder image (installs toolchain + matching kernel headers).
echo "==> building image '$IMAGE' (HEADERS_PKG=$HEADERS_PKG KERNEL_VERSION=${KERNEL_VERSION:-latest})"
echo "==> building image '$IMAGE' (SUITE=$SUITE HEADERS_PKG=$HEADERS_PKG KERNEL_VERSION=${KERNEL_VERSION:-latest})"
docker build --platform linux/arm64 \
--build-arg DEBIAN_SUITE="$SUITE" \
--build-arg HEADERS_PKG="$HEADERS_PKG" \
--build-arg KERNEL_VERSION="$KERNEL_VERSION" \
-t "$IMAGE" .

View File

@ -108,16 +108,26 @@ static void emit_record(u8 kind, u8 value, u8 flags)
/* --- busy-poll helper ---------------------------------------------------- */
/*
* Spin until CLK reaches `want_asserted`, or timeout/ATN/RESET. Caller holds
* local IRQs disabled. Uses a microsecond budget rather than a fixed udelay so
* a slightly-fast C64 is handled correctly (research §1.3).
* Spin until CLK reaches `want_asserted`, or timeout/ATN-change/RESET. Caller
* holds local IRQs disabled. Uses a microsecond budget rather than a fixed
* udelay so a slightly-fast C64 is handled correctly (research §1.3).
*
* The ATN abort is phase-relative, and IEC_RX_ATN means "ATN changed, abort":
* - data_phase: ATN is released; abort if it ASSERTS (C64 interrupts data
* transfer with a new command).
* - command phase: ATN is asserted for the whole command byte; abort if it
* RELEASES (the command sequence is over -> hand back to the state machine).
* Treating any asserted ATN as an abort (the previous behaviour) made command
* reception impossible: ATN is legitimately low the entire command phase, so
* the very first poll aborted before a single bit was clocked.
*/
static enum iec_rx wait_clk(bool want_asserted, unsigned int timeout_us)
static enum iec_rx wait_clk(bool want_asserted, unsigned int timeout_us,
bool data_phase)
{
unsigned int waited = 0;
while (iec_clk_asserted() != want_asserted) {
if (iec_atn_asserted())
if (iec_atn_asserted() == data_phase)
return IEC_RX_ATN;
if (iec_reset_asserted())
return IEC_RX_RESET;
@ -143,7 +153,7 @@ static enum iec_rx receive_byte(u8 *out, bool *eoi, bool data_phase)
*eoi = false;
/* 1. READY-FOR-DATA: wait for talker to release CLK, then release DATA */
rc = wait_clk(false /* released */, IEC_CLK_TIMEOUT_US);
rc = wait_clk(false /* released */, IEC_CLK_TIMEOUT_US, data_phase);
if (rc != IEC_RX_OK)
return rc;
iec_data_release();
@ -171,10 +181,12 @@ static enum iec_rx receive_byte(u8 *out, bool *eoi, bool data_phase)
/* 3. RECEIVE 8 BITS, LSB first */
for (i = 0; i < 8; i++) {
rc = wait_clk(true /* asserted: data invalid/setup */, IEC_CLK_TIMEOUT_US);
rc = wait_clk(true /* asserted: data invalid/setup */,
IEC_CLK_TIMEOUT_US, data_phase);
if (rc != IEC_RX_OK)
goto out;
rc = wait_clk(false /* released: data valid -> sample */, IEC_CLK_TIMEOUT_US);
rc = wait_clk(false /* released: data valid -> sample */,
IEC_CLK_TIMEOUT_US, data_phase);
if (rc != IEC_RX_OK)
goto out;
/* released(high) = bit 1, asserted(low) = bit 0 */

View File

@ -62,13 +62,66 @@ done
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"
command -v modinfo >/dev/null || die "modinfo not found (install kmod)"
# ---- locate the module matching the running kernel ----------------------
# A released package ships one module per supported kernel under modules/.
# The build workflow names each file iec_listener_<kernel_version>.ko, with
# ':' and '/' in the version replaced by '-' (1:6.12.93-1+rpt1 ->
# iec_listener_1-6.12.93-1+rpt1.ko); local builds produce a bare
# iec_listener.ko. The glob below ($MODULE*.ko) matches both.
#
# We select by vermagic, not by filename: the package name carries a Debian
# epoch/revision (1:6.12.93-1+rpt1) that `uname -r` (6.12.93+rpt-rpi-v8) does
# not, so the .ko whose vermagic matches the running kernel is the one to load.
RUNNING="$(uname -r)"
# Where to look, de-duplicated ($(dirname "$0") is often "." when run locally).
SEARCH_DIRS=()
for d in "$(dirname "$0")/modules" "$(dirname "$0")" "./modules" "." "$HOME"; do
skip=0
for s in "${SEARCH_DIRS[@]:-}"; do [ "$s" = "$d" ] && skip=1; done
[ "$skip" = 0 ] && SEARCH_DIRS+=("$d")
done
if [ -z "$KO" ]; then
for cand in "./${MODULE}.ko" "$HOME/${MODULE}.ko" "$(dirname "$0")/${MODULE}.ko"; do
[ -f "$cand" ] && { KO="$cand"; break; }
info "running kernel: $RUNNING"
for dir in "${SEARCH_DIRS[@]}"; do
[ -d "$dir" ] || continue
for cand in "$dir"/${MODULE}*.ko; do
[ -f "$cand" ] || continue
vm="$(modinfo -F vermagic "$cand" 2>/dev/null)" || continue
if [ "${vm%% *}" = "$RUNNING" ]; then
KO="$cand"; break 2
fi
done
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)"
if [ -z "$KO" ]; then
msg="no module matching the running kernel ($RUNNING) found.
Pass a path explicitly (sudo $0 path/to/${MODULE}.ko), or rebuild for
this kernel. Modules seen:"
any=0
for dir in "${SEARCH_DIRS[@]}"; do
[ -d "$dir" ] || continue
for cand in "$dir"/${MODULE}*.ko; do
[ -f "$cand" ] || continue
if vm="$(modinfo -F vermagic "$cand" 2>/dev/null)" && [ -n "$vm" ]; then
note="built for ${vm%% *}"
else
# modinfo couldn't read it - usually not an ELF .ko at all
# (e.g. a still-zipped release file). Show what it really is.
note="UNREADABLE: $(file -b "$cand" 2>/dev/null || echo 'not a kernel module') - unzip/rebuild?"
fi
msg="$msg"$'\n'" $cand ($note)"
any=1
done
done
[ "$any" = 1 ] || msg="$msg"$'\n'" (none)"
die "$msg"
fi
[ -f "$KO" ] || die "module not found: $KO"
info "module: $KO"
info "address: $ADDRESS"

149
launch.sh Executable file
View File

@ -0,0 +1,149 @@
#!/usr/bin/env bash
#
# launch.sh - load the IEC listener module, run the userspace frontend, unload.
#
# This is the "run the whole thing" entry point for a released package. Unlike
# selftest.sh (a one-shot wiring check that loads and immediately unloads), this
# keeps the module loaded for as long as the frontend runs. In order it:
# 1. installs the iecpoc Python frontend into a local virtualenv (.venv),
# 2. detects the running kernel and picks the matching iec_listener_*.ko,
# 3. insmods it (creating /dev/iec0),
# 4. runs `iecpoc`, streaming the decoded C64 traffic to the terminal,
# 5. on exit (Ctrl-C / EOF / error) ALWAYS rmmods again - the Pi is left
# exactly as before.
#
# The module is never left loaded by this script; the EXIT trap unloads it even
# if the frontend crashes or you interrupt it.
#
# Usage:
# sudo ./launch.sh [--address N] [--device PATH] [--ko FILE] [-- <iecpoc args>]
#
# Examples:
# sudo ./launch.sh # address 4, /dev/iec0, annotated trace
# sudo ./launch.sh --address 8 # listen as device 8
# sudo ./launch.sh -- --raw # forward --raw to iecpoc
# sudo ./launch.sh --ko modules/iec_listener_1-6.12.93-1+rpt1.ko
#
# Defaults: address=4 (printer), device=/dev/iec0.
set -euo pipefail
MODULE="iec_listener"
ADDRESS=4
DEVICE="/dev/iec0"
KO=""
VENV=".venv"
LOADED=0
declare -a PASS=()
# Run relative to the script's own directory so modules/, iecpoc/, pyproject.toml
# and .venv resolve the same way whether invoked as ./launch.sh or by full path.
HERE="$(cd "$(dirname "$0")" && pwd)"
cd "$HERE"
# ---- 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 the bus, removes ${DEVICE})"
rmmod "$MODULE" 2>/dev/null && ok "unloaded - Pi left as before" \
|| info "rmmod failed; unload manually: sudo rmmod ${MODULE}"
fi
}
trap cleanup EXIT
# ---- args ---------------------------------------------------------------
# Anything unrecognised (and everything after a literal --) is forwarded to
# iecpoc, so e.g. --raw / --logfile FILE just work.
while [ $# -gt 0 ]; do
case "$1" in
-a|--address) ADDRESS="${2:-}"; shift 2 ;;
--address=*) ADDRESS="${1#*=}"; shift ;;
-d|--device) DEVICE="${2:-}"; shift 2 ;;
--device=*) DEVICE="${1#*=}"; shift ;;
--ko) KO="${2:-}"; shift 2 ;;
--ko=*) KO="${1#*=}"; shift ;;
--) shift; PASS+=("$@"); break ;;
-h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) PASS+=("$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"
command -v modinfo >/dev/null || die "modinfo not found (install kmod)"
command -v python3 >/dev/null || die "python3 not found (sudo apt install python3)"
# ---- 1. python frontend: venv + install (once) --------------------------
# iecpoc declares no third-party runtime deps, so this is offline-safe except
# for the build backend (setuptools), which pip fetches the first time. The
# install is skipped on later runs once the entry point exists.
[ -f "$HERE/pyproject.toml" ] || die "pyproject.toml not found next to $0; package incomplete?"
if [ ! -x "$VENV/bin/iecpoc" ]; then
if [ ! -x "$VENV/bin/python" ]; then
info "creating virtualenv: $VENV"
python3 -m venv "$VENV" || die "could not create venv (sudo apt install python3-venv)"
fi
info "installing iecpoc frontend into $VENV"
"$VENV/bin/pip" install --quiet --disable-pip-version-check . \
|| die "pip install failed (needs network for the build backend, or: sudo apt install python3-pip)"
ok "frontend installed"
fi
# ---- 2. pick the module matching the running kernel ----------------------
# Same vermagic-based selection as selftest.sh: match by what the .ko was built
# against (uname -r), not by filename. See selftest.sh for the full rationale.
RUNNING="$(uname -r)"
if [ -z "$KO" ]; then
info "running kernel: $RUNNING"
for dir in "$HERE/modules" "$HERE" "$HERE/kernel"; do
[ -d "$dir" ] || continue
for cand in "$dir"/${MODULE}*.ko; do
[ -f "$cand" ] || continue
vm="$(modinfo -F vermagic "$cand" 2>/dev/null)" && [ -n "$vm" ] || continue
if [ "${vm%% *}" = "$RUNNING" ]; then KO="$cand"; break 2; fi
done
done
fi
[ -n "$KO" ] && [ -f "$KO" ] || die \
"no module matching the running kernel ($RUNNING) found.
Pass one with --ko PATH, or run ./selftest.sh to list what's available."
VERMAGIC="$(modinfo -F vermagic "$KO" 2>/dev/null)" || die "cannot read vermagic from $KO"
[ "${VERMAGIC%% *}" = "$RUNNING" ] || die \
"kernel mismatch: module built for '${VERMAGIC%% *}' but running '$RUNNING'."
info "module: $KO"
# ---- 3. 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 ${DEVICE} in use?)"
fi
info "insmod ${MODULE} address=${ADDRESS}"
insmod "$KO" address="$ADDRESS" || die "insmod failed; check: dmesg | tail"
LOADED=1
[ -c "$DEVICE" ] || die "${DEVICE} was not created"
ok "loaded; ${DEVICE} present"
# ---- 4. run the frontend (foreground; rmmod runs in the EXIT trap) -------
info "running iecpoc (Ctrl-C to stop) - address ${ADDRESS}, device ${DEVICE}"
echo
set +e
"$VENV/bin/iecpoc" --address "$ADDRESS" --device "$DEVICE" "${PASS[@]}"
ST=$?
set -e
echo
info "frontend exited (status $ST); unloading module"
exit "$ST"