In this guide, you’ll learn how to interface a generic PS2 joystick with a Feather 32u4, so it works as a USB gamepad on Windows and Steam. We’ll cover the hardware, wiring, Arduino IDE firmware, and Steam setup.
Motivation
I left my Xbox 360 controller at home, but I have a PS2 joystick I bought years ago for a retro project. With a Feather 32u4 and a bit of code, we can turn it into a USB gamepad compatible with any PC.
Why Use a Feather 32u4 for HID?
The Feather 32u4 (based on the ATmega32u4) has native USB, allowing you to emulate HID devices (mouse, keyboard, gamepad) without extra hardware. By attaching a PS2 joystick, we can remap its signals and send them as a standard HID report to the PC. This offers:
- Low latency: USB is faster than a Bluetooth module.
- Immediate compatibility: Windows recognizes a HID gamepad without extra drivers.
- Flexibility: Full customization of buttons and axes.
Required Materials
- Feather 32u4 (or any board with native USB-HID).
- PS2 joystick (e.g. retro Tang Nano 20K kit clone).
- Dupont male-to-male wires for GPIO connections.
- USB cable (data + power).
- A PC with Windows and Steam installed.
- Arduino IDE v2.x with HID-Project library installed.
- Optional: PS2-joystick-to-DIP adapter if your joystick has no easily accessible pins.
1. Wiring and Power Supply
PS2 Joystick Pins
PS2 Signal | Description |
---|---|
CLK | Clock |
CMD | Command (MOSI) |
ATT | Attention / SS |
DATA | Data (MISO) |
VCC | Power (+3.3 V) |
GND | Ground (0 V) |
Feather 32u4 Pins
Feather Pin | PS2 Signal | Mode |
---|---|---|
D6 | CLK | OUTPUT (HIGH) |
D5 | CMD | OUTPUT (HIGH) |
D2 | ATT | OUTPUT (HIGH) |
D3 | DATA | INPUT_PULLUP |
VUSB | VCC | +3.3 V |
GND | GND | 0 V |
In my setup, I use a PS2-to-DIP adapter that connects directly to the Feather 32u4. If you don’t have this adapter, solder wires straight to the joystick’s pins.
2. Testing PS2 Communication without Libraries
Before integrating HID, confirm the joystick responds to the PS2 protocol. This sketch sends the standard 9-byte command and prints the response over Serial:
#define PS2_CLK 6
#define PS2_CMD 5
#define PS2_ATT 2
#define PS2_DAT 3
void setup() {
Serial.begin(57600);
pinMode(PS2_CLK, OUTPUT);
pinMode(PS2_CMD, OUTPUT);
pinMode(PS2_ATT, OUTPUT);
pinMode(PS2_DAT, INPUT_PULLUP);
digitalWrite(PS2_CLK, HIGH);
digitalWrite(PS2_CMD, HIGH);
digitalWrite(PS2_ATT, HIGH);
delay(300); // Wait for joystick to initialize
Serial.println("🔍 PS2 test started...");
}
void loop() {
byte cmd[9] = {0x01, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
byte resp[9] = {0};
digitalWrite(PS2_ATT, LOW);
for (int i = 0; i < 9; i++) {
resp[i] = shiftInOut(cmd[i]);
}
digitalWrite(PS2_ATT, HIGH);
Serial.print("RAW: ");
for (int i = 0; i < 9; i++) {
if (resp[i] < 0x10) Serial.print("0");
Serial.print(resp[i], HEX);
Serial.print(" ");
}
Serial.println();
delay(200);
}
byte shiftInOut(byte out) {
byte in = 0;
for (int i = 0; i < 8; i++) {
digitalWrite(PS2_CMD, (out & (1 << i)) ? HIGH : LOW);
digitalWrite(PS2_CLK, LOW);
delayMicroseconds(20);
if (digitalRead(PS2_DAT)) in |= (1 << i);
digitalWrite(PS2_CLK, HIGH);
delayMicroseconds(20);
}
return in;
}
Upload this sketch.
Open the Serial Monitor at 57600 baud.
Press the ANALOG button on the joystick until its red LED lights up.
You should see something like:
RAW: FF 73 5A 00 00 80 80 80 80
73
indicates analog mode (41
would mean digital mode).- Bytes 5–8 are LX, LY, RX, RY values.
If it remains 00 00 ...
, check wiring, ensure DATA
is INPUT_PULLUP
, and confirm the joystick is powered with 3.3 V.
3. Arduino IDE Setup and HID Libraries
To send joystick data as a USB gamepad, we use NicoHood’s HID-Project library.
- Go to Sketch → Include Library → Manage Libraries.
- Search for HID-Project and install the latest version (e.g., 2.8.4).
- Choose Adafruit Feather 32u4 as the board and select the correct COM port.
In HID-Project, the gamepad class is called Gamepad
. Key methods include:
Gamepad.begin()
Gamepad.press(n)
/Gamepad.release(n)
Gamepad.xAxis(value)
,Gamepad.yAxis(value)
,Gamepad.zAxis(value)
,Gamepad.rzAxis(value)
Gamepad.write()
to send the HID report.
4. Complete Firmware to Convert PS2 → USB Gamepad
This sketch combines PS2 bit‑banged reading with USB gamepad emulation. Copy it into a new .ino
file:
#include <HID-Project.h>
#include <HID-Settings.h>
#define PS2_CLK 6 // D6 → CLK
#define PS2_CMD 5 // D5 → CMD (output)
#define PS2_ATT 2 // D2 → ATT (output)
#define PS2_DAT 3 // D3 → DATA (input with pull-up)
void setup() {
Serial.begin(57600); // Optional for debugging
pinMode(PS2_CLK, OUTPUT);
pinMode(PS2_CMD, OUTPUT);
pinMode(PS2_ATT, OUTPUT);
pinMode(PS2_DAT, INPUT_PULLUP);
digitalWrite(PS2_CLK, HIGH);
digitalWrite(PS2_CMD, HIGH);
digitalWrite(PS2_ATT, HIGH);
delay(300);
Gamepad.begin();
}
void loop() {
byte cmd[9] = {0x01, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
byte resp[9];
digitalWrite(PS2_ATT, LOW);
for (int i = 0; i < 9; i++) resp[i] = shiftInOut(cmd[i]);
digitalWrite(PS2_ATT, HIGH);
if (resp[0] == 0x00 && resp[1] == 0x00) {
delay(200);
return;
}
// Debug: print raw data
Serial.print("RAW: ");
for (int i = 0; i < 9; i++) {
if (resp[i] < 0x10) Serial.print("0");
Serial.print(resp[i], HEX);
Serial.print(" ");
}
Serial.println();
// Decode buttons (bytes 3 and 4)
byte d_lo = resp[3] ^ 0xFF;
byte d_hi = resp[4] ^ 0xFF;
// Digital buttons
if (d_hi & 0x40) Gamepad.press(1); else Gamepad.release(1); // Cross/X → button 1
if (d_hi & 0x80) Gamepad.press(2); else Gamepad.release(2); // Circle/O → button 2
if (d_hi & 0x20) Gamepad.press(3); else Gamepad.release(3); // Square/□ → button 3
if (d_hi & 0x10) Gamepad.press(4); else Gamepad.release(4); // Triangle/△ → button 4
if (d_hi & 0x01) Gamepad.press(5); else Gamepad.release(5); // L2 digital → button 5
if (d_hi & 0x02) Gamepad.press(6); else Gamepad.release(6); // R2 digital → button 6
if (d_hi & 0x04) Gamepad.press(7); else Gamepad.release(7); // L1 → button 7
if (d_hi & 0x08) Gamepad.press(8); else Gamepad.release(8); // R1 → button 8
if (d_lo & 0x01) Gamepad.press(9); else Gamepad.release(9); // Select → button 9
if (d_lo & 0x02) Gamepad.press(10); else Gamepad.release(10); // L3 → button 10
if (d_lo & 0x04) Gamepad.press(11); else Gamepad.release(11); // R3 → button 11
if (d_lo & 0x08) Gamepad.press(12); else Gamepad.release(12); // Start → button 12
if (d_lo & 0x10) Gamepad.press(13); else Gamepad.release(13); // D-Pad Up → button 13
if (d_lo & 0x20) Gamepad.press(14); else Gamepad.release(14); // D-Pad Right → button 14
if (d_lo & 0x40) Gamepad.press(15); else Gamepad.release(15); // D-Pad Down → button 15
if (d_lo & 0x80) Gamepad.press(16); else Gamepad.release(16); // D-Pad Left → button 16
// Analog sticks (bytes 5–8)
int8_t lx = map(resp[5], 0, 255, -127, 127);
int8_t ly = map(resp[6], 0, 255, -127, 127);
int8_t rx = map(resp[7], 0, 255, -127, 127);
int8_t ry = map(resp[8], 0, 255, -127, 127);
Gamepad.xAxis(lx);
Gamepad.yAxis(ly);
Gamepad.zAxis(rx);
Gamepad.rzAxis(ry);
Gamepad.write(); // Send HID report
delay(20);
}
byte shiftInOut(byte out) {
byte in = 0;
for (int i = 0; i < 8; i++) {
digitalWrite(PS2_CMD, (out & (1 << i)) ? HIGH : LOW);
digitalWrite(PS2_CLK, LOW);
delayMicroseconds(20);
if (digitalRead(PS2_DAT)) in |= (1 << i);
digitalWrite(PS2_CLK, HIGH);
delayMicroseconds(20);
}
return in;
}
5. Verification on Windows
- In Arduino IDE v2, select Board → Adafruit Feather 32u4 and the correct COM port.
- Upload the sketch.
- Connect the PS2 joystick to the Feather (3.3 V→VUSB, GND→GND, CLK→D6, CMD→D5, ATT→D2, DATA→D3).
- Press the ANALOG button on the joystick until its red LED lights up.
- Open joy.cpl (Win + R →
joy.cpl
). - You should see a device named “HID Game Controller” (or similar).
- In Properties, verify that axes and buttons respond correctly.
6. Steam Configuration
Once Windows recognizes the joystick as HID, set up in Steam:
Open Steam → Settings → Controller → General Controller Settings.
Enable Generic Gamepad Configuration Support.
Connect the Feather.
Click Start Configuration next to “Adafruit Feather 32u4”.
When prompted for the center button (Share/Capture), click Cancel or press Esc to skip.
Map buttons as follows:
- Cross (X) → Button A
- Circle (O) → Button B
- Square (□) → Button X
- Triangle (△) → Button Y
- L2/R2 → Treat as digital buttons
- L1/R1 → LB/RB
- Select → “Back” (optional)
- Start → “Start/Menu”
- D-Pad → D-Pad Up/Right/Down/Left
- Right stick (if present), or skip if not.
Adjust deadzones:
- Left stick: 10%
- Right stick: 15% (if used)
Save the configuration.
If mapping fails, copy this JSON to your clipboard and paste it via Paste from Clipboard:
{
"name": "PS2 Feather Default",
"appid": 0,
"creator": "PS2FeatherUser",
"version": 2,
"mode": "GLOBAL",
"mappings": {
"buttons": {
"BUTTON_1": ["CROSS"],
"BUTTON_2": ["CIRCLE"],
"BUTTON_3": ["SQUARE"],
"BUTTON_4": ["TRIANGLE"],
"BUTTON_5": ["L2"],
"BUTTON_6": ["R2"],
"BUTTON_7": ["L1"],
"BUTTON_8": ["R1"],
"BUTTON_9": ["SELECT"],
"BUTTON_10": ["L3"],
"BUTTON_11": ["R3"],
"BUTTON_12": ["START"],
"BUTTON_13": ["DPAD_UP"],
"BUTTON_14": ["DPAD_RIGHT"],
"BUTTON_15": ["DPAD_DOWN"],
"BUTTON_16": ["DPAD_LEFT"]
},
"axes": {
"AXIS_0": ["LX"],
"AXIS_1": ["LY"],
"AXIS_2": ["RX"],
"AXIS_3": ["RY"]
}
},
"deadzones": {
"AXIS_0": 0.10,
"AXIS_1": 0.10,
"AXIS_2": 0.15,
"AXIS_3": 0.15
}
}
- Copy the entire JSON block to your clipboard.
- In Steam mapping, choose Paste from Clipboard and confirm.
- Save and test in a game.
Conclusion
By following these steps, you’ve turned a PS2 joystick into a working USB gamepad using a Feather 32u4. Now you can enjoy retro or modern games with a fully customizable controller. I hope this guide helps you and inspires more hardware and gaming projects!