Building NES Games in 6502 Assembly: Refactoring

Table of contents

Table of contents

Part I: preparation

Part II: graphics

8. Refactoring

Content:

  • 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.

Constants

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

main

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

constants.inc

.

[Почему этот файл имеет расширение .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

.header

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

header.inc

, 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

.asm

and add a new header file. Beginning of file

.asm

should look like this:

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

header.inc

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

.include

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

VECTORS

.

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

.asm

, 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

.proc

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

main

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

main

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

.asm

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

.import

.)

[Возможно, вы заметили, что в 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

MEMORY {
  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;
}

SEGMENTS {
  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

MEMORY

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

SEGMENTS

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

.asm

And

.inc

we will move to a subfolder

src

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

08-refactoring
   |
   |-- nes.cfg
   |-- src
      |
      |-- constants.inc
      |-- header.inc
      |-- helloworld.asm
      |-- reset.asm

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

08-refactoring

):

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

.asm

creating files

.o

. After that we transfer all files

.o

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

helloworld.nes

.

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 *