I'm trying to make my own custom game controller with 32 buttons. This test code gets recognised by my PC as a game controller
#include <HID.h>
// 32-button gamepad descriptor
static const uint8_t _hidReportDescriptor[] PROGMEM = {
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x05, // Usage (Game Pad)
0xa1, 0x01, // Collection (Application)
0x85, 0x01, // Report ID (1)
// 32 Buttons
0x05, 0x09, // Usage Page (Button)
0x19, 0x01, // Usage Minimum (Button 1)
0x29, 0x20, // Usage Maximum (Button 32)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x95, 0x20, // Report Count (32)
0x75, 0x01, // Report Size (1)
0x81, 0x02, // Input (Data,Var,Abs)
0xc0, // End Collection
};
void setup() {
Serial.begin(9600);
// Add HID descriptor
static HIDSubDescriptor node(_hidReportDescriptor, sizeof(_hidReportDescriptor));
HID().AppendDescriptor(&node);
}
void loop() {
// Do nothing for now - just test if HID device appears
delay(1000);
}
But when running my full code it does not get recognised as a game controller. I find it in the device manager under "USB Composite Devices" but not under "HID".
// NOTE: This is a simple example that only reads the INTA or INTB pin
// state. No actual interrupts are used on the host microcontroller.
// MCP23XXX supports the following interrupt modes:
// * CHANGE - interrupt occurs if pin changes to opposite state
// * LOW - interrupt occurs while pin state is LOW
// * HIGH - interrupt occurs while pin state is HIGH
#include <Adafruit_MCP23X17.h>
#include <HID.h>
// Microcontroller pins attached to INTA/INTB
#define INTA_ARDUINO_PIN 4 // Arduino D4
#define INTB_ARDUINO_PIN 5 // Arduino D5
Adafruit_MCP23X17 mcp1; // First MCP23017 (address 0x27)
Adafruit_MCP23X17 mcp2; // Second MCP23017 (address 0x23)
// Custom HID Report Descriptor for 32-button gamepad
static const uint8_t _hidReportDescriptor[] PROGMEM = {
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x05, // Usage (Game Pad)
0xa1, 0x01, // Collection (Application)
0x85, 0x01, // Report ID (1)
// 32 Buttons
0x05, 0x09, // Usage Page (Button)
0x19, 0x01, // Usage Minimum (0x01)
0x29, 0x20, // Usage Maximum (0x20) // 32 buttons
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x95, 0x20, // Report Count (32)
0x75, 0x01, // Report Size (1)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xc0, // End Collection
};
// Gamepad report structure
typedef struct {
uint8_t reportId;
uint32_t buttons; // 32 buttons packed into 4 bytes
} GamepadReport;
GamepadReport gamepadReport;
void setup() {
Serial.begin(9600);
while (!Serial); // Wait for Serial monitor to open
Serial.println("32-Button Custom Gamepad Controller");
// Initialize custom HID
static HIDSubDescriptor node(_hidReportDescriptor, sizeof(_hidReportDescriptor));
HID().AppendDescriptor(&node);
// Initialize gamepad report
gamepadReport.reportId = 1;
gamepadReport.buttons = 0;
// Initialize MCP23X17
// Make sure your I2C address is correct
// Ax connected -> 0
// Ax unconnected (default) -> 1
// A2 A1 A0
// 0 0 0 0x20
// 0 0 1 0x21
// 0 1 0 0x22
// 0 1 1 0x23
// 1 0 0 0x24
// 1 0 1 0x25
// 1 1 0 0x26
// 1 1 1 0x27
// Initialize first MCP23X17 (A0, A1, A2 all high = 0x27)
if (!mcp1.begin_I2C(0x27)) {
Serial.println("Could not find MCP23X17 #1 (0x27).");
while (1); // Halt if initialization fails
}
// Initialize second MCP23X17 (A0 to GND, A1, A2 high = 0x26)
if (!mcp2.begin_I2C(0x26)) {
Serial.println("Could not find MCP23X17 #2 (0x26).");
while (1); // Halt if initialization fails
}
// Configure Arduino pins connected to MCP23X17 interrupt outputs
// IMPORTANT: D0 (RX) and D1 (TX) are used for Serial communication.
// Using them for other purposes can interfere with Serial.
// Consider using other digital pins (e.g., D4, D5) if possible.
pinMode(INTA_ARDUINO_PIN, INPUT);
pinMode(INTB_ARDUINO_PIN, INPUT);
// Configure interrupts for both MCPs
setupMCPInterrupts(mcp1, "MCP1");
setupMCPInterrupts(mcp2, "MCP2");
Serial.println("Custom Gamepad ready!");
}
void setupMCPInterrupts(Adafruit_MCP23X17 &mcp, const char* name) {
mcp.setupInterrupts(true, false, LOW);
for(int i = 0; i <= 15; i++) {
mcp.pinMode(i, INPUT_PULLUP);
mcp.setupInterruptPin(i, CHANGE);
}
}
void loop() {
// Check if INTA is LOW, indicating an interrupt from either MCP
if (!digitalRead(INTA_ARDUINO_PIN)) {
Serial.println();
Serial.println("Interrupt detected! Reading controller state...");
delay(100); // Stabilization delay
readControllerState();
sendGamepadReport();
// Clear interrupts
mcp1.clearInterrupts();
mcp2.clearInterrupts();
delay(250); // Debounce delay
}
}
void readControllerState() {
// Read both MCPs
uint16_t mcp1State = ~mcp1.readGPIOAB(); // Invert (pressed = 1)
uint16_t mcp2State = ~mcp2.readGPIOAB(); // Invert (pressed = 1)
// Clear current button state
gamepadReport.buttons = 0;
// Map regular buttons (pins 0-15) to gamepad buttons 1-16
gamepadReport.buttons |= mcp1State;
// Map 3-way switches (pins 16-23) to gamepad buttons 17-24
// Each 3-way switch uses 2 buttons (only A and C positions, B = !(A && C))
for (int i = 0; i < 4; i++) {
int pin1 = i * 2; // Even pins: 0, 2, 4, 6 (global 16, 18, 20, 22)
int pin2 = i * 2 + 1; // Odd pins: 1, 3, 5, 7 (global 17, 19, 21, 23)
bool state1 = (mcp2State >> pin1) & 1;
bool state2 = (mcp2State >> pin2) & 1;
int buttonA = 16 + (i * 2); // Buttons 17, 19, 21, 23
int buttonC = 16 + (i * 2) + 1; // Buttons 18, 20, 22, 24
if (state1 && !state2) {
// Position A
gamepadReport.buttons |= (1UL << buttonA);
} else if (!state1 && state2) {
// Position C
gamepadReport.buttons |= (1UL << buttonC);
}
// Center position (B) = no buttons pressed
}
// Map normal switches (pins 24-31) to gamepad buttons 25-32
uint8_t normalSwitches = (mcp2State >> 8) & 0xFF;
gamepadReport.buttons |= ((uint32_t)normalSwitches << 24);
// Display current state
displayControllerState(mcp1State, mcp2State);
}
void displayControllerState(uint16_t mcp1State, uint16_t mcp2State) {
Serial.println("=== GAMEPAD STATE ===");
// Show active buttons
Serial.print("Active Gamepad Buttons: ");
for (int i = 0; i < 32; i++) {
if (gamepadReport.buttons & (1UL << i)) {
Serial.print(i + 1); // Display as 1-32 instead of 0-31
Serial.print(" ");
}
}
Serial.println();
// Show button mapping
Serial.println("Button Mapping:");
Serial.println(" Buttons 1-16: Regular buttons (pins 0-15)");
Serial.println(" Buttons 17-19: 3-Way Switch 0 (A,B,C)");
Serial.println(" Buttons 20-22: 3-Way Switch 1 (A,B,C)");
Serial.println(" Buttons 23-25: 3-Way Switch 2 (A,B,C)");
Serial.println(" Buttons 26-28: 3-Way Switch 3 (A,B,C)");
Serial.println(" Buttons 29-32: Normal switches (pins 24-27)");
Serial.print("Raw button data: 0x");
Serial.println(gamepadReport.buttons, HEX);
Serial.println("=====================");
}
void sendGamepadReport() {
// Send the HID report
HID().SendReport(gamepadReport.reportId, &gamepadReport.buttons, 4);
Serial.println("Gamepad report sent!");
}
Does any one know what the difference is? Why does it work in one case but not the other?

