Building NES Games in 6502 Assembly: Refactoring

Table of contents

Table of contents

Part I: preparation

Part II: graphics

8. Refactoring


  • Constants
  • Header file
  • Import and export ca65
  • Native linker configuration
  • Putting it all together

Before we get into a deeper look at how the NES draws graphics, let’s reflect on what we’ve already created. Now we can make a few improvements that will be useful to us in the future. After refactoring, we will create a useful template for the following projects.


In many places in our code, there are specific numbers that do not change, such as MMIO addresses for communicating with the PPU. Looking at the code, it’s hard to understand what these numbers mean.

Fortunately, these abstract values ​​can be replaced with clear text by declaring constants. Basically, a constant is the name of a single number that cannot be changed. Let’s create constants for the PPU addresses we’ve already used:

[Обычно эти имена являются стандартными наименованиями для различных MMIO-адресов NES. При изучении источников, например, NESDev wiki, вы их встретите.]

Thanks to these constants, our code


becomes much more readable:

Where to put these constants? Usually create a separate file of constants, which can be included in the main file of the assembler code. We will name the constants file


[Почему этот файл имеет расширение .inc, а не .asm? Файл констант содержит не совсем ассемблерный код; в нём нет никаких опкодов. Мы будем использовать расширение .asm для файлов с ассемблерным кодом, а .inc для файлов, которые мы включаем в файл с ассемблерным кодом.]

We will then add the constants file to the top of the file .asm in the following way:

Header file

The same can be done with the segment


, because in general it will be the same for different projects. Let’s create a file

, which will contain the contents of the header. It’s also a good time to add comments:

Now we can remove the section

.segment "HEADER"

our main file


and add a new header file. Beginning of file


should look like this:

When running the assembler and linker, they will take the content

and put it in the right place in the finished ROM, just as if we put it directly in the assembler code file.

Import and export ca65

The complete reset handler can get quite large, so it’s a good idea to put it in a separate file. But we can’t just add it with


because we need to somehow reference the reset handler in the segment



This can be achieved due to the fact that ca65 is able to import and export code .proc. We use the directive .exportto tell the assembler that the particular proc should be available in other files, and the directive .importto use proc elsewhere.

First, let’s create reset.asmcontaining the directive .export:

There are a few things worth mentioning in this file. First, the file has the extension


, because it contains opcodes. Second, we add a constants file so we can use it here. Thirdly, we need to specify which code segment this


so that the linker knows how to put everything together. Fourth, note that we are importing


. Thanks to this, the assembler knows at what memory address the procedure is located.


so that the reset handler can jump to the correct address.

Now that we have a separate reset file, we will use reset_handler inside code:

On line 13, where it used to be

.proc reset_handler

, the procedure from the external file is now imported. Please note that we do not need to specify which file the procedure is taken from – before assembly, the assembler scans all files


for exports, so it already knows what external procedures are available and where they are located. (It is also worth noting that because of this, you will not be able to export two procedures with the same name – the assembler will not understand which one you are referring to in



[Возможно, вы заметили, что в reset.asm используется .segment "CODE", а в нашем основном файле с ассемблерным кодом тоже используется .segment "CODE". Что произойдёт, когда мы ассемблируем и скомпонуем эти файлы? Компоновщик находит всё, что принадлежит к тому же сегменту, и соединяет это. Порядок не особо важен, потому что метки преобразуются в адреса на этапе компоновки.]

We also need to export the procedure mainso that the reset handler can import it and know where to go after it completes.

Native linker configuration

When building the sample project in

Chapter 3

we used the following command:

ld65 helloworld.o -t nes -o helloworld.nes

-t nes

tells ld65 to use the default NES linker configuration. That’s why we have the “STARTUP” segment even though we never used it. The default configuration is fine for the sample project, but as the code gets more complex and larger, it can lead to problems. So instead of using the default configuration, we’ll write our own builder configuration with only the segments and properties we need.

Our own linker configuration will be in the file nes.cfgwhich looks like this:

Code in text form

  HEADER: start=$00, size=$10, fill=yes, fillval=$00;
  ZEROPAGE: start=$10, size=$ff;
  STACK: start=$0100, size=$0100;
  OAMBUFFER: start=$0200, size=$0100;
  RAM: start=$0300, size=$0500;
  ROM: start=$8000, size=$8000, fill=yes, fillval=$ff;
  CHRROM: start=$0000, size=$2000;

  HEADER: load=HEADER, type=ro, align=$10;
  ZEROPAGE: load=ZEROPAGE, type=zp;
  STACK: load=STACK, type=bss, optional=yes;
  OAM: load=OAMBUFFER, type=bss, optional=yes;
  BSS: load=RAM, type=bss, optional=yes;
  DMC: load=ROM, type=ro, align=64, optional=yes;
  CODE: load=ROM, type=ro, align=$0100;
  RODATA: load=ROM, type=ro, align=$0100;
  VECTORS: load=ROM, type=ro, start=$FFFA;
  CHR: load=CHRROM, type=ro, align=16, optional=yes;

In section


indicates the structure of memory areas in which segments can be placed, and in the section


given the names of the segments used in our code, and the memory areas into which they should link. I will not explain in detail what each of the parameters means, a description can be found in

ld65 documentation


To use our own linker configuration, we first need to update the segment names in our code to match the segment names from the configuration file. In our case, it is enough to move "CHARS" in "CHR" and delete "STARTUP".

Putting it all together

And finally, we need to change the file structure a bit. All files




we will move to a subfolder


, and the new linker configuration will be at the top level. After refactoring, the code structure should look like this:

   |-- nes.cfg
   |-- src
      |-- helloworld.asm
      |-- reset.asm

To assemble and link our code, we use the following commands (run from the top level folder



ca65 src/helloworld.asm
ca65 src/reset.asm
ld65 src/reset.o src/helloworld.o -C nes.cfg -o helloworld.nes

To clarify: here we first assemble each file


creating files


. After that we transfer all files


linker. Instead of the default NES linker configuration (

-t nes

) we use our new custom configuration (

-C nes.cfg

). The result of the linker is placed in the same ROM file



A copy of all the files listed above can be downloaded in ZIP format from here. We’ll be using this schematic as the basis for future projects, so before moving on, make sure you can assemble, link, and run the code.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *