Carl
Reference

More about the 6502

You don't need any of this to start writing programs — the syntax page is enough. But a handful of facts about how the 6502 is built make CodeLab's odd-looking addresses and panels click into place. Here's the short tour: what's inside the chip, how it finds your program, why 16-bit values look backwards in memory, and where everything lives.

What's inside the chip — the registers

The 6502 is tiny. Everything it can hold at once lives in six registers — small named slots, each just a byte or two. Think of them as the chip's hands: it can only grip a few things at a time, so most of what a program does is shuttling values between these registers and memory. You can watch all six change, live, in CodeLab's CPU panel.

RegNameSizeHow often — and can you set it?
A Accumulator byte (8-bit) Everyday. The value you compute with — load it, do math/logic, store it.
X Index X byte (8-bit) Everyday. Loop counters and indexed addressing (table,X).
Y Index Y byte (8-bit) Everyday. A second index; the (zp),Y mode uses it.
SP Stack pointer byte (8-bit) Rarely direct. Subroutine calls and interrupts move it for you; reach it only via TSX / TXS.
PC Program counter word (16-bit) Never direct. You can't load or store it — only JMP, branches, JSR / RTS and interrupts change it.
P Status flags byte (8-bit) Automatic. Set as side effects of other instructions, tested by branches; whole-byte access only via PHP / PLP.

In practice you spend almost all your time in A, X and Y — the three you actually load, change and store. SP, PC and P mostly move on their own. Note the sizes: every register is a single byte (8 bits, $00$FF) except PC, which is a 16-bit word — it has to be, since it holds a full memory address. That byte-vs-word split is exactly why addresses are stored as two bytes — in a specific order, which the little-endian section below explains.

The status flags (P)

P is special: you almost never load it directly. Instead, ordinary instructions set its bits as a side effect — nearly every operation updates Z and N from its result — and the branch instructions then read one bit and jump on it. That's the whole loop: do something, a flag records how it went, a branch acts on it. CodeLab's CPU panel shows P as a hex byte; these are the bits inside it, from bit 7 down to bit 0.

BitFlagNameSet when…Act on it with
7 N Negative The last result's high bit (bit 7) was 1. BMI / BPL
6 V Overflow A signed add/subtract overflowed (also set by BIT). BVS / BVC
5 (unused) No flag here — this bit always reads as 1.
4 B Break Marks a BRK vs a hardware IRQ in the copy pushed to the stack — not a bit you toggle. (interrupts lesson)
3 D Decimal BCD-math mode. SED / CLD flip it, but CodeLab does math in binary only — no effect here. SED / CLD
2 I IRQ disable When set, maskable interrupts are ignored. SEI / CLI
1 Z Zero The last result was exactly zero. BEQ / BNE
0 C Carry Carry out of an add / no borrow on subtract; the bit shifted out by shifts & rotates; set by compares. BCC / BCS · CLC / SEC

(Because bit 5 is always 1, a "cleared" P still shows one bit set in the CPU panel — that's normal, not a stuck flag.)

The reset vector — the 6502's main()

In most languages a program begins at main() — something finds it by name and calls it. A 6502 has nothing that clever. The 6502 can address 64 KB of memory: every location has an address from $0000 to $FFFF. On power-on (or reset) the chip does one thing: it reads the two bytes at $FFFC and $FFFD, glues them into a 16-bit address — in a particular byte order we'll get to in a moment — and jumps there. Whatever lives at that address is your program's first instruction.

Those two bytes are called the reset vector. Think of it as a fixed mailbox the chip always checks first: it doesn't know where your code is, so it looks up the address from a place that never moves. (Two more mailboxes sit right beside it — for interrupts. You'll open those in the interrupts lesson.)

Little-endian: addresses look backwards

Here's the part that trips everyone up the first time. A 16-bit address is two bytes — a low half and a high half. The 6502 always stores them low byte first, high byte second. That convention is called little-endian.

So the address $E000 is laid down in memory as the byte $00 followed by the byte $E0 — at $FFFC/$FFFD the reset vector reads 00 E0, not E0 00. Glance at a hex dump and a familiar address can look reversed; it isn't wrong, it's just stored low-then-high.

Once you expect it, it stops surprising you — because it's consistent. The reset vector, the target of a JMP or JSR, an address baked into an instruction, a .word you write — every 16-bit value the 6502 touches follows the same low-then-high rule. When you read the hex dump panel and an address seems backwards, that's this, working exactly as designed.

Why your program lives high in memory

The reset vector is at $FFFC/$FFFD — right at the very top of the address space. For the chip to start cleanly, the memory up there must be dependable: it has to hold a real address the moment power comes on, before anything has had a chance to run. So CodeLab's machine puts the top 8 KB — $E000 through $FFFF (that's $2000 = 8192 bytes) — in ROM: read-only memory that's simply there at power-on. Your program goes in that ROM, and the reset vector points into it.

You never write any of this yourself — that's the Layer-0 deal from the syntax page. CodeLab takes your handful of instructions, places them in ROM, and fills in the reset vector so it points at your first line. That expanded, complete program is exactly what the hex dump and disassembly panels show you — the long run of empty bytes is the rest of the ROM your tiny program didn't fill.

The stack lives at $0100

The 6502 has a built-in stack, and it isn't anywhere you choose — it's hard-wired to the 256 bytes from $0100 to $01FF. When you call a subroutine (JSR) the chip pushes the return address onto that stack; RTS pops it back. Interrupts push there too. You rarely touch it directly, but it's a real, fixed part of memory — which is one reason a 6502 system needs working RAM down at the low addresses, starting from $0000.

So where is the stack pointer itself? Not in memory — it's a dedicated register inside the CPU, right alongside A, X, Y and PC. You can watch it live as SP in CodeLab's CPU panel. It holds just one byte — an offset — and the actual stack address is always $0100 + SP (the high half is hard-wired to $01, which is exactly why the stack can never leave page 1). After reset it starts at $FD, so the first push lands near the top of page 1 and the stack grows downward from there. The only way to read or set it as a value is TSX (copy SPX) and TXS (copy XSP); everything else — JSR, RTS, interrupts, push/pull — moves it for you.

That fixed size is a hard limit: 256 bytes, and not one more. The stack pointer is an 8-bit register; every push writes a byte and counts it down, every pop counts back up. Nothing checks it. Recurse too deeply — or just keep calling without returning — and the pointer runs off the end and wraps around, and new pushes start overwriting the oldest frames still sitting in page 1. There is no error, no trap, no "stack overflow" message. The damage is silent: a clobbered return address means the next RTS pops a garbage value and the CPU jumps to a more or less random place. The program doesn't stop — it derails, running whatever bytes it lands in. (This is why CodeLab's park / idle detector exists: a derailed program usually ends up spinning, and CodeLab notices.)

The flip side of "nothing protects the stack" is "nothing stops you driving it yourself." The stack pointer is just a register you can read and set, and page 1 is ordinary memory. So you don't have to use JSR/RTS as given: jumping into a routine with JMP instead of calling it skips pushing a return frame entirely — the hand-rolled version of a tail call. You can also reset the pointer to throw away frames, or hand-write return addresses to build jump tables. The 6502 gives you the mechanism and no guard rails; what you do with it is your calling convention.

Zero page is special — and fast

The very first 256 bytes, $0000$00FF, are called zero page. Many instructions have a shorter, faster form when the address they touch is in zero page — it takes one less byte and one less clock cycle than a full 16-bit address. A few addressing modes only work through zero page at all. The practical takeaway: when you need a handful of scratch locations, reach for $00$FF first. It's the closest, cheapest shelf in the workshop.

CodeLab's memory map

Putting it together — here's where things live in the machine the labs run on:

AddressWhatWhy it matters
$0000–$00FF Zero page The first 256 bytes — your fastest scratch space.
$0100–$01FF The stack Hardware stack. Subroutine calls and interrupts use it for you.
$0200–$1FFF RAM General read/write memory for your data.
$B000 ViaBase The VIA — storing a byte here lights the 8 LEDs.
$E000–$FFFF ROM Read-only: your program + the reset vector live here.

Everything else in the 64 KB is simply unused by these labs — that's fine, real machines have gaps too.

The 6522 VIA — how a byte becomes light

That $B000 row is its own chip. The 6522 VIA — short for Versatile Interface Adapter — is a separate part, the 6502 CPU's classic companion for talking to the outside world. It sits on the same address bus as the RAM and ROM, but instead of storing bytes it has pins: two 8-bit parallel ports (A and B) and a couple of hardware timers.

In this machine the VIA answers at $B000 (the labs call it ViaBase), and Port B's eight output pins are wired straight to the eight LEDs. So the CPU never "knows" about LEDs at all — it just stores a byte to an address. The VIA turns that byte into voltage on eight pins, and the pins light the lights. That's the whole trick behind talking to hardware on a 6502, and it's the same chip whose Timer 1 the timer lesson uses to keep time without a delay loop.

Explore more