NES 6502 Programming Tutorial - Part 5: Controller Commands
Hey and welcome back to my NES 6502 programming tutorial. Today I'm going to show you how to move the sprite using the controller. The NES controller, as you see here, has eight buttons:
which obviously consists of up, down, left, right, B, A, select, and start. And the NES has two controller ports. But believe it or not, some NES games have 4 player simultaneous play requiring four controllers, which is only possible if you have an NES Four Score or an NES Satellite:
And there are too many peripherals to count, such as the Zapper, the Power Pad, the Miracle Piano, R.O.B. the Robot, the Power Glove, etc. All of which use different states the controller ports read besides the basic eight. So in this tutorial, I'm only going to focus on Controller 1 which reads at the address $4016. If you wanna attempt Controller 2 in the mix, it's at address $4017. So we are going to rework our Sprite Movement project and change up a couple of things.
The first step is that we are going to need to latch both the controllers right after the small chunk of code in the NMI:
Code:
NMI:
LDA #$00
STA $2003 ; set the low byte (00) of the RAM address
LDA #$02
STA $4014 ; set the high byte (02) of the RAM address, start the transfer
LatchController:
LDA #$01
STA $4016
LDA #$00
STA $4016 ; tell both the controllers to latch buttons
Next, we will need to read the memory address $4016 in this exact order and check if there has been an input pressed, and remember, it is in this exact order!
(1) A
(2) B
(3) Select
(4) Start
(5) Up
(6) Down
(7) Left
(8) Right
As an example, we will proceed directly after the LatchController code starting with A for simplicity:
Code:
LatchController:
LDA #$01
STA $4016
LDA #$00
STA $4016 ; tell both the controllers to latch buttons
ReadA:
LDA $4016 ; player 1 - A
AND #%00000001 ; only look at bit 0
BEQ ReadADone ; branch to ReadADone if button is NOT pressed (0)
; add instructions here to do something when button IS pressed (1)
ReadADone: ; handling this button is done
Basically, any action you want to happen when the button is pressed goes in between the BEQ and the subroutine ReadADone. Otherwise it jumps when it is not pressed. And it will continue on to the other buttons in that specific order. So basically, you are adding all this:
Code:
LatchController:
LDA #$01
STA $4016
LDA #$00
STA $4016 ; tell both the controllers to latch buttons
ReadA:
LDA $4016 ; player 1 - A
AND #%00000001 ; only look at bit 0
BEQ ReadADone ; branch to ReadADone if button is NOT pressed (0)
; add instructions here to do something when button IS pressed (1)
ReadADone: ; handling this button is done
ReadB:
LDA $4016 ; player 1 - B
AND #%00000001 ; only look at bit 0
BEQ ReadBDone ; branch to ReadBDone if button is NOT pressed (0)
; add instructions here to do something when button IS pressed (1)
ReadBDone: ; handling this button is done
ReadSelect:
LDA $4016 ; player 1 - A
AND #%00000001 ; only look at bit 0
BEQ ReadSelectDone ; branch to ReadADone if button is NOT pressed (0)
ReadSelectDone:
ReadStart:
LDA $4016 ; player 1 - A
AND #%00000001 ; only look at bit 0
BEQ ReadStartDone ; branch to ReadADone if button is NOT pressed (0)
ReadStartDone:
ReadUp:
LDA $4016 ; player 1 - A
AND #%00000001 ; only look at bit 0
BEQ ReadUpDone ; branch to ReadADone if button is NOT pressed (0)
ReadUpDone:
ReadDown:
LDA $4016 ; player 1 - A
AND #%00000001 ; only look at bit 0
BEQ ReadDownDone ; branch to ReadADone if button is NOT pressed (0)
ReadDownDone:
ReadLeft:
LDA $4016 ; player 1 - A
AND #%00000001 ; only look at bit 0
JMP ReadLeftDone ; branch to ReadADone if button is NOT pressed (0)
ReadLeftDone:
ReadRight:
LDA $4016 ; player 1 - A
AND #%00000001 ; only look at bit 0
JMP ReadRightDone ; branch to ReadADone if button is NOT pressed (0)
ReadRightDone:
In your sprite movement project, you had a chunk of code to automatically move Mario across the screen towards the right. Go ahead and copy and paste this chunk here that you had:
Code:
LDA player_x
STA $0203
STA $020B
TAX
CLC
ADC #$08
STA $0207
STA $020F
INX
STX player_x
And put it inside the controls for the Right:
Code:
ReadRight:
LDA $4016 ; player 1 - A
AND #%00000001 ; only look at bit 0
JMP ReadRightDone ; branch to ReadADone if button is NOT pressed (0)
;Move Mario to the right
LDA player_x
STA $0203
STA $020B
TAX
CLC
ADC #$08
STA $0207
STA $020F
INX
STX player_x
ReadRightDone:
Compile the code using either the batch file, or dragging the asm file into NESASM3, and run it in your emulator. DOH! It's not moving. Looks like we left something out. If the AND result is not equal, well need to jump to the movement code:
Code:
ReadRight:
LDA $4016 ; player 1 - A
AND #%00000001 ; only look at bit 0
BNE DoRight
JMP ReadRightDone ; branch to ReadADone if button is NOT pressed (0)
;Move Mario to the right
DoRight:
LDA player_x
STA $0203
STA $020B
TAX
CLC
ADC #$08
STA $0207
STA $020F
INX
STX player_x
ReadRightDone:
Now recompile it, and run it. Now Mario moves right....and only right . Well need some code to move him left as well:
Code:
ReadLeft:
LDA $4016 ; player 1 - A
AND #%00000001 ; only look at bit 0
BNE DoLeft
JMP ReadLeftDone ; branch to ReadADone if button is NOT pressed (0)
;Move Mario to the left
DoLeft:
LDA player_x
STA $0203
STA $020B
TAX
CLC
ADC #$08
STA $0207
STA $020F
DEX
STX player_x
ReadLeftDone:
Compile and run the code again, and now Mario moves left and right seamlessly. By now your completed code looks like this:
Code:
.inesprg 1 ; 1x 16KB PRG code
.ineschr 1 ; 1x 8KB CHR data
.inesmap 0 ; mapper 0 = NROM, no bank swapping
.inesmir 1 ; background mirroring
;;;;;;;;;;;;;;;
;; DECLARE SOME VARIABLES HERE
.rsset $0000 ;;start variables at ram location 0
player_x .rs 1 ; .rs 1 means reserve one byte of space
;;;;;;;;;;;;;;;
.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
vblankwait1: ; First wait for vblank to make sure PPU is ready
BIT $2002
BPL vblankwait1
clrmem:
LDA #$00
STA $0000, x
STA $0100, x
STA $0400, x
STA $0500, x
STA $0600, x
STA $0700, x
LDA #$FE
STA $0300, x
INX
BNE clrmem
vblankwait2: ; Second wait for vblank, PPU is ready after this
BIT $2002
BPL vblankwait2
LoadPalette:
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
LoadBackgroundPaletteLoop:
LDA background_palette, x ; load data from address (palette + the value in x)
; 1st time through loop it will load palette+0
; 2nd time through loop it will load palette+1
; 3rd time through loop it will load palette+2
; etc
STA $2007 ; write to PPU
INX ; X = X + 1
CPX #$10 ; Compare X to hex $10, decimal 16
BNE LoadBackgroundPaletteLoop ; Branch to LoadBackgroundPaletteLoop if compare was Not Equal to zero
LDX #$00
LoadSpritePaletteLoop:
LDA sprite_palette, x ;load palette byte
STA $2007 ;write to PPU
INX ;set index to next byte
CPX #$10
BNE LoadSpritePaletteLoop ;if x = $10, all done
LDX #$00 ; start at 0
LoadSpritesLoop:
LDA sprites, x ; load data from address (sprites + x)
STA $0200, x ; store into RAM address ($0200 + x)
INX ; X = X + 1
CPX #$10 ; Compare X to hex $10, decimal 16
BNE LoadSpritesLoop ; Branch to LoadSpritesLoop if compare was Not Equal to zero
; if compare was equal to 32, keep going down
LDA #%10000000 ; enable NMI, sprites from Pattern Table 0
STA $2000
LDA #%00010000 ; enable sprites
STA $2001
LDA $0203
STA player_x
Foreverloop:
JMP Foreverloop ;jump back to Forever, infinite loop
NMI:
LDA #$00
STA $2003 ; set the low byte (00) of the RAM address
LDA #$02
STA $4014 ; set the high byte (02) of the RAM address, start the transfer
LatchController:
LDA #$01
STA $4016
LDA #$00
STA $4016 ; tell both the controllers to latch buttons
ReadA:
LDA $4016 ; player 1 - A
AND #%00000001 ; only look at bit 0
BEQ ReadADone ; branch to ReadADone if button is NOT pressed (0)
; add instructions here to do something when button IS pressed (1)
ReadADone: ; handling this button is done
ReadB:
LDA $4016 ; player 1 - B
AND #%00000001 ; only look at bit 0
BEQ ReadBDone ; branch to ReadBDone if button is NOT pressed (0)
; add instructions here to do something when button IS pressed (1)
ReadBDone: ; handling this button is done
ReadSelect:
LDA $4016 ; player 1 - A
AND #%00000001 ; only look at bit 0
BEQ ReadSelectDone ; branch to ReadADone if button is NOT pressed (0)
ReadSelectDone:
ReadStart:
LDA $4016 ; player 1 - A
AND #%00000001 ; only look at bit 0
BEQ ReadStartDone ; branch to ReadADone if button is NOT pressed (0)
ReadStartDone:
ReadUp:
LDA $4016 ; player 1 - A
AND #%00000001 ; only look at bit 0
BEQ ReadUpDone ; branch to ReadADone if button is NOT pressed (0)
ReadUpDone:
ReadDown:
LDA $4016 ; player 1 - A
AND #%00000001 ; only look at bit 0
BEQ ReadDownDone ; branch to ReadADone if button is NOT pressed (0)
ReadDownDone:
ReadLeft:
LDA $4016 ; player 1 - A
AND #%00000001 ; only look at bit 0
BNE DoLeft
JMP ReadLeftDone ; branch to ReadADone if button is NOT pressed (0)
;Move Mario to the left
DoLeft:
LDA player_x
STA $0203
STA $020B
TAX
CLC
ADC #$08
STA $0207
STA $020F
DEX
STX player_x
ReadLeftDone:
ReadRight:
LDA $4016 ; player 1 - A
AND #%00000001 ; only look at bit 0
BNE DoRight
JMP ReadRightDone ; branch to ReadADone if button is NOT pressed (0)
;Move Mario to the right
DoRight:
LDA player_x
STA $0203
STA $020B
TAX
CLC
ADC #$08
STA $0207
STA $020F
INX
STX player_x
ReadRightDone:
RTI
;;;;;;;;;;;;;;
.bank 1
.org $E000
background_palette:
.db $22,$29,$1A,$0F ;background palette 1
.db $22,$36,$17,$0F ;background palette 2
.db $22,$30,$21,$0F ;background palette 3
.db $22,$27,$17,$0F ;background palette 4
sprite_palette:
.db $22,$16,$27,$18 ;sprite palette 1
.db $22,$1A,$30,$27 ;sprite palette 2
.db $22,$16,$30,$27 ;sprite palette 3
.db $22,$0F,$36,$17 ;sprite palette 4
sprites:
;vert tile attr horiz
.db $08, $3A, %00000000, $08 ;sprite 0
.db $08, $37, %00000000, $10 ;sprite 1
.db $10, $4f, %00000000, $08 ;sprite 2
.db $10, $4f, %01000000, $10 ;sprite 3
;;;;;;;;;;;;;;
.org $FFFA ;first of the three vectors starts here
.dw NMI ;when an NMI happens (once per frame if enabled) the
;processor will jump to the label NMI:
.dw RESET ;when the processor first turns on or is reset, it will jump
;to the label RESET:
.dw 0 ;external interrupt IRQ is not used in this tutorial
;;;;;;;;;;;;;;
.bank 2
.org $0000
.incbin "mario.chr" ;includes 8KB graphics file from SMB1
For an added challenge, try moving Mario up and down as well! I haven't decided what I want to do for my next tutorial yet, but maybe I might touch into drawing background tiles, since animation is too advanced of a jump for you noobies. But I'll eventually get to it. Have an awesome day!