I normally don't like code dumps as to me they seem like a way to bloat the size of a book or article. In future installments of this NES series, I will link to a separate zipped source files and just cover the chunks of code that are relevant. For a hello world program, I figured it would be best to have all the code. Granted, a 6502 assembly language Hello World for the NES has a bit more code than your typical Hello World program.
I am using NESASM for my assembler. Other options are possible in which case the code would be slightly different for setting up the ROM file header and banking information. The first thing when creating a ROM is the header file for the ROM. iNES is pretty much the standard format for NES emulators. The assembler needs a bit of information to create this file so the start of the program is assembler bookkeeping.
.inesprg 1 ; 1x 16KB PRG code
.ineschr 1 ; 1x 8KB CHR data
.inesmap 0 ; mapper 0 = NROM, no bank swapping
.inesmir 0 ; horizontal background mirroring
With the assembler given what it needs to output the ROM file, we are ready to start. The thing about early consoles is that they do not have any operating system. This means that when you start a NES program, the console is in an unknown state so the first thing that has to be done is make sure you disable things that you may not want going and set up things like the system stack. One important thing to remember is that the version of the 6502 that the NES has does not support the full instruction set. Sadly Binary Coded Decimal is not available so one of the first thing that you should do is make sure decimal mode is disabled otherwise who knows what could happen. This is followed by disabling the APU and the PPU's rendering.
SEI ; disable IRQs
CLD ; disable decimal mode
STX $4017 ; disable APU frame IRQ
TXS ; Set up stack
INX ; now X = 0
STX $2000 ; disable NMI
STX $2001 ; disable rendering
STX $4010 ; disable DMC IRQs
For some reason, two vertical blanks need to happen before you can be sure that the PPU is working. Some additional housekeeping can be done between the vertical blanks but as we aren't doing much in this program there really is no need to do anything. Once the PPU is ready, we set up the palette. This is done by telling the PPU (through the memory addresses that are mapped to it) that we want to write to PPU memory. PPU memory is separate from the memory the 6502 has access to which has it's advantages, but this means that setting up any graphics means setting up the PPU memory address to write to then writing repeatedly to the PPU port mapped to $2007. The PPU conveniently increments the address to write for you.
jsr WaitForVBlank ; First wait for vblank to make sure PPU ready
; can do a bit of additional initialization here if necessary
jsr WaitForVBlank ; Second vblank. PPU now ready
LDA $2002 ; read PPU status to reset the high/low latch
STA $2006 ; write the high byte of $3F00 address
STA $2006 ; write the low byte of $3F00 address
LDX #$00 ; start out at 0
LDA palette, x ; load data from address (palette + the value in x)
STA $2007 ; write to PPU
INX ; X = X + 1
CPX #$20 ; Compare X to hex $10, decimal 16 - copying 16 bytes = 4 sprites
BNE LoadPalettesLoop ; Branch to LoadPalettesLoop if compare was Not Equal to zero
; if compare was equal to 32, keep going down
Now comes time for for the actual printing of Hello World. Note that the contents of the screen are undefined, and even if 0 would be incorrect so we also have to fill the remainder of the screen with our space character.\ This is done as with the palette by simply setting the screen memory address and sending all the characters to the PPU through port $2007.
STA $2001 ; disable rendering
; first 8 lines blank
LDA $2002 ; read PPU status to reset the high/low latch
STA $2006 ; write the high byte of screen address
STX $2006 ; write the low byte of screen address
; Accumulator alread has $20 (space character) so no need to set it
STA $2007 ; write character to screen
; prepair 2-level loop to fill remainder of screen
LDX #$50 ; this is $50 due to $10(16) printed characters and
; $40 (64) attribute bytes
LDY #3 ; loop through 256 (-80 first pass) 3 times
LDA #$20 ; printing spaces
Of course, we also need to set the attribute table so the colors of the characters would be correct. This could have been avoided by setting all the palettes to the same 4 colors but as this table is conveniently located right after the screen memory we simply need to write 0s to the remaining 64 bytes.
; finally, set up attribute table
Nothing appears yet as we disabled the PPU earlier so now make sure the screen scroll position is set correctly and enable the background again then we do nothing. The NOP instruction isn't actually needed, but it is my favorite 6502 instruction so had to put it in here.
; set scrolling position and screen enable screen
LDA #001000 ; enable background
NOP ; Right now we do nothing in the main loop.
The data that we use for the program has to go somewhere. While this could easily fit with the program on bank 0, I like to at least make use of bank 1 since we need to set up the jump vectors anyway. Jump vectors are addresses at the end of the cartridge that get used when the cartridge is reset.
; Game Data
.db $0F,$19,$2B,$39, $0F,$17,$16,$06, $0F,$39,$3A,$3B, $0F,$3D,$3E,$0F
.db $0F,$14,$27,$39, $0F,$27,$06,$2D, $0F,$0A,$29,$25, $0F,$02,$38,$3C
.db " Hello World!!!", $00
; jump Vectors
.org $FFFA ;first of the three jump vectors starts here
.dw 0 ; Jump for NMIs if enabled;
.dw RESET ; jump for when RESET or first turned on
.dw 0 ;external interrupt IRQ is not used
; Character Tables (tilesets)
.incbin "tileset.chr" ;includes 8KB graphics file from SMB1
And that is all there is to it. Not really that difficult but there is a lot more work than the equivalent C program. Next (after a postmortem and possibly other article) we will take a bit more detailed look at the PPU ports then start playing with sprites.