Saturday, January 10, 2015

Arduino Pro Micro Arcade Controller

A bloke at my local Makerspace decided to build himself a table top arcade machine, which came out pretty damn well!


I had had a similar plan a while back, but never got around to it and so had the buttons and controls on hand which I sold to him. With the build complete and the controls installed, he needed a way to hook the controls up to the MAME PC in the cabinet. I'd been playing around with some Arduino Pro Micro boards for a similar purpose (hooking up retro C64 joysticks to my PC) so these seemed like a really good option. At the current price on eBay of $6.60 from my favourite supplier, this was a really cheap option.

As there were 2 joysticks with 8 buttons each, a single Pro Micro wasn't going to be sufficient, so we used 2. The basic vero board layout was:

2 Joystick Vero board layout

Pretty simple, with just some track cutting to separate the two Pro Micro. Once the tracks were cut we soldered in so 24 pin wide DIP sockets, the end result looked like this:


The wiring to the controls for each Pro Micro was:

PINLEFTRIGHT
2UpUp
3DownDown
4LeftLeft
5RightRight
6Button 1Button 1
7Button 2Button 2
8Button 3Button 3
9Button 4Button 4
10Button 5Button 5
16Button 6Button 6
14Button 7 (Player 1)Button 7 (Player 2)
15Button 8 (Left Paddle or Insert Coin)Button 8 (Right Paddle or Insert Coin)

Once wired up all we needed was some firmware. The Arduino IDE comes with some basic USB HID support for keyboards and mice, but doesn't feature any joystick HID descriptor. To fix this you'll need to change two files in the core Arduino software: HID.cpp and USBAPI.h. These can be found in the hardware\arduino\cores\arduino for the 1.0.6 build and somewhere similar for the newer IDE builds.

NOTE: These files are based on code from this blog: www.imaginaryindustries.com/blog/?p=80

Add the following class definition to USBAPI.h

//================================================================================
//================================================================================
// Joystick
//  Implemented in HID.cpp
//  The list of parameters here needs to match the implementation in HID.cpp


typedef struct JoyState
{
  uint8_t  xAxis;
  uint8_t  yAxis;
  uint8_t  buttons;  // 8 general buttons
} JoyState_t;

class Joystick_
{
public:
  Joystick_();

  void setState(JoyState_t *joySt);

};
extern Joystick_ Joystick;

Then in HID.CPP you'll need to add the Joystick descriptor, look for:

#ifdef HID_ENABLED

and add:

#define JOYHID_ENABLED

Then find:

const u8 _hidReportDescriptor[] = {

and add in:

#ifdef JOYHID_ENABLED

// 8 buttons, X and Y

 0x05, 0x01,   // USAGE_PAGE (Generic Desktop)
 0x09, 0x04,   // USAGE (Joystick)
 0xa1, 0x01,   // COLLECTION (Application)
  0x85, 0x03,   // REPORT_ID (3)  (This is important when HID_SendReport() is called)

  //Buttons:
  0x05, 0x09,   // USAGE_PAGE (Button)
  0x19, 0x01,   // USAGE_MINIMUM (Button 1)
  0x29, 0x08,   // USAGE_MAXIMUM (Button 8)
  0x15, 0x00,   // LOGICAL_MINIMUM (0)
  0x25, 0x01,   // LOGICAL_MAXIMUM (1)
  0x75, 0x01,   // REPORT_SIZE (1)
  0x95, 0x08,   // REPORT_COUNT (8)
  0x81, 0x02,   // INPUT (Data,Var,Abs)

    0x05, 0x01, // USAGE_PAGE (Generic Desktop)
    0xa1, 0x00, // COLLECTION (Physical)
      // 2 8bit Axis
      0x09, 0x30, // USAGE (X)
      0x09, 0x31, // USAGE (Y)
      0x15, 0x80, // LOGICAL_MINIMUM (-128)
      0x25, 0x7F, // LOGICAL_MAXIMUM (127)
      0x75, 0x08, // REPORT_SIZE (8)
      0x95, 0x02, // REPORT_COUNT (2)
      0x81, 0x02, // INPUT (Data,Var,Abs)
    0xc0, // END_COLLECTION
 0xc0     // END_COLLECTION

#endif

Finally you need to add the implementation for the joystick:

//================================================================================
//================================================================================
// Joystick
//  The report data format must match the one defined in the descriptor exactly
//  or it either won't work, or the pc will make a mess of unpacking the data
//

Joystick_::Joystick_()
{
}


#define joyBytes 3   // should be equivalent to sizeof(JoyState_t)

void Joystick_::setState(JoyState_t *joySt)
{
  uint8_t data[joyBytes];

  data[0] = joySt->buttons;  // Break 32 bit button-state out into 4 bytes, to send over USB
  data[1] = joySt->xAxis;  // X axis
  data[2] = joySt->yAxis;  // Y axis

  //HID_SendReport(Report number, array of values in same order as HID descriptor, length)
  HID_SendReport(3, data, joyBytes);
  // The joystick is specified as using report 3 in the descriptor. That's where the "3" comes from
}

Now that all the ground work is done, a simple .ino file will handle the joystick interface:

JoyState_t joySt;

#define MAX_AXIS 4
#define Y_MIN  0
#define Y_MAX 1

#define X_MIN  2
#define X_MAX 3

uint8_t joy_axis[MAX_AXIS] = {2,3,4,5};

#define MAX_BUTTONS 8
uint8_t joy_buttons[MAX_BUTTONS] = {6,7,8,9,10,16,14,15};

void setup()
{
  uint8_t i;
  pinMode(13, OUTPUT);

  for (i = 0; i < MAX_AXIS; i++) {
    pinMode(joy_axis[i], INPUT);
    digitalWrite(joy_axis[i], HIGH);
  }

  for (i = 0; i < MAX_BUTTONS; i++) {
    pinMode(joy_buttons[i], INPUT);
    digitalWrite(joy_buttons[i], HIGH);
  }

  joySt.xAxis = 0;
  joySt.yAxis = 0;
  joySt.buttons = 0;

}

void update_stick() {
  uint8_t i;

  joySt.xAxis = 0;
  joySt.yAxis = 0;
  
  if (digitalRead(joy_axis[Y_MIN]) == LOW) {
    joySt.yAxis = 128;
  } else if (digitalRead(joy_axis[Y_MAX]) == LOW) {
    joySt.yAxis = 127;
  }

  if (digitalRead(joy_axis[X_MIN]) == LOW) {
    joySt.xAxis = 128;
  } else if (digitalRead(joy_axis[X_MAX]) == LOW) {
    joySt.xAxis = 127;
  }

  joySt.buttons = 0;

  for (i = 0; i < MAX_BUTTONS; i++) {
    if (digitalRead(joy_buttons[i]) == LOW) {
      joySt.buttons |= 1 << i;
    }
  }

  Joystick.setState(&joySt);
}

void loop() {
  update_stick();
}

Thes files can be obtained from here: Joystick HID Files but I suggest that you patch the HID.cpp and USBAPI.h files manually to account for any variance between your and my Arduino IDE.

Have fun and keep on hacking!
-(e)