An HID device report is the data packet sent from the device to a host. In the case of a gamepad, a different device report is sent every time a different button is pressed. For example, the A button has a different packet than the B button.
For this project, you'll need to get your gamepad's device reports to update the code. That way, the code will know when you are pressing an A button or the up button on the D-pad. You'll do this by running some Arduino code that prints out the full device report every time a button is pressed.
// SPDX-FileCopyrightText: 2024 Liz Clark for Adafruit Industries // // SPDX-License-Identifier: MIT /********************************************************************* Adafruit invests time and resources providing this open source code, please support Adafruit and open-source hardware by purchasing products from Adafruit! MIT license, check LICENSE for more information Copyright (c) 2019 Ha Thach for Adafruit Industries All text above, and the splash screen below must be included in any redistribution *********************************************************************/ /* This example demonstrates use of both device and host, where * - Device runs on native USB controller (roothub port0) * - Host depends on MCU: * - rp2040: bit-banging 2 GPIOs with Pico-PIO-USB library (roothub port1) * - samd21/51, nrf52840, esp32: using MAX3421e controller (host shield) * * Requirements: * - For rp2040: * - Pico-PIO-USB library * - 2 consecutive GPIOs: D+ is defined by PIN_USB_HOST_DP, D- = D+ +1 * - Provide VBus (5v) and GND for peripheral * - CPU Speed must be either 120 or 240 MHz. Selected via "Menu -> CPU Speed" * - For samd21/51, nrf52840, esp32: * - Additional MAX2341e USB Host shield or featherwing is required * - SPI instance, CS pin, INT pin are correctly configured in usbh_helper.h */ /* Host example will get device descriptors of attached devices and print it out via * device CDC (Serial) as follows: * Device 1: ID 046d:c52f Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 0200 bDeviceClass 0 bDeviceSubClass 0 bDeviceProtocol 0 bMaxPacketSize0 8 idVendor 0x046d idProduct 0xc52f bcdDevice 2200 iManufacturer 1 Logitech iProduct 2 USB Receiver iSerialNumber 0 bNumConfigurations 1 * */ // USBHost is defined in usbh_helper.h #include "usbh_helper.h" #include "tusb.h" // Language ID: English #define LANGUAGE_ID 0x0409 typedef struct { tusb_desc_device_t desc_device; uint16_t manufacturer[32]; uint16_t product[48]; uint16_t serial[16]; bool mounted; } dev_info_t; // CFG_TUH_DEVICE_MAX is defined by tusb_config header dev_info_t dev_info[CFG_TUH_DEVICE_MAX] = { 0 }; void setup() { Serial.begin(115200); #if defined(CFG_TUH_MAX3421) && CFG_TUH_MAX3421 // init host stack on controller (rhport) 1 // For rp2040: this is called in core1's setup1() USBHost.begin(1); #endif Serial.println("TinyUSB Dual Device Info Example with HID Report"); } #if defined(CFG_TUH_MAX3421) && CFG_TUH_MAX3421 //--------------------------------------------------------------------+ // Using Host shield MAX3421E controller //--------------------------------------------------------------------+ void loop() { USBHost.task(); Serial.flush(); } #elif defined(ARDUINO_ARCH_RP2040) //--------------------------------------------------------------------+ // For RP2040 use both core0 for device stack, core1 for host stack //--------------------------------------------------------------------// //------------- Core0 -------------// void loop() { } //------------- Core1 -------------// void setup1() { // configure pio-usb: defined in usbh_helper.h rp2040_configure_pio_usb(); // run host stack on controller (rhport) 1 // Note: For rp2040 pico-pio-usb, calling USBHost.begin() on core1 will have most of the // host bit-banging processing works done in core1 to free up core0 for other works USBHost.begin(1); } void loop1() { USBHost.task(); Serial.flush(); } #endif //--------------------------------------------------------------------+ // TinyUSB Host callbacks //--------------------------------------------------------------------+ void print_device_descriptor(tuh_xfer_t *xfer); void utf16_to_utf8(uint16_t *temp_buf, size_t buf_len); void print_lsusb(void) { bool no_device = true; for (uint8_t daddr = 1; daddr < CFG_TUH_DEVICE_MAX + 1; daddr++) { // TODO can use tuh_mounted(daddr), but tinyusb has a bug // use local connected flag instead dev_info_t *dev = &dev_info[daddr - 1]; if (dev->mounted) { Serial.printf("Device %u: ID %04x:%04x %s %s\r\n", daddr, dev->desc_device.idVendor, dev->desc_device.idProduct, (char *) dev->manufacturer, (char *) dev->product); no_device = false; } } if (no_device) { Serial.println("No device connected (except hub)"); } } // Invoked when device is mounted (configured) void tuh_mount_cb(uint8_t daddr) { Serial.printf("Device attached, address = %d\r\n", daddr); dev_info_t *dev = &dev_info[daddr - 1]; dev->mounted = true; // Get Device Descriptor tuh_descriptor_get_device(daddr, &dev->desc_device, 18, print_device_descriptor, 0); } /// Invoked when device is unmounted (bus reset/unplugged) void tuh_umount_cb(uint8_t daddr) { Serial.printf("Device removed, address = %d\r\n", daddr); dev_info_t *dev = &dev_info[daddr - 1]; dev->mounted = false; // print device summary print_lsusb(); } void print_device_descriptor(tuh_xfer_t *xfer) { if (XFER_RESULT_SUCCESS != xfer->result) { Serial.printf("Failed to get device descriptor\r\n"); return; } uint8_t const daddr = xfer->daddr; dev_info_t *dev = &dev_info[daddr - 1]; tusb_desc_device_t *desc = &dev->desc_device; Serial.printf("Device %u: ID %04x:%04x\r\n", daddr, desc->idVendor, desc->idProduct); Serial.printf("Device Descriptor:\r\n"); Serial.printf(" bLength %u\r\n" , desc->bLength); Serial.printf(" bDescriptorType %u\r\n" , desc->bDescriptorType); Serial.printf(" bcdUSB %04x\r\n" , desc->bcdUSB); Serial.printf(" bDeviceClass %u\r\n" , desc->bDeviceClass); Serial.printf(" bDeviceSubClass %u\r\n" , desc->bDeviceSubClass); Serial.printf(" bDeviceProtocol %u\r\n" , desc->bDeviceProtocol); Serial.printf(" bMaxPacketSize0 %u\r\n" , desc->bMaxPacketSize0); Serial.printf(" idVendor 0x%04x\r\n" , desc->idVendor); Serial.printf(" idProduct 0x%04x\r\n" , desc->idProduct); Serial.printf(" bcdDevice %04x\r\n" , desc->bcdDevice); // Get String descriptor using Sync API Serial.printf(" iManufacturer %u ", desc->iManufacturer); if (XFER_RESULT_SUCCESS == tuh_descriptor_get_manufacturer_string_sync(daddr, LANGUAGE_ID, dev->manufacturer, sizeof(dev->manufacturer))) { utf16_to_utf8(dev->manufacturer, sizeof(dev->manufacturer)); Serial.printf((char *) dev->manufacturer); } Serial.printf("\r\n"); Serial.printf(" iProduct %u ", desc->iProduct); if (XFER_RESULT_SUCCESS == tuh_descriptor_get_product_string_sync(daddr, LANGUAGE_ID, dev->product, sizeof(dev->product))) { utf16_to_utf8(dev->product, sizeof(dev->product)); Serial.printf((char *) dev->product); } Serial.printf("\r\n"); Serial.printf(" iSerialNumber %u ", desc->iSerialNumber); if (XFER_RESULT_SUCCESS == tuh_descriptor_get_serial_string_sync(daddr, LANGUAGE_ID, dev->serial, sizeof(dev->serial))) { utf16_to_utf8(dev->serial, sizeof(dev->serial)); Serial.printf((char *) dev->serial); } Serial.printf("\r\n"); Serial.printf(" bNumConfigurations %u\r\n", desc->bNumConfigurations); // print device summary print_lsusb(); } //--------------------------------------------------------------------+ // String Descriptor Helper //--------------------------------------------------------------------+ static void _convert_utf16le_to_utf8(const uint16_t *utf16, size_t utf16_len, uint8_t *utf8, size_t utf8_len) { // TODO: Check for runover. (void) utf8_len; // Get the UTF-16 length out of the data itself. for (size_t i = 0; i < utf16_len; i++) { uint16_t chr = utf16[i]; if (chr < 0x80) { *utf8++ = chr & 0xff; } else if (chr < 0x800) { *utf8++ = (uint8_t) (0xC0 | (chr >> 6 & 0x1F)); *utf8++ = (uint8_t) (0x80 | (chr >> 0 & 0x3F)); } else { // TODO: Verify surrogate. *utf8++ = (uint8_t) (0xE0 | (chr >> 12 & 0x0F)); *utf8++ = (uint8_t) (0x80 | (chr >> 6 & 0x3F)); *utf8++ = (uint8_t) (0x80 | (chr >> 0 & 0x3F)); } // TODO: Handle UTF-16 code points that take two entries. } } // Count how many bytes a utf-16-le encoded string will take in utf-8. static int _count_utf8_bytes(const uint16_t *buf, size_t len) { size_t total_bytes = 0; for (size_t i = 0; i < len; i++) { uint16_t chr = buf[i]; if (chr < 0x80) { total_bytes += 1; } else if (chr < 0x800) { total_bytes += 2; } else { total_bytes += 3; } // TODO: Handle UTF-16 code points that take two entries. } return total_bytes; } void utf16_to_utf8(uint16_t *temp_buf, size_t buf_len) { size_t utf16_len = ((temp_buf[0] & 0xff) - 2) / sizeof(uint16_t); size_t utf8_len = _count_utf8_bytes(temp_buf + 1, utf16_len); _convert_utf16le_to_utf8(temp_buf + 1, utf16_len, (uint8_t *) temp_buf, buf_len); ((uint8_t *) temp_buf)[utf8_len] = '\0'; } //--------------------------------------------------------------------+ // HID Host Callback Functions //--------------------------------------------------------------------+ void tuh_hid_mount_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* desc_report, uint16_t desc_len) { Serial.printf("HID device mounted (address %d, instance %d)\n", dev_addr, instance); // Start receiving HID reports if (!tuh_hid_receive_report(dev_addr, instance)) { Serial.printf("Error: cannot request to receive report\n"); } } void tuh_hid_umount_cb(uint8_t dev_addr, uint8_t instance) { Serial.printf("HID device unmounted (address %d, instance %d)\n", dev_addr, instance); } void tuh_hid_report_received_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len) { Serial.printf("Received HID report from device %d instance %d: ", dev_addr, instance); for (uint16_t i = 0; i < len; i++) { Serial.printf("%02X ", report[i]); } Serial.printf("\n"); // Continue to receive the next report if (!tuh_hid_receive_report(dev_addr, instance)) { Serial.printf("Error: cannot request to receive report\n"); } }
Device Report UF2
The Arduino code is available as a pre-compiled UF2. You can drag and drop the UF2 file onto your Feather RP2040 USB Host board.
Plug your board into your computer, using a known-good data-sync USB cable, directly, or via an adapter if needed.
Hold down the BOOT/BOOTSEL button (highlighted in red above), and while continuing to hold it (don't let go!), press and release the Reset button (highlighted in blue above). Continue to hold the BOOT/BOOTSEL button until the RPI-RP2 drive appears!
Head over to the Arduino IDE and open up the Serial Monitor (Tools -> Serial Monitor) at 115200 baud. As you press each button on your gamepad, you'll see a different HID report print to the Serial Monitor.
In the printout above, the A, B, Y and X buttons were pressed followed by up, down, left and right on the D-pad. All of these buttons' bytes are located on the fourth byte of the report.
You'll notice a repeating report with 0x08
in the fourth byte. This is the empty report, which is sent when buttons are released and no buttons are currently pressed. When a button is pressed, you'll see that the bitmask is added to the empty byte of 0x08
. For example, an A button prints out as 0x28
. This means that the A button byte is 0x20
, the B button prints out as 0x48
, so its byte is 0x40
, etc.
What about the D-pad? The empty neutral 0x08
actually means that no buttons on the D-pad are pressed. The D-pad buttons in this case start at 0x00
for up, 0x01
for up and right, 0x02
for right, etc.
This was followed by pressing the Start, Back, Right paddle and Left paddle buttons. You'll notice that these bytes are located on the fifth byte of the report, replacing 0x00
as the empty byte.
The next inputs tested were the analog joysticks. Each joystick has an x and y axis. Each axis sends values to a different byte. In this case, the left x axis is on byte 0, the left y axis is on byte 1, the right x axis is on byte 2 and the right y axis is on byte 3. In the screenshot above, the left y axis is being moved. You'll notice that when an x axis is in its default position, it returns a value of 0x80
. A y axis in default position returns a value of 0x7F
.
Log Your Reports
For the Arduino code, you'll need to include the HID reports for each of your gamepad buttons. Similar to the tests in the screenshots above, you'll want to go through and press each button on your gamepad to confirm its byte and byte address location. You'll need to edit the gamepad_report.h file:
// SPDX-FileCopyrightText: 2024 Liz Clark for Adafruit Industries // // SPDX-License-Identifier: MIT // HID reports for Logitech Gamepad F310 // Update defines and combo_report for your gamepad and/or combo! // Byte indices for the gamepad report #define BYTE_LEFT_STICK_X 0 // Left analog stick X-axis #define BYTE_LEFT_STICK_Y 1 // Left analog stick Y-axis #define BYTE_RIGHT_STICK_X 2 // Right analog stick X-axis #define BYTE_RIGHT_STICK_Y 3 // Right analog stick Y-axis #define BYTE_DPAD_BUTTONS 4 // D-Pad and face buttons #define BYTE_MISC_BUTTONS 5 // Miscellaneous buttons (triggers, paddles, start, back) #define BYTE_UNUSED 6 // Unused #define BYTE_STATUS 7 // Status byte (usually constant) // Button masks for Byte[4] (DPAD and face buttons) #define DPAD_MASK 0x07 // Bits 0-2 for D-Pad direction #define DPAD_NEUTRAL 0x08 // Bit 3 set when D-Pad is neutral // D-Pad directions (use when DPAD_NEUTRAL is not set) #define DPAD_UP 0x00 // 0000 #define DPAD_UP_RIGHT 0x01 // 0001 #define DPAD_RIGHT 0x02 // 0010 #define DPAD_DOWN_RIGHT 0x03 // 0011 #define DPAD_DOWN 0x04 // 0100 #define DPAD_DOWN_LEFT 0x05 // 0101 #define DPAD_LEFT 0x06 // 0110 #define DPAD_UP_LEFT 0x07 // 0111 // Face buttons (Byte[4] bits 4-7) #define BUTTON_X 0x10 #define BUTTON_A 0x20 #define BUTTON_B 0x40 #define BUTTON_Y 0x80 // Button masks for Byte[5] (MISC buttons) #define MISC_NEUTRAL 0x00 // Miscellaneous buttons (Byte[5]) #define BUTTON_LEFT_PADDLE 0x01 #define BUTTON_RIGHT_PADDLE 0x02 #define BUTTON_LEFT_TRIGGER 0x04 #define BUTTON_RIGHT_TRIGGER 0x08 #define BUTTON_BACK 0x10 #define BUTTON_START 0x20 #define LEFT_STICK_X_NEUTRAL 0x80 #define LEFT_STICK_Y_NEUTRAL 0x7F #define RIGHT_STICK_X_NEUTRAL 0x80 #define RIGHT_STICK_Y_NEUTRAL 0x7F uint8_t combo_report[] = { BUTTON_A, BUTTON_LEFT_PADDLE, BUTTON_RIGHT_PADDLE }; size_t combo_size = sizeof(combo_report) / sizeof(combo_report[0]);
Each button bitmask is included as a #define
. Use the same variables and update the bitmasks for your controller. At the bottom of the header file, you'll see the combo_report[]
. You'll want to include a list of the buttons you want to use as your combo. By default, the combo will trigger when an A button, left shoulder and right shoulder buttons are pressed together.
uint8_t combo_report[] = { BUTTON_A, BUTTON_LEFT_PADDLE, BUTTON_RIGHT_PADDLE };
More About Device Reports
If you think you'll be reading device reports often, check out the HID Reporter project.
Text editor powered by tinymce.