First Heartbeat: Implementing the 6502 Reset and Loop
Conducting the initial smoke test for the emulator.:: ON THIS PAGE 7
Why this Matters
Up until this point, the emulator was just a collection of structs and functions. This is the first time all the pieces work together:
- The CPU can fetch instructions from memory
- The program counter advances correctly
- Clock cycles are tracked
- The reset vector properly initializes the system
Might not look like it does much, but it's a huge milestone.
The Reset Vector
When the 6502 powers on or receives a reset signal, it doesn't just start executing from address $0000. Instead, it looks at a special location in memory called the Reset Vector, which is a hard-wired address at $FFFC-$FFFD.
These two bytes form a 16-bit address (little-endian) that tells the CPU where to begin execution:
memory.write; // Low byte
memory.write; // High byte
// This creates the address $8000
The 6502 reads these bytes during reset and sets the program counter (PC) to $8000, where our program begins.
Memory Map
Here's what our test program looks like in memory:
Address Value Description
───────────────────────────────────
$8000: EA NOP (do nothing)
$8001: EA NOP
$8002: EA NOP
$8003: 00 BRK (halt)
...
$FFFC: 00 Reset vector (low byte)
$FFFD: 80 Reset vector (high byte)
→ Points to $8000
Understanding the Debug Output
I want to create a way to be able to see the memory states of the 6502 for debugging. The first iteration of this debugging will be just writing to the console, soon I want to find a way to visualize this.
Each line shows the CPU state after executing an instruction:
| | | | |
| | | | |
| | | | |
| | | | |
)
Breaking it down:
| Field | Meaning |
|---|---|
PC: 8000 | Program Counter at $8000 |
A: 00 | Accumulator register = 0 |
X: 00 | X index register = 0 |
Y: 00 | Y index register = 0 |
SP: FD | Stack Pointer at $01FD |
Flags: nv-bdIzc | Status flags (lowercase = not set, uppercase = set) |
Cycles: 2 | Total CPU cycles elapsed (NOP takes 2 cycles) |
For more on registers, see registers.md. For status flags, see status.md.
What's changing:
- PC increments from
$8000→$8001→$8002→$8003 - Cycles increase by 2 each time (NOP takes 2 cycles)
- Everything else stays the same (NOP doesn't modify registers or flags)
The Main Loop
Here's the Rust code that runs the emulator:
The pattern is simple:
- Display the current CPU state
- Fetch the next opcode at PC
- Check if it's a halt condition (BRK)
- Execute the instruction via
cpu.step() - Repeat
What I Learned
The Reset Sequence
The 6502 doesn't just start running—it has a bootstrap process:
- Read reset vector from
$FFFC-$FFFD - Set PC to that address
- Initialize stack pointer to
$FD - Set specific status flags
Fetch-Decode-Execute Cycle
Even though NOP does nothing, it still follows the CPU cycle:
- Fetch opcode from memory at PC
- Decode (look up what
$EAmeans) - Execute (do nothing)
- Increment PC and consume 2 cycles
This foundational pattern will apply to every instruction, and confirms our step() function is working.
What's Next
With this basic loop working correctly, I can move onto the next fundamental instruction LDA (Load Accumulator). This will allow the emulator to:
- Read values from memory
- Store them in the accumulator register
- Update the Zero and Negative flags based on the loaded value