diff --git a/.claude/agents/researcher.md b/.claude/agents/researcher.md new file mode 100644 index 0000000..3fca0f8 --- /dev/null +++ b/.claude/agents/researcher.md @@ -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. + ~2–4 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"). ~5–10 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/-.md` (create the +`_research/` directory if needed; today's date is available to you). Use this +structure: + +``` +# + +> Research tier: · + +## Question + + +## Summary + + +## Findings + + +## Open questions / gaps + + +## Sources +[1] Title — URL (accessed YYYY-MM-DD) +... +``` + +## Rules + +- **You MUST `Write` the full report to `_research/-.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 3–5 + 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`. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/_plans/poc-listener-printer-PLAN.md b/_plans/poc-listener-printer-PLAN.md new file mode 100644 index 0000000..926b51e --- /dev/null +++ b/_plans/poc-listener-printer-PLAN.md @@ -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 VCC–GND. 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 (50–250 µ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' (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 `.`/`` otherwise + + `` 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). diff --git a/_research/commodore-iec-serial-bus-2026-06-18.md b/_research/commodore-iec-serial-bus-2026-06-18.md new file mode 100644 index 0000000..7149ab4 --- /dev/null +++ b/_research/commodore-iec-serial-bus-2026-06-18.md @@ -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, 0–30**. Address 31 is reserved (broadcast +UNTALK/UNLISTEN). Addresses 0–3 are reserved for the C64's internal use. [3][6] + +| Range | Conventional assignment | +|-------|------------------------| +| 0–3 | Internal (C64) — not addressable externally | +| 4–5 | Printers | +| 6–7 | Plotters | +| 8–11 | Disk drives (8 = first, 9 = second, …) | +| 12–30 | 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 (0–30). SA = secondary address (0–15 for OPEN/CLOSE; 0–31 +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 2–14: 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 (40–200 µ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 3–4 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 (0–60 µs) then proceeds with the byte normally. + (The byte is still sent normally through steps 3–7 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 = 20–100 µ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 | 20–60 µ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 50–250 µ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 5–20 µ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 50–250 µ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 VCC–GND │ + └──────────────────────────────────────────────────┘ + +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 6–8 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 (50–250 µ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 0–15 for + OPEN/CLOSE; some say 0–31 for SECOND. Verify against 1541 ROM or Inside + Commodore DOS whether SAs 16–30 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)