| .vscode | ||
| controller_db | ||
| .gitignore | ||
| build.sh | ||
| CMakeLists.txt | ||
| controller_color_config.h | ||
| controller_uart_bridge.py | ||
| host_uart_logger.py | ||
| pico_sdk_import.cmake | ||
| README.md | ||
| requirements.txt | ||
| switch-pico.cpp | ||
| switch_pico_uart.py | ||
| switch_pro_descriptors.h | ||
| switch_pro_driver.cpp | ||
| switch_pro_driver.h | ||
| tusb_config.h | ||
Switch Pico Controller Bridge
Raspberry Pi Pico firmware that emulates a Switch Pro controller over USB and a host bridge that forwards real gamepad input over UART (with rumble round-trip).
What you get
- Firmware (
switch-pico.cpp+switch_pro_driver.*): acts as a wired Switch Pro. Takes controller reports over UART1 and passes rumble from the Switch back over UART. - Python bridge (
controller_uart_bridge.py): reads SDL2 controllers on the host, sends reports over UART, and applies rumble locally. Hot‑plug friendly and cross‑platform (macOS/Windows/Linux). - Colour override (
controller_color_config.h): compile‑time RGB overrides for body/buttons/grips as seen by the Switch.
Hardware wiring (Pico)
- UART1 pins (fixed in firmware):
- TX: GPIO4 (Pico pin 6) → RX of your USB-serial adapter.
- RX: GPIO5 (Pico pin 7) → TX of your USB-serial adapter.
- GND: common ground between Pico and adapter.
- Baud rate: 921600 (default). Some adapters only handle 500,000; both bridges accept a
--baudflag. - Keep logic at 3.3V; do not feed 5V UART into the Pico.
Full hookup checklist
-
Gather the hardware
- Raspberry Pi Pico flashed with the provided firmware.
- USB-A-to-micro USB cable (or USB-C if you use a Pico W) to connect the Pico to the Switch or a PC for testing.
- USB-to-UART adapter capable of 3.3 V logic at 921600 baud (FT232, CP2102, CH340, etc.).
- Three dupont wires (TX, RX, GND). Optionally add heat-shrink or a small proto board if you want something more permanent.
-
Wire the Pico to the USB-to-UART adapter
- Pico GPIO4 → adapter RX (sometimes labelled RXD, DI, or R).
- Pico GPIO5 → adapter TX (TXD, DO, or T).
- Pico GND → adapter GND. Tie grounds even if the adapter is already USB-powered.
- Leave VBUS/VCC unconnected unless your adapter explicitly supports 3.3 V power output and you intend to power the Pico from it (the bridge expects the Pico to be powered from USB instead).
-
Connect everything to the host and Switch
- Plug the USB-to-UART adapter into the computer that will run
controller_uart_bridge.py. Note the COM port (Device Manager > Ports) on Windows or/dev/cu.*//dev/ttyUSB*path on macOS/Linux; pass it via--map/--ports. - Connect the Pico's micro USB port to the Nintendo Switch (via the dock's USB-A port, a USB-C OTG adapter, or a PC if you are only testing). The Pico enumerates as a Switch Pro Controller over USB.
- Any SDL-compatible gamepads you want to use should also be plugged into (or paired with) the same host computer that runs the Python bridge; the bridge is the one reading them.
- Plug the USB-to-UART adapter into the computer that will run
-
Power-on order and sanity checks
- Power the Switch/dock so the Pico gets 5 V over USB; its USB stack must stay alive while the bridge streams data.
- On the host computer, run
python controller_uart_bridge.py --list-controllersto make sure SDL sees your pads, then start the bridge with--map/--ports(or--interactive) referencing the adapter path you found earlier. - Watch the Rich console output: you should see each controller paired with a UART port and the rumble loop logging reconnects if cables are unplugged.
-
Common pitfalls
- A flipped TX/RX pair results in silence (no button presses); swap them if the Pico never shows input.
- Some adapters default to 5 V logic—move the jumper to 3.3 V before touching the Pico.
- If you use multiple adapters, label each cable; COM port numbers can change between boots.
- When testing on a PC before plugging into a Switch, you can verify activity with the lightweight
switch_pico_uart.pyhelper or the Windows "Game Controllers" panel.
Building and flashing firmware
Prereqs: Pico SDK + CMake toolchain set up.
cmake -S . -B build -DSWITCH_PICO_AUTOTEST=OFF -DSWITCH_PICO_LOG=OFF
cmake --build build -j
Flash the UF2 to the Pico (e.g., bootsel + drag-drop or picotool load).
Flags:
SWITCH_PICO_AUTOTEST(default ON in some configs): disable autopilot/test replay withOFF.SWITCH_PICO_LOG: enable/disable UART logging on the Pico.
Changing controller colours
./build.sh now writes a random colour into controller_color_config.h before building so the Switch shows a fresh colour when you flash.
- Use
./build.sh --color FF00AA(or--color 12,34,56) to pick a specific colour applied to the body/buttons/grips. - Use
./build.sh --keep-colorif you want to build without touchingcontroller_color_config.h.
Python bridge (recommended)
Works on macOS, Windows, Linux. Uses SDL2 + pyserial.
Install dependencies with uv (or pip)
# from repo root
uv venv .venv
uv pip install pyserial pysdl2
- SDL2 runtime: install via your OS package manager (macOS:
brew install sdl2; Windows: placeSDL2.dllon PATH or next to the script; Linux:sudo apt install libsdl2-2.0-0or equivalent).
Run
source .venv/bin/activate # or .venv\Scripts\activate on Windows
python controller_uart_bridge.py --interactive
Options:
--map index:PORT(repeatable) to pin controller index to serial (e.g.,--map 0:/dev/cu.usbserial-0001or--map 0:COM5).--ports PORTS...or--interactivefor auto/interactive pairing.--all-portsto include non-USB serial devices in discovery.--ignore-port-desc SUBSTR/--include-port-desc SUBSTRto filter serial ports by description (repeatable).--include-controller-name SUBSTRto only open controllers whose name matches (repeatable).--list-controllersto print detected controllers and their GUIDs, then exit (useful for GUID-based options).--baud 921600(default 921600; use500000if your adapter can’t do 900K).--frequency 1000to send at 1 kHz.--deadzone 0.08to change stick deadzone (0.0-1.0).--zero-sticksto sample the current stick positions on connect and treat them as neutral (cancel drift).--zero-hotkey zto choose the terminal hotkey that re-zeroes all connected controllers on demand (presszby default; pass an empty string to disable).--trigger-threshold 0.35to change analog trigger press threshold (0.0-1.0).--swap-abxyto flip AB/XY globally.--swap-abxy-index N(repeatable) to flip AB/XY for controllers first seen at index N (auto-converts to a stable GUID).--swap-abxy-guid GUID(repeatable) to flip AB/XY for a specific physical controller (GUID is stable across runs).--swap-hotkey xto pick the runtime hotkey that prompts you to toggle ABXY layout for a specific connected controller (defaultx; empty string disables).--sdl-mapping path/to/gamecontrollerdb.txtto load extra SDL mappings (defaults tocontroller_db/gamecontrollerdb.txt).
Runtime hotkeys
- By default, pressing
zin the terminal re-samples every connected controller's sticks and re-applies neutral offsets. Change/disable with--zero-hotkey. - Press
x(configurable via--swap-hotkey) to open an in-CLI prompt and toggle the ABXY layout for a specific connected controller. This updates the controller's stable GUID list immediately; press again to revert. - Hotkeys work only when the bridge is started from a TTY/console that currently has focus. Pass an empty string to either flag to disable that shortcut (useful when running unattended).
- If you launch the bridge with
--swap-abxy(global swap), the per-controller toggle hotkey will show that the layout is enforced globally and will not override it.
Hot-plugging: controllers and UARTs can be plugged/unplugged while running; the bridge will auto reconnect when possible.
Using the lightweight UART helper (no SDL needed)
For simple scripts or tests you can skip SDL and drive the Pico directly with switch_pico_uart.py:
from switch_pico_uart import SwitchUARTClient, SwitchButton, SwitchHat
with SwitchUARTClient("/dev/cu.usbserial-0001") as client:
client.press(SwitchButton.A)
client.release(SwitchButton.A)
client.move_left_stick(0.0, -1.0) # push up
client.set_hat(SwitchHat.TOP_RIGHT)
print(client.poll_rumble()) # returns (left, right) amplitudes 0.0-1.0 or None
SwitchButtonis anIntFlag(bitwise friendly) andSwitchHatis anIntEnumfor the DPAD/hat values.- The helper only depends on
pyserial; SDL is not required.
macOS tips
- Ensure the USB‑serial adapter shows up (use
/dev/cu.usb*for TX). - Some controllers’ Guide/Home buttons are intercepted by macOS; using XInput/DInput mode or disabling Steam’s controller handling helps.
Windows tips
- Use
COMxfor ports (e.g.,COM5). Auto‑detect lists COM ports. - Ensure SDL2.dll is on PATH or alongside the script.
Linux tips
- You may need udev permissions for
/dev/ttyUSB*//dev/ttyACM*(add user todialout/uucpor useudevrules).
Troubleshooting
- No input on Switch: verify UART wiring (Pico GPIO4/5), baud matches both sides, Pico flashed with current firmware, Switch sees a “Pro Controller”.
- Constant buzzing rumble: the bridge filters small rumble payloads; ensure baud isn’t dropping bytes. Try lowering rumble scale in
controller_uart_bridge.pyif needed. - Guide/Home triggers system menu (macOS): try different controller mode (XInput/DInput), disable Steam overlay/controller support, or connect wired.
- SDL can’t see controller: load
controller_db/gamecontrollerdb.txt(default), add your own mapping, or try a different mode on the pad (e.g., XInput).