Arduino Micro gets not recognised as a game controller

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?

Looks like waiting for serial was the problem
This fixed it:

void setup() {
  Serial.begin(9600);

  Serial.println("32-Button Custom Gamepad Controller");

  // Initialize custom HID FIRST
  static HIDSubDescriptor node(_hidReportDescriptor, sizeof(_hidReportDescriptor));
  HID().AppendDescriptor(&node);
  
  // Add small delay for HID to initialize
  delay(100);

  // Initialize gamepad report
  gamepadReport.reportId = 1;
  gamepadReport.buttons = 0;
1 Like