Complete guide for building Joypad OS firmware on macOS, Linux, and Windows.
For macOS users who just want to build:
# 1. Install ARM toolchain (one-time)
brew install --cask gcc-arm-embedded cmake git
# 2. Clone and initialize submodules
git clone https://github.com/joypad-ai/joypad-os.git
cd joypad-os
make init
# 3. Build an app
make usb2pce # or: usb2gc, usb2nuon, usb23do, usb2usb, etc.Output: releases/joypad_<commit>_<app>_<board>.uf2
-
ARM GCC Toolchain (Official from ARM)
brew install --cask gcc-arm-embedded
- Installs to:
/Applications/ArmGNUToolchain/<version>/arm-none-eabi/ - Do NOT use
brew install arm-none-eabi-gcc(missing newlib)
- Installs to:
-
CMake (3.13+)
brew install cmake
-
Git
brew install git
sudo apt update
sudo apt install cmake gcc-arm-none-eabi libnewlib-arm-none-eabi \
build-essential git python3-
Install Chocolatey (if not already installed):
- Open PowerShell as Administrator
- Follow instructions at: https://chocolatey.org/install
-
Install Required Tools:
choco install make gcc-arm-embedded git
-
Add Git Unix Tools to PATH:
# Open PowerShell as Administrator: [Environment]::SetEnvironmentVariable("Path", $env:Path + ";C:\Program Files\Git\usr\bin", "User")
Close and reopen PowerShell.
-
Set ARM Toolchain Path:
# Find the ARM toolchain installation path: Get-ChildItem "C:\Program Files (x86)" -Filter "arm-none-eabi-gcc.exe" -Recurse -ErrorAction SilentlyContinue # Set the environment variable (adjust path based on above output): [Environment]::SetEnvironmentVariable("PICO_TOOLCHAIN_PATH", "C:\Program Files (x86)\Arm GNU Toolchain arm-none-eabi\[version]\bin", "User")
Replace
[version]with your actual version number. Close and reopen PowerShell. -
Verify Installation:
make --version arm-none-eabi-gcc --version rm --version
-
Build:
git clone https://github.com/joypad-ai/joypad-os.git cd joypad-os make init make usb2pce # or other target
- Install MSYS2 from https://www.msys2.org/
- Open "MSYS2 MINGW64" terminal
- Install tools:
pacman -S mingw-w64-x86_64-toolchain mingw-w64-x86_64-cmake mingw-w64-x86_64-make git
- Follow standard build instructions
Use WSL2 with Ubuntu and follow Linux instructions above.
- "PICO_TOOLCHAIN_PATH not set": Ensure the variable points to the
binfolder containingarm-none-eabi-gcc.exe - "rm: command not found": Ensure
C:\Program Files\Git\usr\binis in your PATH - PATH changes not taking effect: Close and reopen PowerShell after modifying environment variables
make help # Display all available targetsConvert USB/Bluetooth controllers to retro console protocols:
make usb2pce # USB → PCEngine/TurboGrafx-16
make usb2gc # USB → GameCube/Wii
make usb2nuon # USB → Nuon DVD players
make usb23do # USB → 3DO
make usb2loopy # USB → Casio Loopy (experimental)
make snes23do # SNES controller → 3DOConvert USB/Bluetooth controllers to USB HID gamepad output:
make usb2usb # USB → USB HID (Feather USB Host)
make usb2usb_rp2040zero # USB → USB HID (RP2040-Zero)
make snes2usb # SNES controller → USB HIDBuild custom controllers from GPIO/analog inputs:
make controller_fisherprice # GPIO buttons → USB HID
make controller_fisherprice_analog # GPIO + analog stick → USB HID
make controller_alpakka # Alpakka controller (Pico)
make controller_macropad # MacroPad RP2040 → USB HIDmake usb2uart # USB → UART (ESP32 Bluetooth bridge)Requires ESP-IDF v6.0+ (separate from the ARM toolchain). See ESP32-S3 docs for full setup.
make bt2usb_esp32s3 # Build
make uf2-bt2usb_esp32s3 # Build + generate .uf2
make flash-uf2-bt2usb_esp32s3 # Build + flash .uf2 via TinyUF2 drive
make flash-bt2usb_esp32s3 # Build + flash via esptool
make monitor-bt2usb_esp32s3 # UART serial monitorRequires nRF Connect SDK v3.1.0+ (separate from the ARM toolchain). See nRF52840 docs for full setup.
make init-nrf # One-time NCS setup
make bt2usb_seeed_xiao_nrf52840 # Build for XIAO
make bt2usb_adafruit_feather_nrf52840 # Build for Feather
make flash-bt2usb_seeed_xiao_nrf52840 # Flash via UF2
make monitor-bt2usb_seeed_xiao_nrf52840 # UART serial monitormake all # Build all RP2040 apps
make releases # Build stable release apps only
make clean # Clean build artifacts| App | Board | Input | Output | Description |
|---|---|---|---|---|
usb2pce |
KB2040 | USB/BT | PCEngine | 5-player multitap, mouse support |
usb2gc |
KB2040 | USB/BT | GameCube | Profiles, rumble, keyboard mode |
usb2nuon |
KB2040 | USB/BT | Nuon | Spinner, in-game reset |
usb23do |
RP2040-Zero | USB/BT | 3DO | 8-player daisy chain, mouse |
usb2loopy |
KB2040 | USB/BT | Loopy | 4 players (experimental) |
snes23do |
RP2040-Zero | SNES | 3DO | Native SNES controller input |
| App | Board | Input | Output | Description |
|---|---|---|---|---|
usb2usb |
Feather USB Host | USB/BT | USB HID | USB gamepad passthrough |
usb2usb_rp2040zero |
RP2040-Zero | USB/BT | USB HID | Compact USB passthrough |
snes2usb |
KB2040 | SNES | USB HID | SNES to USB adapter |
| App | Board | Input | Output | Description |
|---|---|---|---|---|
controller_fisherprice |
KB2040 | GPIO | USB HID | Digital buttons only |
controller_fisherprice_analog |
KB2040 | GPIO+ADC | USB HID | With analog stick |
controller_alpakka |
Pico | GPIO/I2C | USB HID | Alpakka design |
controller_macropad |
MacroPad | 12 keys | USB HID | Macro keypad |
| Board | ID | USB Host | Notes |
|---|---|---|---|
| Adafruit KB2040 | kb2040 |
Yes | Default for most apps |
| Raspberry Pi Pico | pico |
Yes | Standard RP2040 |
| Waveshare RP2040-Zero | rp2040zero |
Yes | Compact form factor |
| Adafruit Feather USB Host | feather_usbhost |
Yes | Built-in USB-A port |
| Adafruit MacroPad RP2040 | macropad |
No | 12 keys + rotary encoder |
Joypad OS supports multiple input sources:
- USB HID - Standard USB gamepads, keyboards, mice
- USB X-input - Xbox 360/One/Series controllers
- Bluetooth HID - Wireless controllers via BT dongle (
src/bt/) - Native - Direct controller protocols (SNES via
src/native/host/)
- Retro Consoles - PCEngine, GameCube, Nuon, 3DO, Loopy (
src/native/device/) - USB Device - HID gamepad, XInput modes (
src/usb/usbd/) - UART - Serial bridge for ESP32 Bluetooth (
src/native/device/uart/)
src/
├── apps/ # App configurations
│ ├── usb2pce/ # PCEngine adapter
│ ├── usb2gc/ # GameCube adapter
│ ├── usb2usb/ # USB passthrough
│ ├── controller/ # Custom controllers
│ └── ...
├── core/ # Shared infrastructure
│ ├── buttons.h # JP_BUTTON_* definitions
│ ├── router/ # Input→Output routing
│ └── services/ # Profiles, players, LEDs, etc.
├── usb/
│ ├── usbh/ # USB Host (input)
│ │ ├── hid/ # HID device drivers
│ │ └── xinput/ # X-input support
│ └── usbd/ # USB Device (output)
├── bt/ # Bluetooth support
│ ├── bthid/ # BT HID device drivers
│ └── transport/ # BT transport layer
└── native/
├── device/ # Console output protocols
│ ├── pcengine/
│ ├── gamecube/
│ ├── nuon/
│ ├── 3do/
│ └── ...
└── host/ # Native controller input
└── snes/
- Core 0: USB/BT polling, device processing, main loop
- Core 1: Console output protocol (timing-critical PIO)
Using wrong toolchain. Fix:
brew uninstall arm-none-eabi-gcc
brew install --cask gcc-arm-embeddedRe-initialize submodules:
make clean
make init
make <target>- Wrong firmware for board
- USB cable is charge-only
- Try different USB port
- Check the controller compatibility list
- Ensure USB hub is powered (if using)
- Try direct connection without hub
- Docs: Home, Hardware Compatibility
- Issues: https://github.com/joypad-ai/joypad-os/issues
- Discord: http://community.joypad.ai/