“Know how to solve every problem that has been solved.” “What I cannot create, I do not understand.” — Richard Feynman
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.

  1. base
  2. 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:

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.