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

5
  • 1
    First prize is a drop-in library ... what does that mean?
    – jsotola
    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
    – jsotola
    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

1 Answer 1

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.

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.