Skip to content

Writing a NES emulator in Javascript: Part 3 (booting the first code)

  Note: This is part of a series of posts on writing a NES emulator in Javascript. You can follow the project on github here.

We need a few things to get NES code booting. One of these will be a NES ROM. Doing this I’ll be using the nestest ROM due to copyright restrictions surrounding using an official Nintendo ROM.

iNES format

The format used by most emulators for reading games is the iNES format, which contains the following sections:

Header (16 bytes)
Trainer, if present (0 or 512 bytes)
PRG ROM data (16384 * x bytes)
CHR ROM data, if present (8192 * y bytes)
PlayChoice INST-ROM, if present (0 or 8192 bytes)
PlayChoice PROM, if present (16 bytes Data, 16 bytes CounterOut) (this is often missing, see PC10 ROM-Images for details)
(source: nesdev wiki)

With the header being formatted as follows:

0-3: Constant $4E $45 $53 $1A (“NES” followed by MS-DOS end-of-file)
4: Size of PRG ROM in 16 KB units
5: Size of CHR ROM in 8 KB units (Value 0 means the board uses CHR RAM)
6: Flags 6
7: Flags 7
8: Size of PRG RAM in 8 KB units (Value 0 infers 8 KB for compatibility; see PRG RAM circuit)
9: Flags 9
10: Flags 10 (unofficial)
11-15: Zero filled
(source: nesdev wiki)

Since I have really no idea what to do with the CHR ROM data yet, it’s the PRG_ROM data we’re interested in, as this contains the executable game code. The nestest ROM header looks like this (hexedit output):

00000000   4E 45 53 1A  01 01 00 00  00 00 00 00  00 00 00 00

The bytes 0x4E, 0x46 and 0x53 are the “NES” string. We can see that bytes 4 and 5 are 1, which means the PRG_ROM will be 16KB and the CHR ROM will be 8KB. Therefore the PRG_ROM starts at 0x10 (we don’t have a trainer file) and continues for 16384 bytes. The quick and dirty ROM parser I wrote does the trick of grabbing the data and putting it in a UInt8Array:

    this.rom = new Uint8Array(this.rom_buffer);
    this.prg_r1_size = this.rom[PRG_ROM_SIZE_INDEX];
    this.chr_size = this.rom[CHR_ROM_SIZE_INDEX];
    const ROM_SIZE = ROM_MULTIPLE_SIZE*this.prg_r1_size;
    this.prg_rom = new Uint8Array(this.rom_buffer.slice(PRG_ROM_START_INDEX, ROM_SIZE));

Will it boot?

The NES does some set-up on each reset that gets flags and registers in the right place to begin executing. One thing to remember is that the program ROM is mapped to a region in memory. From the NESdev wiki it seems that the PRG ROM is loaded at 0xC000. In the memory mapping function this is simulated as follows:

fetch(addr) {
if(addr >= 0 && addr <= 0x7FF) { 
  [..] 
} else if (addr > 0x4020 && addr < 0xFFFF) { 
  if (addr >= 0xC000 && addr < 0xFFFF) {
    return this.rom[(addr-0xC000)];
  }
}

The fetch() function pulls a byte out of memory, and if the requested byte index is from 0xC000 to 0xFFFF we want to return the corresponding bytes from the program ROM.

One small problem

Since the code is only for the CPU, if the ROM attempts to use the PPU the emulator will fail. There’s a small workaround. The default reset vector for the emulator is $C004 but while using the nestest ROM there’s an automated mode that starts at $C000 (thanks to this forum post). For this reason, it was necessary to hardcode the reset vector as $C000 to get things rolling. Since I wrote the last part of this article, the structure of the code change, and the giant switch statement was removed. Here’s what the code to get instructions executing looks like now:

  while (this.running == true) {

    let opcode = this.fetch(this.registers.PC);
    optable[opcode].op.bind(this).call();
    [...]

And things are actually booting! This ended up being a hodgepodge of an article due to being written over a long period in which huge structural changes were happening to the code. In the next article I’ll discuss some of the changes I’ve made to improve execution, and how I went about implementing the opcode table.

Check out the current code here.

Published inemulatorjavascript

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *