Foreword
This is the first part in my series on writing a NES emulator in JavaScript.Since the NES game console also had had a 6502 compatable CPU under cover, we can utilise a lot from my C64 JavaScript emulator on GitHub.
So, in this post I will take my source from my my C64 JavaScript emulator on Github and strip it down to a minimum. This strip down version will be our starting point for our JavaScript NES emulator series.
But, before we start with our stripping down exercise in this post, we be looking at the NES cartridge image format. I think this will be a good starting point.
The cartridge image will be targeting in this series will be PaperBoy.
I will end this post by single stepping through a couple of instructions of the NES cartridge.
I would have preferred that by the end of this post be able to boot a NES cartridge at full speed and then just look at some memory locations if the appropriate welcome was written. However, being new to the whole NES arhitecture I wouldn't be able to interpret at this point the contents of screen memory to tell whether the correct startup message was written.
So for now single stepping as end goal for this post will need to do.
The iNES file format
Most of the classic NES games you can get for download on the Internet in a format called iNES.The iNES format was originally created by Marat Fayzullin when he wrote one of the first NES emulators.
Ever since then, the iNES format has become in someway the defacto standard for providing NES games for NES emulators.
The most important part of the iNES file is the 16 byte header at the start of the file.
Here is a very brief description of the 16 byte header:
- 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-15: Zero filled
I would like to point out that sometimes bytes 9-15 is used to embed extra flags, which I will not be covering in this post.
I will also not be covering in detail what flags Flags 6 and Flag 7 contains. I would, however, like to mention that the upper nybbles of Flags 6 and Flags 7 represent the mapper number. More on mappers later on.
Following the 16 byte header is the actual cartridge data. The data starts with a number of 16KB PRG ROM segments as specified by byte 4 of the header.
Following the PRG ROMS is a number 8KB CHR segments as specified by byte 5 of the header.
Dissecting the PaperBoy cartridge image
As mentioned previously, we will be using PaperBoy as the target game cartridge for this blog series.
So, let us start right away by looking at the 16 byte header of the PaperBoy game cartridge image:
You can see in the ASCII section the header starts physically with the word "NES".
The next piece of relevant information is that byte 4 indicates that this cartridge contains 2 blocks of 16KB program ROM. This is equals 32KB of total program ROM. The NES console have allocated memory addresses 8000h-ffffh for program ROM, So these two program ROMS segments can fit within the memory address space without an issue.
We are, however, not so lucky with the character ROMS. The NES console can only accomodate 8KB of character ROM. In Byte 4, however, we see that this game has 4x8KB character ROMs!
This is where mappers come in, as mentioned earlier on. A mapper is a chip that basically switch sections of ROM in and out of view, very similar to what the makers of the Commodore 64 did with banking.
It is interesting to note that this mapper chip doesn't reside within the NES console, but rather within the game cartridge itself!
Putting the mapper chip within the cartridge provides some freedom by not being restricted to a particular banking scheme, while keeping the overall architecture of the NES console simple.
These mapper functions will need to be emulated and in effect we will need to know which mapper we are dealing with when emulating a NES game cartridge.
The iNES file format gives us this information via the high nibbles of byte 6 and 7. The number formed by these nibbles is an index to a list of mappers origanlly provided by Marat Fayzullin. Here is a very short extraction of the list:
iNES Mapper Number | Mapper Name |
---|---|
0 | NROM, no mapper |
1 | Nintendo MMC1 |
2 | UNROM switch |
3 | CNROM switch |
4 | Nintendo MMC3 |
5 | Nintendo MMC5 |
Let us now see which mapper our game cartridge uses.
The upper nybbles of byte 6 and 7 forms the number zero. Number 0 corresponds to NROM or no mapper, within the list.
This is very confusing, because with this mapper we will not have access to all CHR ROM banks.
This confusing mapper actually confirms other observations I had when trying to play PaperBoy on other NES emulators. All the intro screens would display fine, but when getting to the main game screen you would only see ascii characters displayed. The following screenshot taken from the John NES Lite emulator illustrate this:
We fix this behaviour by just modifying the PaperBoy game image with the correct mapper number.
From the list of mappers I found that CNROM (aka Mapper Number 3) is the correct one. This mapper only provide bank switching capability for CHR ROM banks.
Well, I think we got enough information from the header. Let us start with some coding!
Starting simple
As mentioned previously, I will be using code of C64 emulator as baseline, stripping out C64 specific stuff.
Stripping out the C64 stuff, we are only left with the following files:
Stripping out the C64 stuff, we are only left with the following files:
- Cpu.js
- Memory.js
- index.html
For now I have stripped out the screen canvas.
We also need to modify the logic for attaching an image as highlighted in red. In our C64 emulator we used this to attach a tape image. We need to modify the code to cater for a NES cartridge image.
The code behind the attach button within index.html looks as follow:
We also need to modify the logic for attaching an image as highlighted in red. In our C64 emulator we used this to attach a tape image. We need to modify the code to cater for a NES cartridge image.
The code behind the attach button within index.html looks as follow:
... <button onclick="attachCartridge()">Attach</button> ... function attachCartridge() { mymem.attachCartridge(document.getElementById('file').files[0], mycpu); } ...
And the implementation of attachCartridge within Memory.js is as follows:
this.attachCartridge = function(file, cpu) { var reader = new FileReader(); reader.onload = function(e) { var arrayBuffer = reader.result; var cartridgeData = new Uint8Array(arrayBuffer); var i = 0; var posInData = 0x10; for (i = 0x8000; i < 0x10000; i++) { mainMem[i] = cartridgeData[posInData]; posInData++; } cpu.reset(); alert("Cartridge attached"); } reader.readAsArrayBuffer(file); }
We pass the file that the user selected as parameter and initiate an asynchronous read at the end of the method. When reading is finished the onload callback is invoked loading the contents of the file within the memory array between locations 8000h and ffffh.
Everything the emulator needs is contained within the cartridge image and is therefore no need to load additional ROMS in the background as we did with our C64 JavaScript emulator.
Because we don't need to load additional ROM files in the background, we strictly speakig don't need to serve our emulator from a web server. You should be able to open the emulator directly from your local file system.
There is one final change we need to make to the writeMem function within Memory.js:
this.writeMem = function (address, byteval) { if (address >= 0x8000) return; mainMem[address] = byteval; }
With this code we avoid writes within the program ROM region which is within the region 8000h and ffffh.
You might be wondering why would a program bother to write to the ROM region. Well, you can expect this behaviour when working with NES mappers.
A NES mapper chip intercepts writes to the ROM region and takes the value written as an indication which ROM bank should be made visible.
Since the game we emulate only have bank switching enabled for CHR ROM, we will not worrying about implementing bank switching for now. We only need to ensure that writes to the Program ROM region don't override the ROM contents, which we did with the above if statement.
This is the only coding needed for this post!
A Test Run
As mentioned previously we will only be single stepping in this post.To get a feel on what instructions gets executed here is quick list of the first couple of instructions you will encounter when single stepping:
804a STA $2000 804d STA $2001 8050 STA $0b 8052 SEI 8053 CLD 8054 LDX #$ff 8056 TXS 8057 JSR $813e 813e LDA #$17 8140 STA $8a
Not very exciting, but we will get there!
In summary
In this post we had had look the iNES file format.We also created a stripped down NES emulator and managed to single step a couple of instructions of a game cartridge.
In the next post we will be running our NES emulator at full speed, intercepting writes to the IO ports of the video chip.
We will then examine these writes and determine what we should implement next for emulation.
Till next time!
No comments:
Post a Comment