In the first post, we took a look at the C code that defines what our game does (which, for now, is just writing “Hello, World!” on the screen). This time, we’re going to explore the toolchain that turns our code into a runnable ROM and the startup code that initializes the NES into a known state.
Taking a look at our Makefile, we see that we’re running a few different commands to build our ROM:
The first command invokes cc65, a C compiler, to compile our C code into 6502 assembly.
-Oi tells the compiler to optimize the generated code and to inline more aggressively, as we want to avoid making slow function calls.
--add-source includes our original C source as comments in the resulting assembly code (hello_world.s) so we can poke around if we’re curious.
Finally, we’re invoking ld65, a linker, which combines our object files with its implementation of C standard library for the NES (notice the
nes.lib in the command) into an executable NES ROM.
-o hello_world.nes argument tells the linker that we’d like our executable to be named “hello_world.nes”, while the
-C hello_world.cfg tells the linker to use a configuration file that defines how memory is laid out, amongst other things. We now have an executable NES ROM!
We’ve created a configuration file for
ld65 to do three things:
Defining the memory areas is as simple as defining a start address and a size and assigning it to a name:
The first two areas we create—
RAM—will define sections of the NES CPU RAM, while the rest will define sections of the cartridge ROM chips.
ZP as an explicit area that represents the zero page we talked about in the last post.
It starts at memory address
0x0000 and is 256 (
0x100) bytes in size.
We’re going to skip over the next two pages for now (we’ll use one for the C stack and make specific use of the other in later tutorials) and define the remaining five pages (
0x500 or 1280 bytes) as general-purpose RAM.
We’re going to set aside sixteen (
0x10) bytes at the beginning of our ROM file to hold the iNES header expected by NES emulators at the beginning of
Later in this tutorial we’ll see where that is populated.
file = %0 parameter tells the linker that the header will end up in the file passed in via the
-o option, and
fill = yes says that if we don’t use the entire area, fill the remainder of the space with zero bytes.
The NES CPU expects the PRG ROM in the cartridge to map to the addresses
This is 32 KiB of address space, though cartridges may have as little as 16 KiB of PRG ROM as much as 1 MiB or more!
To be able to make use of more than 32 KiB of PRG ROM, cartidges have a mapper that can control which chunks of PRG ROM are mapped to which CPU addresses.
For this extremely simple game, we’re going to use the simplest mapper: NROM (more specifically the NES-NROM-128).
This mapper will map a single 16 KiB bank of PRG ROM to CPU addresses
0xbfff, which will also be mirrored in
You’ll notice that we don’t make the
PRG area take up the full 16 KiB (
0x4000 bytes), but six bytes short of that at
We’re explicitly setting aside six bytes of space for three 16-bit addresses that the CPU expects in a fixed space at the end of the cartridge space (i.e.
These addresses, which we define in the
VECTORS area, point to interrupt vectors—code that is run when interrupts are triggered—for three different types of interrupts expected by the CPU.
Two of these interrupts we don’t care about just yet, but the other is the
reset vector, which will point to our startup code.
Finally, we create an area for our CHR ROM, which will contain our pattern tables.
The CHR ROM will be mapped to PPU memory addresses
0x1fff, though the cartridge may contain much more than 8 KiB (
0x2000 bytes) of CHR ROM (and/or CHR RAM) which is mapped to these addresses by the mapper.
Our simple NROM mapper just expects 8 KiB of CHR ROM which will map directly to the 8 KiB of PPU address space for pattern tables.
Now that our memory areas are set up, we can define segments and assign them to those memory areas. If multiple segments belong in the same memory area, they are added in the order in which they appear here. Segments are written to the output file in the order in which they appear as well, so we’ll write our header first:
Here we’re creating a segment named
HEADER, telling the linker that it belongs in the
HEADER memory area, and marking it as read-only (
ro) initialized data.
After the iNES header, emulators expect to find one or more 16 KiB PRG ROM banks, so we’ll create a few segments that belong in the
PRG memory area (as well as the
VECTORS memory area, which is part of PRG ROM):
DATA are used by the C compiler to store read-only code, read-only data, and read/write data, respectively.
We’re defining the
STARTUP segment is used for storing read-only code that we’ve written in reset.s to initialize the NES, and the
VECTORS segment will store pointers to our interrupt handlers.
DATA section is read/write (
rw), and our ROM is read-only, anything in
DATA will have to be loaded into
We use the
run = RAM attribute to tell the linker that, even though
DATA will reside in the
PRG memory area, references to anything in
DATA should use addresses from the
After our PRG ROM banks, emulators will expect to find CHR ROM banks:
CHARS segment is the last that will end up in our output file, though we still need to define a few more segments:
We saw the
ZEROPAGE segment in use in the last post when using
#pragma bss-name in our C code to define zero page variables.
BSS segment is used by the C compiler for uninitialized read/write data.
Since both of these have uninitialized types (
bss), they are not written to the output file.
All that’s left for us to do is tell the linker to export a couple of symbols we’ll need to set up the C stack:
We’re going to have the stack take up the second page of RAM (i.e. the 256 bytes starting at address
The last piece of the puzzle is our scary assembly file that contains the NES startup code. We won’t explore this in great depth, but the file is liberally commented for the curious.
After importing a few symbols such as our C
main function (notice the
_ prepended by the C compiler) which we’ll need to call into, we define a few aliases for memory-mapped registers and plop our iNES header into the
start label is where we’ll point our
reset interrupt handler, and it contains a bunch of assembly code to get the NES into a known state (e.g. all interrupts disabled, rendering off, sound off, RAM zeroed, all sprites off-screen, PPU stabilized).
You’ll see at the very end of this label we make use of our
__STACK_SIZE__ symbols to set up the C stack pointer before calling into our C
The remainder of the
STARTUP section is defining the other two interrupt handlers which for now do nothing but
rti—return from interrupt.
Finally, we set the addresses of our interrupt handlers in the
VECTORS segment and include our pattern tables (via
.incbin, which tells the assembler to load the “sprites.chr” file as-is and not to assemble it) into the
That’s it! We’ve covered everything that goes into making a simple NES ROM from scratch.
In the next post, Color in Motion, we’ll create a more interesting game loop and explore using palettes to add color to our creation.