So I finally came around to the third part in the RustN64 series which focuses in getting the basic CPU functionality in place. Basically I tried to get the first instructions in the boot process to be decoded and executed.
I realized that I did not post a single update to my site in the whole of 2017 which is a bit frustrating. One reason why it has taken this long is due to lack of side-project time and the borrowing system in Rust which has been kicking my ass. Though from the project’s start until today I learnt a lot about Rust, including its borrowing system. In two years Rust has also gone through a few nice improvements in terms of developer usability, for instance impl Trait and match bindings.
I know this part of the project has taken far too long to get finished and that this writeup is also quite unstructured. This is because my understanding of the Rust language has changed quite a lot from when the project started and until today which means quite a lot of refactoring has been made and many different parts of the emulator has been redone or improved. Also, I wanted to get past some of the plumbing of the emulator and to some more interresting stuff that actually executes something.
Anyway, enough excuses, let’s get to some development. Last time we implemented some basic functionality for reading a game ROM and a PIF ROM into memory. This time I thought we’d get some N64 code to run and explore what parts of the N64 we’re trying to emulate.
How CPU Works
The CPU used in the N64 is a NEC VR4300 which is based on the MIPS architecture. Its datasheet can be found at http://datasheets.chipdb.org/NEC/Vr-Series/Vr43xx/U10504EJ7V0UMJ1.pdf. The CPU consists of two co-processors - CP0 is a memory management unit (MMU) which handles memory mapping stuff and CP1 is a floating point unit (FPU) which handles floating point calculations. The CPU is memory mapped to a bunch of peripherals through its data and address bus. The ones we’re interested in are the PIF ROM, the reality co-processor (RCP), the main memory, the game cart and the controllers.
The PIF ROM is basically the boot ROM of the system which always loads before the game boots. It sets the CPU into a known state and also does some checksum verifications. Initially we’ll execute the PIF ROM as it has been extracted from the N64 but we might as well simply emulate its functionality by programmatically setting the CPU state. That can however be done at a later time.
The RCP is one of the more interesting components as it’s basically the graphics and audio card of the N64. It handles 3D calculations sort of in the same way as shaders work on modern graphics cards. I’m not currently exactly sure how this will be emulated in RustN64 but I suspect some OpenGL/Direct3D/Vulkan/Metal will be involved. We’ll just have to see once we get there.
Overly simplified what the CPU does is read data (images, models, text, …) and instructions from the cart and execute them one after the other. But it also handles interrupts, memory mapping (including memory table lookups) and much more which we will have to tackle once we get there. At this point though I was just interested in getting something to run so the emulator, at this point, only reads and executes a few instructions and does some very basic memory mapping. But how does the CPU know where to start reading? How does it know at what memory address the very first instruction should be fetched and executed from? That’s where the power-on reset comes into play.
Power-On Reset
When the console is turned on a signal is sent to a the ColdReset pin in the CPU. This in turn triggers the power-on reset interrupt which is described in 9.2.1 in the CPU specification. This says that a number of registries should be initialized but it also triggers a Cold Reset Exception as described in 6.4.4 of the datasheet. This says that a special interrupt vector is triggered at location 0xFFFF FFFF BFC0 0000
in 64-bit mode and this is the location we’re interrested in and is assigned to the program counter (PC). Check out src/cpu/cpu.rs where this is done. We might have to come back to this to do some proper interrupt handling as the Cold Reset is an interrupt but this will do for now.
Virtual To Physical Address
So we know where to read the first instruction (0xFFFF FFFF BFC0 0000
) but what is actually located at that memory location? This is where the memory mapping and virtual to physical address mapping comes in to play. Depending on which state the CPU is in, one address can mean multiple things. One of the things that the Cold Reset interrupt does is it sets ERL=1 in the status registry of the CP0. If we then read section 5.2.4 in the datasheet it states that:
The processor operates in Kernel mode when the Status register contains one or more of the following values:
- KSU=00
- EXL=1
- ERL=1
The same section in the specification says that 0xFFFF FFFF BFC0 0000
is in a memory segment called CKSEG1 which is between 0xFFFF FFFF A000 0000
and 0xFFFF FFFF BFFF FFFF
.
The CPU is now in a state where the PC=0xFFFF FFFF BFC0 0000
and ERL=1 which means that we should read memory from the memory segment called CKSEG1 and looking at table 5-4 in section 5.4.2 the physical address to use is between 0x0000 0000
and 0x1FFF FFFF
. Since we’re at 0xFFFF FFFF BFC0 0000
we subtract 0xFFFF FFFF BFC0 0000
from 0xFFFF FFFF A000 0000
(the base address for CKSEG1) which gives us 0x1FC0 0000
and this is the value that is sent to the address bus to the CPU’s peripherals.
If we now take a look at a memory map of the N64 we can see that 0x1FC0 0000
is actually the PIF Boot ROM which is just what we expect. This is where the N64 system gets set up to a controlled state before the game takes over.
ROM Byte Order
When reading each byte and each word from the ROM they can be read in three different ways, each of which is described at https://en.wikipedia.org/wiki/List_of_Nintendo_64_ROM_file_formats.
- Big endian
- Little endian
- Byte swapped
Different ROM formats each have their own way of handling byte order but how can we determine which byte order to use? We could simply assume that the file is always pre-encoded as, for instance, big endian and discard any other file format. If the ROM should be in some other format then it would have to be re-encoded to big endian before it could be used in RustN64. This is however very limiting and would mean that it would be up to the user to re-encode the ROM or find the same ROM but with a different byte order. Not very practical. Another way would be to look at the first byte of the ROM. It’s actually constant which means it’s interpreted differently depending on the byte order. 0x37
means it’s byte swapped, 0x40
is little endian and 0x80
would be big endian. The handling for this can be found in src/cart.rs but it currently only handles byte swap as my test ROM is byte swapped. My implementation also transforms the whole file upfront, which might not be the most efficient way of doing it but that can be changed later.
Instruction Decode
If we have a look at the datasheet in section 3.1 that there are three instruction formats:
- I-Type (immediate)
- J-Type (jump)
- R-Type (register)
They are all 32 bit formats with the first 6 bits being the opcode and the rest is format dependent. This means that the decoding of these instructions is actually quite easy. I wrap the instruction word in an Instruction
enum which wraps an instruction format; basically a glorified u32
. See src/cpu/instruction.rs. The decoding parts is just a matter of matching the high six bits to the instruction details in chapter 16 of the datasheet.
I’ve started implementing a handfull of instructions that starts executing as soon as the the emulator is booted. Check out src/cpu/cpu.rs. It successfully executes a few instructions before the system apparently gets stuck in an endless loop. My feeling is that interrupts and exceptions need to be implemented for the emulator to continue its processing.
Next Time
I really hope that the next part of the series won’t take as long as this one did. It will be a fun part where I plan to implement a debugger, which I suspect will be needed when implementing interrupts. I also got very inspired by nes-rs where bgourlie implemented a debugger that works over web sockets with a webpage which displays a debug view with the memory layout and instruction list. Very cool! Check it out! It does use ELM for the frontend whereas I plan to use yew which is basically the same thing but all in Rust.
After the debugger we’ll implement interrupts which hopefully will get us to the next step of the emulation. See you then!