Built-in project · SparkFun Pro Micro

Fight stick on a Pro Micro

A four-way lever and six buttons on a Pro Micro, debounced with no added delay and reported as a numpad direction plus the buttons held. The whole build, a SparkFun Pro Micro and 5 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Intermediate Runs in your browser. Free, and no account needed.

Fight stick on a Pro Micro · SparkFun Pro Microlive0.000 s 0.00x
Press Run, then use the arrow keys and click the buttons
The circuit itself, running here on the simulator. Press what can be pressed; click anything else to open it in the editor.

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · SparkFun Pro Micro
// Fight stick: a four-way lever and six buttons on a Pro Micro.
//
// Wiring. Every switch has one leg on GND and the other on a pin with the
// internal pull-up on, so a pressed switch reads LOW and needs no resistor.
//
//   lever    UP 2    DOWN 3    LEFT 4    RIGHT 5
//   buttons  1 on 6  2 on 7    3 on 8    4 on 9    5 on 1    6 on 0
//
// In Mokxi the lever is the console pad's D-pad: the arrow keys work it, or
// click its arms. Buttons 1 to 4 are the arcade buttons: click and hold them.
// Buttons 5 and 6 are the pad's A and B, the Z and X keys, standing in for
// the two extra buttons many stick layouts have. Pins 0 and 1 are Serial1's,
// but this sketch talks over USB, so they are free for switches.
//
// The pad's rocker setting is on, which is what a lever's gate does: it
// cannot be pushed up and down at once.
//
// What the Serial Monitor shows is the controller's state each time it
// changes: the direction in numpad notation (5 is neutral, 6 is right, 2 is
// down, 3 is down-right, and so on) followed by the buttons held.
//
// Debounce without lag. The arcade buttons here have bounce turned on, so
// each press closes, opens again and closes within a couple of milliseconds.
// Waiting for a switch to settle would add that wait to every press. Instead
// a change counts the moment it arrives and the input is then locked for
// LOCK_MS, which throws the bounce away without delaying the press.
//
// What this sketch is not: a USB game controller. On a real Pro Micro you
// would hand the same state to a HID library, such as the Arduino Joystick
// library or Keyboard.press(). Mokxi models this chip's USB as a serial pipe
// and nothing more, so the report goes to the Serial Monitor instead.

const uint8_t INPUTS = 10;
const uint8_t PINS[INPUTS] = {2, 3, 4, 5, 6, 7, 8, 9, 1, 0};
enum { UP, DOWN, LEFT, RIGHT, BTN1 };
const uint8_t BUTTONS = INPUTS - BTN1;

// How long an input ignores its pin after a change has been accepted.
const unsigned long LOCK_MS = 5;

bool held[INPUTS];
unsigned long changedAt[INPUTS];

// The last state printed, so a line only goes out when something changed.
int lastDirection = -1;
uint8_t lastButtons = 0xFF;

void readInputs() {
  unsigned long now = millis();
  for (uint8_t i = 0; i < INPUTS; i++) {
    bool down = digitalRead(PINS[i]) == LOW;
    if (down == held[i]) {
      continue;
    }
    if (now - changedAt[i] < LOCK_MS) {
      continue;  // still inside the lock: this edge is the contact bouncing
    }
    held[i] = down;
    changedAt[i] = now;
  }
}

// One axis from its two switches. Both held gives 0, which is the "neutral"
// SOCD rule. A lever cannot make that state, but a worn gate or a wiring
// fault can, and this way the output is still a valid direction.
int axis(bool negative, bool positive) {
  return (positive ? 1 : 0) - (negative ? 1 : 0);
}

void report(int direction, uint8_t buttons) {
  Serial.print(millis());
  Serial.print(" ms  dir ");
  Serial.print(direction);
  Serial.print("  buttons");
  if (buttons == 0) {
    Serial.print(" -");
  }
  for (uint8_t b = 0; b < BUTTONS; b++) {
    if (buttons & (1u << b)) {
      Serial.print(' ');
      Serial.print(b + 1);
    }
  }
  Serial.println();
}

void setup() {
  for (uint8_t i = 0; i < INPUTS; i++) {
    pinMode(PINS[i], INPUT_PULLUP);
  }
  TX_RX_LED_INIT;
  RXLED0;
  TXLED0;
  Serial.begin(115200);
  Serial.println("Fight stick ready: arrow keys move the lever, click the buttons.");
  Serial.println("dir is numpad notation: 7 8 9 / 4 5 6 / 1 2 3, 5 is neutral.");
}

void loop() {
  readInputs();

  int x = axis(held[LEFT], held[RIGHT]);
  int y = axis(held[DOWN], held[UP]);
  int direction = 5 + x + 3 * y;

  uint8_t buttons = 0;
  for (uint8_t b = 0; b < BUTTONS; b++) {
    if (held[BTN1 + b]) {
      buttons |= (uint8_t)(1u << b);
    }
  }

  // The RX lamp is lit while any button is held (it lights on LOW).
  if (buttons != 0) {
    RXLED1;
  } else {
    RXLED0;
  }

  if (direction != lastDirection || buttons != lastButtons) {
    lastDirection = direction;
    lastButtons = buttons;
    report(direction, buttons);
  }
}

Parts list

7 parts, plus the jumper wires. Every one is in the editor's parts bin.

How it is wired

11 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.

  • SparkFun Pro Micro pin 1; Game console buttons pin A
  • SparkFun Pro Micro pin 0; Game console buttons pin B
  • Ground: SparkFun Pro Micro pin GND; Game console buttons pin GND; Arcade button (1) pin 1; Arcade button (2) pin 1; Arcade button (3) pin 1; Arcade button (4) pin 1
  • SparkFun Pro Micro pin 2; Game console buttons pin UP
  • SparkFun Pro Micro pin 3; Game console buttons pin DOWN
  • SparkFun Pro Micro pin 4; Game console buttons pin LEFT
  • SparkFun Pro Micro pin 5; Game console buttons pin RIGHT
  • SparkFun Pro Micro pin 6; Arcade button (1) pin 2
  • SparkFun Pro Micro pin 7; Arcade button (2) pin 2
  • SparkFun Pro Micro pin 8; Arcade button (3) pin 2
  • SparkFun Pro Micro pin 9; Arcade button (4) pin 2

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.