872 lines
43 KiB
Markdown
872 lines
43 KiB
Markdown
# 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)
|