Writing a Makefile for the N64

When it comes to compiling your .n64 ROM file, the Makefile lies in the centre of it all. It is executed when you run the Make command in a UNIX-esque environment. This kick-starts the compiler and tells it were all the relevant files are and the way to put them all together.

There are three makefiles that come with most of the demos in the SDK, namely the Linux/UNIX version, the DOS version and the IRIX/SGI version. Because it’s the easiest to run using the setup outlined in the IDE, we’ll be focusing on the UNIX version (the one without an extension). Also, all the directories listed in this guide are the default ones from the IDE since we need to have some common ground to explain things properly.

That said, his guide will be written as if for complete newbies. So please bear with me if things get to be too basic. I’m writing this as I learn too. This isn’t the sexiest of topics for a tutorial, but it’s an essential part of the overall program structure and compilation process.

Very quick summary

It’s best if you don’t write your own makefile from scratch – just take one from a demo and adapt it to the needs of your project. You can still reference this guide so that you can understand how it works and how to manipulate it.

One thing that you can notice is that the Makefile for a complex program like nusnake is not that different to a simple one like nu0. This means that once you have a makefile that works for you, you aren’t going to need to change it that much over the course of development.

General formatting tips

  • Lines are not character-delimited; ie each new line is a new action. No need for a semicolon or comma to separate actions or parenthesis/brackets to wrap around if statements.
  • If you want to make a new line continue what the previous one was saying, end the previous line with a backslash \.
  • Hash/pound/number/octothorpe sign (#) denotes a comment line.
  • Variables are ALLCAPS by convention, are set by VAR = value and are called by $(VAR) or ${VAR}
  • The way that makefile actions are structured is like this:
    target: dependency 1 [...]
    [TAB] action1
    [TAB] action2 [...]

    Where target is the file to be output by that command, dependency are the inputs to make the target, and action is the actual command that will be done. There are some variations to the meanings, but that is what it means in this context.

  • When running an active program command, parameters are set with a hyphen (-) followed by a single letter (eg D) and finally the content of the parameter.

The batch file

The batch file and the makefile are intertwined so it’s best to understand the code for the batch in order to get a foundation for the Make. The batch file is identical for every makefile so there isn’t much to learn with regards to editing it, only with understanding it.

set ROOT=c:\ultra

set gccdir=c:\ultra\gcc set PATH=c:\ultra\gcc\mipse\bin;c:\ultra\usr\sbin set gccsw=-mips3 -mgp32 -mfp32 -mfp32 -funsigned-char -D_LANGUAGE_C -D_ULTRA64 -D__EXTENSIONS__ set n64align=on set GCC_CELF=ON cd c:\Documents and Settings\Administrator\My Documents\Sync\nu0 make

Most of it is pretty self-explanatory, but I’ll go into a bit more detail as to what each line means and what it does.

set ROOT=c:\ultra

This sets the variable ROOT which will be used a few times outside the batch, in the makefile.

set gccdir=c:\ultra\gcc
set PATH=c:\ultra\gcc\mipse\bin;c:\ultra\usr\sbin

These two lines set two variables, gccdir and PATH. They are two environment variables used by MinGW to denote the path of gcc (our C compiler).

set gccsw=-mips3 -mgp32 -mfp32 -funsigned-char -D_LANGUAGE_C -D_ULTRA64 -D__EXTENSIONS__

This bit of code outlines the settings for compiling the code.

-mips3 sets the instruction set architecture (ISA) for the target chip as mips3. This means that it needs to issue instructions from level 3 of the MIPS ISA (64 bit instructions).

-mgp32 assumes that general-purpose registers are 32 bits wide.

-mfp32 assumes that floating-point registers are 32 bits wide.

-funsigned-char changes the default char type to unsigned.

-D_LANGUAGE_C specifies the language used in the code; in this case it’s C.

-D_ULTRA64 (unsure) probably enables some settings relevant to the Nintendo 64.

-D__EXTENSIONS__ (unsure) maybe this setting enables the compiler to use a set of defined file extensions.

set n64align=on (unsure) this line seems to appear on all batch files in this format, but I cannot find out what it means.

set GCC_CELF=ON is used in some ‘if’ statements in some Makefiles.

cd c:\Documents and Settings\Administrator\My Documents\Sync\nu0
make

These last two lines change the folder to that of the project and then rune the Makefile.


One strange thing that I found when compiling ROMs using this batch file is that most of it (except the last two lines) can be removed without causing problems. However, I would recommend leaving them in just in case they prove useful at some point.

The basics of C compiling

There are some basic steps to what the Makefile does. Essentially, it’s quite similar to the batch file in that it’s a series of command line instructions put into one file. This way, you don’t have to type it all out each and every time and instead just run the make command once.

Project components

Let’s look at the basics of compiling a project written in C first. With any project, you’ll have some C-code files (.c) and some H-Header files (.h). The C files will contain the bulk of the program, while the H files will contain shared variables and function prototype declarations.

Setup

The purpose of the Makefile is to simplify the individual actions of the compiler and save some time. To show this, let’s consider a simple non-N64 C program with three files: main.c, func.c and head.h.

Example file contents
/* head.h */
#include <stdio.h>
void print_function(void);
/* func.c */
#include "head.h"
void print_function(void) {
    printf("Hello world!");
}
/* main.h */
#include "head.h"
void print_function(void);
int main() {
    print_function();
    return 0;
}

Besides those three files, you have the Makefile whose purpose is to tell the compiler how they should be joined.

The first step to getting the output file is getting the Object file (.o). Object files are the compiled (machine code) version of the C file that has been input. Normally in terminal/cmd, you’d have to type in something like this to get the object files for each .c file:

gcc -I . -c main.c
gcc -I . -c func.c

gcc initiates the GCC program, -I . sets the current folder as the location of headers and -c [file] tells gcc to compile the [file].

Then you’d have to link the .o files together by typing in this:

gcc main.o module.o -o target_file

This uses gcc to link the two .o files together into the final output file called target_file.

Simple Makefile

Since typing the above three lines every time is a bit tedious, we can simplify it with the use of a makefile. Remember that the purpose of this file is to compile C files into O files, and then link the O files into the target file. To do this, we will end up with a makefile that looks something like this:

all: main.o func.o
	gcc main.o func.o -o target_file
main.o: main.c head.h
	gcc -I . -c main.c
func.o: func.c head.h
	gcc -I . -c func.c
clean:
	rm *.o

Let’s go through this bit by bit. Go back through the General formatting tips if you want to understand the syntax better.

The first line is the one that is triggered by default, ie just running ‘make’ in the command line without any parameters. all doesn’t really have any meaning, except to tell the compiler to build all of its dependencies; in this case main.o func.o. In order to do this, it looks further down the file for instructions on how to build them. After that, it performs the action in the 2nd line. This uses gcc to link main.o and func.o into target_file.

The third and fifth line make main.o and func.o from their respective C files and head.h, respectively. They’re pretty much identical to the command lines so there’s not much new to see here.

clean is just an action for the sake of cleanliness. rm *.o removes any file that ends with .o so that all we’re left with is the final file, target_file.

Gcc parameter reference

[Insert list of GCC settings] http://www.rapidtables.com/code/linux/gcc/gcc-l.htm

The N64 Makefile itself

The Makefile is run automatically by the last line in the batch file, so you could consider it an extension of such. It is important to remember that every file is different, so these are just going to be some guidelines on the basics of making one. Once you start building your own, you’ll have to adapt it to the needs of your specific project.

Common variables

There are a number of variables that are useful to set either to make the final compile line easier or because they are environment variables.

Quick tip: Variables that start with LC refer to CFLAGS and therefore relate to the compiler. Likewise, the same happens with variables related to the linker prefixed with LD.

  • APP – The .out (debug output) file.
  • CFLAGS – defined in PRdefs as $(LCDEFS) $(LCINCS) $(LCOPTS) $(GCCFLAG) $(OPTIMIZER)
  • CODEFILES – Your .c (C code) files.
  • CODEOBJECTS – Specifies the name of the .o (object) files, usually based on the .c files.
  • GCCFLAG –  Sometimes used to transmit a -M flag to the gcc and included in CFLAGS. Not used very often.
  • HFILES – Your .h (header) files.
  • LCDEFS – Used for debugging. It sets the symbol (function, variables etc) definition so you can see where they lie in memory.
  • LCINCS – Directories to include that contain header files used (current directory plus libraries).
  • LCOPTS – Compiler options.
  • LDIRT – the map file name. Usually defaults to APP (debug output file).
  • LDFLAGS – Some more options to add to the linker.
  • OPTIMIZER – can be set to -g (debug) or -O (optimise). Just another optional setting to add to the gcc.
  • TARGET – The final name of the ROM (eg. myfile.n64).

There are more variables defined in the PRdefs file, typically included at the top of every makefile.

PRdefs

include $(ROOT)/usr/include/make/PRdefs

The first active line included in every Makefile is to include the PRdefs file. PRdefs stands for pre-definitions, as it is the file which contains some of the required variable definitions commonly used in all N64 Makefiles.

# application name
CC	=	gcc
LD	=	ld
MAKEROM	=	mild

# library path
LIB = $(ROOT)/usr/lib
LPR = $(LIB)/PR

# include path 
INC = $(ROOT)/usr/include

# gcc option
GCCFLAG	=	-c -I$(INC) -D_MIPS_SZLONG=32 -D_MIPS_SZINT=32
CFLAGS	=	$(LCDEFS) $(LCINCS) $(LCOPTS) $(GCCFLAG) $(OPTIMIZER)

# common rules
COMMONRULES	= $(ROOT)/usr/include/make/commonrules

# for CELF
CELFRULES	= $(ROOT)/usr/include/make/celfrules

include $(ROOT)/usr/include/make/celfdefs

Most of the lines in the PRdef file are to set some of the variables that will be used in the rest of the Makefile. These are just some of the most commonly used ones, so it just saves time to include the PRdefs file rather than set them individually each time. Let’s go through each one of them:

CC	=	gcc
LD	=	ld
MAKEROM	=	mild

CC defines the C Compiler, LD defines the linker and MAKEROM specifies the ROM image creation tool. These are the names of various programs used in the compilation process as ‘actions’. As far as I know, it’s best not to overwrite them.

LIB = $(ROOT)/usr/lib
LPR = $(LIB)/PR

These two lines set LIB and LPR directories. This means that these are the directories where the N64 C library is located as well as all of the microcode definitions.

INC = $(ROOT)/usr/include

Similar to the above, this sets the directory for the include folder. This folder contains a whole bunch of .h header files that defines some useful variables and functions that the N64 can take advantage of.

Note that this is a parent directory of that where PRdefs is located, so it can also be used for some the common makefile ‘includes’.

GCCFLAG	=	-c -I$(INC) -D_MIPS_SZLONG=32 -D_MIPS_SZINT=32
CFLAGS	=	$(LCDEFS) $(LCINCS) $(LCOPTS) $(GCCFLAG) $(OPTIMIZER)

This is where things get a bit complicated in the PRdefs file. GCCFLAG and CFLAGS are environment variables for settings to be used in the compilation process.

For the first line, -c tells the gcc to compile, but not link. -I$(INC) tells gcc to include INC (defined in a previous line). -D_MIPS_SZLONG=32 and -D_MIPS_SZINT=32 both define a macro to be used by the preprocessor during compiling. The first case sets the size of a Long data type to 32 bits and the second sets the size of an Int data type to 32 bits as well.

The second line just concatenates a few settings and puts them together just for clarity’s sake. These can be defined later on.

COMMONRULES	= $(ROOT)/usr/include/make/commonrules
CELFRULES	= $(ROOT)/usr/include/make/celfrules
include $(ROOT)/usr/include/make/celfdefs

These three lines serve a very similar purpose. Even though the first two are variable definitions and the third is an include, they all point to files in the same folder as PRdefs. Not much to say about these files except that they contain a few more definitions on how to compile the elf file.

NuSys

If you want to include nusys in your project, you’re going to want to include the following lines in to load the library:

N64KITDIR    = c:\nintendo\n64kit
NUSYSINCDIR  = $(N64KITDIR)/nusys/include
NUSYSLIBDIR  = $(N64KITDIR)/nusys/lib

This only sets variables to store the directory of the library. To include them, you need to call NUSYSINCDIR and NUSYSLIBDIR at the linking stage.

The active Makefile lines

Now that we have the variables set and out of the way, we can focus on the lines in the Makefile that actually get things done.

Most of what you might need is just the makerom/mild and the linker/ld, but there are a few other actions that you might want to use.

Makerom or mild

This is the compilation tool. Its purpose is to convert all of the C code files (.c) into object files (.o). The way it is usually represented is by the variable call $(MAKEROM).

It has whole bunch of options that we’ll go through one at a time.

$(MAKEROM) [-D] [-I] [-U] [-d] [-m] [-o] [-b] [-h] [-s] [-f] [-p] [-r] specfile

$(MAKEROM) calls to the variable MAKEROM, which is defined by default as mild in PRdefs and runs mild.exe.

Now let’s go through each of the individual parameters:

Note that most of them aren’t used that often; the only ones that are essential to know about are -I, -r and -e. And even then, it all depends on context. You might not need all for your specific project.

-D defines a macro to be sent to the preprocessor. This means that you can define constants in the Makefile if you so wish. For example, you can use -Dname to declare name, or -Dname=value to define it with value. Look at the GCCFLAG line in PRdefs

-I is for includes. This includes the directory for header files. You’re probably going to want at least two of these; one for the N64 OS, another for the project folder and any others for additional libraries. For instance, -I. includes the current directory (where the Makefile is located). -I$(ROOT)/usr/include/PR is used for the N64 OS, and -I$(NUSYSINCDIR) is used for the nusys library; where NUSYSINCDIR is defined as c:\nintendo\n64kit\nusys\include.

-U is for undo. It disables any previously defined macro used by -D.

-d is for verbose. This means that instead of giving out the bare minimum, the compiler will show evactly what is going on in every stage, as well as leaving all temporary files in place rather than deleting them. Useful is you want to know what is going on, but produces a lot of clutter if you leave it on.

-m prints out the linker map. This shows where each object is located in memory, and what it contains. Can be used for debugging.

-o Disables checking of overlapping sections. Enable this if you are testing around those areas. Not used too often.

-b replaces the default Boot file (/usr/lib/PR/Boot) with one of your choosing. Not recommended to use.

-h overrides the default ROM header filename (/usr/lib/PR/romheader). Again, it is recommended that you leave this as is.

-s is the ROM’s size, in megabits. Used for burning the game on a real cartridge, so not necessary unless that’s what you want.

-f is for fluff. When making a ROM file with the -s setting, you need to use -f to pad out the gaps in memory to make it fill the defined size completely.

-p overrides the pif bootstrap filename. Much like -b and -h. Again, it is not recommended to change this.

-r is the output name of the rom file. Default is rom.

-B is used for the N64DD to coordinate what gets started upon boot – the cartridge or the DD game. Since we’re not going to be using the N64DD, this is not important.

-e is used to name the .out file, used for debugging.

spec tells the compiler to load the segment data from the spec file. More on this in another guide.

Linker or ld

The linker is used to convert all the .o files into a general .o relocationable object file. This is then used as the input for the mild makerom program. It is run from a variable called $(LD) which has a value of ld and runs ld.exe.

$(LD) [-o $(CODESEGMENT)] [-r $(CODEOBJECTS)] [$(LDFLAGS)]

The way it’s set up on most file is like this. CODESEGMENT is the name of the output, normally CODESEGMENT.o. CODEOBJECTS are the .o files that are input to the linker. LDFLAGS are all the other options, mostly libraries that need to be included.

-o is the output file name for the linker.

-r is another output name, but this time for all the individual .o files. This is optional.

-m outputs a symbol map file.

The rest of these are usually contained within a $(LDFLAGS) variable:

-l The name of the library file to include.

-L is much of the same, but denotes the location of a directory of library files.

Makemask

This is a seldom used program that outputs a cartridge-ready file from the already-present ROM file obtained from the mild program. It has some very simple syntax.

makemask $(TARGETS)

$(TARGETS), like with mild, is the name of the output ROM file. This is the only input.

Conclusion

The Nintendo 64 has a lot of peculiarities that are very easy to break. So like with the rest of development, it’s best to use a demo file as a starting point and just add/remove files and change the settings as needed.

Remember that these are the basic steps for the N64 Makefile process:

  1. Convert .c files to .o files
  2. Link the .o files with the libraries into a single .o file
  3. Convert that .o file into a .n64 (ROM) file
  4. (optional) make it cartridge-ready with makemask.

And with that you should be ready to work with your Makefile.

Search

Subscribe to the mailing list

Follow N64 Squid

  • RSS Feed
  • YouTube

Random featured posts