Building a 6502 Simulator in Python (Part 2)

Table of Contents

From a 12-instruction teaching CPU to a full MOS 6502 emulator, inspired by Ben Eater’s breadboard projects.

Why the 6502?

The MOS 6502 is the processor that defined home computing in the late 1970s and early 1980s. It ran the Apple II, the Commodore 64, the Atari 2600, and the Nintendo Entertainment System. It powered the first affordable personal computers and launched an industry.

What makes it perfect for a simulator project:

  1. Simple enough to understand completely. The 6502 has only 56 distinct instructions (with addressing mode variants giving 151 opcodes total). Compare that to a modern x86 with thousands of instructions. The 6502 is comprehensible by one person.

  2. Complex enough to be interesting. It has 13 addressing modes, a stack, interrupt handling, decimal arithmetic (BCD), and memory-mapped I/O. It’s a real architecture with real programs written for it.

  3. Ben Eater made it tangible. Ben Eater’s YouTube series shows a 6502 running on a breadboard, wired up with actual chips, an LCD display showing output, and LEDs blinking on the data bus. Watching physical signals flow through a real 6502 makes the architecture click in a way that documentation alone never does.

I set out to build the software equivalent: a complete 6502 simulator with the same components Ben wires up on his breadboard (CPU, RAM, ROM, and an LCD display) but running in Python so I can program it, debug it, and extend it. This post is where that landed.

The Architecture

Registers

The 6502 has a minimal register set:

RegisterBitsPurpose
A8Accumulator, the main math register
X8Index register for array indexing
Y8Index register, secondary indexing
SP8Stack pointer, points into page $0100-$01FF
PC16Program counter, address of next instruction
P8Status register holding the CPU flags

The status register (P) packs 7 flags into one byte:

Bit:  7  6  5  4  3  2  1  0
Flag: N  V  -  B  D  I  Z  C

Memory Map

The 6502 addresses 64KB ($0000-$FFFF):

$0000-$00FF  Zero Page (fast access, shorter instructions)
$0100-$01FF  Stack (grows downward from $01FF)
$0200-$5FFF  RAM (general purpose)
$6000-$600F  I/O (LCD display, keyboard, etc.)
$8000-$FFFF  ROM (program code, loaded at startup)
$FFFA-$FFFB  NMI vector
$FFFC-$FFFD  Reset vector (where execution starts)
$FFFE-$FFFF  IRQ/BRK vector

The reset vector at $FFFC is key: when the CPU starts (or resets), it reads the 16-bit address at $FFFC-$FFFD and begins executing from there. This is how the boot process works. The ROM programmer stores their entry point address at $FFFC.

Addressing Modes

This is where the 6502 gets interesting. The same instruction (like LDA, “load accumulator”) has different opcodes depending on how you specify the operand:

ModeSyntaxExampleMeaning
Immediate#$nnLDA #$42Load the literal value $42
Zero Page$nnLDA $10Load from address $0010
Zero Page,X$nn,XLDA $10,XLoad from $0010 + X
Absolute$nnnnLDA $4000Load from address $4000
Absolute,X$nnnn,XLDA $4000,XLoad from $4000 + X
Absolute,Y$nnnn,YLDA $4000,YLoad from $4000 + Y
Indirect($nnnn)JMP ($FFFC)Jump to address stored at $FFFC
(Indirect,X)($nn,X)LDA ($20,X)Pointer from zero page + X
(Indirect),Y($nn),YLDA ($20),YPointer from zero page, then + Y

The indirect modes are what make the 6502 capable of complex data structures (arrays, linked lists, function pointers) despite having only three registers.

What I Built

It’s done. The simulator lives in the 6502-simulator folder of my Physics-Modeling repo, and the components mirror what Ben Eater wires on his breadboard:

CPU Core

Memory System

LCD Display

Assembler

Debugger

A 139-test suite covers every opcode, all 13 addressing modes, both binary and BCD arithmetic, the stack, branches, subroutines, and the famous indirect-JMP page-boundary bug. It all passes.

From MM1 to 6502

The conceptual jump from my college MM1 to the real 6502:

FeatureMM16502
Registers8 general-purpose3 specific (A, X, Y) + SP, PC
Memory64 words65,536 bytes
Instructions1256 (151 with modes)
Instruction sizeFixed 12-bitVariable 1-3 bytes
Addressing modes113
StackNoneHardware stack at $0100-$01FF
InterruptsNoneNMI, IRQ, BRK
I/ONoneMemory-mapped
BCD arithmeticNoYes (decimal mode)

The core concept is identical though: fetch an instruction from memory, decode its fields, execute the operation, repeat. The MM1 taught me the pattern. The 6502 fills in the details of a real-world implementation.

Hello, 6502

Here’s the classic first program. Load each character into the accumulator and store it to the LCD at $6000:

    .org $8000
start:
    LDA #$48        ; 'H'
    STA $6000       ; write to LCD
    LDA #$65        ; 'e'
    STA $6000
    LDA #$6C        ; 'l'
    STA $6000
    LDA #$6C        ; 'l'
    STA $6000
    LDA #$6F        ; 'o'
    STA $6000
    BRK

    .org $FFFC
    .word $8000     ; reset vector -> start

Assemble it, load it into ROM, reset, and run:

from cpu_6502 import CPU6502, Assembler, LCDDisplay

cpu = CPU6502()
lcd = LCDDisplay()
cpu.memory.register_io(0x6000, 0x6001, lcd)
cpu.load_rom(Assembler().assemble(source), 0x8000)
cpu.reset()
cpu.run()
print(lcd)
+----------------+
|Hello           |
|                |
+----------------+

The fun part is single-stepping with the debugger, watching the accumulator and the LCD change one instruction at a time:

executed: $8000: A9 48    LDA #$48
PC: $8002  SP: $FD  Cycles: 2
 A: $48    X: $00    Y: $00
 P: $24 [nv-bdIzc]
LCD row0: "                "
----------------------------------------
executed: $8002: 8D 00 60 STA $6000
PC: $8005  SP: $FD  Cycles: 6
 A: $48    X: $00    Y: $00
 P: $24 [nv-bdIzc]
LCD row0: "H               "

Load a value, store it to the LCD, the cursor advances, the next character appears. Exactly the loop Ben Eater’s breadboard runs, just in software where I can pause it and poke at the registers.

Try It in Your Browser

I built a full walkthrough notebook that assembles programs, single-steps them, dumps memory, disassembles bytes back to assembly, and exercises a loop, a subroutine (JSR/RTS), and decimal-mode (BCD) arithmetic. Every cell is pre-run, so you can read it like an article, or launch it live and change the programs yourself:

Launch in Binder

It takes 30-60 seconds to build the environment the first time, then you’re in a full Jupyter session.

From MM1 to a Real CPU

The goal from Part 1 was to go from my 12-instruction college teaching CPU to a real architecture, and watch a program I wrote in assembly light up a virtual LCD with “Hello” the way Ben Eater’s breadboard does. That’s working now. The core idea never changed between the two: fetch an instruction, decode its fields, execute, repeat. The 6502 just fills in all the messy, wonderful real-world details.

Source code: Physics-Modeling on GitHub (in the 6502-simulator folder). The walkthrough notebook is under extensions/notebooks/.