Emulating a CPU in C++ (6502)

notes.

Emulating a CPU in C++ (6502)

Source: Emulating a CPU in C++ (6502), Dave Poo, 52:27, uploaded 2020-08-23, category Other / Unclear, playlist index 1407.

Dave Poo starts with a practical reason to write an emulator: the work forces him to learn how the processor behaves. He chooses the 6502 because its small instruction set and 8-bit data path make the machine possible to hold in view. The processor belongs to an earlier period of personal computing, yet the same arrangement remains visible in a modern CPU: memory holds code and data, the processor reads instructions, registers hold working values, and a program counter points to the next operation.

The project stays deliberately small. Poo wants a working beginning rather than a complete 6502, so each new instruction exposes another part of the machine. The code grows from a struct with a few fields into a program that loads values, reads different memory locations, and jumps to a subroutine. That sequence gives the video its shape.

The 6502 as a small machine

The 6502 can address 64 kilobytes of memory through a 16-bit address bus. Its data bus is 8 bits wide, so the processor reads and writes one byte at a time. Poo describes it as little-endian, which fits the host computer he is using for the emulator. He leaves cross-platform byte swapping aside because both systems in this first version use the same byte order.

The first 256 bytes form the zero page. The 6502 has special addressing modes that reach this region in one fewer clock cycle than a comparable address elsewhere in memory. Poo treats the zero page as a large set of extra registers, which explains why the arrangement mattered when memory was expensive and processors had few registers. The following 256 bytes hold the stack. A modern program can use more stack space in one thread than the whole 6502 stack provides, although the basic idea remains familiar.

The processor also uses memory-mapped I/O. Hardware occupies regions of the address space, so a machine with 64 kilobytes of addressable memory cannot treat every address as ordinary RAM. Poo has the Commodore 64 in mind, since it was the computer he owned and it used a 6502-family processor. He also mentions the BBC Micro and Apple computers as machines built around related processors.

The first CPU struct contains the program counter, the stack pointer, the accumulator, and the X and Y index registers. The program counter is 16 bits because it must address the full memory range. The other working registers are 8 bits. A processor-status byte stores flags for carry, zero, interrupt disable, decimal mode, break, overflow, and negative results. Poo represents those flags as C++ bit fields, so the emulator can set or clear the individual bits that instructions affect.

This is enough internal state for a first CPU. The struct does not yet execute anything. It only describes the values that execution will change.

Resetting a processor without recreating a computer

A real 6502 starts through a reset sequence. The processor reads a reset vector from memory, enters code in the machine’s ROM, initialises the hardware, and eventually reaches the operating system or BASIC prompt. Poo follows the Commodore 64 reset routine for orientation, then chooses a shorter path for the emulator.

The simplified reset routine points the program counter at 0xFFFC, places the stack at 0x0100, clears the A, X, and Y registers, clears the processor flags, and zeros the memory array. This gives the emulator a predictable state from which it can execute a test program. It does not reproduce the Commodore 64 boot process. Poo says that an accurate version would need to load the machine’s kernel ROM and perform the initialisation that the ROM performs.

That distinction matters because the emulator has two subjects at once. It models some rules of the 6502 instruction set, while it borrows a few visible effects from the computer around the processor. The reset method creates a computer that resembles the C64’s starting state, with a small amount of the actual boot sequence.

Memory, cycles, and the fetch loop

The memory struct is a 64-kilobyte array of bytes. The CPU’s execute function receives memory and a number of clock cycles. Each fetch consumes a cycle, and an instruction can consume further cycles when it reads operands or accesses another memory location. Poo uses the cycle count as the stopping condition, which would let a larger emulator run the processor for a fixed amount of time before redrawing graphics or handling another device.

The first helper fetches a byte from the address in the program counter, decrements the cycle count, and advances the program counter. A separate read helper consumes a cycle without advancing the program counter. That difference becomes important when an instruction reads data from an address stored inside the program. A fetch moves through the instruction stream. A read observes memory at an address that the instruction has calculated.

Execution then follows the familiar structure of a small interpreter. The emulator fetches an opcode, switches over its value, performs the operation associated with that opcode, and repeats until the cycle budget reaches zero. The switch starts with one case for LDA, the load-accumulator instruction.

Immediate loading and the first machine-code program

Poo begins with LDA in immediate mode. The opcode is 0xA9. The next byte contains the value itself, so the instruction A9 42 means “load the hexadecimal value 0x42 into the accumulator”. The emulator fetches the opcode, fetches the operand, stores it in A, and updates the zero and negative flags.

Immediate LDA takes two cycles. One cycle reads the opcode from the 8-bit bus, and the other reads the value that follows it. The instruction occupies two bytes in memory. The source makes the relationship between bytes and cycles visible through the debugger rather than treating the timing table as a separate fact.

The first test program is hardcoded into memory at 0xFFFC, the address chosen by the reset routine. Poo writes A9 42 into that location and executes two cycles. The accumulator ends at 0x42, and the program counter has moved beyond the two-byte program. The test succeeds, then exposes the next problem: the emulator has no instruction that changes the program counter, so continued execution would run through the rest of memory.

Zero-page addressing and indexed lookup

The next form is LDA zero page, opcode 0xA5. Its operand is an 8-bit address in the first 256 bytes of memory. The emulator fetches the opcode, fetches that address, reads the byte stored there, and places the result in A. The read consumes a third cycle because the instruction performs a memory access after reading its two instruction bytes.

Poo puts 0x84 at zero-page address 0x42, changes the test program to load from that address, and runs three cycles. The accumulator receives 0x84, and the same helper updates the zero and negative flags. The code now separates fetching an instruction operand from reading the data that the operand names.

The indexed zero-page form, LDA zero page,X, adds the current X register to the 8-bit address from the instruction. Poo describes the operation as one opcode fetch, one operand fetch, one cycle to apply the index, and one cycle to read the resulting byte. The instruction therefore takes four cycles. The zero page wraps within its 8-bit address range, which is part of what gives the mode its old hardware character.

One assembly mnemonic can therefore describe several machine instructions. Poo counts eight forms of LDA in the table he is using, each with its own opcode, byte length, addressing mode, and cycle count. The 6502 has a small set of basic operations, yet the addressing modes multiply the cases that a complete emulator must implement.

A 16-bit jump through the stack

After the load instructions, the program still has nowhere to go. Poo chooses JSR, jump to subroutine, because it exercises absolute addressing, a 16-bit address, and the stack.

The emulator adds a fetchWord helper. It reads two bytes from the instruction stream in little-endian order, with the low byte first, then combines them into a 16-bit address. A JSR instruction occupies three bytes: one opcode and two address bytes. It takes six cycles in the table Poo follows.

The 6502 pushes the return point minus one onto the stack before replacing the program counter with the target address. Poo implements a word write for the two stack bytes, adjusts the cycle count, and sets the program counter to the target. The test program at 0xFFFC becomes a jump to 0x4242. At that target address he places another immediate load, A9 54, which puts decimal 84 into the accumulator. The debugger shows the jump reaching the second program and the accumulator ending at 84.

The subroutine still cannot return because Poo has not written RTS. The stack now carries a return address, although the emulator has no instruction that reads it back. That unfinished edge is useful: a jump is a change to the program counter, while a subroutine call also creates a promise about where execution should resume.

A CPU is not yet a computer

By the end, the emulator has a CPU and memory that can execute a small handful of instructions. The whole model lives on the host program’s stack. It allocates no heap memory, which Poo points out as another reminder of how small the original machine was.

The processor still has no way to display, print, or hear anything. It can read and write its memory and registers, yet those changes remain invisible until another device observes them. A complete emulated computer would add a video chip with mapped memory and registers, along with the other hardware that turns CPU state into a screen or a serial output. The CPU is the centre of that arrangement, although the surrounding chips give the machine a usable surface.

Poo closes by comparing the emulator with a scripting language. A compiler can lower a language into a small set of simple instructions that an interpreter executes in the same general way. The machine stays simple because the compiler carries much of the work. A full 6502 emulator would need a case for each instruction and addressing mode, tests for their behaviour, and interfaces for the devices around the processor.

The source leaves the project at the point where the method has become clear. A small processor can be understood by representing its registers, modelling its memory, accounting for each bus access, and adding instructions one at a time. The resulting machine remains partial, yet it has executed real 6502 opcodes and exposed the state changes that make the processor work.

Limits of the source

The video is a guided implementation of a partial 6502 emulator. Poo explicitly leaves out most instructions, return handling, a complete test suite, the Commodore 64 ROM, and the I/O devices that would make a full computer. The reset values are chosen to get a useful test running, with the speaker marking the places where they differ from a faithful C64 boot.

The captions contain automatic-recognition errors around hexadecimal values, register names, “zero page”, and “little-endian”. I have normalised those terms where the code and surrounding explanation make the intended meaning clear. The note follows the implementation shown in the video. It does not treat the partial emulator as a complete or cycle-perfect model of every 6502 variant.

Further reading / references

32 paragraphs1,997 words12,084 characters