For this tutorial we will go through some basic NES architecture so we understand what we’re working with, and we will take our first steps into NES development.
The CPU for the NES is a Ricoh 2A03 8-bit processor with a 6502 core, which runs at 1.78MHz. The CPU has a 16-bit address bus which can access 64kb of address space, which makes up a variety of things.
The 64kb of address space includes the following:
- 2kb of internal RAM (used for variables, music, etc.)
- 8kb of ports to access the PPU (Picture Processing Unit)
- 8kb of ports to access the controllers and APU (Audio Processing Unit)
- 8kb of cartridge work RAM, if the cartridge supports it (contains a battery if saved games are used)
- 32kb of program ROM (contains all our game logic and data such as backgrounds and sprites)
The 16-bit addresses are written in hexadecimal, and are referred to in our code as 4 digits preceded by a dollar sign. The memory addresses will range from $0000 to $FFFF, with FFFF representing 64kb. The memory addresses are specific to the items listed above, but we will go into greater detail later.
You may be questioning how games back in the day could hold all the logic and graphics data within 32kb of program ROM, because even back then that wasn’t very much. Mappers were utilized to swap out entire 8kb banks of program ROM with another bank of memory located on the cartridge. With this method, some mappers could allow up to 512kb of program ROM and 256kb of sprite data.
The CPU also contains an Audio Processing Unit (APU), which handles everything we need for playing music or sound effects.
The Picture Processing Unit (PPU) is a separate chip that handles all the graphics, which has 16kb of address space. It includes internal memory for sprites and the color palettes we will be using eventually, as well as the ability to store up to 4 backgrounds.
The 6502 core is important for us to know because we will be using the 6502 instruction set for the assembly code we will be writing. Now, if you’re still reading this and the mention of assembly code didn’t immediately send you running for the hills, let me reassure you that it’s not that bad. Like anything we do, it takes time to get better, but fortunately the 6502 instruction set is one of the simplest and smallest instruction sets available. This is beneficial to us because it will allow us to get up and running much faster. We have a total of 56 possible instructions, but consider that we will probably only really use maybe half of those instructions regularly. The rest are either rarely or never used.
The great thing about this is that once you understand those instructions, that’s what you have. If you want to move a sprite around on the screen, you will need to utilize one or more of those instructions to make that happen. I believe that it allows for added creativity in our work, and we can take great pride in it when we accomplish something.
Throughout these tutorials, we will expand our knowledge of this instruction set, but if you want to check it all out, go here:
Our First Game
It’s time to get started making our first Nintendo game. We will create a new assembly code file called
nes-tutorial.asm. You can obviously
call this whatever you want. In the previous tutorial, we already setup our nesasm assembler, which we will use to assemble our code.
If you would like to follow along in my git repository, download or clone the following repository:
This tutorial repository contains the nesasm assembler as well as instructions to assemble your code for either mac or windows. Then to see the code for this tutorial specifically, checkout the branch tutorial-1:
git checkout -b tutorial-1 remotes/origin/tutorial-1
Time to write code.
nes-tutorial.asm file we need to add a few directives to communicate with the assembler. Directives will start with a period and are indented.
Everything on the line after a semi-colon is a comment. After you add these four lines, you will never have to do this again anywhere else:
.inesprg 1 ; Defines the number of 16kb PRG banks .ineschr 1 ; Defines the number of 8kb CHR banks .inesmap 0 ; Defines the NES mapper .inesmir 1 ; Defines VRAM mirroring of banks
After we define those four directives, we need to add a bank of memory and define where in the CPU’s address space it is located:
.bank 0 .org $C000
A bank can contain at most 8kb of memory, so the address space of this bank will originate at address $C000 and fill 8kb of memory beyond that address. This will be the bank that contains all our game logic, so let’s add something to it.
The Nintendo has an interrupt that gets called whenever the console is started or reset. We can use this to our advantage to setup everything for our game (initial state and graphics, like a title screen for example). We will call this RESET:
RESET: InfiniteLoop: JMP InfiniteLoop
“RESET” and “InfiniteLoop” are labels. They are not indented, and are followed by a colon. We can use labels to our advantage in many ways, one of them being like a GOTO statement in BASIC to jump to a particular label (better than line numbers, right?). We can also think of labels like method names.
JMP (Jump) is the first instruction we have come across so far. Instructions are indented much like the directives are. With the JMP instruction, this tells the processor to jump to whatever label we give it. In this case, we will jump to InfiniteLoop, then immediately jump to InfiniteLoop again, and so on forever. We only ever want to call the RESET method once, so we will execute instructions there, then get ourselves stuck in an infinite loop at the very end.
After we are done with the RESET code, we will add the second interrupt that gets called on every frame. This is called the NMI (non-maskable interrupt), and will be considered our game loop. An NTSC Nintendo in North America and Japan will run at 60 frames per second. The PAL Nintendo in Europe will run at 50 frames per second. We will go into further detail on the NMI in a future tutorial, but just know for now that it gets called every frame and is our game loop:
Once again, the NMI is a label, and RTI (Return From Interrupt) is another instruction. We will eventually put all the game logic we want to get executed each frame between our NMI label and the RTI which denotes the end of the NMI code.
The CPU has a few memory addresses set aside to define three interrupt vectors (NMI, RESET, and IRQ). These three vectors will each take up 2 bytes of memory and will be located at the range $FFFA-$FFFF. We won’t deal with the IRQ now, but just know that it is an interrupt for mappers and audio. We will just set the IRQ to 0 for now:
.bank 1 .org $FFFA .dw NMI .dw RESET .dw 0
The only thing new you see above is the .dw directive (data word). This is used to define a word, meaning 2 bytes of data.
Finally, we are just going to set up an empty bank which we will eventually add our character data to (sprites and background graphics):
.bank 2 .org $0000
That’s enough to get a basic shell going. We will fill in more of this in later tutorials, but this code should assemble. Execute the nesasm assembler on our code file, and it should produce a playable .nes file:
The tutorial git repository contains the nesasm assemblers for both mac and windows, as well as instructions in the README to build it for either platform. It also contains a build script (build.sh) to assemble it for mac if you want something easier.
When we assemble our code, we should see the following output if everything was successful:
NES Assembler (v3.1) pass 1 pass 2
Now we should see a
nes-tutorial.nes file in our root directory. Take this file and open it with whatever NES emulator you choose. When you open
it, you should see a black screen. Congratulations! You just made your first NES game. This is Marvel’s Daredevil Simulator: