Hardware Projects‎ > ‎N64‎ > ‎

N64 Controller

Line protocol

The console/controller communication is performed using a one-wire, half-duplex serial protocol.  The data line idles high.  Data is transmitted at a 250KHz baud rate, with a '1' bit represented by 1uS low, 3uS high, and a '0' bit represented by 3uS low, 1uS high.  Data is transmitted MSB first.  Transmissions begin at the first negative edge and continue until a stop bit is transmitted.  Stop bits are only 3uS, which is important to pay attention to, especially when operating in host (i.e. console) mode, because the controller can (and often will) respond immediately.  The console transmits a stop bit as 1uS low, 2uS high, the controller transmits as 2uS low, 1uS high.

It is possible to implement this communication on a microcontroller by (ab)using a standard UART module.  Although this line protocol is not standard UART, it is possible to very closely mimic it by converting 2 bits of controller data into 1 byte of UART data.  Unfortunately, due to the start and stop bits which result in a 10-bit-per-byte transmission, it is not possible to get the exact duty cycle we need, so instead of 1uS and 3uS, we get 0.8uS and 3.2uS, respectively.  However, this seems to still work just fine.

In order to wire this up, we need to tie the UART's Rx and Tx pins together using a Schottky diode, as shown here


Once everything is wired up, data can be sent and received using the following values:

N64  |  UART
0b00 | 0x08
0b01 | 0xE8
0b10 | 0x0F
0b11 | 0xEF

(Note that the N64 data is MSB first, UART data is LSB first)
 
One thing to be careful of when doing this is that because the Rx and Tx lines are connected together, any data sent out from the UART will also be received back, so if you are using Rx interrupts, they should be disabled during transmission, and the Rx buffer should be cleared at the end of the transmission before re-enabling them (or, if you can disable the Rx function entirely, that works too).  You also need to be very careful about the timing when doing so, because stop bits are only 3uS (2 low, 1 high) which means you can't just read out the Rx buffer normally.  On the PIC18F25K42, I did this by waiting for the TX buffer to be empty, indicating that the stop bit had been loaded into the Tx shift register, then waiting by manually polling the Tx pin until it went low, then high.  Only then did I re-enable the UART's Rx buffer and interrupt.  The timing is pretty tight because the controller can respond immediately after the 1uS high period, but I was able to make it work.

Data protocol


The console polls the controller by sending a 1-byte command.  Some commands are followed by parameters, which may vary in length.  The controller response varies, depending on the command.

Command 0x00 [Status]

The controller responds with 3 bytes.  For a standard gamepad, the first two bytes are 0x05, 0x00.  The third byte contains bit flags.  0x04 indicates an address CRC error in the last communication[1].  0x02 indicates no controller pak inserted.  0x01 indicates a controller pak (or rumble pak) is inserted.  If a pak is inserted with the power on, the controller will return 0x03 exactly once between 0x02 and 0x01 (i.e. 050002, 050003, 050001, 050001...).

[1] I have not confirmed this personally

Command 0x01 [Poll]

The controller responds with 4 bytes containing the button and axis states.

7 6 5 4 3 2 1 0
A B Z Start DU DD DL DR
Reset[1] Reserved[2] L R CU CD CL CR
 X
 Y

[1] Reset is '1' when L+R+Start are all pressed.  When Reset is '1', L and R are both reported as '1' but Start is reported as '0'.
[2] Unknown, always '0' for normal gamepads

Joystick encoder


Pinout:

Pin 1 on the left(marked with a white wire)
P1: Y1
P2: Y0
P3: X1
P4: Gnd
P5: Vcc
P6: X0


Waveforms


Legend

D0: X0
D1: X1
D2: Y0
D3: Y1

X-



X+




Y-




Y+


I don't have any way of measuring whether or not the quadrature encoder is linear relative to the joystick's physical position, but by removing the joystick and manually sending quadrature pulses to the controller, then polling via command 0x01, I was able to confirm that the relationship between quadrature pulses and reported joystick position IS linear.  A single pulse increments/decrements the position by a value of 4.  The position is reported as a signed (two's complement) 8-bit integer.  The OEM controller is capable of reporting values ranging from 0x80-0x7F, but the actual joystick does not cover the entire range.  In my testing, the joystick appears to report approximately +/- 18 steps (shown above in the logic analyzer screenshots), for a range of 0xD0-0x48, but that may vary from one joystick to the next, depending on wear.

On power-on, the X-axis initializes to 0xFE, while the Y-axis initializes to 0x00.  After resetting via command 0xFF, both axes report 0x00.  The reported value does not wrap around, i.e.  0x00, 0x04, 0x08, . . . 0x7C, 0x7F, 0x7F, 0x7F and 0x00, 0xFC, 0xF8, . . . 0x84, 0x80, 0x80, 0x80.