Skip to content

C64 bus

deckerjulian edited this page Oct 6, 2026 · 3 revisions

The C64 bus

The c64bus decoder turns the bus cycles of a Commodore 64, captured at its expansion port, into reads and writes with address and data, names the memory region of every access and disassembles the code that runs - although the 6510 has no SYNC signal that marks opcode fetches. This page shows how to wire and capture a C64, what the decoder shows, and how to try it without hardware.

5 V. The C64 bus works with 5 V; the inputs of a Pico tolerate 3.3 V. Use boards with level shifters (the LogicAnalyzer board of gusmanb has them, with VREF at 5 V) or level shifters or dividers of your own - on the trigger input as well.

What you need

A useful capture needs 24 lines of the bus plus clock and control lines, more than one board has. Two Pico boards with the openSciLab Pico firmware work together as a multi device set of 48 channels. The standard profile C64 expansion port - board B (master) + board A (slave) holds the complete setup: channel names, rate, length, trigger and the decoders.

Wiring at the expansion port

Board B is the first board of the set (it triggers), board A the second one. The channel names of the profile carry the pin of the expansion port in brackets, e.g. A0 (Y).

Board Board channel Channel in the set Signal (expansion port pin)
B CH1-CH14 1-14 Φ2 (E), R/W (5), /RESET (C), /IRQ (4), /NMI (D), /ROML (11), /ROMH (B), /IO1 (7), /IO2 (10), BA (12), /DMA (13), /EXROM (9), /GAME (8), dot clock (6)
B CH15 15 A0 (Y) once more: the reference line for aligning the boards
A CH1-CH16 25-40 A0-A15 (pins Y, X, W, V, U, T, S, R, P, N, M, L, K, J, H, F)
A CH17-CH24 41-48 D0-D7 (pins 21 to 14)

Both boards: GND to pins A and Z, VREF at 5 V. Connect the trigger output of board B to the trigger input of board A (see Multi-device sets).

The second copy of A0 on board B lets openSciLab align board A to board B exactly after every capture (offset and clock drift). Without it the offset is estimated from the Φ2 edges. The status bar and the row Alignment on the Capture tab say how the boards were aligned, e.g. board 2 moved by -1 samples (reference A0 (Y) ref); Data → Align boards measures again.

Capturing

  1. Connect the set and open its data view (Capture... on its device card).
  2. Profiles → Standard profiles → C64 expansion port - board B (master) + board A (slave). The profile sets the channel names, 20 MHz, 1,000 samples before and 30,000 after the trigger, and adds the decoders 6510 Bus (mos6502) and C64 Bus (c64bus).
  3. Press Capture (F5).

Rate and length: 20 MHz gives 20 samples per C64 cycle (about 1 MHz) with an exact clock divider (the Jitter chip shows 0 %). 1,000 + 30,000 samples (1.55 ms) fit into the buffer of a Pico. The dot clock (about 8 MHz) is only roughly captured at this rate.

Trigger: the profile waits for /RESET high on CH3 (a fast pattern trigger on board B). Started while the reset is held, it acts as a rising edge and catches the whole start-up. The C64 has no reset button: a push button that pulls pin C of the expansion port to GND gives you one. Other triggers in Settings... → Trigger:

  • a real rising edge: Edge, channel Channel 3 (board 1);
  • an address on board A, e.g. the reset vector $FFFC: Pattern, First channel 25, Pattern 0011111111111111 (A0 first).

One board only: the profile C64 expansion port - board B only (/RESET rising edge) captures the clock and control lines of board B (CH1-CH14) with a rising edge on /RESET. It has no address and data bus, so it adds no decoders.

What the decoder shows

Row Content
Bus cycles R $FFFC = $E2, W $D020 = $06, and the cycles the VIC takes (BA low). Zoomed out, shorter forms stay readable: FFFC=E2, then E2
Memory region zero page, stack, screen, RAM, BASIC, KERNAL, VIC, SID, colour RAM, CIA 1 and 2, the I/O areas, the vectors; an active cartridge line (/ROML, /ROMH, /IO1, /IO2) names its area instead
Control lines /RESET, /IRQ and /NMI while they are active
Disassembly $FCE2 LDX #$FF, undocumented opcodes marked (undocumented), interrupt sequences
Warnings cycles whose lines change right at the read point

Rest the pointer on an entry to see how it was read: every address, data and control line with its level at the read point (A15 = 1 (+$8000)), the buses as a whole (A = $FFFC), and the bus cycles of an instruction outlined in the Bus cycles row (or the instruction of a bus cycle).

Channels and options

The decoder needs A0-A15, D0-D7, Φ2 and R/W; BA, /RESET, /IRQ, /NMI, /ROML, /ROMH, /IO1 and /IO2 are optional. Channels named as in the profile are assigned automatically.

Option Values
Address format hex or dec
Show memory regions yes or no
Read the bus before falling edge (default) or at falling edge
Read offset (samples) moves the read point by whole samples
Disassemble yes or no

The read point

The 6510 takes the data at the falling edge of Φ2, and the first sample after the edge may already show the next address. The decoder therefore reads address, data and R/W at the last sample before that edge. Cycles whose lines change one sample before or after the read point are marked (unstable: A5 A9) and listed in the Warnings row: the capture is too coarse there. Capture at a higher rate, or take Φ2 from the same board as the bus.

In PulseView the decoder reads at the edge, because libsigrokdecode cannot look back one sample.

The disassembly

Without SYNC the decoder follows the program flow. It synchronises on a RESET, IRQ or NMI vector fetch or on three consistent instructions in a row, checks that the operand bytes are read from the following addresses, follows branches, jumps, JSR/RTS/RTI and interrupt sequences, and synchronises again when a prediction fails. Cycles taken by the VIC (BA low) are skipped.

If the row says No disassembly, the program flow could not be followed, usually because the bus changes at the read point: align the boards (Data → Align boards) or set a Read offset.

The decoder follows Φ2 itself: use it on the timing capture, not on the result of Analyze → State analysis (clocked)....

Reading a reset

A capture of a real C64 starting up shows the whole reset sequence of the 6510: three suppressed stack pushes, the reset vector read at $FFFC/$FFFD, the jump into the KERNAL, LDX #$FF, SEI, TXS, CLD, then the initialisation of the CIAs and the RAM test. If addresses and code do not match the standard KERNAL, the machine runs a patched ROM.

Long rows are easiest to read as a list: click the name of the Disassembly row. The list window shows every instruction with its time and the bus cycles of the other rows as columns; type JSR into the filter to see the calls at a glance, and Copy values copies a plain disassembly listing. See Protocol decoders.

The decoder 6510 Bus (mos6502, from LogicAnalyzer 6.5) runs beside it with simpler rows: data bus, cycle, instructions and address bus.

Without hardware

A capture file: examples/c64-demo.lac in the repository is a generated capture of a C64 at the expansion port (made with a cycle-accurate model of the 6510): a reset, a small program at $C000 that writes "C64!!" to the screen and changes the border colour in a loop, and two CIA interrupts. Open it (Project → Open file...) and add the C64 bus decoder (Add decoder..., search for "C64"): its channels are named as in the profile and are assigned automatically. The Markers tab lists the regions Reset and IRQ to jump to.

A simulator:

  1. In the device list, Simulators → Simulated multi device...: Board Simulation: Pico (24 channels), Boards 2, Connect.
  2. On the Signals tab of its device card choose C64 bus under Simulates and press Apply.
  3. Capture... in the card's header opens the data view.
  4. Profiles → Standard profiles → C64 expansion port - board B (master) + board A (slave), then Capture.

The result is the same capture as c64-demo.lac, decoded. In a flow the same simulator is a device.instrument node with the address sim:pico*2 and signals: {scenario: c64}, and the decoder is the node decode.c64bus (see Simulators and Protocol decoders).

Clone this wiki locally