# 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)