Repository navigation
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.
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.
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.
- Connect the set and open its data view (Capture... on its device card).
-
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). - 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, Pattern0011111111111111(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.
| 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).
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 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.
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)....
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.
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:
- In the device list, Simulators → Simulated multi device...: Board Simulation: Pico (24 channels), Boards 2, Connect.
- On the Signals tab of its device card choose C64 bus under Simulates and press Apply.
- Capture... in the card's header opens the data view.
- 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).
openSciLab · 0.1 beta
Instruments
Logic analyzer
The lab
More