Thursday, March 20, 2014

Hello World NES

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.

  .bank 0
  .org $C000 

RESET:
  SEI          ; disable IRQs
  CLD          ; disable decimal mode
  LDX #$40
  STX $4017    ; disable APU frame IRQ
  LDX #$FF
  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
  
LoadPalettes:
  LDA $2002             ; read PPU status to reset the high/low latch
  LDA #$3F
  STA $2006             ; write the high byte of $3F00 address
  LDA #$00
  STA $2006             ; write the low byte of $3F00 address
  LDX #$00              ; start out at 0
LoadPalettesLoop:
  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.

HelloWorld:
  LDA #$00
  STA $2001    ; disable rendering

; first 8 lines blank
  LDX #0
  LDA $2002             ; read PPU status to reset the high/low latch
  LDA #$20
  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
HelloWorld_topBlank:
  STA $2007 ; write character to screen
  INX
  BNE HelloWorld_topBlank
HelloWorld_printHello:
  LDA hello_string,X
  STA $2007
  INX
  CPX #$10
  BNE HelloWorld_printHello
  
  ; 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
HelloWorld_bottomBlank:
  STA $2007
  INX
  BNE HelloWorld_bottomBlank
  DEY
  BNE HelloWorld_bottomBlank

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
  LDX #$40
  LDA #0
HelloWorld_attributeTable:
  STA $2007
  DEX
  BNE HelloWorld_attributeTable

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
  STA $2005
  STA $2005
  LDA #001000   ; enable background
  STA $2001

MainGameLoop:
  NOP ; Right now we do nothing in the main loop.
  JMP MainGameLoop

; ---------------------------
; Functions  
WaitForVBlank:
  BIT $2002
  BPL WaitForVBlank
  RTS

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

  .bank 1
  .org $E000
palette:
  .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

hello_string:
  .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)
  .bank 2
  .org $0000
  .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.


No comments: