Universal AI agent instructions for working with the Joypad OS codebase. This file is tool-agnostic and works with Claude Code, Cursor, Windsurf, Copilot, Cline, Aider, and other AI coding assistants.
For detailed project architecture, see CLAUDE.md.
Firmware for RP2040, ESP32-S3, and nRF52840 microcontrollers that converts between controller protocols. Plug any USB/Bluetooth controller into retro consoles (GameCube, Dreamcast, PCEngine, Nuon, 3DO, etc.) or use retro controllers on modern systems via USB HID.
brew install --cask gcc-arm-embedded cmake git
git clone https://github.com/joypad-ai/joypad-os.git
cd joypad-os
make init # Initialize submodules
make usb2gc_kb2040 # Build a specific app
make all # Build all RP2040 targets
make clean # Clean build directoryBuild targets follow the pattern: make <app>_<board> (e.g., make usb2gc_kb2040, make bt2usb_pico_w).
Output: releases/joypad_<commit>_<app>_<board>.uf2
- Button constants use
JP_BUTTON_*prefix (W3C Gamepad API order) - Old code may reference
USBR_BUTTON_*orusbretro— these are legacy names - Apps are named
<input>2<output>(e.g.,usb2gc= USB input to GameCube output)
B1=A/Cross B2=B/Circle B3=X/Square B4=Y/Triangle
L1/R1=Bumpers L2/R2=Triggers S1=Select S2=Start
L3/R3=Stick clicks DU/DD/DL/DR=D-pad A1=Home/Guide
All input drivers MUST normalize to HID standard: 0=up, 128=center, 255=down.
- Sony/Xbox/8BitDo: Native HID, no inversion needed
- Nintendo controllers: Invert Y (Nintendo uses 0=down, 255=up)
- Native GC/N64 host: Invert Y when reading
- Core 0: USB/BT polling, input processing, main loop
- Core 1: Console output protocols (timing-critical PIO)
- Use
__not_in_flash_funcfor timing-critical code on Core 1 - On Pico 2 W (RP2350): Core 0's CYW43 driver periodically locks flash — Core 1 functions must be RAM-only
- 32 instruction limit per program — optimize or split
- PIO0/PIO1 assignment matters: CYW43 uses PIO1, PIO-USB uses PIO0 when CYW43 is active
- NeoPixel always claims PIO0 SM — can conflict with PIO-USB
Input → router_submit_input() → Router → profile_apply() → Output
Inputs: USB HID, XInput, Bluetooth, WiFi (JOCP), Native (SNES/N64/GC)
Outputs: PCEngine, GameCube, Dreamcast, Nuon, 3DO, Loopy, USB Device, UART
input_event_t— Unified input event (buttons bitmap + analog axes)OutputInterface— Output abstraction (init, core1_entry, task, rumble, LEDs)- Router modes: SIMPLE (1:1), MERGE (all→one), BROADCAST (all→all)
src/apps/ — App configs (one per input→output combo)
src/core/ — Shared infrastructure (router, profiles, services)
src/usb/usbh/ — USB host input drivers
src/usb/usbd/ — USB device output
src/bt/ — Bluetooth input drivers
src/native/device/ — Console output protocols (PIO)
src/native/host/ — Native controller input (SNES/N64/GC)
src/pad/ — GPIO controller input (custom builds)
esp/ — ESP32-S3 platform (ESP-IDF)
nrf/ — nRF52840 platform (Zephyr/NCS)
- Create
src/usb/usbh/hid/devices/vendors/<vendor>/<device>.c/h - Implement:
_is_device(),_init(),_process(),_disconnect() - Register in
hid_registry.c - Map vendor buttons to
JP_BUTTON_*constants
- Create
src/bt/bthid/devices/vendors/<vendor>/<device>.c/h - Same interface as USB HID drivers
- Register in BT device registry
- Create
src/apps/<appname>/withapp.c,app.h, optionalprofiles.h - Add CMake target in
src/CMakeLists.txt - Add Make targets in
Makefile - Add to
APPSlist in Makefile formake all - Add to
.github/workflows/build.ymlfor CI
- Edit
src/apps/<appname>/profiles.h - Define
profile_twith button remapping and analog thresholds - Profile cycling: SELECT + D-pad Up/Down (2s hold)
- BLE only (no Classic BT) — only BLE controllers work
tud_task_ext(1, false)nottud_task()(blocks forever on FreeRTOS)- BTstack API calls must happen in the BTstack FreeRTOS task
- Classic BT APIs guarded with
#ifndef BTSTACK_USE_ESP32
- BLE only — same as ESP32
- BTstack runs in its own Zephyr thread
- TinyUSB owns USB peripheral (Zephyr USB disabled)
- Debug output uses UART (not CDC/USB)
- Classic BT APIs guarded with
#ifndef BTSTACK_USE_NRF
- pico-sdk 2.2.0 + CMake under
src/ - Submodules in
src/lib/(TinyUSB, BTstack, joybus-pio, tusb_xinput) - Board variants via Makefile target suffix (e.g.,
_kb2040,_pico_w,_rp2040zero)
- ESP-IDF v6.0+ under
esp/ - Requires
~/esp-idfinstallation - Build:
make bt2usb_xiao_esp32s3
- nRF Connect SDK v3.1.0+ under
nrf/ - Setup:
make init-nrf - Build:
make bt2usb_seeed_xiao_nrf52840
GitHub Actions (.github/workflows/build.yml):
- Builds all apps on push to
main - Docker-based ARM cross-compilation
- Artifacts in
releases/directory
- GameCube requires 130MHz clock —
set_sys_clock_khz(130000, true) - PIO state machine conflicts — NeoPixel vs PIO-USB vs CYW43 all compete for PIO blocks
- Flash contention on RP2350 — Core 1 functions must use only inline/RAM-resident calls
- Digital-only triggers — Controllers without analog triggers synthesize analog values in
profile_apply()so threshold logic works uniformly - HCI handle 0x0000 is valid in BLE — Use
HCI_CON_HANDLE_INVALID(0xFFFF) as sentinel, never 0 - BTstack run loop — Custom run loops MUST implement
execute_on_main_thread