Controller reading code
This article provides example code for reading the standard controller.
To read the first player's standard controller, write an odd value and then an even value to $4016, then read $4016 eight times in a row. For each read, if either of the low two bits are set, that means that the corresponding button is pressed. (On a Famicom, bit 0 returns hardwired controllers and bit 1 expansion controllers.) The buttons come in the order of A, B, Select, Start, Up, Down, Left, Right.
; Reads controller ; Out: A=buttons pressed, where bit 0 is A button read_joy: ; Strobe controller lda #1 sta $4016 lda #0 sta $4016 ; Read all 8 buttons ldx #8 loop: pha ; Read next button state and mask off low 2 bits. ; Compare with $01, which will set carry flag if ; either or both bits are set. lda $4016 and #$03 cmp #$01 ; Now, rotate the carry flag into the top of A, ; land shift all the other buttons to the right pla ror a dex bne loop rts
If a DMC sample is playing, it can corrupt the above sequence by inserting an extra $4016 read somewhere between yours if it happens to try to read a sample just as you're reading $4016. The simplest way to compensate is to repeatedly read the controller twice, and see if the two readings match. If they don't, read the controller again and compare with the previous read. Repeat until they match.
; temp is a zero-page variable ; Reads controller. Reliable when DMC is playing. ; Out: A=buttons held, A button in bit 0 read_joy_safe: ; Get first reading jsr read_joy mismatch: ; Save previous reading sta temp ; Read again and compare. If they differ, ; read again. jsr read_joy cmp temp bne mismatch rts
Note that if the player presses or releases a button between two reads, this will interpret that as a corrupt read and read again. Unfortunately, there's no way for it to tell the difference.
One problem with this approach is that it can read the controller more times than necessary, since for example after reading it three times (due to the first two not matching), it only compares the second and third reads; it doesn't compare the first and third, which might match. If the second read was the corrupt one, the second and third won't match, causing a fourth read.
A faster controller reading approach has been discussed, which assumes that the upper bits of $4016 will be the same each time it's read. This can fail on the Famicom, which has a microphone that toggles bit 2.
The result byte buttons should be placed in zero page to save a cycle each time through the loop.
; we reserve one byte for storing the data that is read from controller .zeropage buttons .res 1
When reading from JOYPAD* what is read might be different from $01/$00 for various reasons. (See Controller port registers.) In this code the only concern is bit 0 read from JOYPAD*..
JOYPAD1 = $4016 JOYPAD2 = $4017
This is the end result that will be stored in buttons. 1 if the button was pressed, 0 otherwise.
bit: 7 6 5 4 3 2 1 0 button: A B Select Start Up Down Left Right
This subroutine takes 132 cycles to execute but ignores the Famicom expansion controller. Many controller reading subroutines use the X or Y register to count 8 times through the loop. But this one uses a more clever ring counter technique: $01 is loaded into the result first, and once eight bits are shifted in, the 1 bit will be shifted out, terminating the loop.
; At the same time that we strobe bit 0, we initialize the ring counter ; so we're hitting two birds with one stone here readjoy: lda #$01 sta JOYPAD1 sta buttons lsr a ; now A is 0 sta JOYPAD1 : lda JOYPAD1 lsr a ; bit0 -> Carry rol buttons ; Carry -> bit0; bit 7 -> Carry bcc :- rts
Adding support for controllers on the Famicom's DA15 expansion port and for player 2's controller is straightforward.
.zeropage buttons1: .res 1 buttons2: .res 1 .code readjoy: lda #$01 sta JOYPAD1 sta buttons2 ; player 2's buttons double as a ring counter lsr a ; now A is 0 sta JOYPAD1 : lda JOYPAD1 and #$03 ; ignore bits other than controller cmp #$01 ; Set carry if and only if nonzero rol buttons1 ; Carry -> bit0; bit 7 -> Carry lda JOYPAD2 ; Repeat and #$03 cmp #$01 rol buttons2 ; Carry -> bit0; bit 7 -> Carry bcc :- rts
If your code is intended to be used with APU DMC playback, this code will need to be altered. The NES occasionally glitches the controller port twice in a row if sample playback is enabled, and games using samples need to work around this. For example, Super Mario Bros. 3 reads each controller's data at least two times each frame. First it reads it as normal, then it reads it again. If the two results differ, it does the procedure all over. Because repeated rereads can take a long time, another way is to just use the previous frame's button press data if the results differ:
last_frame_buttons1 = $00 last_frame_buttons2 = $01 first_read_buttons1 = $02 first_read_buttons2 = $03 readjoy_safe: lda buttons2 sta last_frame_buttons2 lda buttons1 sta last_frame_buttons1 ; Read the controllers once and stash the result jsr readjoy lda buttons2 sta first_read_buttons2 lda buttons1 sta first_read_buttons1 ; Read the controllers again and compare jsr readjoy ldx #1 cleanup_loop: ; Ignore read values if a bit deletion occurred lda buttons1,x cmp first_read_buttons1,x beq not_glitched lda last_frame_buttons,x sta buttons1,x not_glitched: ; Other filters, as described below, can go here dex bpl cleanup_loop rts
To reject opposing presses (Up+Down and Left+Right), which are possible on a worn Control Pad:
lda buttons1,x and #$0A ; Compare Up and Left... lsr a and buttons1,x ; to Down and Right beq not_updown ; Use previous frame's directions lda buttons1,x eor last_frame_buttons1,x and #$F0 eor last_frame_buttons1,x sta buttons1,x not_updown:
To instead reject all diagonal presses, simulating a 4-way joystick:
lda buttons1,x and #$0F ; If A & (A - 1) is nonzero, A has more than one bit set beq not_diagonal sec sbc #1 and buttons1,x ; to Down and Right beq not_updown1 ; Use previous frame's directions lda buttons1,x eor last_frame_buttons1,x and #$F0 eor last_frame_buttons1,x sta buttons1,x not_diagonal:
To calculate newly pressed and newly released buttons:
lda buttons1,x eor #$FF and last_frame_buttons1,x sta released_buttons1,x lda last_frame_buttons1,x eor #$FF and buttons1,x sta pressed_buttons1,x