Chat
Search
Ithy Logo

Example NES Demo Using 6502 Assembler

A comprehensive guide to building a simple NES demo in assembly

nes console, 6502 assembly code snippet, vintage hardware

Key Takeaways

  • Development Environment Setup: Learn how to prepare tools like assemblers and emulators for NES programming.
  • Basic NES Code Structure: Understand the organization of an NES assembly demo, from header definition to infinite loop and interrupt vectors.
  • PPU Initialization: Gain insights into the crucial steps needed to initialize the Picture Processing Unit (PPU) for rendering.

Introduction

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.


Setting Up the Development Environment

Required Tools

To start developing on the NES, you need the following tools:

  • Assembler: Popular assemblers include ca65 (from the CC65 suite), NESASM, and asm6. This demo will use ca65 because it is widely adopted and well-documented.
  • Text Editor: A reliable code editor like Visual Studio Code, Notepad++, or Sublime Text is recommended.
  • Emulator: Emulators such as FCEUX allow you to test your ROMs without needing actual hardware.

Environment Configuration

After installing your chosen assembler and emulator, configure your text editor to suit assembly programming. Create a project folder with the following structure:

  • src: Contains the main assembly source file (e.g., demo.s or demo.asm).
  • build: Directory for generated object files and the final ROM file.
  • config: Contains linking configuration files if you are using ca65, such as the demo.cfg file.

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.


Understanding the NES Assembly Demo Structure

Overview

A typical NES ROM written in 6502 assembly involves several distinct sections:

  1. Header: The iNES header identifies the ROM as an NES file and provides details about PRG and CHR ROM sizes, flags, and mirroring configuration.
  2. Zero Page and Variable Declarations: Defines variables used by the code. The zero page is efficient because addressing in this area uses fewer cycles.
  3. Code Segment: Contains the main program logic, including system initialization, disabling interrupts, stack set-up, and the infinite game loop.
  4. Interrupt Vectors: Define where the NES should jump in case of system initialization (reset vector), non-maskable interrupt (NMI), and regular interrupts (IRQ).
  5. CHR Data or Graphics Segment: Holds character graphics data. For minimal demos, you could leave this segment empty or provide blank memory if you are not rendering sprites.

Memory Layout Table

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

Detailed Code Explanation

NES Header

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:

  • Identifier bytes ("NES" followed by $1A).
  • PRG-ROM bank count (each bank is 16KB).
  • CHR-ROM bank count (each bank is 8KB).
  • Flags which might control mirroring, battery backup, and mapper settings.

Sample Header Code


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

Zero Page and Variable Declarations

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.

Example Zero Page Declaration


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

Main Code Segment

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.

System Initialization and Infinite Loop


; 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

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.

Defining Interrupt Vectors


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

CHR Data (Optional)

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 Segment Example


; 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)
  

Putting It All Together: The Complete Demo Code

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.

Complete Demo Code


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


Additional Insights

PPU and Rendering Considerations

The Picture Processing Unit (PPU) is responsible for all graphical output on the NES. When setting up the PPU, it is crucial to understand:

  • Palette Configuration: The NES uses a palette of 64 colors, and your demo sets a basic color at $3F00. More colorful demos can load additional palette data to achieve the desired visual effects.
  • Background and Sprites: While this demo does not load complex background graphics or sprite data, a full game would include detailed CHR data to render images defined by the game designer.
  • Vertical Blank (VBlank): The period during which the PPU is not drawing is ideal for updating VRAM. Polling PPU status ensures that any write operations do not disrupt the rendering process.

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.

Challenges and Tips for NES Assembly Programming

Programming in 6502 assembly for the NES poses unique challenges:

  • Memory Constraints: The NES has a limited memory space, so optimizing your code and managing memory efficiently is vital.
  • Understanding the 6502 Instruction Set: The 6502's limited number of addressing modes and instructions requires careful planning to perform complex operations efficiently.
  • Timing Issues: Many tasks, especially those involving the PPU, require precise timing (such as waiting for VBlank). Use loops that poll system registers to ensure synchronization.
  • Debugging: Debugging assembly code can be tricky. Utilize emulator debugging features, such as breakpoints and memory inspection, to troubleshoot your code.

These tips can help you smoothly transition from simple demos to more advanced projects, such as full-fledged NES games.


Conclusion

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.


References


More


Last updated February 19, 2025
Ask Ithy AI
Export Article
Delete Article