PS3 controller on the Switch (or Xbox, etc.?)

Last year, I posted a couple of articles on using a USB-HID controller on an MSX and on a Nintendo Switch. I also posted an article on a PS3 controller repair.

A happy Pico enjoying a round of Mario Kart. (This particular Pico is doing something else, but it’s content watching its brother play.)

Well, it’s time to put 2 and 2 together and get that silly PS3 controller to work on the Switch! (Or on other devices supported by GP2040-CE, which as I understand includes the Xbox and PS4/PS5?) To do this, we just have to realize that the PS3 controller works on Linux. So we just need to figure out how Linux does it. And a quick Google query (though I can’t remember the words I chose) surfaced up an old patch from 2007 introducing PS3 support to the Linux kernel:

Add the USB HID quirk HID_QUIRK_SONY_PS3_CONTROLLER. This sends an
HID_REQ_GET_REPORT to the the PS3 controller to put the device into
‘operational mode’.

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=4a1a4d8b87389e35c3af04c0d0a95f6a0391b964

Looking into the patch, we see that there isn’t much to it, it’s just, as the description says, sending a single request to the controller:

+	result = usb_control_msg(dev, usb_rcvctrlpipe(dev, 0),
+				 HID_REQ_GET_REPORT,
+				 USB_DIR_IN | USB_TYPE_CLASS |
+				 USB_RECIP_INTERFACE,
+				 (3 << 8) | 0xf2, ifnum, buf, 17,
+				 USB_CTRL_GET_TIMEOUT);

So, how could we do this in 1) our code that we used for the MSX, and 2) in GP2040-CE?

Modifying Pico-PIO-USB to work with the PS3 controller

Pico-PIO-USB isn’t very over-engineered yet(?!), and we can just modify the enumerate_device function in pio_usb_host.c to check if we’re seeing a PS3 controller, and send the request. Near the bottom of this function, we send the “get_hid_report_descrpitor_request” to the USB device. Immediately after we add our own code, as below:

diff --git a/src/pio_usb_host.c b/src/pio_usb_host.c
index 864d048..0efc567 100644
--- a/src/pio_usb_host.c
+++ b/src/pio_usb_host.c
@@ -1018,6 +1018,21 @@ static int enumerate_device(usb_device_t *device, uint8_t address) {
         printf("\n");
         stdio_flush();
 
+        // We need to send a special HID request otherwise the contoller won't do anything
+        if ((device->vid == 0x054c) && (device->pid == 0x0268)) {
+          usb_setup_packet_t get_hid_report_ps3_controller_request =
+              GET_HID_REPORT_PS3_CONTROLLER;
+          control_in_protocol(
+              device, (uint8_t *)&get_hid_report_ps3_controller_request,
+              sizeof(get_hid_report_ps3_controller_request), rx_buffer, LINUX__SIXAXIS_REPORT_0xF2_SIZE);
+          printf("\t\tPS3 controller response:");
+          for (int i = 0; i < LINUX__SIXAXIS_REPORT_0xF2_SIZE; i++) {
+            printf("%02x ", device->control_pipe.rx_buffer[i]);
+          }
+          printf("\n");
+          stdio_flush();
+        }
+
       } break;
       default:
         break;

This requires the following definitions to be added in usb_definitions.h. I just used the same names used in Linux (not the ancient 2007 version, but something recent):

diff --git a/src/usb_definitions.h b/src/usb_definitions.h
index c345bdd..845fbbc 100644
--- a/src/usb_definitions.h
+++ b/src/usb_definitions.h
@@ -302,10 +302,16 @@ enum {
   USB_REQ_REC_OTHER = 0x03,
 };
 
+// some constants taken from Linux source code (.../ch9.h, .../hid.h)
+#define LINUX__USB_REQ_GET_DESCRIPTOR 0x06
+#define LINUX__HID_REQ_GET_REPORT 0x01
+#define LINUX__HID_FEATURE_REPORT 0x02
+#define LINUX__SIXAXIS_REPORT_0xF2_SIZE 17
+
 #define GET_DEVICE_DESCRIPTOR_REQ_DEFAULT                                      \
-  { USB_REQ_DIR_IN, 0x06, 0, 0x01, 0, 0, 0x12, 0 }
+  { USB_REQ_DIR_IN, LINUX__USB_REQ_GET_DESCRIPTOR, 0, 0x01, 0, 0, 0x12, 0 }
 #define GET_CONFIGURATION_DESCRIPTOR_REQ_DEFAULT                               \
-  { USB_REQ_DIR_IN, 0x06, 0, 0x02, 0, 0, 0x09, 0 }
+  { USB_REQ_DIR_IN, LINUX__USB_REQ_GET_DESCRIPTOR, 0, 0x02, 0, 0, 0x09, 0 }
 #define SET_CONFIGURATION_REQ_DEFAULT                                          \
   { USB_REQ_DIR_OUT, 0x09, 0, 0, 0, 0, 0, 0 }
 #define SET_ADDRESS_REQ_DEFAULT                                                \
@@ -313,10 +319,13 @@ enum {
 #define SET_HID_IDLE_REQ_DEFAULT                                               \
   { USB_REQ_TYP_CLASS | USB_REQ_REC_IFACE, 0x0A, 0, 0, 0, 0, 0, 0 }
 #define GET_HID_REPORT_DESCRIPTOR_DEFAULT                                      \
-  { USB_REQ_DIR_IN | USB_REQ_REC_IFACE, 0x06, 0, 0x22, 0, 0, 0xff, 0 }
+  { USB_REQ_DIR_IN | USB_REQ_REC_IFACE, LINUX__USB_REQ_GET_DESCRIPTOR, 0, 0x22, 0, 0, 0xff, 0 }
+#define GET_HID_REPORT_PS3_CONTROLLER                               \
+  { USB_REQ_DIR_IN | USB_REQ_TYP_CLASS | USB_REQ_REC_IFACE, LINUX__HID_REQ_GET_REPORT, 0xf2, LINUX__HID_FEATURE_REPORT+1, 0, 0, LINUX__SIXAXIS_REPORT_0xF2_SIZE, 0 }
+
 #define GET_HUB_DESCRPTOR_REQUEST                                              \
   {                                                                            \
-    USB_REQ_DIR_IN | USB_REQ_TYP_CLASS | USB_REQ_REC_DEVICE, 0x06, 0, 0x29, 0, \
+    USB_REQ_DIR_IN | USB_REQ_TYP_CLASS | USB_REQ_REC_DEVICE, LINUX__USB_REQ_GET_DESCRIPTOR, 0, 0x29, 0, \
         0, 8, 0                                                                \
   }
 #define GET_HUB_PORT_STATUS_REQUEST

And that’s all!

So, will the same modifications work in GP2040-CE too? No, while GP2040-CE uses Pico-PIO-USB to get USB host functionality to work at all while the Pico is busy being a USB device, protocol-y stuff is handled by TinyUSB, which by the way is included in the Pico SDK. Looking at pspassthrough.cpp for inspiration, I found that if we just add the following code to our previously modified version of src/addons/keyboard_host.cpp, our PS3 controller wakes up from its daze and starts sending our inputs!

diff --git a/src/addons/keyboard_host.cpp b/src/addons/keyboard_host.cpp
index 59bc85e..88d82da 100644
--- a/src/addons/keyboard_host.cpp
+++ b/src/addons/keyboard_host.cpp
@@ -2,6 +2,8 @@
 #include "storagemanager.h"
 #include "usbhostmanager.h"
 
+uint8_t report_buffer[64];
+
 bool KeyboardHostAddon::available() {
     const KeyboardHostOptions& keyboardHostOptions = Storage::getInstance().getAddonOptions().keyboardHostOptions;
     return keyboardHostOptions.enabled &&
@@ -77,6 +79,10 @@ void KeyboardHostAddon::preprocess() {
 }
 
 void KeyboardHostAddon::mount(uint8_t dev_addr, uint8_t instance, uint8_t const* desc_report, uint16_t desc_len) {
+  uint8_t* buf = report_buffer;
+  tuh_hid_get_report(dev_addr, instance, 0xf2, HID_REPORT_TYPE_FEATURE, buf, 17);
+
   _keyboard_host_enabled = true;
 }
 
Works for me!

Leave a Reply

Your email address will not be published. Required fields are marked *