CPU / 01
The 6502, for web developers
A couple of registers, a few bytes, and something you can actually run.
When I started my NES emulator, I used Ruby. I wanted to understand what the machine was doing before worrying about making it fast. The first useful milestone was pretty small: load a number into a register, then increment another one.
That's what we're going to do here. The NES CPU is based on the 6502. It reads bytes from memory, interprets them as instructions, and changes its state. If you've written an interpreter, part of this should look familiar.
First, run something
The example below runs a small NES program in the same Rust/WebAssembly emulator used on this site. It saves A, X and the flags, then draws those saved values using the PPU. JavaScript builds the ROM and displays the emulator's framebuffer.
With the defaults, you'll see A 42, X 01, N 0 Z 0. These numbers are hexadecimal. Change X to FF and run it again. The increment wraps around to 00, and Z becomes 1.
Run a small NES program
The result is rendered by the same WebAssembly emulator as the home page.
LDA #$42
LDX #$00
INX
STA $0200
STX $0201
PHP
PLA
STA $0202Ready. Change the inputs or run the defaults.
The ROM includes startup and display code around the example. ROM source
The drawing code needs some setup, so the panel shows the instructions we're interested in. You can also download the complete generated ROM and load it in the emulator on the home page. No game ROM is needed.
A register is a very small variable
The 6502 has an accumulator called A, two index registers called X and Y, a stack pointer, a program counter and a status register. A, X and Y each hold one byte: a value from 0 to 255.
The program counter, or PC, is different. It holds the 16-bit address of the next instruction byte. That gives the CPU a 64 KB address space. It doesn't mean the NES has 64 KB of RAM. We'll get to that in the memory-mapped I/O guide.
The $ prefix means hexadecimal. $42 is 66 in decimal; $FF is 255. If you work with JavaScript, 0x42 is the same number. Assembly just uses a different spelling.
LDA #$42 ; put the literal value $42 into A
LDX #$00 ; start X at zero
INX ; increment X
The # matters. LDA #$42 loads the number itself. LDA $42 reads a byte from memory address $0042. Same instruction family, different addressing mode.
Where do the instructions come from?
The CPU doesn't read the letters LDA. An assembler turns that text into bytes. For the three instructions above, the bytes are:
A9 42 A2 00 E8
A9 means LDA in immediate mode. The following byte, 42, is its operand. A2 means LDX immediate, followed by 00. E8 means INX and doesn't need an operand.
Imagine those bytes start at $8000. The CPU fetches A9, then 42, so PC reaches $8002. After LDX, it reaches $8004. After INX, $8005. Instructions don't all take the same number of bytes. They don't all take the same number of clock cycles either.
That's the basic job of the emulator: fetch an opcode, dispatch to an implementation, and account for the time it took. In the book's Crystal CPU, loading A ends up in a method this small:
def lda(value)
@a = value
set_z_flag(@a)
set_n_flag(@a)
end
The immediate addressing handler fetches the operand and calls this method. Other LDA handlers get their value from memory first. The operation stays the same.
Flags are state too
Z records whether the result was zero. N copies bit 7 of the result. When interpreting a byte as a signed two's-complement number, that bit is its sign bit. An unsigned value like $80 still sets N; the CPU doesn't have a separate signed register type.
Try X = 7F. After INX, X is 80 and N is set. Try X = FF. X becomes 00 and Z is set.
The flags in this example belong to the result of INX, which runs after the loads. Changing A to zero won't leave Z set if INX then produces 01. This is an easy detail to miss when a later branch seems to make no sense.
STA and STX save the results without changing N or Z. The program captures the status with PHP before the display code starts changing registers. What you see is that snapshot, rather than a live debugger stopped after each instruction.
And the memory write?
STA $0200 stores A in RAM. Its bytes are 8D 00 02: opcode, low address byte, high address byte. The 6502 uses little-endian addresses, so $0200 is encoded as 00 02.
This was enough to give me a useful starting point when building the emulator. A few instructions, some memory, and tiny programs where I could work out the expected result by hand. Branches and interrupts can wait until that works.
Something to try
Set A to 00 and X to FF. Before running it, predict all four displayed values. Then change only X to 7F.
Check the result
The first run saves A = $00, X = $00, N = 0, Z = 1. The second saves A = $00, X = $80, N = 1, Z = 0. INX determines the final N and Z values in both runs.
Where this fits in the book
Chapters 3 and 3.1 build the CPU skeleton, LDA, INX and their tests in Crystal. Chapter 3.2 adds stores and addressing modes. The browser example uses the Rust port so you can run it without installing Crystal.
If you're curious about the hardware side, my breadboard CPU post comes at the same question from the other direction.
References: 6502 instruction reference, CPU memory map, the book's source code.
Related book chapters: 3, 3.1 and 3.2. Crystal.