Zero One wrote:
This question is more about how I would architecture the code, rather than the NES itself. The CPU has 2KB internal RAM, which I've represented in a Cpu class as:
Code:
unsigned char memory[0x0800]
However, the memory map on the wiki mentions mirrors, PPU registers, APU and I/O, and cartridge space. Would it be better to hold these in the Cpu class, or, for example, access the PPU registers through a Ppu class instead? Probably just some Read/Write methods in the PPU class that the Cpu class can call. And then the same for APU in an Apu class and cartridge space in a Rom class.
If you ever hope to handle mappers (i.e. emulate any game bigger than SMB1) you're going to have to implement an abstract memory system: your CPU instruction handlers need to be able to say "data = read(address)" or "write(address, data)" without any baked-in knowledge of whether "address" represents RAM, ROM, a memory-mapped register, or unmapped space (open bus).
If you're programming in C++ as it sounds like you are, the most natural way to do this is with virtual methods. Everything that can be mapped into the CPU address space (RAM, ROM, the PPU, even the CPU itself since it has memory-mapped registers) should inherit from an abstract "Memory" class (an interface, really) that has pure virtual "read" and "write" methods, and each class should implement those methods appropriately. RAM should simply read from and write to an array. ROM should do the same thing as RAM for reads, but ignore writes. The PPU and other devices with memory-mapped registers should do whatever is necessary for each register they have.
Next, you have a Bus class which contains an array of pointers to Memory objects (they have to be pointers, not references, because you'll be setting them dynamically depending on what kind of cartridge is plugged in) If your array of Memory pointers is called "handler", your read and write methods will look like this:
Code:
uint8_t Bus::read(uint16_t addr)
{
return handler[addr]->read(addr);
}
void Bus::write(uint16_t addr, uint8_t data)
{
handler[addr]->write(addr, data);
}
Your Bus class will also have a method to install Memory objects into the array of handlers.
If you're programming in another object-oriented language, the specifics (inheritance, interfaces, references, pointers) may vary but the principle is the same: you'll have a Bus class that maps an address to the correct object to read from or write to, and each class will implement its particular write and read behaviour via runtime polymorphism.