460 lines
10 KiB
ArmAsm
460 lines
10 KiB
ArmAsm
; CH05 - Programming Games for NES
|
|
; Base NES game shell demo
|
|
|
|
;*****************************************************************
|
|
; Define NES control register values
|
|
;*****************************************************************
|
|
|
|
; Define PPU Registers
|
|
PPU_CONTROL = $2000 ; PPU Control Register 1 (Write)
|
|
PPU_MASK = $2001 ; PPU Control Register 2 (Write)
|
|
PPU_STATUS = $2002; PPU Status Register (Read)
|
|
PPU_SPRRAM_ADDRESS = $2003 ; PPU SPR-RAM Address Register (Write)
|
|
PPU_SPRRAM_IO = $2004 ; PPU SPR-RAM I/O Register (Write)
|
|
PPU_VRAM_ADDRESS1 = $2005 ; PPU VRAM Address Register 1 (Write)
|
|
PPU_VRAM_ADDRESS2 = $2006 ; PPU VRAM Address Register 2 (Write)
|
|
PPU_VRAM_IO = $2007 ; VRAM I/O Register (Read/Write)
|
|
SPRITE_DMA = $4014 ; Sprite DMA Register
|
|
|
|
; Define APU Registers
|
|
APU_DM_CONTROL = $4010 ; APU Delta Modulation Control Register (Write)
|
|
APU_CLOCK = $4015 ; APU Sound/Vertical Clock Signal Register (Read/Write)
|
|
|
|
; Joystick/Controller values
|
|
JOYPAD1 = $4016 ; Joypad 1 (Read/Write)
|
|
JOYPAD2 = $4017 ; Joypad 2 (Read/Write)
|
|
|
|
; Gamepad bit values
|
|
PAD_A = $01
|
|
PAD_B = $02
|
|
PAD_SELECT = $04
|
|
PAD_START = $08
|
|
PAD_U = $10
|
|
PAD_D = $20
|
|
PAD_L = $40
|
|
PAD_R = $80
|
|
|
|
;*****************************************************************
|
|
; Define NES cartridge Header
|
|
;*****************************************************************
|
|
|
|
.segment "HEADER"
|
|
INES_MAPPER = 0 ; 0 = NROM
|
|
INES_MIRROR = 0 ; 0 = horizontal mirroring, 1 = vertical mirroring
|
|
INES_SRAM = 0 ; 1 = battery backed SRAM at $6000-7FFF
|
|
|
|
.byte 'N', 'E', 'S', $1A ; ID
|
|
.byte $02 ; 16k PRG bank count
|
|
.byte $01 ; 8k CHR bank count
|
|
.byte INES_MIRROR | (INES_SRAM << 1) | ((INES_MAPPER & $f) << 4)
|
|
.byte (INES_MAPPER & %11110000)
|
|
.byte $0, $0, $0, $0, $0, $0, $0, $0 ; padding
|
|
|
|
;*****************************************************************
|
|
; Import both the background and sprite character sets
|
|
;*****************************************************************
|
|
|
|
.segment "TILES"
|
|
.incbin "example.chr"
|
|
|
|
;*****************************************************************
|
|
; Define NES interrupt vectors
|
|
;*****************************************************************
|
|
|
|
.segment "VECTORS"
|
|
.word nmi
|
|
.word reset
|
|
.word irq
|
|
|
|
;*****************************************************************
|
|
; 6502 Zero Page Memory (256 bytes)
|
|
;*****************************************************************
|
|
|
|
.segment "ZEROPAGE"
|
|
|
|
nmi_ready: .res 1 ; set to 1 to push a PPU frame update,
|
|
; 2 to turn rendering off next NMI
|
|
gamepad: .res 1 ; stores the current gamepad values
|
|
|
|
d_x: .res 1 ; x velocity of ball
|
|
d_y: .res 1 ; y velocity of ball
|
|
|
|
;*****************************************************************
|
|
; Sprite OAM Data area - copied to VRAM in NMI routine
|
|
;*****************************************************************
|
|
|
|
.segment "OAM"
|
|
oam: .res 256 ; sprite OAM data
|
|
|
|
;*****************************************************************
|
|
; Remainder of normal RAM area
|
|
;*****************************************************************
|
|
|
|
.segment "BSS"
|
|
palette: .res 32 ; current palette buffer
|
|
|
|
;*****************************************************************
|
|
; Some useful functions
|
|
;*****************************************************************
|
|
|
|
.segment "CODE"
|
|
; ppu_update: waits until next NMI, turns rendering on (if not already), uploads OAM, palette, and nametable update to PPU
|
|
.proc ppu_update
|
|
lda #1
|
|
sta nmi_ready
|
|
loop:
|
|
lda nmi_ready
|
|
bne loop
|
|
rts
|
|
.endproc
|
|
|
|
; ppu_off: waits until next NMI, turns rendering off (now safe to write PPU directly via PPU_VRAM_IO)
|
|
.proc ppu_off
|
|
lda #2
|
|
sta nmi_ready
|
|
loop:
|
|
lda nmi_ready
|
|
bne loop
|
|
rts
|
|
.endproc
|
|
|
|
;*****************************************************************
|
|
; Main application entry point for starup/reset
|
|
;*****************************************************************
|
|
|
|
.segment "CODE"
|
|
.proc reset
|
|
sei ; mask interrupts
|
|
lda #0
|
|
sta PPU_CONTROL ; disable NMI
|
|
sta PPU_MASK ; disable rendering
|
|
sta APU_DM_CONTROL ; disable DMC IRQ
|
|
lda #$40
|
|
sta JOYPAD2 ; disable APU frame IRQ
|
|
|
|
cld ; disable decimal mode
|
|
ldx #$FF
|
|
txs ; initialise stack
|
|
|
|
; wait for first vBlank
|
|
bit PPU_STATUS
|
|
wait_vblank:
|
|
bit PPU_STATUS
|
|
bpl wait_vblank
|
|
|
|
; clear all RAM to 0
|
|
lda #0
|
|
ldx #0
|
|
clear_ram:
|
|
sta $0000,x
|
|
sta $0100,x
|
|
sta $0200,x
|
|
sta $0300,x
|
|
sta $0400,x
|
|
sta $0500,x
|
|
sta $0600,x
|
|
sta $0700,x
|
|
inx
|
|
bne clear_ram
|
|
|
|
; place all sprites offscreen at Y=255
|
|
lda #255
|
|
ldx #0
|
|
clear_oam:
|
|
sta oam,x
|
|
inx
|
|
inx
|
|
inx
|
|
inx
|
|
bne clear_oam
|
|
|
|
; wait for second vBlank
|
|
wait_vblank2:
|
|
bit PPU_STATUS
|
|
bpl wait_vblank2
|
|
|
|
; NES is initialized and ready to begin
|
|
; - enable the NMI for graphical updates and jump to our main program
|
|
lda #%10001000
|
|
sta PPU_CONTROL
|
|
jmp main
|
|
.endproc
|
|
|
|
;*****************************************************************
|
|
; NMI Routine - called every vBlank
|
|
;*****************************************************************
|
|
|
|
.segment "CODE"
|
|
.proc nmi
|
|
; save registers
|
|
pha
|
|
txa
|
|
pha
|
|
tya
|
|
pha
|
|
|
|
lda nmi_ready
|
|
bne :+ ; nmi_ready == 0 not ready to update PPU
|
|
jmp ppu_update_end
|
|
:
|
|
cmp #2 ; nmi_ready == 2 turns rendering off
|
|
bne cont_render
|
|
lda #%00000000
|
|
sta PPU_MASK
|
|
ldx #0
|
|
stx nmi_ready
|
|
jmp ppu_update_end
|
|
cont_render:
|
|
|
|
; transfer sprite OAM data using DMA
|
|
ldx #0
|
|
stx PPU_SPRRAM_ADDRESS
|
|
lda #>oam
|
|
sta SPRITE_DMA
|
|
|
|
; transfer current palette to PPU
|
|
lda #%10001000 ; set horizontal nametable increment
|
|
sta PPU_CONTROL
|
|
lda PPU_STATUS
|
|
lda #$3F ; set PPU address to $3F00
|
|
sta PPU_VRAM_ADDRESS2
|
|
stx PPU_VRAM_ADDRESS2
|
|
ldx #0 ; transfer the 32 bytes to VRAM
|
|
loop:
|
|
lda palette, x
|
|
sta PPU_VRAM_IO
|
|
inx
|
|
cpx #32
|
|
bcc loop
|
|
|
|
; enable rendering
|
|
lda #%00011110
|
|
sta PPU_MASK
|
|
; flag PPU update complete
|
|
ldx #0
|
|
stx nmi_ready
|
|
ppu_update_end:
|
|
|
|
; restore registers and return
|
|
pla
|
|
tay
|
|
pla
|
|
tax
|
|
pla
|
|
rti
|
|
.endproc
|
|
|
|
;*****************************************************************
|
|
; IRQ Clock Interrupt Routine
|
|
;*****************************************************************
|
|
|
|
.segment "CODE"
|
|
irq:
|
|
rti
|
|
|
|
;*****************************************************************
|
|
; Main application logic section includes the game loop
|
|
;*****************************************************************
|
|
.segment "CODE"
|
|
.proc main
|
|
; main application - rendering is currently off
|
|
|
|
; initialize palette table
|
|
ldx #0
|
|
paletteloop:
|
|
lda default_palette, x
|
|
sta palette, x
|
|
inx
|
|
cpx #32
|
|
bcc paletteloop
|
|
|
|
; clear 1st name table
|
|
jsr clear_nametable
|
|
|
|
; draw some text on the screen
|
|
lda PPU_STATUS ; reset address latch
|
|
lda #$20 ; set PPU address to $208A (Row = 4, Column = 10)
|
|
sta PPU_VRAM_ADDRESS2
|
|
lda #$8A
|
|
sta PPU_VRAM_ADDRESS2
|
|
|
|
ldx #0
|
|
textloop:
|
|
lda welcome_txt, x
|
|
sta PPU_VRAM_IO
|
|
inx
|
|
cmp #0
|
|
beq :+
|
|
jmp textloop
|
|
:
|
|
|
|
; place our bat sprite on the screen
|
|
lda #180
|
|
sta oam ; set Y
|
|
lda #120
|
|
sta oam + 3 ; set X
|
|
lda #1
|
|
sta oam + 1 ; set pattern
|
|
lda #0
|
|
sta oam + 2 ; set attributes
|
|
; place our ball sprite on the screen
|
|
lda #124
|
|
sta oam + (1 * 4) ; set Y
|
|
sta oam + (1 * 4) + 3 ; set X
|
|
lda #2
|
|
sta oam + (1 * 4) + 1 ; set patter + (1 * 4)n
|
|
lda #0
|
|
sta oam + (1 * 4) + 2 ; set atttibutes
|
|
; set the ball velocity
|
|
lda #1
|
|
sta d_x
|
|
sta d_y
|
|
|
|
; get the screen to render
|
|
jsr ppu_update
|
|
|
|
mainloop:
|
|
; skip reading controls if and change has not been drawn
|
|
lda nmi_ready
|
|
cmp #0
|
|
bne mainloop
|
|
; read the gamepad
|
|
jsr gamepad_poll
|
|
; now move the bat if left or right pressed
|
|
lda gamepad
|
|
and #PAD_L
|
|
beq NOT_GAMEPAD_LEFT
|
|
; gamepad has been pressed left
|
|
lda oam + 3 ; get current X
|
|
cmp #0
|
|
beq NOT_GAMEPAD_LEFT
|
|
sec
|
|
sbc #1
|
|
sta oam + 3 ; change X to the left
|
|
NOT_GAMEPAD_LEFT:
|
|
lda gamepad
|
|
and #PAD_R
|
|
beq NOT_GAMEPAD_RIGHT
|
|
; gamepad has been pressed right
|
|
lda oam + 3 ; get current X
|
|
cmp #248
|
|
beq NOT_GAMEPAD_RIGHT
|
|
clc
|
|
adc #1
|
|
sta oam + 3 ; change X to the left
|
|
NOT_GAMEPAD_RIGHT:
|
|
; now move our ball
|
|
lda oam + (1 * 4) + 0 ; get the current Y
|
|
clc
|
|
adc d_y ; add the Y velocity
|
|
sta oam + (1 * 4) + 0 ; write the change
|
|
cmp #0 ; have we hit the top border
|
|
bne NOT_HITTOP
|
|
lda #1 ; reverse direction
|
|
sta d_y
|
|
NOT_HITTOP:
|
|
lda oam + (1 * 4) + 0
|
|
cmp #210 ; have we hit the bottom border
|
|
bne NOT_HITBOTTOM
|
|
lda #$FF ; reverse direction (-1)
|
|
sta d_y
|
|
NOT_HITBOTTOM:
|
|
lda oam + (1 * 4) + 3 ; get the current x
|
|
clc
|
|
adc d_x ; add the X velocity
|
|
sta oam + (1 * 4) + 3
|
|
cmp #0 ; have we hit the left border
|
|
bne NOT_HITLEFT
|
|
lda #1 ; reverse direction
|
|
sta d_x
|
|
NOT_HITLEFT:
|
|
lda oam + (1 * 4) + 3
|
|
cmp #248 ; have we hot the right border
|
|
bne NOT_HITRIGHT
|
|
lda #$FF ; reverse direction (-1)
|
|
sta d_x
|
|
NOT_HITRIGHT:
|
|
|
|
; ensure our changes are rendered
|
|
lda #1
|
|
sta nmi_ready
|
|
jmp mainloop
|
|
.endproc
|
|
|
|
.segment "CODE"
|
|
.proc clear_nametable
|
|
lda PPU_STATUS ; reset address latch
|
|
lda #$20 ; set PPU address to $2000
|
|
sta PPU_VRAM_ADDRESS2
|
|
lda #$00
|
|
sta PPU_VRAM_ADDRESS2
|
|
|
|
; empty nametable
|
|
lda #0
|
|
ldy #30 ; clear 30 rows
|
|
rowloop:
|
|
ldx #32 ; 32 columns
|
|
columnloop:
|
|
sta PPU_VRAM_IO
|
|
dex
|
|
bne columnloop
|
|
dey
|
|
bne rowloop
|
|
|
|
; empty attribute table
|
|
ldx #64 ; attribute table is 64 bytes
|
|
loop:
|
|
sta PPU_VRAM_IO
|
|
dex
|
|
bne loop
|
|
rts
|
|
.endproc
|
|
|
|
;*****************************************************************
|
|
; gamepad_poll: this reads the gamepad state into the variable labelled "gamepad"
|
|
; This only reads the first gamepad, and also if DPCM samples are played they can
|
|
; conflict with gamepad reading, which may give incorrect results.
|
|
;*****************************************************************
|
|
|
|
.segment "CODE"
|
|
.proc gamepad_poll
|
|
; strobe the gamepad to latch current button state
|
|
lda #1
|
|
sta JOYPAD1
|
|
lda #0
|
|
sta JOYPAD1
|
|
; read 8 bytes from the interface at $4016
|
|
ldx #8
|
|
loop:
|
|
pha
|
|
lda JOYPAD1
|
|
; combine low two bits and store in carry bit
|
|
and #%00000011
|
|
cmp #%00000001
|
|
pla
|
|
; rotate carry into gamepad variable
|
|
ror
|
|
dex
|
|
bne loop
|
|
sta gamepad
|
|
rts
|
|
.endproc
|
|
|
|
;*****************************************************************
|
|
; Our default palette table 16 entries for tiles and 16 entries for sprites
|
|
;*****************************************************************
|
|
|
|
.segment "RODATA"
|
|
default_palette:
|
|
.byte $0F,$15,$26,$37 ; bg0 purple/pink
|
|
.byte $0F,$09,$19,$29 ; bg1 green
|
|
.byte $0F,$01,$11,$21 ; bg2 blue
|
|
.byte $0F,$00,$10,$30 ; bg3 greyscale
|
|
.byte $0F,$18,$28,$38 ; sp0 yellow
|
|
.byte $0F,$14,$24,$34 ; sp1 purple
|
|
.byte $0F,$1B,$2B,$3B ; sp2 teal
|
|
.byte $0F,$12,$22,$32 ; sp3 marine
|
|
|
|
welcome_txt:
|
|
.byte 'W','E','L','C', 'O', 'M', 'E', 0
|