The Nintendo Entertainment System (NES) is an iconic gaming platform known for its simplicity and rich history. Programming for the NES typically involves using 6502 assembly language, which provides direct interaction with the console's hardware. In this guide, we will walk you through a comprehensive example demo that sets up a basic NES program using assembler.
The code samples presented here illustrate core aspects of NES development. We will cover the essentials such as setting up the development environment, writing and understanding the NES header, initializing the CPU and PPU, and finally assembling a simple infinite loop structure. Although this demo does not render complex graphics or display elaborate messages, it forms the basis for any further NES programming projects.
To start developing on the NES, you need the following tools:
After installing your chosen assembler and emulator, configure your text editor to suit assembly programming. Create a project folder with the following structure:
Once set up, ensure your assembler’s path is added to your system’s environment variables for easy command-line usage. Regularly testing the ROM in the emulator will help you catch errors early.
A typical NES ROM written in 6502 assembly involves several distinct sections:
Below is an HTML table summarizing the memory map pertinent to this NES demo:
Segment | Start Address | Size | Description |
---|---|---|---|
Header | $0000 | 16 bytes | Identifies the ROM and its configuration |
Zero Page | $0200 | 256 bytes | Fast access memory for variables |
PRG-ROM (Code) | $C000 | Typically 16KB or multiples | Main program code resides here |
Interrupt Vectors | $FFFA | 6 bytes | Holds NMI, Reset, and IRQ vectors |
The NES header is the first section of your ROM. It is essential for the NES emulator or console to correctly interpret the ROM’s layout and configuration settings. The header typically includes:
; NES iNES Header – 16 bytes total
.segment "HEADER"
.db "NES", $1A ; Magic number identification
.db $02 ; Number of 16KB PRG-ROM banks
.db $01 ; Number of 8KB CHR-ROM banks
.db %00000000 ; Flags 6 - mirroring, battery, trainer
.db %00000000 ; Flags 7 - VS/Playchoice, NES 2.0
.db 0, 0, 0, 0, 0, 0, 0, 0 ; Padding bytes
This header tells the emulator that there are two 16KB banks for program code and one 8KB bank for character graphics, with no special flags set.
The zero page in the 6502 architecture is a section of RAM that offers more efficient addressing. For small variables or frequently used data, it is ideal to store them in this area.
; Zero Page Variables
.segment "ZEROPAGE"
pointer: .res 2 ; Reserve 2 bytes for a pointer or temporary storage
In this example, we reserve two bytes in the zero page for use as a pointer or a temporary variable. Reserving memory in the zero page increases code efficiency, an important aspect in constrained systems like the NES.
The main code segment initializes the NES environment, sets up critical registers, disables interrupts during setup, and implements an infinite loop to keep the CPU busy. Initializing the PPU is a fundamental part of this process.
; Code Segment – starting point for the program
.segment "CODE"
.org $C000 ; Starting address for PRG-ROM code
Reset:
SEI ; Disable interrupts to ensure a clean start
CLD ; Clear decimal mode for correct arithmetic processing
; Initialize the stack pointer
LDX #$FF
TXS
; Disable rendering during PPU setup
LDA #$00
STA $2000 ; PPU Control: Disable NMI and rendering
STA $2001 ; PPU Mask: Disable rendering details
; Wait for PPU to be ready
WaitVBlank:
LDA $2002 ; Read the PPU status
AND #%10000000 ; Check vertical blank flag (bit 7)
BEQ WaitVBlank ; Wait until the flag is set
; Set up background color through palette change
; Write operation sequence to PPU address registers
LDA #$3F
STA $2006 ; High byte of VRAM address for palette ($3F00)
LDA #$00
STA $2006 ; Low byte of VRAM address for palette
LDA #$0F
STA $2007 ; Write a value to set a specific background color
; Enable NMI and rendering now that setup is complete
LDA #%10001000 ; Enable NMI and set pattern table for background
STA $2000
LDA #%00011110 ; Enable background and sprite rendering, disable left-side clipping
STA $2001
MainLoop:
JMP MainLoop ; Infinite loop to keep CPU active
After system initialization, the code enters an infinite loop. In a typical game, this would be replaced with the game’s main loop logic, handling game state updates, input, and rendering.
Interrupt vectors are critical to NES programs. They let the system know where to jump when specific events occur, such as a non-maskable interrupt (NMI) which is usually fired during the vertical blank period. This is the ideal moment for tasks like updating graphics because the PPU is less busy.
; Interrupt Vectors Location (placed at the end of the PRG-ROM)
.segment "VECTORS"
.org $FFFA
.dw NMI ; Address of the non-maskable interrupt handler
.dw Reset ; Address of the reset vector (start of program)
.dw IRQ ; Address of the IRQ handler (unused in this demo)
; Interrupt Handlers
NMI:
RTI ; Return from interrupt (simple NMI handler)
IRQ:
RTI ; Return from interrupt (simple IRQ handler)
In our simple demo, both the NMI and IRQ routines merely return immediately. A more complex program might perform background tasks or update graphics during these interrupts.
For a fully realized NES game, you would include graphics data in a dedicated segment. In our demo, this section is included for completeness but may be left blank if no sprites or background tiles are needed.
; CHR Data Segment – Typically holds sprite or tile data
.segment "CHARS"
.res 8192, $00 ; Reserve 8KB for character graphics (if not provided, this could be replaced by actual graphics data)
Combining the components discussed above, below is a full example of a simple NES demo in 6502 assembly. This program initializes the NES by setting up the iNES header, zero page, the code segment with CPU and PPU initialization, and finally includes interrupt vectors.
; Example NES Demo in 6502 Assembly Language
;-------------------------------------------------
; iNES Header
;-------------------------------------------------
.segment "HEADER"
.db "NES", $1A ; NES identifier
.db $02 ; Number of 16KB PRG-ROM banks
.db $01 ; Number of 8KB CHR-ROM banks
.db %00000000 ; Flags 6
.db %00000000 ; Flags 7
.db 0, 0, 0, 0, 0, 0, 0, 0 ; Reserved padding
;-------------------------------------------------
; Zero Page Declarations
;-------------------------------------------------
.segment "ZEROPAGE"
pointer:
.res 2 ; Reserve 2 bytes for temporary data
;-------------------------------------------------
; Main Code Segment
;-------------------------------------------------
.segment "CODE"
.org $C000 ; PRG-ROM code starting at $C000
Reset:
SEI ; Disable interrupts
CLD ; Clear decimal mode
; Initialize the stack pointer
LDX #$FF
TXS
; Disable rendering during initialization
LDA #$00
STA $2000 ; Disable PPU NMI and rendering
STA $2001 ; Disable all rendering
; Wait for VBlank to safely write to PPU registers
WaitVBlank:
LDA $2002 ; Read PPU status
AND #%10000000 ; Check vertical blank (bit 7)
BEQ WaitVBlank ; Wait until our read returns non-zero
; Set up the background color palette
; The following accesses PPU address registers to set the background color at $3F00.
LDA #$3F
STA $2006 ; Set the high byte of VRAM address ($3F00)
LDA #$00
STA $2006 ; Set the low byte of VRAM address
LDA #$0F
STA $2007 ; Write a color value to the palette
; Configure PPU control for NMI and background pattern table selection
LDA #%10001000 ; Enable NMI and select background pattern table ($0000 or $1000)
STA $2000
; Configure PPU mask to enable background and sprite rendering
LDA #%00011110
STA $2001
MainLoop:
JMP MainLoop ; Infinite loop to keep the CPU running
;-------------------------------------------------
; Interrupt Vectors and Handlers
;-------------------------------------------------
.segment "VECTORS"
.org $FFFA
.dw NMI ; NMI Vector
.dw Reset ; Reset Vector
.dw IRQ ; IRQ/BRK Vector
NMI:
RTI ; Simple NMI handler (can be extended)
IRQ:
RTI ; Simple IRQ handler
;-------------------------------------------------
; CHR Data Segment (Optional)
;-------------------------------------------------
.segment "CHARS"
.res 8192, $00 ; Reserve 8KB for character ROM (or insert your graphic data)
To compile this code using ca65, you would typically save the above code into a file (e.g., demo.s) and create a configuration file (demo.cfg) to set up the memory segments. Then, use these terminal commands:
# Assemble the source code
ca65 demo.s -o demo.o
# Link the object file to create the ROM
ld65 -C demo.cfg demo.o -o demo.nes
Finally, test the ROM by launching it in an emulator such as FCEUX. If everything is set up correctly, the emulator will display a screen with the background color we specified, indicating successful initialization.
The Picture Processing Unit (PPU) is responsible for all graphical output on the NES. When setting up the PPU, it is crucial to understand:
Understanding these aspects is crucial if you plan to expand the demo into a game, as it lays the groundwork for more advanced graphical programming on the NES.
Programming in 6502 assembly for the NES poses unique challenges:
These tips can help you smoothly transition from simple demos to more advanced projects, such as full-fledged NES games.
This comprehensive example demonstrates how to set up and write a simple NES demo using 6502 assembly language. We started by discussing the necessary development tools and environment configuration, followed by a detailed explanation of the NES code structure including header setup, variable allocation in the zero page, CPU and PPU initialization, and the simplistic infinite loop that forms the core of the demo’s runtime.
While the demo primarily focuses on initialization and a minimal rendering setup, it lays a solid foundation for more complex NES projects. By understanding these fundamental concepts, you are well-equipped to further explore NES game development—expanding upon graphic renderings, game logic, and incorporating sound.