Add planning docs, research notes, and project config
Co-Authored-By: Claude Code
This commit is contained in:
parent
fabee6db40
commit
85936464bd
110
.claude/agents/researcher.md
Normal file
110
.claude/agents/researcher.md
Normal 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.
|
||||
~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/<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 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`.
|
||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.idea
|
||||
439
_plans/poc-listener-printer-PLAN.md
Normal file
439
_plans/poc-listener-printer-PLAN.md
Normal 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 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' <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).
|
||||
871
_research/commodore-iec-serial-bus-2026-06-18.md
Normal file
871
_research/commodore-iec-serial-bus-2026-06-18.md
Normal 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, 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)
|
||||
Loading…
x
Reference in New Issue
Block a user