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.
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
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=4a1a4d8b87389e35c3af04c0d0a95f6a0391b964
HID_REQ_GET_REPORT to the the PS3 controller to put the device into
‘operational mode’.
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;
}