Add planning docs, research notes, and project config

Co-Authored-By: Claude Code
This commit is contained in:
Christian Werner 2026-06-18 03:18:05 +02:00
parent fabee6db40
commit 85936464bd
4 changed files with 1421 additions and 0 deletions

View File

@ -0,0 +1,110 @@
---
name: researcher
description: >-
Use this agent to research a topic on the web using the Playwright MCP browser
plus WebSearch/WebFetch. It scales its effort to the context it is given: a
one-line topic gets a quick scan, a richly specified brief gets a deep,
multi-source investigation. It writes a markdown report to _research/ and
returns a short summary. Invoke for "research X", "find out about Y", "gather
sources on Z", competitive/landscape scans, or background investigation before
a decision.
tools: WebSearch, WebFetch, Read, Write, Glob, mcp__playwright__browser_navigate, mcp__playwright__browser_navigate_back, mcp__playwright__browser_snapshot, mcp__playwright__browser_click, mcp__playwright__browser_type, mcp__playwright__browser_fill_form, mcp__playwright__browser_select_option, mcp__playwright__browser_press_key, mcp__playwright__browser_hover, mcp__playwright__browser_wait_for, mcp__playwright__browser_evaluate, mcp__playwright__browser_take_screenshot, mcp__playwright__browser_tabs, mcp__playwright__browser_close, mcp__playwright__browser_handle_dialog
model: sonnet
---
# Web Research Agent
You research a topic on the live web and produce a sourced markdown report. You
have two ways to read the web:
- **WebSearch / WebFetch** — fast. Use these first to discover sources and skim
static pages (docs, articles, blog posts, READMEs).
- **Playwright MCP browser** — for anything WebFetch can't handle: pages behind
JavaScript rendering, interactive UIs, search results you need to click
through, content that requires navigation, or when you must confirm what a
page actually shows. Always `browser_snapshot` before interacting so you act
on the real DOM, and `browser_close` when done.
## Scale effort to the context you were given
Read the request and pick a tier. The richer and more specific the brief, the
deeper you go. State which tier you chose at the top of your report.
- **Quick scan** — a bare topic or one-line question, no constraints.
~24 sources, WebSearch + WebFetch only, browser only if a key page won't
fetch. A few paragraphs. Aim for a fast, correct answer. A single bare
sentence like "research X" or "the effects of X on Y" is a quick scan **even
when X is interesting** — an inherently rich subject does not raise the tier;
only the caller adding sub-questions, constraints, or named entities does.
- **Standard** — a topic with some framing (a few questions, a goal, "compare
A and B"). ~510 sources, cross-check important claims across at least two,
use the browser where pages are interactive or JS-heavy. Structured report
with sections.
- **Deep dive** — a detailed brief: multiple sub-questions, named entities to
cover, depth/recency requirements, or "be thorough / leave no stone
unturned". 10+ sources, browser-driven exploration of primary sources, follow
citation trails, note disagreements between sources, and call out gaps. Long,
thoroughly sourced report.
When the tier is ambiguous, infer from signal volume rather than asking — more
detail in the prompt means more research. Only escalate or downshift if the
prompt explicitly sets a bound ("just a quick look", "exhaustive review").
## Method
1. Restate the question and list the sub-questions you will answer.
2. Discover sources with WebSearch; prefer primary and authoritative ones.
3. Read with WebFetch first; switch to the browser when a page is dynamic,
gated behind interaction, or when fetched content looks truncated or wrong.
4. Verify load-bearing claims against a second independent source. Distinguish
fact from opinion and note publication dates — flag anything that may be
stale.
5. Track every source URL as you go so citations are accurate.
## Output
Write the report to `_research/<kebab-topic>-<YYYY-MM-DD>.md` (create the
`_research/` directory if needed; today's date is available to you). Use this
structure:
```
# <Topic>
> Research tier: <quick scan | standard | deep dive> · <YYYY-MM-DD>
## Question
<what was asked>
## Summary
<the answer up front 3-6 sentences>
## Findings
<sections per sub-question, with inline [n] citations>
## Open questions / gaps
<what you could not confirm, conflicting info, or what to research next>
## Sources
[1] Title — URL (accessed YYYY-MM-DD)
...
```
## Rules
- **You MUST `Write` the full report to `_research/<kebab-topic>-<YYYY-MM-DD>.md`
before sending your final message.** The `Write` tool creates the `_research/`
directory automatically — just write the path. Producing findings without
writing the file is a FAILED task, no matter how good the research is.
- **Your final message to the caller is NOT the report.** It contains exactly:
(1) the chosen tier, (2) the report file path you just wrote, and (3) a 35
sentence summary of the key findings. Do NOT paste the report body, the
Findings sections, or the full Sources list into the final message — that
content lives only in the file.
- Cite specific URLs for every non-obvious claim. Never invent a source or a
fact — if you couldn't verify it, say so in "Open questions / gaps".
- Browse only public information. Do not log in, submit forms with credentials,
or interact with anything that looks like it requires authorization.
- Do all browser extraction first. As your FINAL browser action, call
`browser_close` to shut the browser down completely — closing tabs alone
leaves the browser process idling on `about:blank`. Never call
`browser_evaluate` or any other browser tool after `browser_close`.

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.idea

View File

@ -0,0 +1,439 @@
# PoC Implementation Plan — Pure IEC Listener Device (Linux kernel module + Python, Raspberry Pi Zero 2 W)
> Status: draft · 2026-06-18
> Grounded in: `_research/commodore-iec-serial-bus-2026-06-18.md`
>
> **Architecture decision:** the timing-critical bit loop and handshake run in a
> **Linux kernel module** (interrupt + busy-poll context, above the scheduler).
> Userspace **Python** handles decoding, PETSCII, debug output, and CLI. The exact
> kernel-module design (char device vs. relay/debugfs, interrupt vs. polled CLK,
> `udelay` budgets) is to be refined after a dedicated research pass on Linux
> kernel GPIO/real-time techniques — see §4 and the "to research" markers.
---
## 1. Goal & Success Criteria
Build a **proof-of-concept Commodore IEC *listener* device** on a Pi Zero 2 W
running Raspberry Pi OS. The timing-critical IEC handshake lives in a **Linux
kernel module**; a **Python** userspace program decodes and debug-prints. The
device behaves like a printer (default primary address **4**): it never talks
back with data, it only **listens**.
**The PoC is successful when, connected to a real C64, it:**
1. **Responds correctly** to the C64 so the C64 never reports `DEVICE NOT PRESENT`
and never hangs:
- Acknowledges ATN within 1 ms (pulls DATA low).
- Performs the full listener byte-receive handshake (ready-for-data, per-bit
sampling, per-byte acknowledge) for every byte the C64 sends.
- Detects and acknowledges EOI on the last byte.
- Releases the bus cleanly on UNLISTEN and on RESET.
2. **Debug-prints, in real time, everything the C64 sends:**
- Each **command byte** received during the ATN phase, decoded symbolically
(e.g. `LISTEN 4`, `OPEN SA=0`, `UNLISTEN`, `CLOSE SA=1`), with raw hex.
- Each **data byte** received in the data phase, as raw hex + PETSCII-decoded
character, with an EOI marker on the final byte.
- State transitions (IDLE → ATN → addressed-listener → receiving → idle).
A typical exercising program on the C64 is:
```basic
OPEN 1,4 : REM open printer at device 4
PRINT#1,"HELLO WORLD"
PRINT#1,"LINE TWO"
CLOSE 1
```
The PoC must log the resulting command sequence and the `"HELLO WORLD"`/`"LINE TWO"`
byte streams, while keeping the C64 happy.
**Out of scope for the PoC:** acting as talker, returning data to the C64,
secondary-address channel semantics beyond logging, real printing/PETSCII
rendering, JiffyDOS/fast loaders, multiple simultaneous devices.
---
## 2. Why "listener-only" is the right PoC
From the research, a pure listener has a radically reduced surface:
| Line | Role in a pure listener | Pi direction |
|-------|-------------------------|--------------|
| ATN | C64 drives; we react | **input** |
| CLK | Talker (C64) drives during data; we sample | **input** |
| DATA | Listener drives (ack ATN, ready, byte-ack, EOI-ack) | **output** (open-collector) |
| RESET | C64 drives; we react | **input** |
| SRQ | unused on stock C64 | not connected |
So we drive **exactly one** bus line (DATA) and read three. No turnaround, no
EOI generation, no 60 µs talker timing. The only timing the device generates is
the 1 ms ATN ack and the per-byte ack — both forgiving. The tight constraint is
*sampling* the C64's ~20 µs bit clock while receiving (see §6).
---
## 3. Hardware
### 3.1 Interface circuit (per the research, simplified for one output)
- **DATA (drive):** open-collector driver. Two equivalent options:
- **7406/74LS06** inverting OC buffer: Pi GPIO → 7406 input → bus. Pi HIGH ⇒ bus
pulled low (asserted). *(Software must invert.)*
- **NPN transistor** (2N3904/BC547): Pi GPIO → 1 kΩ → base; emitter → GND;
collector → DATA line. Pi HIGH ⇒ transistor on ⇒ bus low. Same inversion.
- Use whichever is on hand; the 7406 also gives spare drivers for later (CLK,
talker mode). Recommend the **7406** to match the eventual full device.
- **ATN, CLK, RESET (sense):** resistor divider 5 V→~3.1 V into each Pi input
(e.g. 3.3 kΩ top / 2.2 kΩ to GND). **No inversion** in sensing: bus low (0 V,
asserted) ⇒ Pi reads LOW.
- 100 nF bypass cap across the 7406 VCCGND. Common GND between C64 IEC pin 2 and Pi.
- **Do not connect bus 5 V to any Pi pin directly — Pi GPIO is not 5 V tolerant.**
### 3.2 Pin map (Pi Zero 2 W, 40-pin header)
| IEC signal | Pi GPIO (BCM) | Header pin | Direction | Logic note |
|------------|---------------|------------|-----------|------------|
| ATN | GPIO 2 | 3 | input (divider) | bus low = Pi low = asserted |
| CLK | GPIO 17 | 11 | input (divider) | bus low = Pi low = asserted |
| DATA (in) | GPIO 18 | 12 | output → 7406 → bus | Pi HIGH = bus asserted (inverted) |
| RESET | GPIO 3 | 5 | input (divider) | bus low = Pi low = asserted |
| GND | — | 6 (or any) | — | tie to IEC pin 2 |
> Convention used in code: a helper layer converts electrical reads/writes into
> **logical** `asserted=True / released=False` so the rest of the code reasons in
> the protocol's true/false sense and never juggles the inversions inline.
---
## 4. Timing strategy — kernel module for the bit loop
The research is clear that the ~20 µs data-bit window cannot be met reliably from
Linux userspace (50250 µs scheduler jitter), and that language alone does not
fix this — the scheduler can preempt any userspace process mid-bit. The working
Pi precedents (Ninepin, raspbiec) therefore put the handshake in a **kernel
module**. We adopt that here: the bit-level handshake runs **in kernel context,
above the normal scheduler**, where we control preemption and can busy-wait with
`udelay()` at microsecond resolution.
**What runs in the kernel module (the real-time half):**
- Owns the GPIO lines directly (kernel GPIO/`gpiod` API, no userspace mmap).
- **ATN edge → interrupt** (`request_irq`, falling edge): the ISR pulls DATA low
to satisfy the ≤ 1 ms presence deadline immediately, then drives the receive
state machine.
- The **byte-receive handshake** (§6): busy-poll CLK with `udelay()` waits, sample
DATA per bit, ack each byte, detect+ack EOI. Runs with interrupts/preemption
controlled for the duration of a byte.
- **Just enough command decoding to handle addressing** (LISTEN/UNLISTEN/
TALK/UNTALK + secondary address): the module must know whether *it* is the
addressed listener to decide whether to participate in the following data phase.
- Pushes every received byte — tagged `command` vs `data`, with an `EOI` flag and
a timestamp — up to userspace through a buffer.
**What runs in Python userspace (the non-real-time half):**
- Reads the tagged byte stream from the module and produces all debug output:
symbolic command decode, PETSCII glyphs, EOI markers, state-transition trace.
- Configuration (device address, flags) and CLI.
- No timing responsibility whatsoever.
**Userspace ↔ kernel interface — options to decide during the kernel research
pass (§9 Phase 0.5 / "to research"):**
- A **character device** `/dev/iec0`: `read()` returns tagged byte records;
`ioctl`/sysfs sets the device address. Simple, blocking-friendly. *(Leading
candidate.)*
- Or a **relay/ring buffer** (`relayfs`/`debugfs`) for high-rate, low-overhead
one-way push of trace records.
- Records are fixed-size structs: `{ kind: cmd|data, value: u8, flags: eoi|..., ts_ns }`.
**Deferred to the kernel-module research day (explicitly out of scope for this
revision):**
- Interrupt-driven vs. fully busy-polled CLK; whether to use a threaded IRQ,
a high-res timer, or spin with local IRQs disabled for a byte.
- Exact `udelay`/timeout constants vs. spec (§3 table) and sd2iec's empirical
values; how long is safe to keep preemption/IRQs off.
- Whether to pin the module's work to an isolated core (`isolcpus`) and steer
other IRQs away, à la raspbiec.
- `PREEMPT_RT` vs. stock Raspberry Pi OS kernel; building against the running
kernel headers; module signing / `dtoverlay` for the GPIO pins.
- Robustness of GPIO toggling latency inside the ISR on the BCM2710A1.
> These open items are recorded so the follow-up research has a concrete target
> list; the rest of this plan does **not** depend on resolving them and can be
> built (Python side, host-testable) in parallel.
---
## 5. Software architecture
Two components: a kernel module (real-time half) and a Python program (everything
else), connected by a character device.
```
kernel/
iec_listener.c # the kernel module — owns GPIO, ISR, receive handshake,
# addressing decode, /dev/iec0 char device
iec_listener.h # shared record format (struct iec_record) + ioctl defs
iec_lines.h # logical line layer: inversion + which GPIO per signal;
# atn_asserted(), clk_asserted(), data_in(),
# data_assert()/data_release() (handles 7406 inversion)
Makefile # builds against /lib/modules/$(uname -r)/build
dts/iec-overlay.dts# (to research) pin reservation / pull config overlay
iecpoc/ # Python userspace
__init__.py
device.py # opens /dev/iec0, reads struct iec_record stream, ioctl addr
decode.py # command-byte decoder + PETSCII→ASCII table for debug output
log.py # formats the annotated debug trace (§8)
main.py # CLI: --address (default 4), --raw, --logfile
tests/ # host-runnable, no Pi/kernel needed
test_decode.py # command decoding + PETSCII glyphs
test_device.py # parse a captured /dev/iec0 record stream → expected trace
docs/
wiring.md # the §3 circuit + photos
kernel-notes.md # findings from the kernel-module research day
```
### 5.1 Logical line layer (`iec_lines.h`, in-kernel)
A thin header so the handshake code reasons in protocol **true/false** terms and
never juggles inversions inline (mirrors the original userspace plan, now in C):
- `atn_asserted()` — true when ATN GPIO reads low
- `clk_asserted()` — true when CLK GPIO reads low
- `data_in()` — read DATA line (the talker drives it during bit transfer)
- `data_assert()` / `data_release()` — drive DATA true / float it; encapsulates
the 7406 inversion (assert ⇒ Pi GPIO HIGH)
- `reset_asserted()` — true when RESET GPIO reads low
### 5.2 Userspace record stream (`device.py`)
The module exposes a stream of fixed-size records on `/dev/iec0`:
```c
struct iec_record { // see iec_listener.h
__u8 kind; // 0 = command byte, 1 = data byte, 2 = state event
__u8 value; // the byte (or event code for kind==2)
__u8 flags; // bit0 = EOI, bit1 = addressed-to-us, ...
__u8 _pad;
__u64 ts_ns; // kernel monotonic timestamp
};
```
`device.py` simply `read()`s these and hands them to `decode.py`/`log.py`. This
makes the whole userspace side host-testable by replaying a captured record file.
---
## 6. The listener receive handshake (heart of the PoC — in the kernel module)
Implements the listener side of research §3.2§3.3, **in `iec_listener.c`**.
Receives **one byte**, yields `(value, eoi)` and emits an `iec_record`. Used for
both command bytes (during ATN) and data bytes (ATN released). The only
difference: during ATN we don't EOI; we still run the same loop but EOI won't be
signalled. The pseudocode below is language-agnostic; in the module the waits are
`udelay()`/busy-poll and the preemption/IRQ discipline is per the §4 research items.
```
receive_byte(): // kernel context
# Entry: we are an addressed listener (or in command phase). We currently hold
# DATA asserted (low) from the previous step / ATN ack.
1. READY-FOR-DATA:
wait until CLK released (C64 says "ready to send": CLK high/false)
then DATA_release() # "I am ready for data"
2. EOI DETECT (data phase only):
start timer; poll CLK.
if CLK stays released (not asserted) for >= 200 µs (Tye):
-> EOI for this byte:
DATA_assert() # ack EOI, hold >= 60 µs (Tei)
wait >= 60 µs
DATA_release()
eoi = True
else eoi = False
(During ATN phase, C64 starts the byte well within 200 µs, so eoi stays False.)
3. RECEIVE 8 BITS, LSB first:
for i in 0..7:
busy-poll until CLK asserted (low) # talker: data invalid / setup
busy-poll until CLK released (high) # talker: data VALID -> sample now
bit = NOT data_in_asserted() # released(high)=1, asserted(low)=0
value |= bit << i
4. BYTE ACKNOWLEDGE:
DATA_assert() # within Tf (<=1000 µs) pull DATA low = "got it"
# remain asserted; this also satisfies "not ready" before the next byte's RFD
return (value, eoi)
```
Notes:
- The DATA sense for the bit (step 3) is the line the *C64* (talker) sets; as the
sole listener we are not pulling DATA during bit sampling, so reading it is valid.
- Step 3 is the 20 µs-window-critical part → this is precisely why it lives in the
kernel module (§4). All other steps are millisecond-forgiving.
- No printing/logging in kernel context. Each received byte is pushed as an
`iec_record` to the `/dev/iec0` buffer; userspace formats it (§5.2, §8).
---
## 7. State machine (in the kernel module)
Implements research §4, listener subset only. Runs in `iec_listener.c`. The
addressing decode here is the part that *must* be in-kernel (it decides whether we
participate in the data phase); the richer symbolic decode for display stays in
userspace. Each received byte and each state transition is emitted as an
`iec_record` for userspace to log.
```
IDLE:
DATA_release(); CLK is input. Busy-poll ATN (and RESET).
ATN asserted -> ATN_ACK
ATN_ACK (<=1 ms):
DATA_assert() # within 1000 µs — answers presence check
-> RECEIVE_COMMAND
RECEIVE_COMMAND: # ATN still low
while ATN asserted:
(b, _) = receive_byte()
emit_record(kind=command, value=b) # userspace decodes + prints
decode (addressing only):
$20..$3E LISTEN n :
addressed_listener = (n == MY_ADDRESS)
$3F UNLISTEN : addressed_listener = False
$40..$5E TALK n : log only (we never talk); addressed_talker=False
$5F UNTALK : ignore
$60..$6F SECOND/OPEN-channel (after LISTEN): record current_sa
$E0..$EF CLOSE sa : log; mark channel closed
$F0..$FF OPEN sa : log; expect filename/data bytes in data phase
other : ignore (not ours)
ATN released ->
if addressed_listener: LISTENER
else: IDLE # someone else's business
LISTENER: # ATN high, data phase
loop:
if ATN asserted: -> RECEIVE_COMMAND # C64 interrupts with new command
(b, eoi) = receive_byte()
emit_record(kind=data, value=b, eoi=eoi) # userspace: hex + PETSCII + EOI
if eoi: # last byte of this PRINT#/stream; stay listening for next command
continue
# exit on ATN (new command) or RESET
ANY STATE, RESET asserted:
DATA_release(); reset all flags; -> IDLE
```
Address matching: `MY_ADDRESS` defaults to 4 (printer), CLI-overridable.
---
## 8. Debug output (a primary deliverable)
`decode.py` + `log.py` produce a readable trace. Example for the §1 BASIC program:
```
[IDLE] waiting for ATN
[ATN] asserted -> DATA low (ack)
[CMD] $24 LISTEN 4 (addressed: ME)
[CMD] $F0 OPEN SA=0
[ATN] released -> LISTENER
[DATA] $48 'H'
[DATA] $45 'E'
[DATA] $4C 'L'
...
[DATA] $44 'D' <EOI> (end of PRINT# record)
[ATN] asserted -> command phase
[CMD] $3F UNLISTEN
...
[CMD] $24 LISTEN 4
[CMD] $E0 CLOSE SA=0
[ATN] released -> not addressed -> IDLE
```
Requirements:
- Command bytes: raw `$hex` + mnemonic + decoded primary/secondary address +
whether it targets us.
- Data bytes: raw `$hex` + PETSCII glyph (printable) or `.`/`<ctrl>` otherwise +
`<EOI>` marker on EOI byte.
- Logging runs in the **userspace Python process**, fully decoupled from the
bit-critical kernel path; the per-record `ts_ns` (set in-kernel) lets us spot
dropped/garbled bytes and inter-byte gaps.
- A `--raw` flag dumps a hex stream only; default is the annotated trace.
---
## 9. Phased build & milestones
**Phase 0 — host-side userspace scaffolding (no Pi/kernel needed)**
- `decode.py`, `log.py`, PETSCII table, `device.py` record parser.
- Define `struct iec_record` (in `iec_listener.h`) and a captured-record file
format; unit tests that replay a scripted record stream → expected §8 trace.
- Exit: `pytest` green for decoding + trace formatting from a scripted stream.
**Phase 0.5 — kernel-module research day (separate, see §4 "deferred" list)**
- Resolve: interrupt vs. polled CLK, `udelay` budgets, preemption discipline,
isolcpus/IRQ steering, `PREEMPT_RT` vs. stock, char-dev vs. relayfs, building
against kernel headers, GPIO `dtoverlay`/pull config.
- Output: `docs/kernel-notes.md` and a refinement of §4/§6/§7 constants.
**Phase 1 — kernel module bring-up: presence + command phase**
- `iec_listener.c` skeleton: GPIO claim, `iec_lines.h`, ATN IRQ that pulls DATA
(≤ 1 ms ack), `RECEIVE_COMMAND`, `/dev/iec0` emitting command records.
- Wire DATA-drive + ATN/CLK/RESET sense (§3); verify lines with a meter/LED and a
module `selftest` ioctl before connecting the C64.
- Test on a real C64: confirm `OPEN 1,4` no longer says `DEVICE NOT PRESENT`, and
command bytes (`LISTEN 4`, `OPEN`) appear in the Python trace. (Command-phase
timing is forgiving — good first kernel milestone.)
- Exit: C64 `OPEN 1,4 : CLOSE 1` → correct, hang-free command trace via Python.
**Phase 2 — data-phase reception (the hard part)**
- Implement the §6 bit loop in-kernel with the Phase-0.5 timing discipline.
- Test `PRINT#1,"HELLO WORLD"` and progressively longer strings; verify every
byte and the EOI marker against the trace. Use a logic analyzer if available.
- Measure error rate over many repetitions; characterize any garbling.
- Exit: multi-line `PRINT#` reliably (>~99% of runs) byte-perfect with correct
EOI; C64 never hangs.
**Phase 3 — robustness & polish**
- RESET handling, ATN-interrupts-mid-stream, clean re-arm between transfers,
graceful module unload (release GPIO/DATA).
- Python CLI (`--address`, `--raw`, log file), README, `docs/wiring.md`.
- Stress test: rapid repeated OPEN/PRINT/CLOSE; long sessions.
- Exit: stable over a 10-minute interactive C64 session.
---
## 10. Risks & fallbacks
| Risk | Likelihood | Fallback |
|------|-----------|----------|
| Even in-kernel, busy-poll mid-byte gets preempted/IRQ'd and garbles a bit | Medium | Disable local IRQs/preemption for the duration of a byte; if still marginal, isolcpus + steer other IRQs away (raspbiec pattern); last resort `PREEMPT_RT`. Quantify in Phase 0.5. |
| Keeping IRQs off too long for a byte harms system stability | Medium | Bound the off-window to one byte (~few hundred µs); re-enable between bytes. A design constraint to validate in Phase 0.5. |
| Kernel module build/ABI churn against Raspberry Pi OS kernel | Medium | Build against installed kernel headers; pin kernel version; document in `kernel-notes.md`. |
| 7406 inversion / divider miswire | Medium | `iec_lines.h` logical layer isolates it; module `selftest` ioctl toggles DATA and reads ATN/CLK with the C64 off. |
| C64 has JiffyDOS | Low (printer rarely uses it) | Out of scope; document. Standard protocol still works for a printer device. |
| ATN response missed | Low | Hardware IRQ pulls DATA in the ISR; 1 ms budget is ample for kernel interrupt latency. |
**Honest expectation:** Phase 0 (Python) and Phase 1 (kernel command phase) are
low-risk. Phase 2's 20 µs data sampling is the genuine experiment — but running
it in-kernel is the approach the working Pi precedents use, so the odds are good
once the Phase-0.5 timing discipline is settled. The Python/kernel split keeps all
display logic host-testable regardless of how the kernel timing shakes out.
---
## 11. References
- Protocol/timing/electrical: `_research/commodore-iec-serial-bus-2026-06-18.md`
(esp. §3 byte transfer, §4 state machine, §5 Pi notes).
- Code references for the kernel-module bit loop (study first in Phase 0.5):
**FozzTexx/ninepin** and **Flogistoni/raspbiec** — both are Linux kernel
modules driving the IEC handshake on a Pi (closest precedent to this plan).
Also sd2iec `src/iec.c` (clean handshake + empirical timing constants) and
dhansel/IECDevice (readable C++ ATN/critical-path handling).

View File

@ -0,0 +1,871 @@
# Commodore 64 IEC Serial Bus — Foundational Protocol Reference
> Research tier: deep dive · 2026-06-18
---
## Question
Complete, precise understanding of the standard Commodore serial IEC bus for a
device emulator running on a Raspberry Pi Zero 2 W. Covers physical/electrical
layer, signaling roles, byte-transfer handshake with full timing, protocol
state machine, and practical implementation notes for 3.3 V GPIO bit-banging.
---
## Summary
The Commodore IEC serial bus is a 5 V open-collector three-wire protocol (ATN,
CLK, DATA) with a 6-pin DIN connector. Logic is inverted: a line is "true"
(asserted) when it is pulled to 0 V and "false" when it floats to 5 V via a
1 kΩ pull-up. Bytes are sent LSB-first with a software handshake; the minimum
clock half-period is 60 µs when a device is the talker. A Raspberry Pi Zero 2 W
running stock Linux cannot reliably meet those timings from userspace due to
kernel scheduling jitter; practical implementations either use a Linux kernel
module, a PREEMPT_RT kernel with CPU isolation, or (for cycle-exact emulation)
a bare-metal approach. All existing successful Pi implementations (Pi1541, Ninepin,
raspbiec) use either bare metal or a kernel module to handle the µs-level
handshake. Level shifting is mandatory: the bus is 5 V and Pi GPIO pins are
3.3 V-only; the recommended approach is a 7406/74LS06 open-collector hex-inverter
on the drive side (matching original Commodore hardware) plus a resistor divider
or buffer for sensing.
---
## Findings
### 1. Physical and Electrical Layer
#### 1.1 Connector and Pinout
The IEC bus uses a **6-pin DIN 45322** female connector on the computer and on
each peripheral. Cables daisy-chain from the C64 to the first device, then from
device to device (the connectors are paired: an IN socket and a pass-through OUT
socket on each peripheral).
| Pin | Signal | Active level | Direction (from C64) | Function |
|-----|----------|--------------|----------------------|----------|
| 1 | /SRQ | Low | IN (device → host) | Service Request — unused by C64 Kernal; on C128 repurposed as Fast Serial CLK |
| 2 | GND | — | — | Ground reference |
| 3 | /ATN | Low | OUT (host → all) | Attention — host-only; asserted to start command phase |
| 4 | /CLK | Low | Bidirectional | Clock / handshake (talker drives it during data phase) |
| 5 | /DATA | Low | Bidirectional | Serial data and handshake |
| 6 | /RESET | Low | OUT (host → all) | Bus reset — also connected to device CPU reset circuits |
The leading `/` reflects the active-low convention.
Sources: [1][3][6]
#### 1.2 Open-Collector / Wired-AND Logic
All signal lines except GND are **open-collector** (open-drain). No device ever
drives a line high. Instead, each line is pulled up to +5 V through a **1 kΩ
resistor** inside the C64 (and in each disk drive, where an on-board 1 kΩ may
be present). Any device—or the host—can pull a line to 0 V. The logical result
is a wired-AND: the line reads 0 V (true/asserted) if *any* participant pulls it
low; the line returns to 5 V (false/released) only when *every* participant
releases it. [2][6]
**Critical inversion convention:**
```
Electrical 0 V = logical TRUE = "asserted" / "line is held"
Electrical 5 V = logical FALSE = "released" / "line is free"
```
This is the opposite of most modern logic conventions. All timing descriptions
in Commodore documentation use the logical sense (true/false), not the voltage.
Keeping this straight is essential for implementation. [2][4]
#### 1.3 Voltage Levels and Electrical Characteristics
| Parameter | Value |
|-----------|-------|
| Bus supply / pull-up rail | +5 V |
| Pull-up resistor (C64 internal) | 1 kΩ to +5 V |
| TTL output driver (Commodore hardware) | 7406 / 74LS06 hex inverting open-collector buffer |
| Max sink current per 7406 output | ~40 mA (VOL ≤ 0.4 V at 40 mA) |
| Normal sink current per device | ~5 mA per active line |
| Approximate practical device limit due to current | ~8 devices; spec says up to 31 |
| Signal cable max length | 1.8 m (6 ft) shielded |
| Logic family | 5 V TTL |
The C64 drives CLK, DATA, and ATN through **7406 open-collector inverters** with
internal pull-ups [6][8]. Peripheral drives (1541) use the same chip. Because
the output is an inverter, software writes a *logical 1* to drive the line *low*
on the bus — another inversion that must be tracked in firmware.
#### 1.4 Level Shifting for Raspberry Pi Zero 2 W
**The problem:** Pi GPIO pins are 3.3 V logic and are **NOT 5 V tolerant**. The
IEC bus idles at +5 V. Directly connecting the bus to a Pi GPIO will destroy the
GPIO immediately or over time. [9]
**The recommended solution — 7406-based driver (matches Pi1541 Option B):**
```
+5V
|
1kΩ (pull-up already on IEC bus inside C64 or drive)
|
IEC bus line ────────────┤───── to other devices
|
7406 output (open-collector) ──── 7406 input ◄──── Pi GPIO (output)
(inverts: Pi HIGH → line low)
For sensing (Pi reads bus):
IEC bus line ─── voltage divider ─── Pi GPIO (input)
5V──[3.3kΩ]──┬──[1.8kΩ]──GND
└── Pi GPIO
(divides 5V→~3.1V; a low on bus passes through as low; high gives ~3.1V ≈ 3.3V)
```
**Practical Pi-to-IEC interface approaches (in order of recommendation):**
1. **7406 output driver + resistor-divider input** — This exactly mirrors how
original Commodore hardware works. Pi GPIO writes to the 7406 input (3.3 V
logic compatible), which produces an inverted open-collector output at 5 V.
A resistor divider (e.g. 3.3 kΩ / 1.8 kΩ, or 1 kΩ / 2 kΩ) scales the 5 V
bus to ≤ 3.3 V for the Pi input pin. Used by Pi1541 Option B. [9][10]
2. **Bidirectional level-shifter module (BSS138 FET-based)** — SparkFun-style
I2C level converters work for a single peripheral. Fail when multiple devices
are on the bus because each FET shunts 5 V pull-up current through the Pi pin
(risk of damage with 3+ devices). This is Pi1541 Option A, flagged as risky.
[9][10]
3. **74HCT245 / CD4050 + separate TX/RX pins** — Use 74HCT245 as a unidirectional
buffer for output (5 V tolerant input, 5 V OC compatible), and a CD4050 or
resistor divider for input. This is the "5-wire" mode used by Ninepin. [13]
4. **Pure resistor voltage divider for sensing only; transistor (2N3904 / BC547)
for driving** — NPN transistor: base driven by Pi GPIO through 1 kΩ, emitter
to GND, collector to IEC line + pull-up. When Pi drives base HIGH, transistor
turns on and pulls IEC line low. Simple, cheap, and electrically equivalent to
an open-collector output. [9]
**ATN is host-only (output from C64, input to device):** The device only needs
to *read* ATN, never drive it. A simple resistor divider for ATN sensing suffices.
**RESET is also host-only:** The device should monitor RESET and react (re-initialize
internal state). It does not need to drive RESET back. A resistor divider input
suffices.
**SRQ is unused on standard C64:** Can be left unconnected for a standard IEC
device emulator. [5][7]
**Important note on Pi GPIO current limits:** The RP Zero 2 W GPIO can sink
about 16 mA per pin (absolute max). With a 1 kΩ pull-up to 5 V, a low on the
bus demands ~5 mA, which is within spec. However, multiple bus pull-ups in
parallel can add up; the 7406 approach avoids stressing Pi GPIO entirely.
---
### 2. Bus Signaling Roles and States
#### 2.1 Participants
| Role | Who | Description |
|------|-----|-------------|
| **Controller** | C64 (always) | Drives ATN; initiates and terminates all command sequences; cannot be a device |
| **Talker** | One device at a time | Sends byte stream on DATA/CLK; designated by TALK command |
| **Listener** | One or more devices | Receives byte stream; designated by LISTEN command |
Multiple listeners can receive simultaneously; only one talker is allowed. [3][6]
#### 2.2 ATN Line Behavior
The /ATN line is driven **exclusively by the C64** (host/controller). Devices
only read it and react.
- ATN asserted (line low): all devices must become receivers within **≤ 1000 µs**
by pulling DATA low. Failure to meet this deadline causes the C64 Kernal to
report "DEVICE NOT PRESENT". [1][3][12]
- During ATN: the C64 sends command bytes (using the normal CLK/DATA handshake)
that are addressed to all devices. Every device listens and decodes commands.
- ATN released (line high): signals end of command phase. Devices that were
designated talker or listener take up those roles.
#### 2.3 Device Addresses (Primary)
Primary addresses are **5-bit values, 030**. Address 31 is reserved (broadcast
UNTALK/UNLISTEN). Addresses 03 are reserved for the C64's internal use. [3][6]
| Range | Conventional assignment |
|-------|------------------------|
| 03 | Internal (C64) — not addressable externally |
| 45 | Printers |
| 67 | Plotters |
| 811 | Disk drives (8 = first, 9 = second, …) |
| 1230 | Third-party / user devices |
#### 2.4 Command Byte Structure
Commands are one byte each, sent during ATN phase:
| Command | Byte value | Effect |
|---------|------------|--------|
| LISTEN | `$20` + PA | Device at primary address PA becomes a listener |
| UNLISTEN| `$3F` | All devices stop listening |
| TALK | `$40` + PA | Device at PA becomes the talker |
| UNTALK | `$5F` | All devices stop talking |
| SECOND (reopen) | `$60` + SA | Secondary address SA; sent after LISTEN or TALK |
| CLOSE | `$E0` + SA | Close channel SA (sent after LISTEN) |
| OPEN | `$F0` + SA | Open/name channel SA (sent after LISTEN; filename bytes follow in data phase) |
PA = primary address (030). SA = secondary address (015 for OPEN/CLOSE; 031
for SECOND). [3][6][7]
**Secondary addresses (SA)** specify a sub-channel within a device:
- Disk drive ch 0: program channel
- Disk drive ch 1: sequentially accessed file
- Disk drive ch 214: additional data channels
- Disk drive ch 15: command / error channel (always open)
#### 2.5 Typical Operation Sequences
**Loading a file from drive 8:**
```
ATN asserted
$28 (LISTEN 8)
$F0 (OPEN ch 0) + filename bytes in data phase
ATN released
[data bytes — filename — transferred to drive]
ATN asserted
$3F (UNLISTEN)
$48 (TALK 8)
$60 (SECOND ch 0)
ATN released ← talk-attention turnaround happens here
[data bytes — file content — transferred from drive to C64]
ATN asserted
$5F (UNTALK)
ATN released
```
**Sending a command to the drive command channel (ch 15):**
```
ATN asserted
$28 (LISTEN 8)
$6F (SECOND ch 15)
ATN released
[command string bytes, e.g. "S:file"]
ATN asserted
$3F (UNLISTEN)
ATN released
```
---
### 3. Byte Transfer Protocol — Full Timing-Accurate Description
This is the heart of the IEC protocol. All byte transfers — both during ATN
(commands) and in data phase — use the same handshake. The **talker** (sender)
controls CLK. The **listener(s)** (receivers) control DATA between bytes.
Bits go out **LSB first** (bit 0, bit 1, …, bit 7). [1][2][3]
#### 3.1 Timing Parameter Reference Table
Sources: [2][6][4] (cross-referenced). Note: the C64 Programmer's Reference
Guide and Jan Derogee's "IEC Dissected" both provide these values; they agree
closely. Where sources differ, both values are shown.
| Name | Description | Min | Typ | Max | Notes |
|------|-------------|-----|-----|-----|-------|
| **Tat** | ATN response time — device must pull DATA after ATN asserted | — | — | 1000 µs | Miss = "DEVICE NOT PRESENT" |
| **Tne** | Non-EOI response to RFD (talker waits after receivers ready before starting bits) | 40 µs | — | 200 µs | No minimum in PRG spec; ≥40 µs is safe |
| **Ts** | Bit set-up time (talker holds CLK low = data invalid, before placing bit) | 20 µs | — | — | **60 µs minimum for external (device) talker** due to C64 video DMA |
| **Tv** | Data valid time (CLK released = high; receiver must sample DATA here) | 20 µs | — | — | **60 µs minimum for external talker** |
| **Tf** | Frame handshake (receiver must pull DATA within this time after all 8 bits sent) | 0 µs | 20 µs | 1000 µs | Timeout = frame error |
| **Tbb** | Between-bytes time (talker must hold CLK true after frame ack before releasing for next byte) | 100 µs | — | — | Gives listener time to re-arm |
| **Tye** | EOI response time (listener waits after CLK released before deciding it's EOI) | 200 µs | — | — | ≥200 µs without CLK going true = EOI signal |
| **Tei** | EOI response hold (listener pulls DATA low for this long to ack EOI) | 60 µs | — | — | |
| **Try** | Talker response after EOI ack (CLK goes true this long after DATA released by ack-er) | 0 µs | 30 µs | 60 µs | |
| **Tpr** | Byte-acknowledge hold (after last bit, receiver pulls DATA true for this long to ack byte) | 20 µs | 30 µs | — | **60 µs if device is talker** |
| **Ttk** | Talk-attention turnaround: CLK release to new sender pulling CLK | 20 µs | 30 µs | 100 µs | |
| **Tdc** | Talk-attention acknowledge: new receiver must pull DATA after ATN release | 0 µs | — | — | Immediate |
| **Tda** | Talk-attention ack hold (new receiver holds DATA while new talker asserts CLK) | 80 µs | — | — | |
| **Tfr** | EOI acknowledge (listener holds DATA low when acking EOI) | 60 µs | — | — | Same as Tei |
**Key rule for device-as-talker:** Both Ts and Tv must be **≥ 60 µs** because
the C64's 6567/6569 VIC-II chip halts the CPU for ~40 µs every ~500 µs during
video DMA ("bad lines"). If the device sends bits faster, the C64 misses them.
For the C64 sending to the device (device as listener), the C64 uses 20 µs
timing; devices must sample fast enough to catch 20 µs pulses. [2][4]
#### 3.2 Normal Byte Transfer Sequence (Non-EOI)
```
LEGEND: CLK: 1 = line true = pulled low (0V)
0 = line false = released (5V)
DATA: 1 = line true = pulled low (0V)
0 = line false = released (5V)
State CLK DATA Who acts Notes
─────────────────────────────────────────────────────────────────────
0. Initial: 1 1 Talker holds CLK Receiver holds DATA
(between bytes, talker holds CLK true after previous byte's ACK)
1. READY-TO-SEND: 0 1 Talker releases CLK "I am ready to send"
No time limit — talker may wait as long as needed here.
2. READY-FOR-DATA:0 0 Listener(s) release DATA one by one.
Transmission cannot start until DATA = 0 (all released).
↑ This is where the talker watches for EOI (see §3.3).
If DATA stays 0 for ≥ Tne (40200 µs) with no EOI signaled,
talker proceeds to step 3.
3. BIT 0 — DATA INVALID:
1 bit0 Talker pulls CLK true (data now invalid);
Hold ≥ Ts (60 µs for device talker; 20 µs for C64-as-talker).
Simultaneously places bit 0 on DATA line (1=low, 0=high).
4. BIT 0 — DATA VALID:
0 bit0 Talker releases CLK (data now valid).
Hold ≥ Tv (60 µs device talker; 20 µs C64 talker).
← Receiver samples DATA while CLK = 0.
Repeat steps 34 for bits 1 through 7 (total 8 bits).
5. END OF BYTE:
1 0 Talker pulls CLK true; releases DATA to 0.
Receiver must pull DATA true (1 = low) within Tf (max 1000 µs).
Talker holds CLK true until it sees DATA = 1 (the ACK).
6. FRAME ACKNOWLEDGE:
1 1 Receiver has pulled DATA true within 1000 µs.
Talker holds CLK for ≥ Tbb (100 µs) before releasing it for next byte.
7. NEXT BYTE → return to step 1.
```
ASCII waveform sketch for one byte (simplified, not to scale):
```
┌────Byte ready──────────────────────────────────────────────────────────────┐
CLK │ (true=low in electrical terms; drawn high for readability of logical state) │
│ 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 │
│ 1 ACK │
DATA │ 1 b0 b0 b1 b1 b2 b2 ... b7 b7 0 1 │
│ (1=held true by receiver initially; each bit placed here while CLK=1/low) │
Phase: │ RTS RFD |<─── bit 0 ────>| <─── bit 1 ────> ... <─── bit 7 ────>| ACK │
│ wait │ inval │ valid │ │ │
```
#### 3.3 EOI (End Or Identify) Handshake
EOI signals the **last byte** in a stream. It is implemented as a **timing
sidechannel** — no extra wire, no extra bit.
**How the talker signals EOI:**
At step 2 (Ready-for-Data), after all receivers have released DATA (DATA = 0),
the talker **deliberately delays** before starting bit 0. Specifically, the
talker waits **≥ Tye = 200 µs** before pulling CLK true.
**How the receiver detects EOI:**
After the receiver releases DATA (step 2) and DATA goes 0, the receiver starts
a timer. If CLK does not go true (i.e., the talker does not start the bit)
within **Tye (200 µs)**, the receiver knows this is the last byte.
**EOI acknowledgment sequence:**
```
1. Receiver detects ≥ 200 µs without CLK going true → sets EOI flag internally.
2. Receiver pulls DATA true (1 = low) for ≥ Tei = 60 µs.
3. Receiver releases DATA to false (0 = high).
4. Talker sees DATA pulse, waits Try (060 µs) then proceeds with the byte normally.
(The byte is still sent normally through steps 37 above.)
5. After receiving the last byte (step 6 ACK), receiver knows the stream is done.
```
If DATA remains at 0 for **≥ 512 µs** after step 2 and no byte begins, this is
an empty-stream / timeout condition (no data at all). [2][4]
#### 3.4 Talk-Attention Turnaround
When the C64 sends a TALK command and then releases ATN, **roles must reverse**:
the C64 goes from being the ATN-sender to becoming a listener, and the designated
device goes from listener to talker. This requires a coordinated bus state
transition:
```
During ATN phase (C64 as sender of commands):
C64 holds CLK true; device(s) hold DATA true (they are receivers).
TURNAROUND sequence after TALK command, when ATN is released:
Step A: C64 releases CLK to false (0) while keeping ATN released.
Step B: C64 pulls DATA true (1) — C64 is now in listener position.
Step C: Device sees CLK = false, DATA = true:
Device pulls CLK true (1) — device is asserting its talker position.
Device releases DATA to false (0).
State: CLK = 1 (device holds it), DATA = 0 (device released) → correct initial
talker state for device to begin sending bytes.
Timing:
Ttk = 20100 µs: how long after ATN release before device must pull CLK.
Tdc = 0 µs minimum: C64 pulls DATA immediately after ATN release.
Tda = 80 µs minimum: C64 must hold DATA true while device asserts CLK.
```
After turnaround, the device is the talker (controlling CLK) and the C64 is the
listener. The device then begins the normal byte-send sequence (step 1 above).
[2][3][6]
---
### 4. Protocol Flow / Device State Machine
The following describes what a **device emulator** must implement. The C64
always initiates; the device reacts.
#### 4.1 ASCII State Diagram
```
┌───────────────────────────────┐
│ IDLE │
│ CLK = 0, DATA = 0 (released) │
└──────────────┬────────────────┘
│ ATN asserted (line low)
┌───────────────────────────────┐
│ ATN_RESPONSE │
│ Pull DATA low within 1000 µs │◄──────────────────────┐
└──────────────┬────────────────┘ │
│ DATA asserted; receive command byte(s) │
▼ │
┌───────────────────────────────┐ │
│ RECEIVE_COMMAND │ │
│ Receive byte via CLK/DATA │ │
│ (standard handshake, no EOI) │ │
└──────────────┬────────────────┘ │
│ Decode command byte │
┌──────────────────┼──────────────────┐ │
│ │ │ │
LISTEN+addr TALK+addr UNLISTEN / │
for me? for me? UNTALK / other │
│ │ │ │
▼ ▼ └─── ignore ─────────┘
┌─────────────────┐ ┌─────────────┐
│ LISTEN_SETUP │ │ TALK_SETUP │
│ wait for SA │ │ wait for SA │ (SA = secondary address byte)
└────────┬────────┘ └──────┬──────┘
│ ATN released │ ATN released → TURNAROUND
▼ ▼
┌─────────────────┐ ┌──────────────────────────┐
│ LISTENER │ │ TALKER │
│ Receive bytes │ │ Send bytes until │
│ via handshake │ │ UNTALK or ATN asserted │
│ ACK each byte │ │ Signal EOI on last byte │
└────────┬────────┘ └──────────────────────────┘
│ UNLISTEN or ATN asserted
┌─────────────────┐
│ UNLISTEN │
│ Release DATA │
└────────┬────────┘
└──────────────────────────────────────► IDLE
```
#### 4.2 Device Behavior on Each State
**IDLE:**
- Device releases CLK and DATA (both = 0 / false electrically).
- Monitor ATN line continuously.
**ATN_RESPONSE (≤ 1 ms critical deadline):**
- ATN goes low (true) → device *immediately* (within 1000 µs) pulls DATA low.
- This is the most timing-critical deadline. Missing it = "DEVICE NOT PRESENT".
- In a Linux implementation: use an interrupt handler, not polling.
**RECEIVE_COMMAND:**
- Device is a receiver. Receive bytes using the standard handshake.
- ATN is held low throughout the command phase; no EOI is sent during ATN.
- Decode each byte. Most bytes go to all devices:
- If byte = `$20``$3E` (LISTEN+addr): note the address; if it matches ours,
set "I am addressed as listener" flag.
- If byte = `$3F` (UNLISTEN): clear listener flag.
- If byte = `$40``$5E` (TALK+addr): note; if address matches, set "I am
addressed as talker" flag.
- If byte = `$5F` (UNTALK): clear talker flag.
- If addressed, next byte will be secondary address (`$60`/`$E0`/`$F0` + SA).
**ATN released (line goes high):**
- If we were told to TALK: perform turnaround (see §3.4), then enter TALKER state.
- If we were told to LISTEN (or OPEN/CLOSE): enter LISTENER state.
- If neither: return to IDLE.
**LISTENER:**
- Receive bytes using standard handshake; ACK each byte.
- Watch for EOI (timing sidechannel at step 2).
- Watch for ATN assertion (new command, may interrupt transfer mid-stream).
**TALKER:**
- Send bytes one at a time using standard handshake.
- On the last byte: use EOI timing (delay ≥ 200 µs before first bit).
- Wait for listener ACK after each byte.
- Watch for ATN assertion — immediately stop sending, become receiver of commands.
#### 4.3 RESET Line Behavior
The /RESET line is driven low by the C64 when the user presses RESTORE+RUN/STOP
or on power-on. Devices should:
1. Monitor RESET (input).
2. On RESET asserted (low): immediately release CLK and DATA, re-initialize all
internal state (clear channel state, close files, return to IDLE).
3. After RESET released (high): return to IDLE and await ATN.
The 1541 uses the RESET pulse from the IEC bus to reset its own 6502 CPU via
a 74LS14 Schmitt trigger and 7406 conditioner circuit. An emulator should perform
the equivalent in firmware (re-initialize state machine). [11]
#### 4.4 Device Not Present Detection
From the C64 Kernal's perspective:
- **LISTEN path:** After ATN is released following a LISTEN command, the C64 watches
DATA. If no device acknowledged ATN (DATA never went low within 1000 µs), the
Kernal detects "DEVICE NOT PRESENT" and can report an error without hanging.
- **TALK path:** The C64 sends TALK + SA, releases ATN, and waits for the device
to perform the turnaround (pull CLK, release DATA). If this never happens, the
Kernal hangs indefinitely (a notorious C64 bug). Therefore **a device emulator
must always respond to ATN within 1000 µs**, even if it intends to do nothing
useful — a response prevents the hang. [12]
---
### 5. Practical Implementation Notes for Raspberry Pi Zero 2 W
#### 5.1 The Timing Challenge
The tightest IEC timings are:
| Deadline | Value | Nature |
|----------|-------|--------|
| ATN response (pull DATA) | 1000 µs (1 ms) | Hard deadline; miss = device not present |
| Bit-valid sample window | 2060 µs | Must read DATA while CLK is released |
| Bit set-up (device talker, Ts) | 60 µs min | Must hold CLK low before each bit |
| Bit valid (device talker, Tv) | 60 µs min | Must hold CLK released while DATA is valid |
| EOI detect (Tye) | 200 µs | Must time the gap from RFD to first bit |
Standard Linux on a Pi Zero 2 W (ARM Cortex-A53 quad-core) has **scheduler
jitter in the range of 50250 µs** in userspace even with `SCHED_FIFO`, and
worst-case latencies of several milliseconds under load. This makes reliable
60 µs timing from userspace difficult but not impossible in practice — the IEC
protocol is slow enough that brief jitter is often tolerated. [14][15]
The **ATN 1 ms deadline** is the most forgiving — it is within Linux interrupt
latency range with a real-time kernel. The **60 µs bit-clocking** is tighter but
the C64 itself often waits longer in practice (the Kernal loop around bit-sampling
takes time). Real-world sd2iec devices running on ATmega644 at 20 MHz have proven
that a microcontroller running at even modest speed is sufficient.
#### 5.2 Approaches in Order of Real-Time Reliability
**1. Bare-metal (no Linux) — best timing, used by Pi1541:**
Pi1541 runs on the Circle bare-metal framework, bypassing Linux entirely. The
ARM core is completely dedicated to bit-banging the IEC bus. This achieves cycle-
exact 1541 emulation. Overkill for a standard-protocol-only device emulator, but
provides maximum margin. Not suitable if you need Linux services (network, SD
filesystem). [16]
**2. Linux kernel module with interrupt-driven state machine — good, used by Ninepin and raspbiec:**
A kernel module (LKM) handles GPIO interrupts for CLK and ATN state changes and
executes the byte-transfer handshake in interrupt context, with `udelay()` for
microsecond waits. This provides latency in the 520 µs range for interrupt
response, which is sufficient. Userspace handles filesystem and high-level logic.
This is the recommended approach for a Linux-based device emulator. [13][17]
**3. PREEMPT_RT kernel + CPU isolation + userspace busy-wait — marginal:**
With a real-time kernel (`PREEMPT_RT`) and one core isolated from the scheduler
(`isolcpus=3`), a userspace process using `clock_nanosleep()` and spinning on
GPIO registers can achieve < 10 µs jitter for short periods. Practical but
fragile; any interrupt or cache miss blows the timing. May be acceptable if the
C64's Kernal is tolerant (it often is for the 60 µs clocks). [14]
**4. Userspace polling with pigpio — risky for tight windows:**
`pigpio` can achieve ~1 µs GPIO pulse generation using DMA wave tables, but
interrupt-driven input capture has 50250 µs typical latency. Adequate for the
1 ms ATN deadline, unreliable for 60 µs bit sampling. Not recommended as the
primary IEC state machine. [15]
#### 5.3 Recommended GPIO Circuit for Pi Zero 2 W
```
┌───── IEC Bus (5V open-collector) ─────┐
│ │
IEC connector other devices
┌────┴─────────────────────────────────────────────┐
│ Per signal line (ATN, CLK, DATA): │
│ │
│ DRIVE (Pi → bus): │
│ Pi GPIO ──[1kΩ]──► 7406 input │
│ 7406 output (OC) ──► bus │
│ (Pi HIGH → 7406 output low → bus pulled low) │
│ (Pi LOW → 7406 output Hi-Z → bus floats high) │
│ Note: 7406 inverts. Pi "1" = bus asserted. │
│ │
│ SENSE (bus → Pi): │
│ bus ──[3.3kΩ]──┬──[2.2kΩ]──GND │
│ └── Pi GPIO input │
│ (bus = 5V → Pi sees ~3.1V ≈ HIGH) │
│ (bus = 0V → Pi sees 0V = LOW) │
│ Note: no inversion. Bus asserted (0V) = Pi LOW. │
│ │
│ BYPASS: 100nF cap across 7406 VCCGND │
└──────────────────────────────────────────────────┘
For ATN (read-only from device perspective):
Use SENSE only; no DRIVE circuit needed.
For RESET (read-only from device perspective):
Use SENSE only.
For SRQ:
Leave unconnected (not used in standard C64 protocol).
```
**7406 pin note:** The 7406 has VCC at +5 V and GND. Its inputs accept 3.3 V
levels (input threshold ~1.4 V), so Pi 3.3 V GPIO drives it correctly. Its
open-collector outputs swing to the 5 V bus. One 7406 IC provides 6 inverting
drivers — enough for CLK and DATA (plus spares). [8][9]
**Alternative:** Use a 74LS07 (non-inverting open-collector buffer) if you want
to avoid tracking the software inversion. Pi HIGH → bus released; Pi LOW → bus
pulled low. More intuitive but same electrical result.
#### 5.4 GPIO Pin Suggestions for Pi Zero 2 W
(Following Pi1541 Option B convention, adapting to Pi Zero 2 W 40-pin header)
| IEC Signal | Suggested Pi GPIO | Direction | Notes |
|------------|------------------|-----------|-------|
| ATN | GPIO 2 (pin 3) | Input only | Interrupt-capable; used by Pi1541 |
| CLK | GPIO 17 (pin 11) | Input + Output via 7406 | Interrupt-capable |
| DATA | GPIO 18 (pin 12) | Input + Output via 7406 | Interrupt-capable |
| RESET | GPIO 3 (pin 5) | Input only | |
| SRQ | (unconnected) | — | Not needed |
Ensure all interrupt-capable pins are configured with `edge=both` so CLK
transitions can be captured in kernel ISR context.
#### 5.5 Interrupt Approach for ATN (Critical)
ATN going low must be detected within 1 ms. On Linux:
```c
// Kernel module approach (pseudocode)
irq = gpio_to_irq(ATN_GPIO);
request_irq(irq, atn_isr, IRQF_TRIGGER_FALLING, "iec_atn", NULL);
static irqreturn_t atn_isr(int irq, void *dev) {
// Pull DATA low immediately — within ~5-20 µs of interrupt delivery
gpio_set_value(DATA_OUT_GPIO, 1); // 1 = drive bus low via 7406
schedule_work(&iec_work); // hand off to workqueue for state machine
return IRQ_HANDLED;
}
```
If using userspace only: check ATN at the top of every loop iteration; with a
tight polling loop on an isolated core this can achieve < 100 µs response time.
#### 5.6 Existing Open-Source Implementations to Study
| Project | Platform | Approach | Protocol level | Notes |
|---------|----------|----------|----------------|-------|
| **sd2iec** | ATmega644/ATmega32 | Bare metal firmware | Full protocol + Commodore DOS | Best reference for standard protocol; clean state machine in `iec.c` [18] |
| **Pi1541** | Raspberry Pi (bare metal) | Circle bare metal | Cycle-exact 1541 emulation | Best for 1541 ROM fidelity; overkill for protocol-only use [16] |
| **Ninepin (FozzTexx)** | Raspberry Pi + Linux | LKM + userspace | Standard protocol, .d64 + dirs | Best reference for LKM approach on RPi [13] |
| **raspbiec** | Raspberry Pi + Linux | LKM state machine | Basic load/save PRG | Another LKM reference; GPIO interrupt-driven state machine [17] |
| **IECDevice (dhansel)** | Arduino, ESP32, RP2040 | Arduino library | Protocol + file layer | Very clean C++ API; Pico port closest to our target SBC [12] |
| **Uno2IEC** | Arduino + PC | Arduino + serial | Protocol bridged to PC | Protocol bridging approach; less relevant for Pi target [19] |
| **VICE emulator** | x86 PC | Software emulation | Cycle-exact C64 | Reference for Kernal behavior; not directly portable to MCU |
**Best starting points:**
1. **sd2iec `src/iec.c`** — Read this first. It implements the raw IEC
byte handshake in tight ISR-driven C, with clearly named functions for
`iec_getc()` / `iec_putc()` and ATN detection. Timing constants are explicit.
2. **dhansel/IECDevice** — Higher-level C++ but very readable; the ATN handling
code shows exactly the 1 ms critical path and three approaches to meeting it.
3. **Ninepin** — Linux LKM that actually runs on a Pi and has a working voltage
divider interface circuit schematic.
#### 5.7 Known Gotchas for Pi Zero 2 W
1. **The ATN 1 ms window is non-negotiable.** Use a hardware interrupt, not
polling, for ATN detection. The C64 Kernal will hang permanently if the device
never responds to a TALK command.
2. **The 60 µs clock when acting as talker is critical.** The C64's VIC-II steals
cycles ~every 500 µs. If you clock bits faster than 60 µs, the C64 misses
them. Budget ≥ 70 µs per half-period (CLK-low, CLK-high) when the device is
talker.
3. **The C64 as talker uses 20 µs timing.** When receiving (device as listener),
the device must sample CLK transitions at ≤ 20 µs resolution. Linux GPIO
interrupts can achieve this; userspace polling probably cannot.
4. **Voltage divider input RC time constant:** With a 3.3 kΩ + 2.2 kΩ divider
and Pi GPIO input capacitance (~5 pF), the RC time constant is ~5.5 kΩ × 5 pF
≈ 28 ns — negligible. No filtering concern.
5. **Multiple 1 kΩ pull-ups:** If multiple devices are on the bus, their combined
pull-up current can exceed 7406 specifications. The 7406 datasheet allows
parallel pull-ups; just ensure no single line has more than 68 pull-ups (very
unlikely in practice). The Pi Zero 2 W should have only one pull-up path unless
connected alongside real drives.
6. **Linux SD card / USB / network activity causes GPIO latency spikes.** During
an IEC transfer, avoid SD card access or file I/O on the same core handling
IEC. Use CPU isolation (`isolcpus`) and do storage prefetching before starting
a transfer.
7. **JiffyDOS and fast loaders:** These use ≤ 4 µs timing and cannot be bit-banged
from Linux at all without dedicated hardware. If the C64 has JiffyDOS, the
emulator will need to either detect and decline JiffyDOS (the C64 then falls
back to standard), or implement it in a real-time core/MCU. Detection: JiffyDOS
checks whether the drive responds in a specific way to certain byte sequences
before switching protocols. sd2iec implements this detection.
8. **RESET handling:** The C64 can pulse RESET low during normal operation (e.g.
cartridge resets). The device emulator must handle this gracefully — release
all lines, clear all state, re-arm for next ATN.
---
## Open Questions / Gaps
1. **Exact C64 Kernal TALK timeout:** The Kernal is documented to hang permanently
if no device responds after a TALK command (ATN turnaround), but the precise
wait loop count has not been confirmed against the Kernal ROM source in this
research. It is commonly stated as "indefinite" but may have a very long
timeout in practice. Verify against Kernal ROM disassembly at $EE13$EE56
(TALK send routine).
2. **C64 as talker bit timing (Ts/Tv = 20 µs) — actual measured values:** The
20 µs figure comes from the Butterfield article and spec; different C64 Kernal
versions (and different 6510 clock tolerances) may produce slightly different
values. Verify with logic analyzer on real hardware to ensure the Pi has
enough margin for the 20 µs sample window.
3. **Pull-up resistor values in practice:** The spec says 1 kΩ inside C64 and
drives. Some community members report that removing extra pull-ups when adding
custom devices reduces bus glitches. Verify on real hardware with the Pi
attached whether combined pull-ups cause any issues.
4. **Pi Zero 2 W GPIO interrupt latency — actual measured worst case:** Published
figures (50250 µs typical, up to 4 ms worst-case under load) are from various
Pi models. The Zero 2 W with its Cortex-A53 may behave differently. Measure
actual interrupt latency under representative load before committing to a pure
Linux approach.
5. **PREEMPT_RT availability for Pi Zero 2 W kernel:** Recent Raspberry Pi OS
releases include PREEMPT_RT patches. Confirm current kernel version and whether
the Zero 2 W's mainline kernel supports `CONFIG_PREEMPT_RT`.
6. **sd2iec timing constants vs. Commodore spec:** The sd2iec source uses 45 µs
for some bit timings and 73 µs for EOI acknowledgment, slightly different from
the 60 µs spec values. These were empirically tuned for compatibility. Whether
the Pi implementation should use the spec values or the sd2iec empirical values
should be validated on real hardware.
7. **Secondary address range for OPEN:** Sources consistently say SA 015 for
OPEN/CLOSE; some say 031 for SECOND. Verify against 1541 ROM or Inside
Commodore DOS whether SAs 1630 can be OPENed or only accessed via SECOND.
---
## Sources
[1] "How The VIC/64 Serial Bus Works" — Jim Butterfield, Compute! Issue 38, July 1983
https://www.atarimagazines.com/compute/issue38/073_1_HOW_THE_VIC_64_SERIAL_BUS_WORKS.php
(accessed 2026-06-18)
[2] "Commodore Peripheral Bus: Part 4: Standard Serial" — Michael Steil (pagetable.com)
https://www.pagetable.com/?p=1135
(accessed 2026-06-18)
[3] "Commodore Peripheral Bus: Part 2: Bus Arbitration, TALK/LISTEN" — Michael Steil
https://www.pagetable.com/?p=1031
(accessed 2026-06-18)
[4] "IEC-bus documentation as used for the development of the 1541-III" — Jan Derogee, 2008
https://janderogee.com/projects/1541-III/files/pdf/IEC_disected-IEC_1541_info.pdf
Mirror: https://retro-bobbel.de/zimmers/cbm/programming/serial-bus.pdf
(accessed 2026-06-18; PDF — binary, not directly readable by WebFetch)
[5] "Commodore Peripheral Bus: Overview" — Michael Steil (pagetable.com)
https://www.pagetable.com/?p=1018
(accessed 2026-06-18)
[6] "Serial Port — C64-Wiki"
https://www.c64-wiki.com/wiki/Serial_Port
(accessed 2026-06-18)
[7] "Commodore bus — Wikipedia"
https://en.wikipedia.org/wiki/Commodore_bus
(accessed 2026-06-18)
[8] "cbmbus_doc — GitHub (mist64)"
https://github.com/mist64/cbmbus_doc
(accessed 2026-06-18)
[9] Pi1541 Project Homepage (hardware/level-shifting docs)
https://cbm-pi1541.firebaseapp.com/
(accessed 2026-06-18)
[10] "Pi1541io Revision 2" — hackup.net
https://www.hackup.net/2018/06/pi1541io-revision-2/
(accessed 2026-06-18)
[11] "Commodore 1540/1541 Service Manual: Reset Logic" — devili.iki.fi
https://www.devili.iki.fi/Computers/Commodore/C1541/Service_Manual/Page_06.html
(accessed 2026-06-18)
[12] "IECDevice — Library to connect Arduino/Pico/ESP32 to the Commodore IEC bus" — dhansel
https://github.com/dhansel/IECDevice
(accessed 2026-06-18)
[13] "Ninepin — Raspberry Pi retro peripheral adapter" — FozzTexx
https://github.com/FozzTexx/ninepin
Discussion thread: https://www.lemon64.com/forum/viewtopic.php?t=48991
(accessed 2026-06-18)
[14] "GPIO interrupt latency issue" — Raspberry Pi Forums
https://forums.raspberrypi.com/viewtopic.php?t=322560
(accessed 2026-06-18)
[15] "Lowdown on real-time Linux GPIO hardware control on Raspberry Pi" — Quorten Blog
https://quorten.github.io/quorten-blog1/blog/2018/12/29/rpi-rt-linux-gpio
(accessed 2026-06-18)
[16] Pi1541 — GitHub
https://github.com/pi1541/Pi1541
C64-Wiki entry: https://www.c64-wiki.com/wiki/Pi1541
(accessed 2026-06-18)
[17] "raspbiec — Commodore serial (IEC) bus interface to Raspberry Pi" — Flogistoni
https://github.com/Flogistoni/raspbiec
(accessed 2026-06-18)
[18] "sd2iec firmware — src/iec.c" — Ingo Korb, GPL
https://github.com/rkrajnc/sd2iec/blob/master/src/iec.c
Official site: https://www.sd2iec.de/
(accessed 2026-06-18)
[19] "Uno2IEC — Arduino 1541 emulator" — Larswad
https://larswad.github.io/uno2iec/
(accessed 2026-06-18)
[20] "Commodore IEC Serial Bus Manual for C64 Plus/4" — commodore.ca mirror
https://www.commodore.ca/wp-content/uploads/2018/11/Commodore-IEC-Serial-Bus-Manual-C64-Plus4.txt
(accessed 2026-06-18)
[21] "Making a C64/C65 compatible computer: Hardware-Accelerated IEC Serial Interface — Part 2" — c65gs blog
https://c65gs.blogspot.com/2023/12/hardware-accelerated-iec-serial.html
(accessed 2026-06-18)