An implementation of the Joybus protocol used by N64 and GameCube controllers, for 32-bit microcontrollers.
- C implementation, no external dependencies (besides backend-specific SDKs)
- Provides both host mode and target mode functionality
- Host mode allows communication with N64/GameCube controllers from a microcontroller
- Target mode allows you to build custom N64/GameCube controllers using a microcontroller
- Near-ASIC timing accuracy for reliable communication
- Pre-built targets for N64 controllers and GameCube controllers
- Raspberry Pi Pico and Pico 2 (and other RP2xxx-based boards)
- Silicon Labs EFM32/EFR32 Series 1 and Series 2 MCUs
- Espressif ESP32 (ESP32-C3, ESP32-C6, ESP32-S3, ESP32-H2)
libjoybus is a key part of WavePhoenix, my open source implementation of a GameCube WaveBird receiver.
libjoybus is also used in my open-source 4-port USB GameCube controller adapter project.
You can find a number of additional examples in the examples/ directory.
Please let me know if you build something with libjoybus! I love seeing my projects used in the wild, and I'll consider adding it to the examples list!
Copy the library into your project, add it as a git submodule, or fetch it with FetchContent. Set your JOYBUS_BACKEND, for example rp2xxx for the Pico SDK, and link your executable against the joybus target:
# Set the backend (after pico_sdk_init() on the Pico SDK)
set(JOYBUS_BACKEND rp2xxx)
# Download libjoybus as part of a build, using FetchContent
include(FetchContent)
FetchContent_Declare(libjoybus GIT_REPOSITORY https://github.com/loopj/libjoybus.git GIT_TAG main)
FetchContent_MakeAvailable(libjoybus)
# ...or if bundling as a copy or git submodule
add_subdirectory(libjoybus)
# Link your executable against the joybus target
target_link_libraries(my_app pico_stdlib joybus)See the Pico SDK examples for complete projects.
For ESP-IDF projects, libjoybus is packaged as a component. From your project directory, add it as a git dependency:
idf.py add-dependency --git https://github.com/loopj/libjoybus.git libjoybusThen add libjoybus to the REQUIRES list in your main/CMakeLists.txt:
idf_component_register(SRCS "main.c" INCLUDE_DIRS "." REQUIRES libjoybus)The component manager downloads libjoybus on the next idf.py build. See the ESP-IDF examples for complete projects.
For Simplicity SDK projects, libjoybus is packaged as a Silicon Labs SDK extension.
-
Follow the Silicon Labs Guide to add this repository as an extension.
-
Open your project's Software Components tab and install the
libjoybuscomponent.
Clone into your SDK's extension folder and trust the extension:
slc signature trust -extpath <path_to_sdk>/extension/libjoybusAdd to your project's .slcp file:
sdk_extension:
- id: libjoybus
version: 0.9.0
component:
- id: libjoybus
from: libjoybusSee the Gecko examples for complete projects.
You can find the full API documentation here, but here are some basic examples to get you started.
Before using libjoybus, you need to initialize the Joybus interface for your
platform. Here's an example for the RP2040:
#include <joybus/joybus.h>
#include <joybus/backend/rp2xxx.h>
struct joybus_rp2xxx rp2xxx_bus;
struct joybus *bus = JOYBUS(&rp2xxx_bus);
int main() {
// Initialize the Joybus on a specific GPIO pin and PIO instance
joybus_rp2xxx_init(&rp2xxx_bus, joybus_rp2xxx_config_default(JOYBUS_GPIO));
// ...your code here
return 0;
}In host mode, libjoybus allows a microcontroller to communicate with N64 and
GameCube controllers. This allows you to use input data from N64 and GameCube
controllers in your projects.
#include <joybus/joybus.h>
struct joybus_rp2xxx rp2xxx_bus;
struct joybus *bus = JOYBUS(&rp2xxx_bus);
void read_controller() {
// Read a GameCube controller in analog mode 3 with the rumble motor off
struct joybus_gcn_controller_state input;
int rc = joybus_gcn_read(bus, JOYBUS_GCN_ANALOG_MODE_3, JOYBUS_GCN_MOTOR_STOP, &input);
if (rc < 0) {
// ...handle read error
return;
}
// Do something with the input state
if (input.buttons & JOYBUS_GCN_BUTTON_A) {
// The A button is pressed
}
}
void main() {
// Initialize the Joybus and enable it in host mode
joybus_rp2xxx_init(&rp2xxx_bus, joybus_rp2xxx_config_default(MY_GPIO));
joybus_enable(bus, JOYBUS_MODE_HOST);
// Read the controller state in a loop
while (1) {
read_controller();
sleep_ms(10);
}
}In target mode, libjoybus allows a microcontroller to act as an N64 or GameCube
controller. This allows you to create custom controllers that can interface with
N64, GameCube, and Wii consoles.
I've provided built-in targets for N64 controllers and GameCube controllers so you can just populate the input state and let libjoybus handle the rest.
#include <joybus/joybus.h>
struct joybus_rp2xxx rp2xxx_bus;
struct joybus *bus = JOYBUS(&rp2xxx_bus);
struct joybus_target_gcn_controller controller;
void main() {
// Initialize the Joybus
joybus_rp2xxx_init(&rp2xxx_bus, joybus_rp2xxx_config_default(MY_GPIO));
// Initialize a GameCube controller target and attach it to the bus
joybus_target_gcn_controller_init(&controller);
joybus_attach_target(bus, JOYBUS_TARGET(&controller));
// Enable the Joybus in target mode
joybus_enable(bus, JOYBUS_MODE_TARGET);
// At this point the target will respond to commands from a connected console!
// Modify the input state as needed, for example based on GPIO or ADC readings
while (1) {
// Clear previous button state
controller.input.buttons &= ~JOYBUS_GCN_BUTTON_MASK;
// Simulate pressing the A button
controller.input.buttons |= JOYBUS_GCN_BUTTON_A;
// Simulate setting the analog stick position
controller.input.stick_x = 200;
controller.input.stick_y = 200;
sleep_ms(10);
}
}Target replies are timing critical. A flash fetch or cache miss inside a
command handler can delay the reply, so libjoybus places its latency-critical
functions in RAM on every supported platform. A single-controller build uses
under 1 KB of RAM for this.
Define JOYBUS_USE_RAM_FUNCS=0 in your build to keep everything in flash and
save the RAM.
Application code that runs inside the reply path should be placed in RAM too,
or a flash fetch there will undo the library's placement. Mark these functions
with JOYBUS_RAM_FUNC:
byte_receivedon a custom target- Reset and motor callbacks on the built-in N64 and GameCube controllers
read_blockandwrite_blockon a custom N64 pak
#include <joybus/attributes.h>
JOYBUS_RAM_FUNC
static void on_motor_change(struct joybus_target_gcn_controller *controller, uint8_t state)
{
gpio_put(MOTOR_PIN, state);
}On ESP32 devices, writing to flash while the bus is active will cause missed commands in target mode and transfer timeouts in host mode. ESP-IDF disables the cache for the whole operation and masks every interrupt that is not marked cache-safe, so the bus receives nothing until the write finishes. This includes NVS writes, such as storing Bluetooth pairing keys.
Disable the bus around the write where you can. When you cannot, enable
CONFIG_JOYBUS_ESP32_ISR_IRAM_SAFE in menuconfig on ESP-IDF or in prj.conf
on Zephyr to keep the interrupt running through a flash write. Every callback
the bus invokes must then be marked JOYBUS_RAM_FUNC, including the transfer
callback passed to host functions.
This project is licensed under the MIT License. See the LICENSE file for details.