Events Assembly Emulators Hardware

The Wheel Reinvention Jam is the Handmade Network's yearly hackathon: a week of building software from scratch, or a replacement for a program that frustrates you. For the 2024 jam, from 23 to 29 September, I made 6502 The Spreadsheet. The video above is the jam's recap show, starting where it gets to the spreadsheet.

What it is

The 6502 is the 8-bit processor in the BBC Micro and the Apple II, and in variants of it, the Commodore 64 and the NES. 6502 The Spreadsheet has all of its instructions with their actions completely unrolled, so you don't have to pick through detailed documentation to understand what they do or how to emulate them accurately. There's a row for each of the 151 opcodes, in opcode order:

The 6502 instructions in opcode order
The 6502 instructions in opcode order

On the left of each row is the machine code: the bytes, the instruction's name, the cycles it takes and the form of its arguments. LL and HH stand for the low and high bytes that follow the opcode. Then there's a short description, and then the emulation: the addresses it reads from, the new value of each register and flag, and what it writes to memory.

Instructions as expressions

Every effect is a small expression, using only + - << >> & | and brackets, so it's valid in many programming languages just as it is, from JavaScript to Go. Here's STA $LL, which stores the accumulator at an address in zero page:

ColumnValue
Bytes85 LL
NameSTA
Cycles3
Args$LL
writeByteAddressLL
writeByteValueA
The row for STA $LL, without its empty columns

Every other column in the row is empty. No register or flag changes, and an empty PC = means the program counter simply moves on past the instruction, two bytes here. INX, which adds one to the X register, has a result, a new X, and the two flags it sets:

ColumnValue
BytesE8
NameINX
Cycles2
result (word) =X + 1
X =result
zero =(result | -result) >> 7
negative =result >> 7
The row for INX, without its empty columns

The negative flag is just bit 7 of the result, and the flags that aren't mentioned keep their values.

Generating the emulator

The spreadsheet writes the emulator itself. A formula on each row joins its expressions into a line of code, and the results fill a column for each language, which can be copied and pasted straight into your own code. Together they make a function, Emulate6502, with no dependencies other than the registers and the memory passed into it, which runs the instruction at PC and moves PC on to the next one. It makes Go, C and JavaScript:

The Go code in the code generation zone
The Go code in the code generation zone

Here's the Go it generates for STA $LL:

  if instruction == 0x85 { LL := memory[PC + 1]; *PC = PC + 2; memory[(LL)&0xFFFF] = A; return true }
The generated Go for STA $LL

The function reads the byte at PC, which is the instruction, and then there's an if statement like this for every opcode. The one that matches does the instruction's memory reads, works out the new values for the registers and flags, does its memory writes, and moves PC on. An emulator just calls it for each instruction in turn.

If you don't want to mess around with code generation, a program can have the spreadsheet itself in memory, as an array of lines, and process a row on the fly to perform or disassemble the instruction. There are few enough distinct expressions that you can check for each one as a string and do the equivalent calculation, with no expression parsing at all.

Stepping backwards

The spreadsheet also records, for each instruction, the information it trashes, and what needs to change to roll it back. INX overwrites the status register, and STA overwrites the byte at the address it writes to. If an emulator saves these values as it runs, you can step backwards through the instructions you've run when you're debugging.

Source code