What you need to know first 1 concepts, 1 layers
The requisite-knowledge inventory for this page, bottom-up: the primitives at the base, combined upward until you reach what this page assumes. Skim the layers you already own; start wherever the ground gets unfamiliar.
- base
- ↳you are here
Emulating the 6502
Conceptual Summary
The 6502 is an 8-bit microprocessor that was widely used in early personal computers and game consoles (Apple II, Commodore 64, NES). Emulating the 6502 means reproducing, in software, exactly what the chip does on each clock cycle: fetch a byte, decode it into an operation and an addressing mode, read or write memory, update the registers and flags, and account for the cycles it took.
6502 Architecture
The 6502's entire programmer-visible state is tiny — six registers and a flat 64 KB memory:
- 8-bit data bus, 16-bit address bus (64 KB address space)
- Three 8-bit registers: Accumulator (A), X, Y
- 8-bit status register (flags: N, V, B, D, I, Z, C)
- 16-bit program counter (PC)
- 8-bit stack pointer (SP) — the stack lives in page 1,
$0100–$01FF - 56 instruction mnemonics across 151 legal opcodes
- 13 addressing modes
That state maps almost directly onto a C struct. The flags are individual bits of one status byte, so it is cleanest to name them as masks:
#include <stdint.h>
typedef struct {
uint8_t a, x, y; // accumulator and index registers
uint8_t sp; // stack pointer, indexes into $0100-$01FF
uint16_t pc; // program counter
uint8_t status; // flags, packed as NV-BDIZC
uint8_t mem[65536]; // 64 KB flat address space
uint64_t cycles; // total cycles elapsed
} CPU;
enum Flag { C=0x01, Z=0x02, I=0x04, D=0x08, B=0x10, U=0x20, V=0x40, N=0x80 };
static void set_flag(CPU *c, enum Flag f, int on) {
if (on) c->status |= f; else c->status &= ~f;
}
// Z and N are set together from a result so often it deserves a helper.
static void set_zn(CPU *c, uint8_t v) {
set_flag(c, Z, v == 0);
set_flag(c, N, v & 0x80);
}
That enum rewards a closer look. The status register is a single byte — eight bits in a
row — and each flag is permanently assigned to one of those bits. The enum gives each flag the value
whose bit pattern has only that one bit turned on. Lining the values up in binary makes the
pattern obvious:
flag bit binary hex
---- --- -------- ----
C 0 0000 0001 0x01
Z 1 0000 0010 0x02
I 2 0000 0100 0x04
D 3 0000 1000 0x08
B 4 0001 0000 0x10
U 5 0010 0000 0x20
V 6 0100 0000 0x40
N 7 1000 0000 0x80
Read down the binary column: a single 1 shifting one place left each row, so each value
is double the one above — 1, 2, 4, 8, 16, 32, 64, 128. That is all "a power of two" means here: a
byte with exactly one bit set. Such a value is called a mask, because you use it to
pick out (or change) that one bit and leave the other seven alone. The flags it names are carry
(C), zero (Z — last result was 0), interrupt-disable (I),
decimal mode (D), break (B), an unused bit (U, always 1),
signed overflow (V), and negative (N — bit 7 of the result).
Because each flag is its own bit, you set, clear, or test just that flag with one bitwise operator —
the other seven are untouched. Suppose the status byte is 0010 0001 (only
U and C are on). The three moves are:
set Z status |= Z 0010 0001 | 0000 0010 = 0010 0011
clear Z status &= ~Z 0010 0011 & 1111 1101 = 0010 0001
test C status & C 0010 0001 & 0000 0001 = 0000 0001 (nonzero)
Each line is one rule. OR (|) with the mask turns its bit on —
OR-ing any bit with 0 leaves that bit as it was, so only the masked bit changes. Clearing
uses ~Z, which is "every bit except Z's"; AND-ing with it forces that one bit to 0 and
keeps the rest (AND-ing a bit with 1 leaves it alone). Testing ANDs with the mask, so
the result is nonzero exactly when the bit was on — a zero result means the flag is clear. Those three
moves are what the set_flag helper wraps, and they let the rest of the emulator treat the
one packed byte as eight independent switches. (Keeping it one byte also matches the hardware:
PHP and PLP push and pull the whole register at once.)
Emulation Approach
The processor does the same three things forever: fetch the opcode the program counter points at, decode it into an operation plus an addressing mode, and execute it. The cleanest way to encode 256 opcodes is a dispatch table: one entry per opcode byte, each holding a function for the operation, a function that resolves the operand address, and the instruction's base cycle count.
typedef uint16_t (*AddrMode)(CPU *c); // returns the effective operand address
typedef void (*Operation)(CPU *c, uint16_t addr);
typedef struct {
Operation op;
AddrMode mode;
uint8_t cycles; // base cost; some modes add +1 on a page cross
} Instr;
extern Instr table[256]; // filled in below, indexed by opcode byte
void step(CPU *c) {
uint8_t opcode = c->mem[c->pc++]; // 1. fetch (and advance PC past it)
Instr ins = table[opcode]; // 2. decode
uint16_t addr = ins.mode(c); // resolve the operand (advances PC over its bytes)
ins.op(c, addr); // 3. execute
c->cycles += ins.cycles; // account for time
}
Everything else is filling in that table and writing the small, independent functions it points
to. Memory access goes through the mem[] array, but in a real machine some addresses
are not RAM — they are memory-mapped I/O (the NES PPU registers, a Commodore SID chip). Production
emulators route reads and writes through read(addr) / write(addr, value)
functions that dispatch on the address, so a write to $2007 talks to the graphics chip
instead of storing a byte. The CPU core does not change; only the bus behind it does.
Instruction Set
Each operation is a short function that reads its operand from the resolved address, transforms a
register, and sets the affected flags. LDA (load accumulator) is the simple case:
void lda(CPU *c, uint16_t addr) {
c->a = c->mem[addr];
set_zn(c, c->a); // load affects only Z and N
} ADC (add with carry) is the one everyone gets wrong, because it sets four flags and
the overflow flag has subtle logic. Carry is just the ninth bit of an unsigned
add. Overflow is about signed arithmetic: it should be set when two numbers of the same
sign produce a result of the opposite sign. That condition is captured by a single bit trick.
void adc(CPU *c, uint16_t addr) {
uint8_t m = c->mem[addr];
uint16_t sum = c->a + m + (c->status & C ? 1 : 0);
// Overflow: A and m share a sign (~(A^m) high bit) AND the result's sign
// differs from A's ((A^sum) high bit). Both true => signed overflow.
int overflow = (~(c->a ^ m) & (c->a ^ sum)) & 0x80;
set_flag(c, C, sum > 0xFF); // carry out of bit 7
set_flag(c, V, overflow);
c->a = sum & 0xFF;
set_zn(c, c->a); // Z and N from the truncated result
}
The real chip also has a decimal mode (the D flag): with it set,
ADC and SBC treat their operands as two packed BCD digits. It is rarely
exercised — and the NES's 6502 variant disables it entirely — so many emulators implement the
binary path first and add BCD only when targeting hardware that uses it.
With the operations written, the table wires opcodes to (operation, mode, cycles). A few rows:
Instr table[256] = {
[0xA9] = { lda, imm, 2 }, // LDA #immediate
[0xA5] = { lda, zp, 3 }, // LDA zeropage
[0xAD] = { lda, abs_, 4 }, // LDA absolute
[0xBD] = { lda, absx, 4 }, // LDA absolute,X (+1 cycle if page crossed)
[0x69] = { adc, imm, 2 }, // ADC #immediate
[0x65] = { adc, zp, 3 }, // ADC zeropage
[0x71] = { adc, indy, 5 }, // ADC (zeropage),Y (+1 cycle if page crossed)
// ... 151 legal opcodes in total
}; Addressing Modes
An addressing mode is just a rule for turning the bytes after the opcode into the address the operation works on. The whole instruction set is 56 operations multiplied across these 13 rules. Each mode is a one-line function that reads its operand bytes (advancing the PC) and returns an effective address:
// Immediate: the operand byte IS the value — its "address" is where it sits.
uint16_t imm(CPU *c) { return c->pc++; }
// Zero page: a one-byte address into $0000-$00FF (faster, smaller code).
uint16_t zp(CPU *c) { return c->mem[c->pc++]; }
// Zero page,X: index, but the sum WRAPS within the zero page (& 0xFF).
uint16_t zpx(CPU *c) { return (c->mem[c->pc++] + c->x) & 0xFF; }
// Absolute: a full 16-bit little-endian address (low byte first).
uint16_t abs_(CPU *c) {
uint16_t a = c->mem[c->pc] | (c->mem[c->pc + 1] << 8);
c->pc += 2;
return a;
}
// Absolute,X: absolute base + X. Crossing a 256-byte page costs one extra cycle.
uint16_t absx(CPU *c) {
uint16_t base = c->mem[c->pc] | (c->mem[c->pc + 1] << 8);
c->pc += 2;
uint16_t a = base + c->x;
if ((a & 0xFF00) != (base & 0xFF00)) c->cycles++; // page-cross penalty
return a;
}
// (Indirect),Y: read a 16-bit pointer from the zero page, then add Y.
uint16_t indy(CPU *c) {
uint8_t zp_addr = c->mem[c->pc++];
uint16_t base = c->mem[zp_addr] | (c->mem[(uint8_t)(zp_addr + 1)] << 8); // pointer wraps in zp
uint16_t a = base + c->y;
if ((a & 0xFF00) != (base & 0xFF00)) c->cycles++;
return a;
}
The remaining modes are variations on these: zeropage,Y, absolute,Y,
(indirect,X), the implied/accumulator modes (no operand), and the relative mode used
by branches (a signed 8-bit offset added to the PC). One mode hides a famous hardware bug:
indirect JMP. When its pointer sits on a page boundary
($xxFF), the original 6502 fetches the high byte from $xx00 instead of the
next page — a wraparound an accurate emulator must reproduce, because real programs depended on
avoiding it.
Timing and Cycles
A 6502 instruction takes a fixed base number of cycles, with two situations that add to it. First,
indexed reads that cross a 256-byte page boundary cost one extra cycle — the chip computed the low
byte of the address first, and a carry into the high byte required a second memory fetch. That is
the cycles++ inside absx and indy above.
Second, branches: a branch not taken costs 2 cycles; taken, 3; taken and crossing a page, 4. So the branch operation adds the penalties itself:
void branch(CPU *c, uint16_t addr) { // addr = target computed by the relative mode
c->cycles += 1; // taken branch: +1
if ((addr & 0xFF00) != (c->pc & 0xFF00))
c->cycles += 1; // ...and +1 more if it leaves the page
c->pc = addr;
}
Cycle counts matter because the CPU is not alone. On the NES the picture-processing unit runs at
three dots per CPU cycle, and games rely on writing to PPU registers at precise moments in the
frame. So the main loop does not run an instruction and move on — it runs step(), reads
how many cycles it consumed, and advances every other chip by that much before fetching again:
while (running) {
uint64_t before = c->cycles;
step(c);
uint64_t spent = c->cycles - before;
ppu_tick(spent * 3); // PPU runs 3x the CPU clock
apu_tick(spent); // audio stays in lockstep too
}
That is the whole shape of a 6502 emulator: a struct of state, a fetch–decode–execute step driven
by a 256-entry table, small per-operation and per-mode functions, and a cycle budget the rest of
the machine is slaved to. The hard parts are not the common instructions — they are the edge cases
the hardware actually had: the overflow flag, decimal mode, the page-cross penalty, and the
indirect-JMP bug. Get those right and real ROMs run.