Checkpoint #2: Match nestest
Last updated:
The 6502 has a lot of opcodes, and writing them all before running anything is a
long stretch with no feedback. nestest removes that. You make your emulator
print its state in the same format, run it alongside the log, and stop at the
first line that doesn't match — that line points straight at the bug.
So you build the CPU incrementally: implement an opcode, see how much further you get before the next mismatch, implement the one it tripped on, repeat. You're never debugging more than one instruction at a time.
The trap with a CPU is reading the whole instruction set before running
anything. Don't. The point of nestest is that the failing log is the
to-do list: it tells you which opcode to write next, one at a time. So we build
the smallest CPU that runs, point it at nestest, and let it tell us what's
missing.
The loop itself is small. Every opcode is unimplemented to start with:
loop {
log_upcoming_instruction_and_state()
opcode = read(PC); PC++
switch (opcode) {
// nothing here yet
default: panic("unimplemented opcode {opcode:02X} at {PC-1:04X}")
}
}
First rung: match line 1, with zero opcodes
nestest runs in automated mode: you force PC = $C000 at startup instead
of using the reset vector, and the rest of the state is the standard reset —
A=X=Y=00, P=24, SP=FD, and the cycle counter already at 7 (the reset
sequence burns seven cycles). So the first line of the log is:
C000 4C F5 C5 JMP $C5F5 A:00 X:00 Y:00 P:24 SP:FD CYC:7
(The 4C F5 C5 bytes and the JMP $C5F5 disassembly are a separate concern:
printing them means knowing each instruction's length, which only falls out once
you decode opcodes. Add those columns as you go, or leave them off the diff at
first — they're for humans, not for matching.)
The development loop
Run it against nestest.log and diff. With the skeleton, line 1's state
matches, then the CPU fetches 4C and panics: unimplemented opcode 4C. That's
your instruction:
- Look up
4Cin the instruction reference — it'sJMPabsolute. Implement just that one. - Rerun. The CPU now executes the jump, logs line 2, and panics on its opcode.
- Repeat. Implement the opcode it tripped on, see how much further you get.
You're never holding more than one instruction in your head. When the log stops
mismatching, you've matched nestest — and the CPU is the part you can now
trust.