Skip to content

USB SNES-like controller example for arduino #3023

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 17, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions USB_SNES_Gamepad/Arduino_USB_Host/gamepad_reports.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
//
// SPDX-License-Identifier: MIT

// HID reports for USB SNES-like controller

// Byte indices for the gamepad report
#define BYTE_DPAD_LEFT_RIGHT 0 // D-Pad left and right
#define BYTE_DPAD_UP_DOWN 1 // D-Pad up and down
// bytes 2,3,4 unused
#define BYTE_ABXY_BUTTONS 5 // A, B, X, Y
#define BYTE_OTHER_BUTTONS 6 // Shoulders, start, and select


#define DPAD_NEUTRAL 0x7F
// D-Pad directions
#define DPAD_UP 0x00
#define DPAD_RIGHT 0xFF
#define DPAD_DOWN 0xFF
#define DPAD_LEFT 0x00


// Face buttons (Byte[5])
#define BUTTON_NEUTRAL 0x0F
#define BUTTON_X 0x1F
#define BUTTON_A 0x2F
#define BUTTON_B 0x4F
#define BUTTON_Y 0x8F


// Miscellaneous buttons (Byte[6])
#define BUTTON_MISC_NEUTRAL 0x00
#define BUTTON_LEFT_SHOULDER 0x01
#define BUTTON_RIGHT_SHOULDER 0x02
#define BUTTON_SELECT 0x10
#define BUTTON_START 0x20
182 changes: 182 additions & 0 deletions USB_SNES_Gamepad/Arduino_USB_Host/snes_gamepad_simpletest.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// SPDX-FileCopyrightText: 2025 Tim Cocks 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 usb host with a SNES-like game controller
* - Host depends on MCU:
* - rp2040: bit-banging 2 GPIOs with Pico-PIO-USB library (roothub port1)
*
* 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"
*/

// USBHost is defined in usbh_helper.h
#include "usbh_helper.h"
#include "tusb.h"
#include "Adafruit_TinyUSB.h"
#include "gamepad_reports.h"

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

// USB HID object
Adafruit_USBD_HID usb_hid;

// 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;

bool printed_blank = false;

void setup() {
if (!TinyUSBDevice.isInitialized()) {
TinyUSBDevice.begin(0);
}
Serial.begin(115200);
// Setup HID
usb_hid.setPollInterval(2);
usb_hid.setReportDescriptor(desc_hid_report, sizeof(desc_hid_report));
usb_hid.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();
}
}

#if 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

//--------------------------------------------------------------------+
// 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) {

if (report[BYTE_DPAD_LEFT_RIGHT] != DPAD_NEUTRAL ||
report[BYTE_DPAD_UP_DOWN] != DPAD_NEUTRAL ||
report[BYTE_ABXY_BUTTONS] != BUTTON_NEUTRAL ||
report[BYTE_OTHER_BUTTONS] != BUTTON_MISC_NEUTRAL){

printed_blank = false;

//debug print report data
// Serial.print("Report data: ");
// for (int i = 0; i < len; i++) {
// Serial.print(report[i], HEX);
// Serial.print(" ");
// }
// Serial.println();

if (report[BYTE_DPAD_LEFT_RIGHT] == DPAD_LEFT){
Serial.print("Left ");
}else if (report[BYTE_DPAD_LEFT_RIGHT] == DPAD_RIGHT){
Serial.print("Right ");
}

if (report[BYTE_DPAD_UP_DOWN] == DPAD_UP){
Serial.print("Up ");
}else if (report[BYTE_DPAD_UP_DOWN] == DPAD_DOWN){
Serial.print("Down ");
}

if ((report[BYTE_ABXY_BUTTONS] & BUTTON_A) == BUTTON_A){
Serial.print("A ");
}
if ((report[BYTE_ABXY_BUTTONS] & BUTTON_B) == BUTTON_B){
Serial.print("B ");
}
if ((report[BYTE_ABXY_BUTTONS] & BUTTON_X) == BUTTON_X){
Serial.print("X ");
}
if ((report[BYTE_ABXY_BUTTONS] & BUTTON_Y) == BUTTON_Y){
Serial.print("Y ");
}

if ((report[BYTE_OTHER_BUTTONS] & BUTTON_LEFT_SHOULDER) == BUTTON_LEFT_SHOULDER){
Serial.print("Left Shoulder ");
}
if ((report[BYTE_OTHER_BUTTONS] & BUTTON_RIGHT_SHOULDER) == BUTTON_RIGHT_SHOULDER){
Serial.print("Right Shoulder ");
}
if ((report[BYTE_OTHER_BUTTONS] & BUTTON_START) == BUTTON_START){
Serial.print("Start ");
}
if ((report[BYTE_OTHER_BUTTONS] & BUTTON_SELECT) == BUTTON_SELECT){
Serial.print("Select ");
}
Serial.println();
} else {
if (! printed_blank){
Serial.println("NEUTRAL");
printed_blank = true;
}
}

// Continue to receive the next report
if (!tuh_hid_receive_report(dev_addr, instance)) {
Serial.println("Error: cannot request to receive report");
}
}
103 changes: 103 additions & 0 deletions USB_SNES_Gamepad/Arduino_USB_Host/usbh_helper.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// SPDX-FileCopyrightText: 2024 Ha Thach 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
*********************************************************************/

#ifndef USBH_HELPER_H
#define USBH_HELPER_H

#ifdef ARDUINO_ARCH_RP2040
// pio-usb is required for rp2040 host
#include "pio_usb.h"

// Pin D+ for host, D- = D+ + 1
#ifndef PIN_USB_HOST_DP
#define PIN_USB_HOST_DP 16
#endif

// Pin for enabling Host VBUS. comment out if not used
#ifndef PIN_5V_EN
#define PIN_5V_EN 18
#endif

#ifndef PIN_5V_EN_STATE
#define PIN_5V_EN_STATE 1
#endif
#endif // ARDUINO_ARCH_RP2040

#include "Adafruit_TinyUSB.h"

#if defined(CFG_TUH_MAX3421) && CFG_TUH_MAX3421
// USB Host using MAX3421E: SPI, CS, INT
#include "SPI.h"

#if defined(ARDUINO_METRO_ESP32S2)
Adafruit_USBH_Host USBHost(&SPI, 15, 14);
#elif defined(ARDUINO_ADAFRUIT_FEATHER_ESP32_V2)
Adafruit_USBH_Host USBHost(&SPI, 33, 15);
#else
// Default CS and INT are pin 10, 9
Adafruit_USBH_Host USBHost(&SPI, 10, 9);
#endif
#else
// Native USB Host such as rp2040
Adafruit_USBH_Host USBHost;
#endif

//--------------------------------------------------------------------+
// Helper Functions
//--------------------------------------------------------------------+

#ifdef ARDUINO_ARCH_RP2040
static void rp2040_configure_pio_usb(void) {
//while ( !Serial ) delay(10); // wait for native usb
Serial.println("Core1 setup to run TinyUSB host with pio-usb");

// Check for CPU frequency, must be multiple of 120Mhz for bit-banging USB
uint32_t cpu_hz = clock_get_hz(clk_sys);
if (cpu_hz != 120000000UL && cpu_hz != 240000000UL) {
while (!Serial) {
delay(10); // wait for native usb
}
Serial.printf("Error: CPU Clock = %lu, PIO USB require CPU clock must be multiple of 120 Mhz\r\n", cpu_hz);
Serial.printf("Change your CPU Clock to either 120 or 240 Mhz in Menu->CPU Speed \r\n");
while (1) {
delay(1);
}
}

#ifdef PIN_5V_EN
pinMode(PIN_5V_EN, OUTPUT);
digitalWrite(PIN_5V_EN, PIN_5V_EN_STATE);
#endif

pio_usb_configuration_t pio_cfg = PIO_USB_DEFAULT_CONFIG;
pio_cfg.pin_dp = PIN_USB_HOST_DP;

#if defined(ARDUINO_RASPBERRY_PI_PICO_W)
// For pico-w, PIO is also used to communicate with cyw43
// Therefore we need to alternate the pio-usb configuration
// details https://github.com/sekigon-gonnoc/Pico-PIO-USB/issues/46
pio_cfg.sm_tx = 3;
pio_cfg.sm_rx = 2;
pio_cfg.sm_eop = 3;
pio_cfg.pio_rx_num = 0;
pio_cfg.pio_tx_num = 1;
pio_cfg.tx_ch = 9;
#endif

USBHost.configure_pio_usb(1, &pio_cfg);
}
#endif

#endif
Loading