Dec 3, 2024

Building an NES Emulator: Part 2—CPU Registers, Memory, and Mappers

In this blog series, I build an NES emulator and document the process. Join me in Part 2, where I talk about CPU memory!

Article hero image

The NES CPU oddly works a lot like a real human brain. It has short-term memory, where it keeps stuff temporarily but swaps it out just as quickly. It has mid-term memory, where things take a little more effort to recall but are still relatively easy to access. And, just like our long-term memory, the things it needs permanent access to are stored in… cartridges? 🤔 Yeah, close enough. In this post, I want to take a closer look at how the NES CPU handles data and storage.

Registers

Consider a cash register for a moment; it temporarily stores money in its drawers before storing it in a bank. Cash registers are usually not that big, but they do provide a quick way for cashiers to give and receive money to customers. A CPU register works the same way: it’s a small storage location that temporarily holds data, and accessing this data is incredibly fast. The NES CPU has six registers:

  • A (Accumulator): An 8-bit register where results of arithmetic operations are stored.
  • X and Y (Index Registers): 8-bit general-purpose registers that assist in various operations.
  • S (Stack Pointer): 8-bit stack pointer. It points to the current location on the stack, which holds return addresses and local variables. Imagine a “choose your adventure” book with branching paths. You might write the current page number on a sticky note each time you take a branching path, stacking the notes on top of each other. This allows you to backtrack the exact path you took. The CPU stack works in a similar way; it allows the CPU to jump to different branches and eventually return to where it started without losing track of what variables it was using. The stack is a fundamental part of computing and isn’t limited to the NES CPU.
  • PC (Program Counter): Holds the memory address of the next instruction to be executed. In simpler terms, it’s the page number in the “pick your adventure” book.
  • P (Processor Status): 8-bit processor status register. This holds global CPU state flags, which include:
    • Negative: Last operation result was negative.
    • Overflow: Result was too large for the register.
    • Break: A software interrupt occurred.
    • Decimal: Enables Binary-Coded Decimal (BCD) mode (not used in the NES).
    • Interrupt: CPU is processing an interrupt.
    • Zero: Last operation result was zero.
    • Carry: Operation resulted in a carry, like carrying a 1 in addition.

Memory

Let’s talk about Random Access Memory (RAM). RAM can hold more data than the CPU registers, but it’s also a bit slower to access. RAM is not volatile, meaning it loses its contents when the power is turned off. The NES has 2KiB (2048 bytes) of RAM, ranging from address $0000 to $07FF. I need to make an important distinction. The physical RAM of the CPU ends at $07FF, but the NES treats all addresses from $0000 to $1FFF as RAM, for a total of 8KiB (8192 bytes). It’s like having a pocket but calling it four different names. Whether I put something in my “pocket,” “pouch,” “hip sock,” or “hand hole,” I’m referring to the same physical location. In the same vein, addresses $0800, $1000, and⁣ $1800 all wrap back to $0000. This is called mirroring, and the NES uses it everywhere.

Why Mirroring?

At first glance, it might seem like mirroring provides extra space, but that’s not true; if I only have 2 pockets, I can’t hold 8 items just by calling my 2 pockets 8 different names.

Why is mirroring used, then? Because it was convenient.

If you recall from my previous post, the 6502 CPU has an 8-bit data bus, which allows it to transfer 8 bits of data at a time. It also has a 16-bit address bus, giving it a 64 KiB addressable range from $0000 to $FFFF. This doesn’t mean the chip comes with 64 KiB of RAM! These addresses are like empty house lots—they provide manufacturers with the space to build whatever they want. Nintendo chose to build a 2 KiB “house” for RAM, along with other “houses” (PPU and APU). These components didn’t use all 64 KiB of addressable space, so Nintendo filled the gaps with mirroring to ensure that every address from $0000 to $FFFF pointed to something valid.

Why didn’t Nintendo just design their own custom CPU that mapped addresses directly to the physical components without mirroring? It would have been too expensive. Instead, they adapted a general-purpose CPU to suit their needs, and mirroring was a byproduct of that decision.

Memory Layout

Now that we know mirroring is more of a quirk than a feature, we can look at the NES memory layout without too much head-scratching:

  • $0000-$07FF: 2 KiB of RAM. The CPU uses this for temporary storage, much like modern RAM in a computer. Subregions of this memory have specific purposes:
    • $0000-$00FF: This is the Zero Page, and it’s special because addresses here can be shortened—you don’t need to include the leading $00. For example, $00FF can be accessed as $FF using the appropriate addressing mode (I’ll cover addressing modes in a future post). Accessing the Zero Page takes fewer CPU cycles, so it’s often used for performance-critical operations.
    • $0100-$01FF: The Stack. Do you recall the analogy of using sticky notes to track branching paths in a choose-your-own adventure book? We store those “sticky notes” here. The stack starts at $01FF and “grows” down as data is added. When data is removed, the stack “shrinks” back up. The stack pointer is responsible for keeping track of the top of the stack.
    • $0200-$07FF: General-purpose RAM
  • $0800-$0FFF: Mirrored RAM, points back to $0000-$07FF
  • $2000-$2007: PPU Registers, used to control the Picture Processing Unit.
  • $2008-$3FFF: Mirrored PPU registers, points back to $2000-$2007 every 8 bytes.
  • $4000-$401F: APU and I/O Registers, controlling the Audio Processing Unit and reading input (like controllers).
  • $4018-$401F: Disabled. The original 6502 CPU used these addressable registers, but the NES does not use them.
  • $4020-$FFFF: Cartridge space. This range is where the cartridge hardware takes over, providing ROM, mappers, and other features. Some notable sections include:
    • $6000-$7FFF: Battery-backed Save RAM, used for saving game progress.
    • $8000-$FFFF: Cartridge ROM. This is where the game code and data live. Mapper registers also live here. Mappers extend the memory range of the NES without changing the physical hardware. Further information about mappers can be found below.
    • $8000-$8FB0 and $C000-$FFFF: Some NES music uses audio samples, and these are stored in these ranges. Implementing sample playback can be tricky because this is a very crowded area of memory.
    • $FFFA-$FFFB: The Non-Maskable Interrupt (NMI) Vector points to the NMI handler. NMIs occur when the CPU needs to handle something important, like a frame update or controller input. The handler saves the CPU state, processes the interrupt, and restores the state to continue where it left off. “Non-maskable” means that the CPU cannot ignore this interrupt.
    • $FFFC-$FFFD: The Reset Vector points to the reset handler, the starting point for the CPU when the console is turned on or reset.
    • $FFFE-$FFFF: The IRQ (Interrupt Request) Vector points to the IRQ handler. Unlike NMIs, IRQs are maskable, meaning the CPU can ignore them if it’s busy. They’re often used for less-critical tasks like updating the screen or managing audio playback.

Mappers

The CPU assumes that ROM data spans $8000–$FFFF, which is 32 KiB. Let’s say our cartridge is also 32 KiB and aligns perfectly with this model, but its internal addresses start at $0000. In this case, the mapper’s job is straightforward: it creates a two-way translation between $8000–$FFFF and $0000–$7FFF. For example, a CPU read from $8004 corresponds to a cartridge read from $0004. At its core, that’s all a mapper does.

However, not all games fit neatly into a 32 KiB ROM. Some, like Kirby’s Adventure, are as large as 768 KiB. The solution is to use banks, which is pretty intuitive when you think about it. It’s like when a teacher uses a projector to display slides: the students focus on the projected image, while the teacher switches out slides as needed. In this analogy:

  • The 32 KiB ROM space the CPU uses is the projection.
  • The slides are the banks.
  • The mapper is the teacher, swapping out banks in real time.

Mappers handle both reads and writes, and they work for both program data (PRG) and graphics data (CHR). Essentially, mappers are hardware built into the cartridge—some were designed by Nintendo, while others were created by third-party companies. The NES has over 400 types of mappers, each varying in how they handle mapping, banking, and additional features. A major portion of ‘late-game’ NES emulator development involves adding support for more mappers to improve compatibility with a broader range of games.

Conclusion

I didn’t set out to dedicate an entire post to memory. When I first started building the CPU, I didn’t think much about it; I just declared a 64 KiB array and jumped straight into addressing modes and opcodes. Honestly, this approach works fine, and I’d even recommend starting with a flat memory model. But eventually, you’ll probably hit an architectural roadblock like I did. I had opcodes, addressing modes, and passing tests, but I couldn’t figure out how it all fit together. Studying memory and layout made everything click—memory mappings are what tie it all together.

There’s more left to explore in the CPU! In the next post, I’ll cover addressing modes and instruction handlers for opcodes. Stay tuned!

Emulation

Back to Blog