Nintendo 64 Part 2: What Is a Bootloader?
Remember, this is a rambling journey of my experiences figuring out N64 development. Not, in any sense, a guide for other people.
Talking on the N64 discord I guess someone figured out what I was doing, and I see this message:
@Vanadium are you trying to build your whole toolchain from scratch?
Uhh, yes. I am not about to go back and start using GCC 2.x again, and I don’t want to do my development inside a Windows XP image running some devkit of dubious origin. Or maybe I just didn’t know about those things. (And what use is half-crazy, when you can go full crazy?)
Zero to ROM Image
Now that I have a basic toolchain, the next step is to figure out how to create a ROM image. So, what do the docs say?
The docs don’t say much, it turns out. The official SDK gives you a tool which just spits out a ROM image, either for the development hardware or for release hardware. So let’s gather the information we need.
I found the page En64: N64 Memory.
The N64 uses virtual memory and has four segments, selected by the top three bits:
“Unmapped” to me implies that there’s a 29-bit physical address space here where our RAM and peripherals go. So, what does physical memory look like?
|DP Command Registers|
|DP Span Registers|
|MIPS Interface (MI) Registers|
|Video Interface (VI) Registers|
|Audio Interface (AI) Registers|
|Peripheral Interface (PI) Registers|
|RDRAM Interface (RI) Registers|
|Serial Interface (SI) Registers|
|Cartridge Domain 2 Address 1|
|Cartridge Domain 1 Address 1|
|Cartridge Domain 2 Address 2|
|Cartridge Domain 1 Address 2|
|PIF Boot ROM|
|Cartridge Domain 1 Address 3|
|External SysAD Device|
So when I read about how the bootloader works, I’ll refer to this map to make sense of it.
There’s an analysis at Retro Reversing: Introduction to Nintendo 64 Bootcode. It seems to be fairly detailed, with assembly and some decompiled C, but I don’t find it easy to understand.
Another analysis, mupen64plus - SoftResetNotes.wiki, describes in more plain terms (English) what the boot code is actually doing. During boot, the PIF (peripheral interface) prevents the CPU from starting until the PIF has finished doing its job—and it sounds like that job is to verify that the game is an authentic, licensed game. That means communicated with the CIC chip on the cartridge. If you can’t copy the CIC chip, you can’t pirate games or make unlicensed games.
Copying the CIC chip is not what I’m worried about, though. The CIC chip is undoubtedly emulated by the flashcart that I’ll use for testing. What I do want to know is what I need to put in the ROM, the locations in ROM where I need to put data, and the address in memory where that will be loaded.
On , a user named radorn on the ASSEMblergames forum writes,
The N64 uses a security scheme in which there are several components involved.
Apart from the tabs in the cartridge bay, the electronic measures consist of this.
The console has the PIF, which stands for Peripheral InterFace, for which there are two kinds, PIF-NUS and PIF(P)-NUS (NTSC and PAL respectively), which take care of controlling the peripherals, like the controllers, and one very special kind of peripheral, the CIC microcontrollers located on the cartridges.
These babies not only have NTSC and PAL variants, but also there are at least 5 variants for each zone, and each game works with ONE and ONLY ONE of these. There's some communications going on between the PIF the CIC and some checksumming involving the game's ROM that should come out right for the game to boot (some games with extra protection also make additional checks during the game, like PD, and will degrade the experience if this fails, which happens with bootloaders which can only patch the initial check for booting).
Ok, so my understanding is this (more or less):
- The PIF verifies that the cartridge is authentic by communicating with the CIC on the cartridge. This involves a value called the “CIC seed”, which is different for each version of the CIC.
- Once verified, the PIF releases the NMI on the CPU, which allows it to boot. The CPU starts running code at
0xBFC00000, which is mapped to ROM in the PIF.
- The code in the PIF initializes the system, loads 1 MiB of game data from cartridge memory to physical address
0x00000400, and verifies that the cartridge data has the correct checksum.
- Jump to start.
Now we’re getting somewhere. It looks like the first 4 KB of the cartridge ROM contains header fields and a bootloader, and we need to make sure that our checksums match.
Take a look at En64: ROM for an explanation of the ROM format. We are only interested in whatever we need to get this to boot correctly.
Apparently, the header is always the same:
0x80371240 in big endian. The ROM file itself is… just an image of the cartridge ROM, and the header of a ROM file is just the header of the actual ROM data that would be found on a real cartridge. This is a bit different from how NES ROM files work (see Nesdev Wiki: INES). For a NES ROM file, there is a header that describes the memory layout, the type of mapper chip, and what extra functionality is on the cartridge—this includes a description of any battery-backed SRAM that the NES cartridge has for saved games. The N64 rom files don’t have this header, so I don’t know how an emulator or flashcart knows how much EEPROM or SRAM to give to a game. Asked a question about it on Discord:
Me: How does an N64 emulator (or a flashcart) know what kind of memory a game has for saved data? Is there something in the ROM file that says, “hey, I have 2K EEPROM?”? Or does an emulator just give every game a meg of save space?
Answer: Usually a database
Answer: With the EverDrive, there’s a
Apparently, the EverDrive uses a database of CRC values to match ROM images with the save types, but there is a way to override it. The docs read,
The developer ID
EDwill cause the config to be loaded from the ROM header (one byte at offset
0x3F) instead of using the save database built into the ED64 menu.
And there are five save methods, which you can see listed on the μ64: Game Save Method List page.
|Save Type||Size||Number of Games (USA)|
|Controller Pak||32 KiB||201|
|Cartridge EEPROM||512 B||63|
|Cartridge EEPROM||2 KiB||12|
|Cartridge SRAM||32 KiB||14|
|Cartridge Flash||128 KiB||14|
According to Wikipedia: Nintendo 64 Accessories,
Upon launch, the Controller Pak was initially useful, and even necessary for earlier Nintendo 64 games. Over time, the Controller Pak lost popularity to the convenience of a battery backed SRAM or EEPROM found in some cartridges. Because the Nintendo 64 uses a Game Pak cartridge format that allows saving data on the cartridge itself, few first party and second party games use the Controller Pak. The vast majority are from third-party developers.
I’m not going to really figure this out now—exactly how to override the EverDrive 64 save type. If I implement save data, I’ll try to keep the data within the 512 bytes. Why? Maybe this gives me options for making a real cart down the line. Maybe that’s just a fantasy.
The tool in the Nintendo 64 SDK for making ROM images is called, appropriately, MakeROM. This tool takes the object files (the C compiler output), links them into a ROM image, and adds the bootloader code. From reading the docs, this appears to be a front-end to a linker, and it uses an input called a “spec file” to generate a link script.
MakeROM is associated with a tool called Mild (or
mild.exe). I’m not exactly sure what the relationship is between MakeROM and Mild.
There is an open-source alternative to MakeROM or Mild called Spicy. It’s written in Go and creates a linker script from a Nintendo 64 ROM spec file, and presumably does the other steps necessary to create a ROM image from object files.
My plan for now…
- Find a Nintendo 64 SDK,
- Build a sample program,
- Test the ROM with an emulator and on real hardware,
- Make changes.