A small, cycle-accurate C++ library for emulating a MOS 6502 CPU, designed for use in NES emulators and other 6502-based system emulators.
- All 56 official opcodes across all official addressing modes.
- Cycle-accurate: dummy reads and writes occur exactly as on real hardware.
- NMI (edge-triggered) and IRQ (level-triggered) interrupt support.
- Optional BCD arithmetic support. (disabled by default; the NES 2A03 has no BCD)
- Clean abstract interface — implement two virtual functions and you're running.
- Unknown opcode hook for logging or custom behaviour.
MOS6502/
MOS6502.h Core CPU class (abstract)
MOS6502.cpp Opcode dispatch, addressing modes, and operations
examples/nes/
cpu.h NES CPU class (derives from MOS6502)
cpu.cpp NES memory map, MMC1 mapper, and test ROM console output
main.cpp iNES ROM loader and entry point
Requires a C++17 compiler (GCC or Clang) and GNU Make.
make # Release build → mos6502_example
make debug # Debug build → mos6502_debug
make run # Build and run with examples/nes/official_only.nes
make clean # Remove all build artefacts
make CXX=clang++ # Use a different compilerDerive from MOS6502, implement Load() and Store(), then call Reset() and Run().
class MySystem : public MOS6502 {
public:
uint8_t Load(uint16_t address) override { ... }
void Store(uint16_t address, uint8_t value) override { ... }
};
MySystem sys;
sys.Reset();
sys.Run(); // returns when Halt() is calledSignal NMI or IRQ from outside the CPU (e.g. from PPU or APU emulation code):
// NMI — edge-triggered; the CPU acknowledges and clears it automatically
sys.Signal(MOS6502::NMI, true);
// IRQ — level-triggered; your device must de-assert it when done
sys.Signal(MOS6502::IRQ, true); // assert
sys.Signal(MOS6502::IRQ, false); // de-assert (from inside Load/Store)Call Halt() from inside Load() or Store() to stop the run loop cleanly:
void Store(uint16_t address, uint8_t value) override {
ram[address] = value;
if (address == 0x1234) { Halt(); }
}BCD is disabled by default (matching the NES 2A03). To enable it in a system that uses it, set enableBCD = true in your derived class constructor.
Override OnUnknownOpcode to log or handle opcodes that are not part of the official 6502 instruction set:
void OnUnknownOpcode(uint8_t opcode) override {
std::fprintf(stderr, "Unknown opcode: %02X at PC=%04X\n", opcode, PC.w - 1);
Halt();
}#pragma once
#include <cstdint>
#include "MOS6502.h"
class CPU : public MOS6502 {
public:
explicit CPU(uint8_t *ram) : ram(ram) {}
uint8_t Load(uint16_t address) override {
return ram ? ram[address] : 0x00;
}
void Store(uint16_t address, uint8_t value) override {
if (ram) { ram[address] = value; }
if (address == 0x1234) { Halt(); }
}
private:
uint8_t *ram = nullptr;
};#include <cstdio>
#include <cstdint>
#include "cpu.h"
int main() {
uint8_t ram[65536] = {};
FILE *f = std::fopen("rom.bin", "rb");
if (f) { std::fread(ram, 1, sizeof(ram), f); std::fclose(f); }
CPU cpu(ram);
cpu.Reset();
cpu.Run();
}- Cycle accuracy is implicit: every
Load()andStore()call corresponds to one real CPU cycle. Per-cycle side effects (PPU tick, APU tick, mapper IRQ counters) can be driven from within those callbacks. - There is no internal cycle counter exposed; the caller drives timing externally via the memory access callbacks.
- The NES example implements the MMC1 mapper (iNES mapper 1) and supports Blargg's
official_only.nestest ROM. - Inspired by the 6502 core in higan.