Nintendo 64 console with EverDrive cartridge

Nintendo 64 Part 2: What Is a Bootloader?

, Nintendo 64, Programming

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.

Memory Map

I found the page En64: N64 Memory.

The N64 uses virtual memory and has four segments, selected by the top three bits:

RangeDescription
0x00000000-0x7fffffffmapped
0x80000000-0x9fffffffunmapped, cached
0xa0000000-0xbfffffffunmapped, uncached
0xc0000000-0xdfffffffmapped
0xe0000000-0xffffffffmapped

“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?

RangeDescription
0x00000000-0x03EFFFFFRDRAM Memory
0x04000000-0x040FFFFFSP Registers
0x04100000-0x041FFFFFDP Command Registers
0x04200000-0x042FFFFFDP Span Registers
0x04300000-0x043FFFFFMIPS Interface (MI) Registers
0x04400000-0x044FFFFFVideo Interface (VI) Registers
0x04500000-0x045FFFFFAudio Interface (AI) Registers
0x04600000-0x046FFFFFPeripheral Interface (PI) Registers
0x04700000-0x047FFFFFRDRAM Interface (RI) Registers
0x04800000-0x048FFFFFSerial Interface (SI) Registers
0x05000000-0x05FFFFFFCartridge Domain 2 Address 1
0x06000000-0x07FFFFFFCartridge Domain 1 Address 1
0x08000000-0x0FFFFFFFCartridge Domain 2 Address 2
0x10000000-0x1FBFFFFFCartridge Domain 1 Address 2
0x1FC00000-0x1FC007BFPIF Boot ROM
0x1FC007C0-0x1FC007FFPIF RAM
0x1FD00000-0x7FFFFFFFCartridge Domain 1 Address 3
0x80000000-0xFFFFFFFFExternal SysAD Device

So when I read about how the bootloader works, I’ll refer to this map to make sense of it.

Boot Process

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).

Internet Archive copy of forum thread

Ok, so my understanding is this (more or less):

  1. 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.
  2. 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.
  3. 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.
  4. 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.

ROM Layout

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 save_db.txt https://github.com/N64-tools/ED64/blob/develop/docs/rom_config_database.md

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,

Developer Override

The developer ID ED will 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 TypeSizeNumber of Games (USA)
Controller Pak32 KiB201
Cartridge EEPROM512 B63
Cartridge EEPROM2 KiB12
Cartridge SRAM32 KiB14
Cartridge Flash128 KiB14

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.

MakeROM

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.

Conclusion

My plan for now…

  1. Find a Nintendo 64 SDK,
  2. Build a sample program,
  3. Test the ROM with an emulator and on real hardware,
  4. Make changes.