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 :)

asked Jun 25, 2023 at 15:55
5
  • 1
    First prize is a drop-in library ... what does that mean? Commented Jun 25, 2023 at 16:39
  • >First prize is a drop-in library -- Being able to simply install an existing library into Arduino that makes a second Joystick HID device available for Pi Pico like MHeironimus's ArduinoJoystickLibrary does for ATMega32u4 based boards. Commented Jun 25, 2023 at 16:47
  • this may help... eleccelerator.com/tutorial-about-usb-hid-report-descriptors Commented Jun 25, 2023 at 16:57
  • 1
    I got some useful advice about how to use TinyUSB directly from the arduino-pico maintainer. If I manage to get something working I'll write it up as an answer. Commented Jun 25, 2023 at 22:15
  • 1
    OK, I got it working, in short, select "Adafruit TinyUSB" as the USB stack and then modify the Adafruit hid_gamepad.ino example to have two different GP reports similar to the hid_composite example. The hard bit then is dealing with the Linux kernel's stupidity and having to enable HID_QUIRK_MULTI_INPUT mode to enable multiple HID reports of the same type on a single device to work. Bit late now, will write it up properly tomorrow. Commented Jun 26, 2023 at 1:40

2 Answers 2

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;
}
answered Dec 9, 2024 at 14:24
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.

answered Jun 26, 2023 at 22:01

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.