The N64 is by no means resource limited, so writing software for it in C is perfectly reasonable. One thing you must keep in mind, though: coding for the N64 requires extensive knowledge of both C and MIPS R4K assembly. However, assembly will only have to be used in small routines that initialize the N64 (or handle exceptions). You also have to be familiar with the GNU toolchain (binutils and gcc namely).
This tutorial is, obviously, for a UNIX environment. If you're lazy, there's a link to an automated build script at the bottom of the page!
Initial Steps[]
The version of GCC this document will be covering is a few years old, and the newer compilers are unable to compile it for whatever reason. To combat this, grab a GCC compatibility compiler (3.x should do; they're usually in the repos). I'll be using GCC 3.2.3 ('gcc32') for my examples.
Choose a directory that you want the compiled binaries and set this to $DIR. For example,
export DIR=`pwd`/root
Next, set $GCC to the compiler you are going to use (the compatability one). For example,
export GCC=gcc32
Building binutils[]
Download the appropriate version (2.16 - link in the links section), extract, change into that directory and run the following commands:
./configure --target=mips64 --prefix=$DIR --program-prefix=mips64- --with-cpu=mip64vr4300 make CC=$GCC make install
Hopefully everything went well, and now you'll have a binutils package targeting MIPS.
Compiling GCC[]
GCC needs to use some of the binaries that you compiled above, so do the following:
export PATH=$PATH:$DIR/bin
This will make GCC be able to find them.
As with binutils, download the appropriate version (3.3 - link in the links section), extract, change into the created directory, and run the following commands:
./configure --target=mips64 --prefix=$DIR --program-prefix=mips64- --with-cpu=mip64vr4300 -with-languages=c --disable-threads make CC=$GCC make install
Coding examples[]
While coding C for the N64 is no different than any other platform (except that you don't have any libraries at your disposal), you may, at times, have to write to memory mapped registers. The following example (which utilizes DMA) demonstrates this:
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **
** N64 DMA **
** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
typedef struct
{
/* Pointers to data */
void *ramp;
void *romp;
/* Filesizes (8-byte aligned) */
u32 size_ramrom; /* RAM -> ROM */
u32 size_romram; /* RAM <- ROM */
/* Status register */
u32 status;
} DMA_REG;
/* DMA status flags */
enum
{
DMA_BUSY = 0x00000001,
DMA_ERROR = 0x00000008
};
/* DMA registers ptr */
static volatile DMA_REG * dmaregs = (DMA_REG*)0xA4600000;
/* Copy data from ROM to RAM */
int dma_write_ram ( void *ram_ptr, void *rom_ptr, u32 length )
{
/* Check that DMA is not busy already */
while( dmaregs->status & DMA_BUSY );
/* Write addresses */
dmaregs->ramp = (u32)ram_ptr & 0x00FFFFFF; /* ram pointer */
dmaregs->romp = (u32)rom_ptr & 0x1FFFFFFF; /* rom pointer */
/* Write size */
dmaregs->size_romram = length - 1;
/* Wait for transfer to finish */
while( dmaregs->status & DMA_BUSY );
/* Return size written */
return length & 0xFFFFFFF8;
}
You may also have to use inline assembly a fair bit. The function below sets a breakpoint on a region of memory:
enum
{
BREAKPOINT_READ = 1,
BREAKPOINT_WRITE = 2
};
/* Set breakpoint */
void bp_set ( u32 addr, u8 flags )
{
addr &= 0x3FFFF8; /* assuming lower 4MB, also doubleword */
flags &= 0x03; /* only lower two bits */
addr |= flags;
asm("mtc0 %0, $18\n" /* WatchLo */
"mtc0 $zero, $19\n" /* WatchHi */
::"r"(addr));
}