RP2040 firmware that lets a SCUF Envision Pro play on a Nintendo Switch, by bridging the controller's Corsair Slipstream wireless dongle to a Switch-compatible USB gamepad.
The Switch only accepts a small set of wired USB controllers. This adapter hosts the dongle on one USB port, decodes its vendor-specific protocol, and presents itself on the other port as a HORIPAD — a profile the console already trusts.
Status: working. Tested on a real Switch through a dock; gameplay is smooth with no dropouts.
SCUF Envision Pro
│ (Slipstream 2.4 GHz)
▼
Slipstream dongle ──USB──► RP2040 ──USB──► Nintendo Switch dock
PIO-USB host native USB device
(core 1) (core 0)
The dongle does not speak a documented protocol, and the public information
about it is wrong in several specific ways. Everything in
docs/protocol.md was measured on hardware. Highlights:
- The dongle is
2E95:434E, not Corsair's1B1C:3A08. Every Linux driver in the wild targets1B1C:3A05, which is the wired controller — a different device with a different protocol. - It changes USB identity depending on whether the controller is awake. Awake, it is a 5-interface composite device with headset audio; asleep, it re-enumerates as a single HID interface with a different PID. The adapter has to survive a full unmount/remount as a routine event.
- The gamepad data is vendor-defined HID, not a standard gamepad descriptor, and lives on interfaces 3 and 4 — not interface 0.
- The triggers and D-pad share a packed 24-bit field that is not
byte-aligned. This is why an existing Linux driver reports the right trigger
as "12-bit, 0–4092": it is really 10-bit 0–1023, read two bits too low, with
the left trigger's high bits underneath.
4092 = 1023 × 4.
| Board | Waveshare RP2040-Zero, RP2040, 2 MB flash |
| System clock | 120 MHz — not the 125 MHz default; Pico-PIO-USB requires it |
| Dongle port | D+ = GP9, D− = GP10, 22 Ω series on each, VBUS from the 5 V pad |
| Status LED | WS2812 on GP16, driven by PIO1 |
| Debug UART | UART0, GP0 = TX, GP1 = RX, 115200 8N1 |
Every board-specific number lives in
include/board_config.h — nothing else hardcodes a
GPIO. PIO0 is left free for Pico-PIO-USB; the LED uses PIO1.
Salvage a USB-A socket (a cut-up extension cable works). Only four wires matter:
| USB wire | Signal | To |
|---|---|---|
| Red | VBUS | 5 V pad |
| White | D− | GP10 |
| Green | D+ | GP9 |
| Black | GND | GND |
Put 22 Ω in series on D+ and D− only. D+ must be the lower numbered pin —
Pico-PIO-USB's default PIO_USB_PINOUT_DPDM means D− = D+ + 1. If you wire it
the other way round, PIO_USB_PINOUT_DMDP flips it in one line.
Do not add the 1.5 kΩ pull-up mentioned in the Pico-PIO-USB docs; that is for its device mode. As a host we must not pull D+ up — the dongle does that itself, and detecting it is how the firmware knows something is attached.
Readable at a glance with no serial adapter attached:
| Colour | Meaning |
|---|---|
| red → green → blue | boot self-test |
| amber, breathing | powered, not enumerated by the host |
| cyan | host enumerated us, but no dongle/controller |
| green | both links up, inputs flowing |
| dim blue | host suspended us |
| white flash | a byte arrived on the debug UART |
Requires the pico-sdk 2.3.0, an
arm-none-eabi GCC 10+, CMake 3.13+, and Ninja.
git clone --recurse-submodules <this repo>
cd Scuf2SwitchThe Pico-PIO-USB submodule is required — CMake fails with a clear message if
it is missing.
Linux / macOS
export PICO_SDK_PATH=~/pico/pico-sdk
cmake -S . -B build -G Ninja
cmake --build buildWindows — tools/env.ps1 sets up one shell without
touching your system PATH. It defaults to ~/pico/pico-sdk and will borrow
STM32CubeIDE's bundled ARM toolchain if you happen to have it; override
PICO_SDK_PATH / PICO_TOOLCHAIN_PATH to use your own.
. .\tools\env.ps1
cmake -S . -B build -G Ninja
cmake --build buildOn Windows you may also want prebuilt pioasm and picotool from
pico-sdk-tools, since
the SDK otherwise builds them from source and needs a native C++ compiler.
cmake -S . -B build -G Ninja -DLOG_LEVEL=4| Option | Default | Meaning |
|---|---|---|
LOG_LEVEL |
3 |
0=silent 1=err 2=warn 3=info 4=debug. Higher levels compile away entirely. |
M1_DEMO_MOTION |
0 |
Synthetic stick circle when no dongle is attached, for testing the device side alone. |
PICO_BOARD |
waveshare_rp2040_zero |
No BOOT button needed. The firmware runs a console on the debug UART;
sending it B calls reset_usb_boot():
.\tools\flash.ps1 -Port COM11 -BuildThat matters because the Switch descriptor set is a byte-exact copy of GP2040-CE's and deliberately has no picotool reset interface — without the console trick, every reflash would need physical access to the board.
There is also a USB fallback for when the UART is unavailable: the HID OUT
endpoint accepts A5 5A C3 3C <cmd>, where 01 reboots to BOOTSEL. Writing
nine bytes to /dev/hidrawN on Linux is enough. This exists because it was
genuinely needed — the board had to be recovered while its serial link was
still broken.
Otherwise: hold BOOT, tap RESET, drag build/scuf2switch.uf2 onto RPI-RP2.
tools/remote.sh builds and flashes over SSH on a Linux box; set
export REMOTE=you@yourbox first.
- System Settings → Controllers and Sensors → Pro Controller Wired Communication → ON. Without this the console ignores wired USB pads.
- Plug the adapter into a USB-A port on the dock — not the console's USB-C.
- Wake the controller. The LED goes green when both links are up.
115200 8N1 on the UART pins. Single keystrokes:
| Key | Action |
|---|---|
B |
reboot into BOOTSEL for reflashing |
R |
reboot |
S |
toggle decoded controller state + translated report, 10 Hz |
T |
toggle raw host report trace (changed bytes only) |
C / D |
start / dump an on-device capture of changed reports |
? |
status and help |
The capture buffer exists because reports arrive at ~1 kHz — far faster than 115200 baud can stream — and because it decouples exercising the controller from any live capture window. It records only reports that changed, rate-limiting analog fields while never dropping a button edge.
src/mapping.c is the only file you need. One table, one
translate function, no configuration system.
Mapping is by physical position, not label. The SCUF uses the Xbox diamond, so its bottom face button ("A") maps to the Switch's bottom face button ("B"):
SCUF (Xbox layout) Switch
Y X
X B -> Y A
A B
Notes on the defaults:
- Analog triggers become digital ZL/ZR at 307 of 1023 (~30 %).
- G-keys carry Capture and Home, not the rear paddles. Each rear button also fires whatever the controller's own profile maps it to, so mapping a paddle would fire two things at once. The G-keys emit only their own bit. Clear the paddle mappings in SCUF's profile editor first if you want to use them.
CMakeLists.txt build, options, Pico-PIO-USB wiring
include/board_config.h every board-specific pin and constant
include/scuf_report.h SCUF report layout and decoded state
include/switch_report.h Switch report format
include/tusb_config.h TinyUSB device + PIO-USB host config
src/main.c core 0: device stack, console, LED
src/usb_host.c core 1: PIO-USB host, descriptor dumps, capture
src/scuf_parse.c report 0x06 -> controller state
src/mapping.c controller state -> Switch report <- edit this
src/usb_descriptors.c Switch descriptors, verbatim from GP2040-CE
src/ws2812.pio WS2812 driver (pico-examples)
docs/protocol.md everything measured about the dongle
tools/ build environment and flashing helpers
Both cost real debugging time and both fail silently:
tusb_time_millis_api()is only implemented in TinyUSB's BSP (hw/bsp/board.c). Use TinyUSB host without linking that BSP and the weaktusb_time_delay_ms_api()spins on a clock that never advances — enumeration stalls immediately after "Device Attach" with no error at all. This project implements both insrc/usb_host.c.CFG_TUH_ENUMERATION_BUFSIZEdefaults to 256, and this dongle's configuration descriptor is 419 bytes. TinyUSB trips an internal assert inprocess_enumerationwith no visible diagnosis. Set it to 1024.
The pico-sdk also forces -DCFG_TUSB_DEBUG=0, so an #ifndef guard in
tusb_config.h never fires — you have to #undef it to get any TinyUSB
tracing at all.
| Component | Licence | Use |
|---|---|---|
| GP2040-CE | MIT, © 2021 Jason Skuby | Switch USB descriptors, copied byte-for-byte |
| Pico-PIO-USB | MIT | USB host on PIO (submodule) |
| pico-examples | BSD-3-Clause | WS2812 PIO program |
| pico-sdk / TinyUSB | BSD-3-Clause / MIT |
The descriptor arrays in src/usb_descriptors.c are byte-identical to
GP2040-CE's, verified by diffing the parsed arrays rather than by eye. Please
don't tidy them.
This project is licensed under the MIT Licence — see LICENSE.
Not affiliated with SCUF, Corsair, HORI or Nintendo.