mirror of
https://github.com/H4zeyaf/Ghostcontrol-PS5-USB-Controller-Patcher-Cyclone2.git
synced 2026-10-06 09:00:18 +02:00
268 lines
8.4 KiB
Markdown
268 lines
8.4 KiB
Markdown
# Nintendo Switch Pro Controller USB Protocol Research
|
||
|
||
This document captures the complete research into making the 8BitDo Ultimate 2 (Nintendo Switch Pro Controller mode) work on PS5 via raw USB FS access.
|
||
|
||
---
|
||
|
||
## Device Identification
|
||
|
||
| Property | Value |
|
||
|----------|-------|
|
||
| VID:PID (Nintendo mode) | `0x057e:0x2009` |
|
||
| VID:PID (Native mode) | `0x2dc8:0x310b` |
|
||
| PS5 device node | `/dev/ugen2.2` |
|
||
| USB bus | `usbus2`, depth=1 port=2 speed=FS |
|
||
| Kernel driver (pre-detach) | `usb_hid0` |
|
||
| IN endpoint | `ep=0x81`, wMaxPacketSize=64, bInterval=1ms |
|
||
| OUT endpoint | `ep=0x02`, wMaxPacketSize=64 (**NOTE: 0x02 not 0x01 like genuine Nintendo**) |
|
||
|
||
---
|
||
|
||
## PS5 USB Access Method
|
||
|
||
Direct kernel HID access via `usb_hid0` is blocked (`EPERM`). The working path:
|
||
|
||
```c
|
||
USB_IFACE_DRIVER_DETACH(iface=0) // detach usb_hid0
|
||
USB_FS_INIT(ep_index_max=N) // allocate N endpoint slots
|
||
USB_FS_OPEN(ep_no=0x81, ep_index=0) // IN endpoint
|
||
USB_FS_OPEN(ep_no=0x02, ep_index=1) // OUT endpoint
|
||
USB_FS_START(ep_index=0) // start interrupt IN transfer
|
||
USB_FS_COMPLETE(ep_index=0) // block until data
|
||
```
|
||
|
||
### Why Two-Pass Init
|
||
|
||
The IN endpoint (0x81) cannot be opened on the first try in some states. A "pass 1" is required:
|
||
|
||
```c
|
||
// Pass 1: unlock endpoint
|
||
FS_INIT(1 slot) → DETACH → FS_OPEN(ep=0x81) → FS_UNINIT → close()
|
||
|
||
// Pass 2: actual session
|
||
open() → DETACH → FS_INIT(2 slots) → FS_OPEN IN(0x81) + OUT(0x02)
|
||
```
|
||
|
||
Pass 1 does nothing functionally — it just exercises the endpoint to unlock it for pass 2.
|
||
|
||
---
|
||
|
||
## Critical Finding: Subcmd 0x03 Must Be Sent LAST
|
||
|
||
**Wrong order (pipe breaks):**
|
||
```
|
||
[80 02] → [80 04] → subcmd 0x03 0x30 ← USB pipe break, controller disconnects
|
||
```
|
||
|
||
**Correct order (works):**
|
||
```
|
||
[80 02] → [80 04] ← USB handshake only
|
||
respond to 0x81 handshake ← controller responds with 0x81 0x01 / 0x81 0x02
|
||
[80 04] again ← handshake ack
|
||
subcmd 0x40 0x01 ← enable IMU
|
||
subcmd 0x48 0x01 ← enable vibration
|
||
subcmd 0x30 0x01 ← set player LED
|
||
subcmd 0x03 0x30 ← set input report mode (LAST)
|
||
```
|
||
|
||
Sending `subcmd 0x03` first causes a USB pipe break (`EBUSY` on `USB_FS_COMPLETE`) and the controller enters a reconnect loop that requires 2–3 additional handshake rounds to recover. With the correct order, `subcmd 0x03` is accepted cleanly and streaming starts immediately.
|
||
|
||
---
|
||
|
||
## Complete Initialization Sequence
|
||
|
||
### Phase 1: Two-Pass USB FS Setup
|
||
|
||
```c
|
||
// Pass 1 (unlock)
|
||
fd1 = open("/dev/ugen2.2", O_RDWR);
|
||
USB_FS_INIT(fd1, ep_index_max=1);
|
||
USB_IFACE_DRIVER_DETACH(fd1, iface=0);
|
||
USB_FS_OPEN(fd1, ep_index=0, ep_no=0x81, max_bufsize=64);
|
||
USB_FS_UNINIT(fd1);
|
||
close(fd1);
|
||
usleep(30000);
|
||
|
||
// Pass 2 (working session)
|
||
fd = open("/dev/ugen2.2", O_RDWR);
|
||
USB_IFACE_DRIVER_DETACH(fd, iface=0);
|
||
USB_FS_INIT(fd, ep_index_max=2);
|
||
USB_FS_OPEN(fd, ep_index=0, ep_no=0x81, max_bufsize=64); // IN
|
||
USB_FS_OPEN(fd, ep_index=1, ep_no=0x02, max_bufsize=64); // OUT
|
||
```
|
||
|
||
### Phase 2: Initial USB Handshake (Blind)
|
||
|
||
```c
|
||
send_out([0x80, 0x02]); // handshake start
|
||
usleep(30000);
|
||
send_out([0x80, 0x04]); // disable USB timeout
|
||
usleep(50000);
|
||
```
|
||
|
||
### Phase 3: Controller-Initiated Handshake Response
|
||
|
||
The controller sends `0x81` handshake packets. Respond **within ~200ms or the controller gives up:**
|
||
|
||
```
|
||
Controller sends: 0x81 0x01 → Host responds: [0x80, 0x02]
|
||
Controller sends: 0x81 0x02 → Host responds: [0x80, 0x04]
|
||
```
|
||
|
||
### Phase 4: Subcmd Init Sequence (MUST be in this order)
|
||
|
||
```c
|
||
send_subcmd(timer=1, subcmd=0x40, data=0x01); // enable IMU
|
||
usleep(50000);
|
||
send_subcmd(timer=2, subcmd=0x48, data=0x01); // enable vibration
|
||
usleep(50000);
|
||
send_subcmd(timer=3, subcmd=0x30, data=0x01); // set player LED
|
||
usleep(50000);
|
||
send_subcmd(timer=4, subcmd=0x03, data=0x30); // input mode = full push (LAST)
|
||
```
|
||
|
||
Subcmd packet format (12 bytes):
|
||
```c
|
||
uint8_t sc[12] = {0};
|
||
sc[0] = 0x01; // output report ID
|
||
sc[1] = timer; // 4-bit counter, must increment each packet
|
||
// sc[2..9] = neutral rumble = {0,1,0x40,0x40,0,1,0x40,0x40}
|
||
sc[10] = subcmd;
|
||
sc[11] = data;
|
||
```
|
||
|
||
### Phase 5: Read Loop
|
||
|
||
After `subcmd 0x03`, the controller immediately sends streaming data:
|
||
|
||
```c
|
||
while (1) {
|
||
USB_FS_START(ep_index=0);
|
||
USB_FS_COMPLETE(ep_index=0, timeout=200ms);
|
||
// process pkt[0..63]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Streaming Packet Format
|
||
|
||
After the init sequence, the controller sends `0x00`-prefixed packets at ~60Hz:
|
||
|
||
```
|
||
Byte 0: 0x00 — always 0x00 (report ID stripped by 8BitDo firmware)
|
||
Byte 1: timer — 8-bit counter, increments ~6-8 units per frame
|
||
Byte 2: 0x70 — battery/connection (0x70 = USB, full charge)
|
||
Byte 3: right_btns — Y X B A SR SL R ZR (bits 0-7)
|
||
Byte 4: misc_btns — Minus Plus R3 L3 Home Capture (bits 0-5)
|
||
Byte 5: left_btns — Down Up Right Left SR SL L ZL (bits 0-7)
|
||
Bytes 6-8: left stick (12-bit packed, center ~0x800)
|
||
Bytes 9-11: right stick (12-bit packed, center ~0x7FF)
|
||
Byte 12: vibration status (0x0b = idle)
|
||
Bytes 13-47: IMU data (3 × 6-axis samples, when IMU enabled)
|
||
```
|
||
|
||
**Note:** Genuine Nintendo Switch Pro Controllers send `0x30` as byte 0 (the report ID). The 8BitDo sends `0x00`. The rest of the layout is identical.
|
||
|
||
### Stick Decoding (12-bit packed)
|
||
|
||
```c
|
||
uint16_t lx = buf[6] | ((buf[7] & 0x0F) << 8); // 0–4095, center ~2048
|
||
uint16_t ly = (buf[7] >> 4) | ((uint16_t)buf[8] << 4); // 0–4095, center ~2047
|
||
uint16_t rx = buf[9] | ((buf[10] & 0x0F) << 8);
|
||
uint16_t ry = (buf[10] >> 4) | ((uint16_t)buf[11] << 4);
|
||
|
||
// Map to 0–255 for ScePadData:
|
||
uint8_t x = (uint8_t)((v * 255u) / 4095u);
|
||
```
|
||
|
||
### Button Bit Mapping
|
||
|
||
```
|
||
buf[3] — right face buttons:
|
||
bit 0: Y → PS5 Square
|
||
bit 1: X → PS5 Triangle
|
||
bit 2: B → PS5 Cross
|
||
bit 3: A → PS5 Circle
|
||
bit 6: R → PS5 R1
|
||
bit 7: ZR → PS5 R2
|
||
|
||
buf[4] — shared buttons:
|
||
bit 0: Minus → PS5 Create/Share
|
||
bit 1: Plus → PS5 Options
|
||
bit 2: R3 → PS5 R3
|
||
bit 3: L3 → PS5 L3
|
||
bit 4: Home → PS5 PS button
|
||
|
||
buf[5] — left buttons:
|
||
bit 0: Down → PS5 Down
|
||
bit 1: Up → PS5 Up
|
||
bit 2: Right → PS5 Right
|
||
bit 3: Left → PS5 Left
|
||
bit 6: L → PS5 L1
|
||
bit 7: ZL → PS5 L2
|
||
```
|
||
|
||
---
|
||
|
||
## Reconnect Handling
|
||
|
||
On controller unplug/replug:
|
||
- `USB_FS_START` or `USB_FS_COMPLETE` returns `ENXIO` or `ENOTTY`
|
||
- Close all endpoints, `USB_FS_UNINIT`, `close(fd)`
|
||
- Re-scan `ugen` paths for the device (20s timeout)
|
||
- Run the full two-pass init + handshake sequence again
|
||
|
||
On reconnect without full power cycle:
|
||
- First packet may be all-zeros (`00 00 00 00...`) — discard it
|
||
- Then `0x81` handshake packets appear — run Phase 3 and 4 again
|
||
|
||
---
|
||
|
||
## USB FS COMPLETE Polling
|
||
|
||
`USB_FS_COMPLETE` is non-blocking. Poll at 50ms intervals for up to `timeout_ms + 300ms`:
|
||
|
||
```c
|
||
int max_polls = (int)((timeout_ms + 300) / 50) + 1;
|
||
for (int w = 0; w < max_polls; w++) {
|
||
if (ioctl(fd, USB_FS_COMPLETE, &co) == 0) break;
|
||
if (errno != EBUSY) { /* error */ break; }
|
||
usleep(50000);
|
||
}
|
||
```
|
||
|
||
**Do not** use 5-retry × 10ms — the USB transfer timeout is 200ms so you'll stop polling before the transfer can complete, leaving the endpoint stuck in `EBUSY`.
|
||
|
||
---
|
||
|
||
## Things That Do Not Work
|
||
|
||
| Approach | Result |
|
||
|----------|--------|
|
||
| `USB_GET_REPORT_DESC` | `errno=25 ENOTTY` |
|
||
| `read(fd, buf, 64)` without DETACH | `errno=1 EPERM` |
|
||
| `libSceHidControl` | Controller not auto-registered; ShellCore not initialized |
|
||
| `libSceUsbd` | Not loaded in SceShellCore or SceShellUI |
|
||
| Soft-reinit after `[80 04]` | Controller goes silent (wrong approach) |
|
||
| Sending `subcmd 0x03` first | USB pipe break → reconnect loop |
|
||
| Responding to `0x81 0x02` without subcmds | Controller silent after `[80 04]` |
|
||
|
||
---
|
||
|
||
## Research Probe History
|
||
|
||
Probes run on PS5 (`usb_handshake_probe v1` through `v12`) at port 6986, reports fetched from FTP `/data/ghostpad/`:
|
||
|
||
| Version | Key Discovery |
|
||
|---------|--------------|
|
||
| v1 | Confirmed soft-reinit after subcmd 0x03 pipe-break works |
|
||
| v2–v3 | Fixed COMPLETE polling (50ms intervals needed, not 10ms) |
|
||
| v4–v5 | Identified full USB bus-reset after `[80 04]` in second handshake |
|
||
| v6 | Second subcmd 0x03 also causes bus-reset — infinite loop |
|
||
| v7–v8 | Discovered controller gives up if host takes >200ms to respond to 0x81 |
|
||
| v9 | Tight 200ms loop — controller responds within timing window |
|
||
| v10 | Added IMU/vibration subcmds — still no streaming |
|
||
| v11 | **BREAKTHROUGH**: subcmd 0x03 sent LAST → immediate 0x30 streaming |
|
||
| v12 | Full 64-byte packet dump — confirmed format, verified stick decode |
|