Table of Contents:
Before we move on to learning more about how the NES draws graphics, let's take some time to reflect on what we've already built. There are a number of improvements we can make now that will be useful in the future. By refactoring now, we will create a useful template for our upcoming projects.
There are several places in our code where we use a particular number that doesn't change, such as MMIO addresses for talking with the PPU. It's hard to tell what these numbers are referring to when looking at the code.
Thankfully, we can replace these abstract numbers with descriptive text, by declaring constants. A constant is essentially a name for one number, which can't be changed. Let's create constants for the PPU addresses we've used so far: These names are the generally agreed-upon standard naming for the various MMIO addresses available in the NES. When looking through references like the NESDev wiki, you will see these names used.
PPUCTRL = $2000 PPUMASK = $2001 PPUSTATUS = $2002 PPUADDR = $2006 PPUDATA = $2007
With these constants, our main code becomes much more readable:
25 .proc main 26 PPUSTATUS 27 $3f # 28 PPUADDR 29 $00 # 30 PPUADDR 31 $29 # 32 PPUDATA 33 %00011110 # 34 PPUMASK 35 forever: 36 forever 37
Where do we put these constants? The common approach is to make a separate constants file, which
can be included into our main assembly file. We'll call the constants file
Why does this file end in
.inc instead of
.asm? The constants file
is not exactly assembly code; it doesn't have any opcodes. We will
.asm extension for assembly code files, and
.inc for files
which are included into an assembly code file.
Then, we include the constants file at the top of our
.asm file like this:
We can do the same thing with the
.header segment, since it will generally
be the same from project to project. Let's make a
header.inc file to hold
our header content. Now would also be a good time to add some comments:
"HEADER" "NES", $1a ; Magic string that always begins an iNES header $02 ; Number of 16KB PRG-ROM banks $01 ; Number of 8KB CHR-ROM banks %00000001 ; Vertical mirroring, no save RAM, no mapper %00000000 ; No special-case flags set, no mapper $00 ; No PRG-RAM present $00 ; NTSC format
Now we can delete the
.segment "HEADER" section of our main
file, and include our new header file. The top of our
.asm file should now
look like this:
"constants.inc" "header.inc" "CODE"
When the assembler and linker run, they will take the contents of
and put them in the correct place in the output ROM, exactly the same as if we had
put it directly into the assembly file.
ca65 Imports and Exports
A full reset handler can become quite large, so it can be useful to put it into a separate
file. But we can't just
.include it, because we need a way to reference the reset handler
The solution is to use ca65's ability to import and export
.proc code. We use
.export directive to inform the assembler that a certain proc should be
available in other files, and the
.import directive to use the proc
First, let's create
reset.asm, including the
1 "constants.inc" 2 3 "CODE" 4 main 5 reset_handler 6 .proc reset_handler 7 8 9 $00 # 10 PPUCTRL 11 PPUMASK 12 vblankwait: 13 PPUSTATUS 14 vblankwait 15 main 16
There are a few things I'd like to point out in this file. First, the file ends in
because it contains opcodes. Second, we include the constants file so that it can be used here.
Third, we need to specify which code segment this
.proc belongs in, so the linker
knows how to put everything together. Finally, note that we are importing
way, the assembler knows what memory address the
main proc is located at, so
the reset handler can jump to the correct address.
Now that we have a separate reset file, we'll use
reset_handler inside our code:
4 "CODE" 5 .proc irq_handler 6 7 8 9 .proc nmi_handler 10 11 12 13 reset_handler 14 15 main 16 .proc main 17 ; contents of main here 18 19 20 "VECTORS" 21 nmi_handler, reset_handler, irq_handler
On line 13, where our
.proc reset_handler used to be located, we now import
the proc from an external file. Note that you do not need to specify which file the proc
comes from - the assembler scans all
.asm files for exports before it starts
assembling, so it already knows what external procs are available and where they are located.
(Note that this also means you can't export two procs with the same name - the assembler
will have no way to tell which one you are referring to in an
You may have noticed that
and our main assembly file also uses
.segment "CODE". What happens
when we assemble and link these files? The linker finds everything that belongs
to the same segment and puts it together. The order does not particularly matter,
since labels are converted into addresses at link time.
We also have to export our
main proc, so that the reset handler can import
it and know where to jump to when it is finished.
Custom Linker Configuration
When we linked our sample project back in Chapter 3, we used this command:
ld65 helloworld.o -t nes -o helloworld.nes
-t nes tells ld65 to use the default linker configuration for
the NES. This is why we have the "STARTUP" segment, despite never using it.
While the default configuration works for the sample project, it can lead
to problems as our code becomes larger and more complicated. So, instead
of using the default configuration, we will write our own linker configuration
with only the segments and features that we need.
Our custom linker config will be in a file called
will look like this:
HEADER: ZEROPAGE: STACK: OAMBUFFER: RAM: ROM: CHRROM: HEADER: ZEROPAGE: STACK: OAM: BSS: DMC: CODE: RODATA: VECTORS: CHR:
MEMORY section lays out the regions of memory that segments
can be placed into, while the
SEGMENTS section describes the
segment names we can use in our code and which memory areas they should be
linked into. I won't be going into detail on what each setting means, but
you can find in-depth documentation in the
To use this custom linker configuration, we first need to update the segment
names in our code to match the config file's segment names. In our case,
the only needed changes are moving
Putting It All Together
Finally, we need to update the structure of our files a bit. We will move
all of the
.inc files into a sub-directory,
src, with our new linker config at the top level. The code
we have after all of our refactoring should now look like this:
08-refactoring | |-- nes.cfg |-- src | |-- constants.inc |-- header.inc |-- helloworld.asm |-- reset.asm
To assemble and link our code, we will use the following commands (run from
ca65 src/helloworld.asm ca65 src/reset.asm ld65 src/*.o -C nes.cfg -o helloworld.nes
To be clear, what we are doing here is first assembling each
file to create
.o files. Once that is done, we pass all of the
.o files to the linker. Instead of using the default NES
linker config (
-t nes), we use our new custom config
-C nes.cfg). The output from the linker is placed into
helloworld.nes ROM file.
If you would like to download a copy of the files listed above, here is a ZIP file of everything so far. We'll be using this setup as a base for our future projects, so be sure that you are able to assemble, link, and run the code before moving on.