Skip to main content

Overview

The N64Backend class implements the Joybus protocol for communicating with Nintendo 64 consoles. It handles bidirectional communication over a single data line, responding to console polls and sending controller state.
The N64 backend uses the same Joybus protocol family as GameCube but with a different report format and timing characteristics.

Platform Support

  • Raspberry Pi Pico: ✅ Supported (PIO-based)
  • Arduino (AVR): ✅ Supported (Nintendo library)

Constructor

Raspberry Pi Pico

N64Backend(
    InputState &inputs,
    InputSource **input_sources,
    size_t input_source_count,
    uint data_pin,
    PIO pio = pio0,
    int sm = -1,
    int offset = -1
)
inputs
InputState&
required
Reference to the global input state structure.
input_sources
InputSource**
required
Array of pointers to input source objects.
input_source_count
size_t
required
Number of elements in the input_sources array.
data_pin
uint
required
GPIO pin number for the Joybus data line (bidirectional communication).
pio
PIO
default:"pio0"
PIO block to use for Joybus communication (pio0 or pio1).
sm
int
default:"-1"
State machine index to use. -1 for automatic allocation.
offset
int
default:"-1"
Program offset in PIO instruction memory. -1 for automatic allocation.

Arduino (AVR)

N64Backend(
    InputState &inputs,
    InputSource **input_sources,
    size_t input_source_count,
    int polling_rate,
    int data_pin
)
polling_rate
int
required
Expected polling rate from the console in Hz (typically 60-240). Set to 0 to disable delay compensation.
data_pin
int
required
Arduino pin number for the Joybus data line.
The AVR version uses the polling rate to calculate optimal timing delays, ensuring reports are ready just before the next poll.

Configuration Example

// Pico configuration
const Pinout pinout = {
    .joybus_data = 28,  // GPIO 28 for data line
    // ... other pins
};

N64Backend n64_backend(
    inputs,
    input_sources,
    input_source_count,
    pinout.joybus_data  // data_pin
);
// Arduino configuration
N64Backend n64_backend(
    inputs,
    input_sources,
    input_source_count,
    240,  // polling_rate (240 Hz)
    2     // data_pin (Arduino pin 2)
);

Backend ID

CommunicationBackendId BackendId() {
    return COMMS_BACKEND_N64;
}

SendReport Method

void SendReport();
The SendReport() method implements the N64 polling response protocol:

Pico Implementation

  1. Scans slow and medium-speed inputs
  2. Waits for console poll signal
  3. Scans fast inputs immediately after poll
  4. Updates output state based on game mode
  5. Maps outputs to N64 report format
  6. Sends report to console
ScanInputs(InputScanSpeed::SLOW);
ScanInputs(InputScanSpeed::MEDIUM);

_n64.WaitForPoll();

ScanInputs(InputScanSpeed::FAST);

UpdateOutputs();

_n64.SendReport(&_report);

Arduino Implementation

  1. Scans all inputs at once
  2. Updates output state based on game mode
  3. Maps outputs to N64 report format
  4. Sends report to console
  5. Delays until just before next expected poll
ScanInputs();
UpdateOutputs();

_n64.write(_data);

delayMicroseconds(_delay);

Report Structure

N64 controllers report the following:

Digital Buttons

  • A, B
  • Z (mapped from buttonR)
  • L, R (trigger digital)
  • Start
  • D-pad: Up, Down, Left, Right
  • C-buttons: C-Up, C-Down, C-Left, C-Right

Analog Input

  • Control stick: X/Y (signed 8-bit, -128 to +127)
Unlike GameCube, the N64 uses signed 8-bit integers for analog stick values, with 0 representing center position.

C-Button Mapping

The right analog stick is mapped to the C-buttons:
_report.c_left = _outputs.rightStickX < 128;
_report.c_right = _outputs.rightStickX > 128;
_report.c_down = _outputs.rightStickY < 128;
_report.c_up = _outputs.rightStickY > 128;
This allows modern analog sticks to control games expecting C-button digital input.

Analog Stick Conversion

The firmware converts unsigned 8-bit stick values to signed:
// Pico
_report.stick_x = _outputs.leftStickX - 128;
_report.stick_y = _outputs.leftStickY - 128;

// Arduino
_data.report.xAxis = _outputs.leftStickX - 128;
_data.report.yAxis = _outputs.leftStickY - 128;

Polling Rate and Timing

Pico (PIO-based)

  • Uses hardware state machines for precise timing
  • Automatically synchronizes with console polls
  • Immediate response to poll signal

Arduino (Software-based)

  • Requires manual polling rate configuration
  • Calculates delay: (1000000 / polling_rate) - 850
  • Leaves 850μs for processing before next poll
  • Typical N64 polling rates: 60-240 Hz

GetOffset Method (Pico Only)

int GetOffset();
Returns the PIO program offset, useful for sharing PIO resources between multiple backends.

Hardware Requirements

Signal Line

  • Voltage: 3.3V logic (N64 uses 3.3V natively)
  • Pull-up: 1kΩ resistor to 3.3V
  • Cable: Connect to controller port data line

Pin Configuration

  • Bidirectional data line (input/output)
  • Must support fast GPIO operations
  • Pico: Any GPIO pin
  • Arduino: Pin 2 recommended (interrupt capable)
Ensure proper voltage levels. N64 uses 3.3V, which is compatible with Pico but may require level shifting for 5V Arduino boards.

Usage Example

#include "comms/N64Backend.hpp"

const Pinout pinout = {
    .joybus_data = 28,
};

static InputState inputs;
static InputSource *input_sources[] = { &gpio_input };
size_t input_source_count = 1;

// Pico
N64Backend n64_backend(
    inputs,
    input_sources,
    input_source_count,
    pinout.joybus_data
);

// Arduino
N64Backend n64_backend(
    inputs,
    input_sources,
    input_source_count,
    240,              // 240 Hz polling
    2                 // Pin 2
);

void loop() {
    n64_backend.SendReport();
}

Console Detection

The N64 backend can be automatically selected based on console detection:
CommunicationBackendConfig backend_config = {
    .backend_id = COMMS_BACKEND_N64,
};

Performance Notes

  • Pico: Uses hardware PIO for precise timing, minimal CPU overhead
  • Arduino: Software-based, requires careful timing calibration
  • Polling rates: N64 supports variable rates (60-240 Hz)
  • Input lag: Typically under 1ms with proper configuration

Differences from GameCube Backend

FeatureN64GameCube
Analog rangeSigned 8-bit (-128 to +127)Unsigned 8-bit (0-255)
C-buttonsDigital (4 buttons)Analog C-stick
TriggersDigital onlyDigital + Analog
Polling timingSingle waitStart + End detection
Default polling60-240 Hz125-200 Hz

Troubleshooting

If the console doesn’t detect the controller:
  • Check data pin connection and pull-up resistor
  • Verify voltage levels (3.3V)
  • Ensure polling rate matches console expectations
  • Check for proper grounding between controller and console
  • Try cleaning the controller port contacts

Advanced: Polling Rate Optimization

For Arduino, optimize the polling rate based on your use case:
// For most games (60 Hz)
N64Backend n64_backend(inputs, input_sources, count, 60, 2);

// For fast-paced games (higher rate)
N64Backend n64_backend(inputs, input_sources, count, 240, 2);

// Disable delay (advanced, may cause timing issues)
N64Backend n64_backend(inputs, input_sources, count, 0, 2);

See Also

Build docs developers (and LLMs) love