16. Controller Input
« 15. Background Scrolling 17. Object Pools »
Table of Contents:
- A History of Controllers
- Controller Hardware
- Bit Shifts and Rotations
- Using Controller Data
- Homework
With the basics of NES graphics covered, let's turn our attention to reading player input from the controller(s). In this chapter, we'll look at the history of input devices for the system, how to read the state of controller buttons, and how to use controller input to create actual gameplay.
A History of Controllers
The most successful home console prior to the NES was the Atari
2600 (or "Video Computer System" / VCS).
The Atari 2600 joystick.
The 2600's controller was a joystick with a single
button, which meant players only had a limited ability to provide
input.
In place of a joystick, the Famicom/NES controllers use a
directional pad (or "D-pad"), shaped like a plus sign. This
setup, invented by Gunpei Yokoi,
The "Game & Watch Multiscreen" edition of Donkey Kong.
Yokoi would also go on to produce Kid Icarus and Metroid
for the NES, as well as designing the Virtual Boy. His practice
of "lateral thinking with withered technology" continues to
be the foundation of Nintendo's designs.
first made an appearance in Nintendo's "Game & Watch Multiscreen"
Donkey Kong system in 1982.
The Famicom controllers, released only one year later in 1983, were an evolution of the Multiscreen design. The two controllers each feature a D-pad, two action buttons (labelled "A" and "B"), and two supplemental buttons ("Select" and "Start"). On the Famicom, the controllers are clearly labelled "I" and "II", and player two's controller features a microphone with its own volume slider. Why did the Player 2 controller feature a microphone? At the time of the Famicom's release, karaoke had become very popular in Japan. The microphone would allow developers to create karaoke games for the Famicom, with the player's voice projected through their TV speakers. I'm not aware of any Famicom games that actually used that functionality, and the microphone was dropped when the controllers were revised for the NES. Some Famicom games do use the microphone as a sort of additional input by having the player scream into it at certain points. This functionality has been preserved in Mesen2, which allows you to set a key for microphone input when the Player 2 controller is set to "Famicom controller".

On the Famicom, the two controllers were hard-wired into the console. To facilitate the addition of future input methods, the Famicom console also had a front-facing, 15-pin expansion port, normally hidden behind a red plastic cover. This expansion port was where the Famicom's version of the "Zapper" light gun connected, as well as multiplayer accessories.


When the Famicom was re-designed for its US release as the NES, the controllers underwent some minor, but significant, changes. The microphone was dropped, making the "Player 1" and "Player 2" controllers identical. Additionally, the controllers were now detachable, and connected to the console via a specially-designed plug. This made using alternative controllers (such as the Zapper) much easier, since players could simply disconnect a standard controller and plug in something else in its place.

Photo by Evan Amos.
Nintendo (and its hardware partners) created several alternative controllers over the lifespan of the system. The NES Advantage was an arcade-style joystick, with the ability to set the A or B buttons to "Turbo" (automatic rapid button presses when held down), as well as a "slow motion" feature (in reality, a "Turbo" setting for the start button). The NES Max controller resembled more modern controller designs, with "wings" for better ergonomics and a sliding directional pad that looks similar to an analog stick, but is still digital input. Later controller releases included the NES Satellite and NES Four Score, each of which allowed four controllers to connect to the system at the same time and enabled simultaneous four-player gameplay in games that were coded to take advantage of it.

Perhaps the most unusual alternative controller, and the one which likely did more to cement Nintendo's success in the US than anything else, was R.O.B., the "Robotic Operating Buddy". Launched as part of the NES "Deluxe Set" in 1985, R.O.B. was a battery-powered "robot" with photodiodes for "eyes", which could watch the TV for specially-coded flashes of light and move in response to them. Only two licensed games were ever released to take advantage of R.O.B.: Gyromite, a pack-in game in the Deluxe Set, and Stack-Up, also released in 1985. While R.O.B. was a failure in terms of driving development of new games, it was a cool-looking (for the time) robot that enticed many players/parents to buy the Deluxe Set.
Controller Hardware
An NES controller has eight inputs: four on the directional pad (Up, Down, Left, Right), and four actual buttons (Select, Start, A, B). Each input can be in one of two states: pressed, or not pressed. This sounds like the perfect opportunity to store controller state in a byte of memory, with one bit for the state of each button. From an electrical standpoint, though, sending eight bits of data at a time would require eight wires. The NES controller connector only has seven pins, making this strategy impossible.

Instead of sending all eight button states in parallel, the NES controller uses a "parallel-in, serial-out shift register". The controller has one eight-bit register inside of itself. When the controller receives a signal from the console on the "Latch" wire, it begins to continuously fill the register with the current state of the buttons. Turning off the Latch signal causes the shift register to go back to serial mode, saving the last button states before the signal was turned off. Then, when the controller receives a signal on the "Clock" wire, it outputs the state of a single button at a time (in a specific order) on the appropriate Data wire. So, to read the state of all buttons on the controller, your code must:
- Send a "1" to the controller latch pin
- Send a "0" to the controller latch pin
- Send eight Clock signals to the controller, and listen for eight bits of data over the Data pin
There are, as you might expect, special memory-mapped I/O addresses for
performing these actions. Memory address $4016
sets the latch state
for both Controller 1 and Controller 2. Once the controllers' shift
registers are filled, reading from $4016
will return one bit of
data (one button state) from Controller 1, and reading from $4017
will return one bit
of data from Controller 2.
On the NES Satellite or NES Four Score, the first eight reads of
$4016
return the button states for Controller 1, and the next eight
reads return the button states for Controller 3. A final set of eight
reads returns a "signature" pattern of bits, which allows the game
to determine whether or not a four-player adapter is connected to the console.
A similar process is used with $4017
to read the states of Controller 2
and Controller 4.
To convert the flow above to assembly code, we first write a "1" to $4016
,
then write a "0" to the same address.
LDA #$01
STA $4016
LDA #$00
STA $4016
(Note: as with other MMIO addresses, it's common to set constant names
for $4016
and $4017
. Later, I will use CONTROLLER1
and CONTROLLER2
.)
Reads from $4016
each return a single button state, with bit 0
set if the button is pressed and cleared if it is not (all other bits
are cleared). The button states are returned in this order: A, B, Select,
Start, Up, Down, Left, Right. Storing these eight button states to
eight separate zero-page addresses would be very inefficient, but
thankfully, there is a simple technique that can store
those eight button states in a single byte. In order to use it, though,
we will need to learn a few more 6502 opcodes.
Bit Shifts and Rotations
A bit shift moves the bits within a byte left or right. The 6502 has
two opcodes that can shift bits: ASL
(Arithmetic Shift Left) and
LSR
(Logical Shift Right). ASL
moves all bits in a byte one position
to the left, dropping the leftmost bit (bit 7) into the carry flag and adding a zero to
replace the now-empty rightmost bit (bit 0). LSR
does the opposite,
moving all bits in a byte one position to the right, dropping the rightmost
bit into the carry flag and setting the leftmost bit to zero. Because of how binary numbers
work, performing a left shift is the same as multiplying by two (so long as
the result fits within a single byte), and a right shift is the same as
dividing by two and rounding down.
The rotation opcodes (ROL
"ROtate Left" and ROR
"ROtate Right") shift bits
just like ASL
and LSR
, but rather than filling empty bits with zeroes,
they move whatever is stored in the carry flag into the empty bit position.
When using a rotation opcode, the contents of the carry flag are shifted into
the byte before one of the byte's bits are shifted into the carry flag.
Let's look at a few examples.
; Our starting byte - equivalent to decimal 15
LDA #%00001111
STA $8000
; Shift left.
ASL $8000
; Memory address $8000 now contains 00011110,
; equivalent to decimal 30.
; The carry flag contains 0, because
; that was the left-most bit.
; Shift back to the right.
LSR $8000
; Memory address $8000 is now back to 00001111.
; The carry flag still contains 0.
; Shift right again.
LSR $8000
; Memory address $8000 now contains 00000111,
; equivalent to decimal 7.
; Note that the carry flag is now 1 - when
; the rightmost bit was shifted right, it went
; into the carry flag.
; This time, let's rotate right.
ROR $8000
; Memory address $8000 now contains 10000011,
; and the carry flag contains 1 again.
; What happened?
; The "1" from the carry flag moved into the
; leftmost bit position, and the "1" in the
; rightmost bit position dropped off into the
; carry flag.
; Let's do that a few more times:
ROR $8000
; Memory address $8000: 11000001, carry flag: 1
ROR $8000
; Memory address $8000: 11100000, carry flag: 1
ROR $8000
; Memory address $8000: 11110000, carry flag: 0
ROR $8000
; Memory address $8000: 01111000, carry flag: 0
; We can also shift or rotate the accumulator directly:
LDA $8000
ROL A
LSR A
; The results of the rotate and shift are only in
; the accumulator, not stored back into $8000.
Ring Counters
Now that we've looked at shifts and rotations, let's put them
to use to store controller data in a single byte. Remember, asking
for a button state (reading from $4016
) returns the state of a
button in bit 0, with the bit set if the button is pressed or
cleared if the button is not pressed. A ring counter makes use
of rotations to run a loop exactly eight times, transferring the
results of eight reads from $4016
into a single byte.
To set up the ring counter, we'll first need a byte of memory to store our controller data. Since controller data is updated frequently, a byte of zero-page RAM is ideal.
pad1: .res 1
Next, we will set the initial state of pad1
to the byte 00000001
.
LDA #%00000001
STA pad1
Each time we read a button state from the controller, we will use
shift and rotation opcodes to first transfer the bit that represents
the button state into the carry flag, and then rotate it onto pad1
.
When the "1" from bit 0 rotates all the way left and falls off into
the carry flag, we know that we have transferred eight button states
and we can end the loop (by checking at the end of each loop iteration
against BCC
, Branch if Carry Clear).
Here's a look at the full code:
; write a "1", then a "0", to CONTROLLER1 ($4016)
; in order to lock in button states
LDA #$01
STA CONTROLLER1
LDA #$00
STA CONTROLLER1
; initialize pad1 to 00000001
LDA #%00000001
STA pad1
get_button_states:
LDA CONTROLLER1 ; Get the next button state
LSR A ; Shift the accumulator right one bit,
; dropping the button state from bit 0
; into the carry flag
ROL pad1 ; Shift everything in pad1 left one bit,
; moving the carry flag into bit 0
; (because rotation) and bit 7
; of pad1 into the carry flag
BCC get_button_states ; If the carry flag is still 0,
; continue the loop. If the "1"
; that we started with drops into
; the carry flag, we are done.
At the end of this loop, the eight bits of pad1
will contain
the state of all eight buttons on the controller, as follows:
To capture the state of player 2's controller buttons, use
the same ring counter, but substitute CONTROLLER2
($4017
)
for CONTROLLER1
, and designate a second byte of zero-page RAM
for storing button states (pad2
).
Bit | Button |
---|---|
0 | Right |
1 | Left |
2 | Down |
3 | Up |
4 | Start |
5 | Select |
6 | B |
7 | A |
Using Controller Data
Once you have captured the state of the controller's buttons, the
next step is making use of that data in your game code. To do so,
we can use the logical filters introduced in the last chapter to
test whether or not the buttons we care about are set, and then
branch based on the zero flag. To make that testing easier, I like
to set constants for each button's position in pad1
:
BTN_RIGHT = %00000001
BTN_LEFT = %00000010
BTN_DOWN = %00000100
BTN_UP = %00001000
BTN_START = %00010000
BTN_SELECT = %00100000
BTN_B = %01000000
BTN_A = %10000000
Once you have these constants, the checks themselves are fairly simple. Here is how we would test whether or not the start button is pressed:
LDA pad1 ; Load button states into accumulator
AND #BTN_START ; Must use "#"! Not a memory address!
BNE start_pressed ; Branch to code you want to
; run when start is pressed
As a quick refresher, AND
lets you selectively filter out
bits from the accumulator. Any bits that are "0" in AND
's
operand will be set to zero in the accumulator; any bits that
are "1" in AND
's operand will stay as they are in the
accumulator. By using 00010000
as our operand for AND
,
we ensure that all bits except the bit that represents
the state of the start button will be zero. That way, if
the start button is not pressed, the result of our AND
will be zero, regardless of how many other buttons on the
controller are pressed.
Let's apply this to the game we've been building. Instead of
having the player's ship automatically move left and right,
we will instead read the controller and move the player's
ship appropriately. We already isolated our player-updating
code into its own subroutine (.proc update_player
), which
should make things a bit simpler. We will need to make the
following changes from the last chapter:
- Read the state of the controller in NMI
- Update our
update_player
subroutine to test for different button presses and move the sprites accordingly
Let's start with reading the controller. We will make a new
subroutine for the ring counter and call it from NMI. First, though,
we will need some new constants in our constants.inc
:
10 CONTROLLER1 = $4016
11 CONTROLLER2 = $4017
12
13 BTN_RIGHT = %00000001
14 BTN_LEFT = %00000010
15 BTN_DOWN = %00000100
16 BTN_UP = %00001000
17 BTN_START = %00010000
18 BTN_SELECT = %00100000
19 BTN_B = %01000000
20 BTN_A = %10000000
With that out of the way, let's create a new file for controller-related
subroutines. This will make it possible to re-use the same
controller code in multiple projects. Inside the file (I'm calling
it controllers.asm
), we'll write a subroutine that uses
the ring counter technique we saw earlier:
1 .include "constants.inc"
2
3 .segment "ZEROPAGE"
4 .importzp pad1
5
6 .segment "CODE"
7 .export read_controller1
8 .proc read_controller1
9 PHA
10 TXA
11 PHA
12 PHP
13
14 ; write a 1, then a 0, to CONTROLLER1
15 ; to latch button states
16 LDA #$01
17 STA CONTROLLER1
18 LDA #$00
19 STA CONTROLLER1
20
21 LDA #%00000001
22 STA pad1
23
24 get_buttons:
25 LDA CONTROLLER1 ; Read next button's state
26 LSR A ; Shift button state right, into carry flag
27 ROL pad1 ; Rotate button state from carry flag
28 ; onto right side of pad1
29 ; and leftmost 0 of pad1 into carry flag
30 BCC get_buttons ; Continue until original "1" is in carry flag
31
32 PLP
33 PLA
34 TAX
35 PLA
36 RTS
37 .endproc
One thing to note is the .importzp pad1
on line 4 - we will
need to make sure that we reserve a byte in zero-page with that
name and export it with .exportzp
. Here is the updated
ZEROPAGE
segment in our main file (now called input.asm
):
I've also removed the player_dir
byte; it was only needed
because we were continually moving the player's ship either
left or right. Since we don't need to track that anymore,
we've freed up another byte of zero-page RAM.
4 .segment "ZEROPAGE"
5 player_x: .res 1
6 player_y: .res 1
7 scroll: .res 1
8 ppuctrl_settings: .res 1
9 pad1: .res 1
10 .exportzp player_x, player_y, pad1
We have a fair number of things that we are keeping track of, but we are only using five of the 256 zero-page addresses available to us. Even so, it's still important to use zero-page only for things that will be updating frequently, since later additions like audio or tracking a large number of enemies can take up a big chunk of zero-page addresses.
Next, let's update the NMI handler to read controller state once per frame:
18 .import read_controller1
19
20 .proc nmi_handler
21 LDA #$00
22 STA OAMADDR
23 LDA #$02
24 STA OAMDMA
25 LDA #$00
26
27 ; read controller
28 JSR read_controller1
Remember that you need to import any subroutines you want to use that are exported in a different file (line 18).
Everything is in place to work with controller data. Now, it's
time to update the update_player
subroutine.
While virtually all of this subroutine will be replaced, a
portion of it that might still be useful is the code to detect
collisions with the edge of the screen. To keep the logic
simpler, this chapter will only focus on using controller
input, but it's a good homework exercise to re-implement
bounds checking on your own, essentially ignoring controller
presses that would put the ship outside of a certain area.
Our logic for updating the player position will work as follows
(the exact order of checking directions is arbitrary):
- Check if the player pressed Left. If so, decrement
player_x
. - Check if the player pressed Right. If so, increment
player_x
. - Check if the player pressed Up. If so, decrement
player_y
. - Check if the player pressed Down. If so, increment
player_y
.
This setup gives us a one-to-one mapping between controller presses and movement on screen, which will have a "stiff" feel. In a later chapter, we'll add rudimentary physics, but this simpler approach will let us focus on the controller.
The updated update_player
subroutine is as follows:
102 .proc update_player
103 PHP ; Start by saving registers,
104 PHA ; as usual.
105 TXA
106 PHA
107 TYA
108 PHA
109
110 LDA pad1 ; Load button presses
111 AND #BTN_LEFT ; Filter out all but Left
112 BEQ check_right ; If result is zero, left not pressed
113 DEC player_x ; If the branch is not taken, move player left
114 check_right:
115 LDA pad1
116 AND #BTN_RIGHT
117 BEQ check_up
118 INC player_x
119 check_up:
120 LDA pad1
121 AND #BTN_UP
122 BEQ check_down
123 DEC player_y
124 check_down:
125 LDA pad1
126 AND #BTN_DOWN
127 BEQ done_checking
128 INC player_y
129 done_checking:
130 PLA ; Done with updates, restore registers
131 TAY ; and return to where we called this
132 PLA
133 TAX
134 PLA
135 PLP
136 RTS
137 .endproc
That rounds out the updates to the code. Let's build everything and verify that it works as expected, by running the following commands in the top-level directory for the project.
ca65 src/backgrounds.asm
ca65 src/controllers.asm
ca65 src/reset.asm
ca65 src/input.asm
ld65 src/backgrounds.o src/controllers.o src/input.o src/reset.o -C nes.cfg -o input.nes
The resulting .nes file should let you move the player ship freely using whatever your emulator has mapped to the D-pad inputs. Here it is running in an emulator in your browser! Note that since we are now using the controller, the emulator has an on-screen controller. There is also support for keyboard shortcuts, using the following keys (for QWERTY keyboards). You will need to first click on the emulator area or otherwise focus it in order for keypresses to be registered.
Controller | Keyboard |
---|---|
Right | Right |
Left | Left |
Down | Down |
Up | Up |
Start | Enter |
Select | Tab |
B | s |
A | a |
Homework
Here are some exercises to try now that we have covered the basics of controller input. You can download the code from this chapter as a starting point.
- Using the code from Chapter 14 as a guide, make it so the player's ship cannot wrap around the screen. When the ship approaches an edge, ignore controller input that would move it off screen.
- Make it so the ship responds differently when multiple buttons are pressed at the same time. As an example, holding "B" while pressing a direction could make the player's ship move twice as fast as normal.
- Let the player "pause" the game. When Start is pressed, stop scrolling the screen and ignore controller directional movement. When Start is pressed again, resume scrolling and listening to inputs.