Skip to content

PSX2USB Adapter (QT Py / KB2040 / Pi Pico)

PlayStation 1 / PlayStation 2 controllers to USB HID gamepad. Auto-detects digital pads, DualShock, DualShock 2 (with pressure), neGcon, Dual Analog flightstick, GunCon, JogCon, and PlayStation Mouse.

Parts Needed

  • One of:
  • Adafruit QT Py RP2040 (~$10)
  • Adafruit KB2040 (~$10)
  • Raspberry Pi Pico (~$4)
  • PlayStation controller extension cable (cut to expose wires), or a salvaged PSX/PS2 console-side socket
  • One 1 kΩ to 10 kΩ resistor (pull-up on DAT)
  • Hookup wire (22–26 AWG), soldering iron

PSX Controller Connector

The PSX/PS2 controller plug is 9-pin. Looking into the console-side socket (or at the back of a cut extension cable):

  _________________________
 |   1   2   3 | 4   5   6 | 7   8   9   |
  \__|___|___|_|_|___|___|_|_|___|___|__/

 1 = DAT  (data from controller, open-drain)     6 = ATT  (attention / chip select, active-low)
 2 = CMD  (commands to controller)               7 = CLK  (clock, ~250 kHz - 500 kHz)
 3 = +7V  (rumble motor power, OPTIONAL)         8 = (not connected for slot 1)
 4 = GND                                         9 = ACK  (acknowledge, not required)
 5 = +3.3V (controller logic power)

Pin 3 (+7V) only matters if you want rumble on a DualShock — wire it to a separate 7.5 V supply not through the MCU. Do not wire pin 3 to a USB 5 V rail; the rumble motors brown-out the controller and corrupt analog reads.

Wiring

The active-pull-up PIO needs CLK and ATT on consecutive GPIOs (it's a 2-bit side-set with CLK on the lower pin). The other two lines can be on any free pin. Per-board pin assignments:

QT Py RP2040 (primary target)

⚠️ The QT Py RP2040 numbers its analog pads in descending GPIO order — A0 is GP29, A3 is GP26 (the reverse of the KB2040). Wire by the pad label in the table below, not by assuming A0 is the lowest GPIO.

QT Py pad GPIO PSX Pin Signal
A0 GP29 1 DAT
A1 GP28 2 CMD
A2 GP27 6 ATT
A3 GP26 7 CLK
3V 5 +3.3V
GND 4 GND

KB2040

Same GPIOs as the QT Py (CLK=GP26, ATT=GP27, CMD=GP28, DAT=GP29), but the KB2040 numbers its A0–A3 pads in ascending GPIO order (A0 is GP26), so the pad labels are the reverse of the QT Py.

KB2040 pad GPIO PSX Pin Signal
A0 GP26 7 CLK
A1 GP27 6 ATT
A2 GP28 2 CMD
A3 GP29 1 DAT
3.3V 5 +3.3V
GND 4 GND

Raspberry Pi Pico

Pico GPIO PSX Pin Signal
GP20 7 CLK
GP21 6 ATT
GP19 2 CMD
GP22 1 DAT
3V3 OUT 5 +3.3V
GND 4 GND

Pull-up on DAT

DAT is open-drain; the controller pulls it low for 0 bits and lets it float for 1. The RP2040's internal pull-up works for most modern pads, but old analog pads (e.g. Sony SCPH-110) read cleaner with an external 1 kΩ to 10 kΩ resistor from DAT to +3.3V. The firmware's active-pull-up PIO trick handles the rest.

Build and Flash

# Build
make psx2usb_qtpy            # or psx2usb_kb2040 / psx2usb_pico

# Flash: hold BOOTSEL while connecting USB
make flash-psx2usb_qtpy      # or psx2usb_kb2040 / psx2usb_pico

Output file: releases/joypad_<commit>_psx2usb_<board>.uf2

Testing

  1. Wire the adapter per the table above.
  2. Plug the adapter into a PC via USB.
  3. The board enumerates as a Joypad SInput controller by default.
  4. Plug a PSX controller into the cable. Open a gamepad tester (gamepad-tester.com, or config.joypad.ai for the live joypad-web view) and verify:
  5. Digital pad — all 12 face/d-pad/shoulder buttons + Select/Start register.
  6. DualShock — both analog sticks center near 128, L3/R3 click, ANALOG button toggles modes (a brief A1/Guide press fires on each toggle).
  7. DualShock 2 — per-button pressure visible in joypad-web's PS3-mode pressure view.

Output Modes

The adapter ships in SInput mode (the default — full feature set, recognized by Steam/SDL). Double-click the BOOTSEL button to cycle output modes: SInput → XInput → PS3 → PS4 → Switch → Keyboard/Mouse → …; triple-click resets to SInput. The NeoPixel reflects the active mode (white = SInput, green = XInput, blue = PS3/PS4, red = Switch, yellow = KB/Mouse).

Single press of BOOTSEL emits A1 (Guide/PS button) while held — useful for navigating console home menus or triggering a PS-button init without a Select+Start combo.

PS3 mode emits an authentic DS3 descriptor (works on real PS3 consoles).

Supported Devices

Device Type ID Notes
Digital pad (SCPH-1080) 0x41 Standard 12-button pad
DualShock 0x73 Analog sticks auto-enabled
DualShock 2 0x79 + 12-byte per-button pressure
neGcon 0x23 Namco NPC-101; twist + I/II/L analog
Dual Analog flight 0x53 SCPH-1110 / Dual Analog "red" mode
GunCon 0x63 Namco light gun; aim on right stick (needs CRT)
JogCon 0xE3 Namco paddle wheel on left-stick X
PlayStation Mouse 0x12 SCPH-1090; relative cursor + 2 buttons

Notes

  • No CPU overclock required (standard 125 MHz).
  • ANALOG button → A1/Guide is detected via the analog ↔ digital mode toggle; it fires a brief A1 pulse on every press so a console PS-button init still registers.
  • Rumble motor bytes are driven from the SInput/PS3 feedback state. The 7.5 V motor rail must be supplied externally (NOT through the QT Py / KB2040 / Pico).
  • See PSX input docs for protocol details (SIO bus, active pull-up, controller ID layout).
  • See psx2usb app docs for feature details + USB output mode list.