⭅ Previous (System Timing) | Next (Audio(APU)) ⭆ |
Now that we’ve wrapped up a high level model for the graphics system, its time to add interactivity to our emulator. We’ll take a look at how the NES controller port works, and how the standard controller uses the port. After understanding how the port works, we’ll know how to add controller support to our emulator.
Also available on Youtube
The NES features an iconic controller. What some people don’t realize is that the system was actually designed to support a wealth of other peripherals. This lead to a fairly generic design for the controller port, to enable controllers besides the standard dpad + buttons.
Here’s what the port looks like on the NES. Note the Famicom has a much larger port, with different wiring. I’ll focus on the NES connector.
Lets take a look at the electrical pinout for this connector, which will help outline the functionality.
GND - O
CLK - O O - 5V
OUT - O O - D3
D0 - O O - D4
Reference: https://www.nesdev.org/wiki/Controller_port_pinout
GND and 5V are standard power connections. Most interesting are the CLK, OUT, and D lines. The CLK signal is suggestive of timing. Since the NES wanted to support a wide variety of hardware, this connector is designed to read bits serially(one at a time).
The standard NES controller accomplishes this using a shift register. A shift register essentially allows the storing of a certain number of bits. The register can then be “shifted”, exposing a different one of those bits on its output. After shifting through all 8 positions, the register is empty and will return meaningless data. These single bits are sent over the OUT pin.
The D pins connect to the data bus of the CPU. These enable the controller to receive information from the system, useful for some specialized controllers. These arent used by the standard controller.
Lets take a look at the standard NES controller:
_
_| |_
|_ _| <Select> <Start> (B) (A)
|_|
With the 4 dpad directions and four buttons, the controller has exactly 8 buttons. Given the shift register and the structure of the controller port, this means the software interacts with the controller by saving or latching the state of the controller, then shifting until all the button bits have been read out.
Now that we know what we want to accomplish, lets see how software achieves this using a few more MMIO registers.
First, the controller state needs to be stored in the shift register. The shift register has a pin called strobe, which when high stores the current value in the register. While the strobe is high, reading from the shift register will continually read the first buttom.
After setting the strobe low, the shift register will hold its previous state, changing only in response to a shift.
The strobe is controlled via a MMIO register at 0x4016. Only one bit is used, and it is the 1s place or least significant bit. So writing 0x01 to 0x4016 sets the strobe, and writing 0x00 releases it. After a set and release the shift register is loaded with the state of all 8 buttons.
Finally controller bits can be read out one at a time. These are communicated via bit 1 of the value read from this address. Player 1 is read from 0x4016, and player 2 from 0x4017. Though each controller is read from a separate address, both controllers share the strobe write at 0x4016.
Bits are read out of the standard controller according to the ordering:
Now that we know how the controller works, we can build a simple program that just logs the state read from the controller port. It will read the controller during vblank, and use the sprites to display which buttons are pressed.
The screen will always contain sprites for each of the buttons. If the button is pressed, it will light up. Otherwise, it will use a more muted color.
The snippet below demonstrates the key part of the interrupt handler. Like the OAMDMA code we saw for sprites, this keeps a copy of OAM in page 0x0200 then uses dma to copy into OAM.
; Constants
CTRL1 = $4016
; now read each bit for the buttons. If set, use palette 1. otherwise palette 0
ldy #$06 ; OAM offset. 2nd slot(4) + 2 for attribute position
ldx #$08 ; Sprite&tile num
.btn_update:
lda CTRL1
and #$1 ; clear unmapped bits, want 0 or 1
; use value as palette directly.
; 0 = unpressed should be same as bg
; 1 = pressed = active palette
; write attr
sta (0), y
; then add +4 to move to next oam slot
tya
clc
adc #$04
tay
dex
bne .btn_update
When running the code, it should look something like this. The respective buttons light up when the key is pressed, and are grey otherwise.
⭅ Previous (System Timing) | Next (Audio(APU)) ⭆ |