1

I want to use a Waveshare Pi Pico Zero to connect two standard NES controllers to a computer over a single USB port.

I'm using the Waveshare RP2040 Zero board definition from https://github.com/earlephilhower/arduino-pico which seems to provide a Joystick HID library that was compatible with this Teensy targeted NES_to_USB sketch. However, that only works for a single NES controller.

I found Arduino-USB-HID-RetroJoystickAdapter that supports two joysticks or NES controllers using a single board using the MHeironimus ArduinoJoystickLibrary. Unfortunately it seems that particular Joystick library only supports ATmega32u4 based Arduino boards (e.g. Leonardo, Pro Micro).

Ask

A solution for exposing two USB HID Joystick devices on a single Pi Pico Zero board. First prize is a drop-in library like MHeironimus ArduinoJoystickLibrary that is compatible with https://github.com/earlephilhower/arduino-pico 's RP2040 core. Happy to hear of more DIY solutions too, not sure if I'll be able to follow though :)

NeilenMarais
  • 219
  • 3
  • 8

2 Answers2

2

Update: more recent version of Adafruit_TinyUSB_Arduino supports having multiple HID instances (defaults to two). So to avoid the HID_QUIRK_MULTI_INPUT hack for linux, you could instead expose each controller as a different HID interface.

Here is your example updated:

#include "Adafruit_TinyUSB.h"
#if CFG_TUD_HID < 2
  #error "Requires two HID instances support. See https://github.com/adafruit/Adafruit_TinyUSB_Arduino/commit/b75604f794acdf88daad310dd75d3a0724129056"
#endif

// NB NB!!! Select "Adafruit TinyUSB" for USB stack

// HID report descriptor using TinyUSB's template uint8_t const desc_hid_report[] = { TUD_HID_REPORT_DESC_GAMEPAD() };

// USB HID object. For ESP32 these values cannot be changed after this declaration // desc report, desc len, protocol, interval, use out endpoint Adafruit_USBD_HID usb_hid[] { Adafruit_USBD_HID(desc_hid_report, sizeof(desc_hid_report), HID_ITF_PROTOCOL_NONE, 2, false), Adafruit_USBD_HID(desc_hid_report, sizeof(desc_hid_report), HID_ITF_PROTOCOL_NONE, 2, false) };

// Report payload defined in src/class/hid/hid.h // - For Gamepad Button Bit Mask see hid_gamepad_button_bm_t // - For Gamepad Hat Bit Mask see hid_gamepad_hat_t hid_gamepad_report_t gp[2]; // Two gamepad descriptors

void setup() {

// Manual begin() is required on core without built-in support e.g. mbed rp2040 if (!TinyUSBDevice.isInitialized()) { TinyUSBDevice.begin(0); }

Serial.begin(115200); usb_hid[0].begin(); usb_hid[1].begin();

// If already enumerated, additional class driverr begin() e.g msc, hid, midi won't take effect until re-enumeration if (TinyUSBDevice.mounted()) { TinyUSBDevice.detach(); delay(10); TinyUSBDevice.attach(); }

Serial.println("Adafruit TinyUSB HID multi-gamepad example"); }

uint8_t gp_i = 0;

void loop() { #ifdef TINYUSB_NEED_POLLING_TASK // Manual call tud_task since it isn't called by Core's background TinyUSBDevice.task(); #endif

// not enumerated()/mounted() yet: nothing to do if (!TinyUSBDevice.mounted()) { return; }

if ( !usb_hid[gp_i].ready() ) return;

Serial.print("Testing gamepad nr: "); Serial.println(gp_i);

// Reset buttons Serial.println("No pressing buttons"); gp[gp_i].x = 0; gp[gp_i].y = 0; gp[gp_i].z = 0; gp[gp_i].rz = 0; gp[gp_i].rx = 0; gp[gp_i].ry = 0; gp[gp_i].hat = 0; gp[gp_i].buttons = 0;

usb_hid[gp_i].sendReport(0, &gp[gp_i], sizeof(gp[gp_i])); delay(2000);

// Random touch Serial.println("Random touch"); gp[gp_i].x = random(-127, 128); gp[gp_i].y = random(-127, 128); gp[gp_i].z = random(-127, 128); gp[gp_i].rz = random(-127, 128); gp[gp_i].rx = random(-127, 128); gp[gp_i].ry = random(-127, 128); gp[gp_i].hat = random(0, 9); gp[gp_i].buttons = random(0, 0xffff); usb_hid[gp_i].sendReport(0, &gp[gp_i], sizeof(gp[gp_i])); delay(2000);

// select the other gamepad gp_i = (gp_i + 1) % 2; }

1

By selecting the Adafruit TinyUSB USB stack option (Tools->USB Stack->Adafruit TinyUSB) of the arduino-pico core instead of the default (Pico SDK) the USB HID descriptors can be configured directly to expose two HID gamepads.

The disadvantage of this approach is that the easy to use preconfigured devices (e.g. Joystick, Mouse and Keyboard) are not configured, but this gives you the option to use the full flexibility of TinyUSB to configure USB devices. While I've used it with my Waveshare Pi Pico Zero board it should work with any Arduino board that Adafruit TinyUSB supports.

Here is a test sketch that sets up a HID device with two gamepads similar to the "Hey how about two players?" example in https://eleccelerator.com/tutorial-about-usb-hid-report-descriptors/. I modified the Adafruit TinyUSB hid_gamepad.ino example by adding a second Gamepad report and shortening the "demo" loop to save space:

/*********************************************************************
 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) 2021 NeKuNeKo for Adafruit Industries All text above, and the splash screen below must be included in any redistribution *********************************************************************/

#include "Adafruit_TinyUSB.h"

// NB NB!!! Select "Adafruit TinyUSB" for USB stack

// HID report descriptor using TinyUSB's template uint8_t const desc_hid_report[] = { TUD_HID_REPORT_DESC_GAMEPAD(HID_REPORT_ID(1)), // First gamepad report-id 1 TUD_HID_REPORT_DESC_GAMEPAD(HID_REPORT_ID(2)) // Second gamepad report-id 2 };

// USB HID object. For ESP32 these values cannot be changed after this declaration // desc report, desc len, protocol, interval, use out endpoint Adafruit_USBD_HID usb_hid(desc_hid_report, sizeof(desc_hid_report), HID_ITF_PROTOCOL_NONE, 2, false);

// Report payload defined in src/class/hid/hid.h // - For Gamepad Button Bit Mask see hid_gamepad_button_bm_t // - For Gamepad Hat Bit Mask see hid_gamepad_hat_t hid_gamepad_report_t gp[2]; // Two gamepad descriptors

void setup() { #if defined(ARDUINO_ARCH_MBED) && defined(ARDUINO_ARCH_RP2040) // Manual begin() is required on core without built-in support for TinyUSB such as mbed rp2040 TinyUSB_Device_Init(0); #endif

Serial.begin(115200); usb_hid.begin();

// wait until device mounted while( !TinyUSBDevice.mounted() ) delay(1); Serial.println("Adafruit TinyUSB HID multi-gamepad example"); }

uint8_t gp_i = 0;

void loop() { if ( !usb_hid.ready() ) return;

Serial.print("Testing gamepad nr: "); Serial.println(gp_i);

// Reset buttons Serial.println("No pressing buttons"); gp[gp_i].x = 0; gp[gp_i].y = 0; gp[gp_i].z = 0; gp[gp_i].rz = 0; gp[gp_i].rx = 0; gp[gp_i].ry = 0; gp[gp_i].hat = 0; gp[gp_i].buttons = 0; // gp_i + 1 is the HID report-id, i.e. 1 for first gamepad and 2 for the // second, as defined in desc_hid_report[] above. usb_hid.sendReport(gp_i + 1, &gp[gp_i], sizeof(gp[gp_i])); delay(2000);

// Random touch Serial.println("Random touch"); gp[gp_i].x = random(-127, 128); gp[gp_i].y = random(-127, 128); gp[gp_i].z = random(-127, 128); gp[gp_i].rz = random(-127, 128); gp[gp_i].rx = random(-127, 128); gp[gp_i].ry = random(-127, 128); gp[gp_i].hat = random(0, 9); gp[gp_i].buttons = random(0, 0xffff); usb_hid.sendReport(gp_i + 1, &gp[gp_i], sizeof(gp[gp_i])); delay(2000);

// select the other gamepad gp_i = (gp_i + 1) % 2; }

Linux HID Malarkey

For some reason the Linux kernel by default ignores multiple reports of the same type of HID device. I.e. a device with a mouse, keyboard and joystick device reports are no problem, but if a device has multiple of the same HID reports, say, two gamepads, Linux will only see the first instance. To enable multiple instances the HID_QUIRK_MULTI_INPUT setting must be enabled for the specific USB device.

These instructions worked on Ubuntu 22.04, you may need to adjust them for your distro.

After programming the example sketch I could see the ID of my Pi Pico USB device with `lsusb:

$ lsusb
...
Bus 001 Device 011: ID 239a:cafe Adafruit RP2040 Zero
...

Note down the USB device identifier (239a:cafe) and create a file in /etc/modprobe.d to configure the usbhid module:

$ echo "options usbhid quirks=0x239a:0xcafe:0x040" | sudo tee /etc/modprobe.d/adafruit_hid_quirk.conf

$ sudo update-initramfs -u

Requires a reboot to take effect, e.g.:

$ sudo shutdown -r now

The 0x239a:0xcafe is from the lsusb output and 0x040 is the magic number to enable HID_QUIRK_MULTI_INPUT mode. We need update-initramfs since usbhid is usually loaded too early in the boot process for the settings from /etc/modprobe.d to take effect without it being in the initrd.

Once you have rebooted you can check if the usbhid setting took effect:

$ cat /sys/module/usbhid/parameters/quirks
0x239a:0xcafe:0x040,(null),(null),(null)

and you should see two joystick devices after programming the sketch:

ls /dev/input/js*
/dev/input/js0  /dev/input/js1

Testing

An easy way to test is to navigate to https://gamepad-tester.com/. After a short period you should see something like:

Player 1 and Player 2 Gamepads

You should see Player1's values toggle and then Player2's values until you unplug your pico.

NeilenMarais
  • 219
  • 3
  • 8