The program below is written for xa65, a 6502/R65C02/65816 cross-assembler. It should be portable to other 6502 assemblers with minor tweaks. (The xa65 website is down as of the time of writing, but the program can be found in many places on the web, or from your friendly neighborhood Linux distro's package system.)
Some Notes
The $200X addresses used below are magic registers which control the NES's video memory. See NES PPU for details on their function, and for some brief info on how images are stored in the NES. (It's complicated.)
This program eschews the normal method of storing graphics in ROM (called CHR-ROMs) in favor of copying the graphics from PRG-ROM into CHR-RAM. There is not really a good reason for this, except you have to deal with fewer bank files.
Resources
hello.65
; load this at $8000
* = $8000
; Graphics data.
; Each line is a character:
; space H E L O , W R D !
chrdata:
.byte 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
.byte 0,102,102,126,102,102,102,0,0,0,0,0,0,0,0,0
.byte 0,126,96,120,96,96,126,0,0,0,0,0,0,0,0,0
.byte 0,96,96,96,96,96,126,0,0,0,0,0,0,0,0,0
.byte 0,60,102,102,102,102,60,0,0,0,0,0,0,0,0,0
.byte 0,0,0,0,0,24,24,48,0,0,0,0,0,0,0,0
.byte 0,102,102,102,102,126,102,0,0,0,0,0,0,0,0,0
.byte 0,124,102,124,120,108,102,0,0,0,0,0,0,0,0,0
.byte 0,124,102,102,102,102,124,0,0,0,0,0,0,0,0,0
.byte 0,24,24,24,24,0,24,0,0,0,0,0,0,0,0,0
.byte $ff
; The message "Hello, World!",
; chosen from the characters above.
message:
.byte 1,2,3,3,4,5,0,6,4,7,3,8,9,$ff
; Colors
palette:
.byte 13,32,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0
; the beginning
int_reset:
; load graphics data into CHR-RAM
lda #0
sta $2006
sta $2006
ldx #0
ldc: lda chrdata,x
cmp #$ff
beq cdone
sta $2007
inx
jmp ldc
cdone:
; write the message to the name table
lda #$20
sta $2006
sta $2006
ldx #0
ldm: lda message,x
cmp #$ff
beq mdone
sta $2007
inx
jmp ldm
mdone:
; load the palette
lda #$3f
sta $2006
lda #$00
sta $2006
ldx #0
ldp: lda palette,x
sta $2007
inx
cpx #$10
bne ldp
; reset PPU pointer
; (otherwise everything is offset)
lda #0
sta $2006
sta $2006
; enable PPU
lda #%10001000
sta $2000
lda #%00011110
sta $2001
; done, just wait around
main: jmp main
; these interrupts aren't needed for this program
int_vblank:
int_irqbrk:
rti
; fill the rest of this bank with zeroes
.dsb $bffa-*, 0
; except for the interrupt vectors
.word int_vblank, int_reset, int_irqbrk
hello.nes
To get the above code to work in an emulator, just assembling it isn't quite enough. You need to wrap it with a header that gives the emulator a little more information about your program. This is beyond the scope of this writeup.
Here is the assembled ROM, gzipped and base64 encoded. Save this into hello.b64 and run base64 -d hello.b64 | gunzip > hello.nes, then use your favorite NES emulator.
H4sIAF2cPk4AA+3QIQ7CMBTG8Y4NBoIE2UBCegQOQAIGt2C4wFQVB0AsS+cwOwS4Kk5A0DiOAA6J
QY+OBLYZFPL/S833Xt9r0uViNfTED1qnWusqp/EmjtMqx6VanrrbetrcIeWktq/cWPUSnWzWjewk
9VkphfzGwmv5ftAWnSD0u72ir5ovWZF3lDt7cRTmXDzCPFT36Gys+tZ3Vf1m7MwV30OuczBl9Tq4
PD9r7DYXyo5zT0Uym//6JgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/mOUncwoewEUtjtMEEAAAA==
Pseudo_Intellectual says re NES Hello World (how-to): this could be done much more easily using the NES' Family BASIC 8)
raincomplex says haha, indeed. although, going that route, you may as well use the C64
Pseudo_Intellectual says Fair enough, though the C64 won't come pre-loaded with Donkey Kong sprites to dance while you greet the world 8)