Built-in project · Arduino Uno R3

Arcade lever on an Arduino Uno

A Sanwa-style arcade lever on four pins, debounced the way a stick board does it and printed in numpad notation: 5 neutral, 8 up, 9 up-right. The whole build, an Arduino Uno and 1 more part, 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.

Beginner About 10 minutes to build and run. Runs in your browser. Free, and no account needed.

The parts retail for about $33. On Mokxi they're free.

The circuit runs here once the page has loaded.

What you'll learn

  • Reading four switches with INPUT_PULLUP
  • Debouncing with a short lockout instead of a delay
  • Turning button states into one number
  • Numpad notation used by fighting games
  • Difficulty: Beginner
  • Time: About 10 minutes
  • Board: Arduino Uno R3
  • Parts: 2

How it works

The lever is four microswitches, one each for up, down, left and right, all sharing a ground. Each direction goes to its own pin (5, 4, 3 and 2) with the internal pull-up on, so a pin reads LOW while that direction is held. A corner holds two switches at once.

The sketch keeps a held flag for each switch and turns the four flags into a numpad number: 5 is neutral, 8 up, 2 down, 4 left, 6 right, and 7, 9, 1 and 3 are the corners. It prints a line only when that number changes, and the Uno's own LED on pin 13 stays lit while the lever is off center.

Parts list

2 parts, plus the jumper wires. They retail for about $33; on Mokxi they're free.

Wiring table

Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.

Part and pinConnects to
Arcade joystick lever pin GNDArduino Uno R3 pin GND
Arcade joystick lever pin UPArduino Uno R3 pin 5
Arcade joystick lever pin DOWNArduino Uno R3 pin 4
Arcade joystick lever pin LEFTArduino Uno R3 pin 3
Arcade joystick lever pin RIGHTArduino Uno R3 pin 2

The code

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

sketch.ino · Arduino Uno R3
// Arcade lever: a Sanwa JLF style lever on four pins, read the way a fight
// stick board reads it, and printed in numpad notation.
//
//   lever   GND -> GND   UP -> 5   DOWN -> 4   LEFT -> 3   RIGHT -> 2
//   LED     the board's own, on pin 13: lit while the lever is off center
//
// A lever is four microswitches around the foot of the shaft, one leg of each
// on the common. With INPUT_PULLUP on, a pin reads LOW while its direction is
// held, and a corner holds two of them.
//
// Fighting game players write directions as the keys of a numpad: 5 is
// neutral, 8 up, 2 down, 4 left, 6 right, and the corners are 7, 9, 1 and 3.
// That is what this prints, every time the lever moves.
//
// # Debounce without a delay
//
// A microswitch chatters for a millisecond or two as it closes. A stick board
// cannot afford to wait that long on every press, so it takes the new reading
// at once and then ignores that switch for a few milliseconds. Turn on the
// lever's bounce property and watch: one line per move, not three.
//
// Drag the ball on the lever, or use the arrow keys or W A S D. Change the
// gate to 4way and the corners are gone.

const uint8_t PINS[4] = {5, 4, 3, 2};  // UP, DOWN, LEFT, RIGHT
const unsigned long LOCK_MS = 5;

bool held[4];
unsigned long changedAt[4];
uint8_t shown = 5;

const char *const NAMES[10] = {"", "down-left", "down", "down-right", "left",
                               "neutral", "right", "up-left", "up", "up-right"};

// The numpad direction for the four switches. A lever cannot hold opposite
// directions, but four separate buttons can, and then the two cancel.
uint8_t numpad() {
  int x = (held[3] ? 1 : 0) - (held[2] ? 1 : 0);
  int y = (held[0] ? 1 : 0) - (held[1] ? 1 : 0);
  return 5 + x + 3 * y;
}

void setup() {
  Serial.begin(9600);
  for (uint8_t i = 0; i < 4; i++) pinMode(PINS[i], INPUT_PULLUP);
  pinMode(LED_BUILTIN, OUTPUT);
  Serial.println("Arcade lever ready: 5 is neutral");
}

void loop() {
  unsigned long now = millis();
  for (uint8_t i = 0; i < 4; i++) {
    if (now - changedAt[i] < LOCK_MS) continue;  // still settling
    bool down = digitalRead(PINS[i]) == LOW;
    if (down != held[i]) {
      held[i] = down;
      changedAt[i] = now;
    }
  }
  uint8_t direction = numpad();
  if (direction != shown) {
    shown = direction;
    Serial.print(direction);
    Serial.print(" ");
    Serial.println(NAMES[direction]);
    digitalWrite(LED_BUILTIN, direction == 5 ? LOW : HIGH);
  }
}

Code walkthrough

The key functions of the sketch, and what each one does.

numpad sketch.ino

This turns the four held flags into one number. Left and right make x (+1 for right, -1 for left), up and down make y. The result is 5 + x + 3 * y. Opposite directions cancel, so a stuck pair of buttons reads as neutral on that axis.

// The numpad direction for the four switches. A lever cannot hold opposite
// directions, but four separate buttons can, and then the two cancel.
uint8_t numpad() {
  int x = (held[3] ? 1 : 0) - (held[2] ? 1 : 0);
  int y = (held[0] ? 1 : 0) - (held[1] ? 1 : 0);
  return 5 + x + 3 * y;
}

setup sketch.ino

Opens the serial port at 9600, sets all four lever pins to INPUT_PULLUP so they read HIGH until a switch closes, and makes the built-in LED an output. Then it prints a ready message.

void setup() {
  Serial.begin(9600);
  for (uint8_t i = 0; i < 4; i++) pinMode(PINS[i], INPUT_PULLUP);
  pinMode(LED_BUILTIN, OUTPUT);
  Serial.println("Arcade lever ready: 5 is neutral");
}

loop sketch.ino

For each switch, it skips the pin if it changed less than LOCK_MS (5 ms) ago. Otherwise it reads the pin and, if the state changed, accepts it at once and records the time. That is debounce without a delay. Then it computes the numpad direction and, only when it differs from the last one shown, prints it and sets the LED.

void loop() {
  unsigned long now = millis();
  for (uint8_t i = 0; i < 4; i++) {
    if (now - changedAt[i] < LOCK_MS) continue;  // still settling
    bool down = digitalRead(PINS[i]) == LOW;
    if (down != held[i]) {
      held[i] = down;
      changedAt[i] = now;
    }
  }
  uint8_t direction = numpad();
  if (direction != shown) {
    shown = direction;
    Serial.print(direction);
    Serial.print(" ");
    Serial.println(NAMES[direction]);
    digitalWrite(LED_BUILTIN, direction == 5 ? LOW : HIGH);
  }
}

Troubleshooting

The usual mistakes with this circuit, and the fix for each.

  • Every direction shows as held, or nothing ever registers. This project
    Check that the lever's GND pin goes to the Uno GND. The switches pull the pins to ground, so without it they float.
  • Left and right (or up and down) are swapped in the output. This project
    Check the pin order: UP is pin 5, DOWN pin 4, LEFT pin 3, RIGHT pin 2.
  • Directions read randomly or stay stuck. Arcade joystick lever
    Each switch connects its pin to the common GND. Use INPUT_PULLUP on every direction pin and treat LOW as pressed.
  • The lever registers two directions or bounces. Arcade joystick lever
    Diagonals close two switches at once, which is normal. Debounce the inputs for a few milliseconds and handle both bits together.
  • Upload fails or nothing runs after wiring pins 0 and 1 to a module. Arduino Uno R3
    Pins 0 and 1 are the USB serial port. Move the module to other pins, or unplug it while uploading. Also check the right board is selected.
  • Pins misbehave or the board resets when you power motors or servos from it. Arduino Uno R3
    Each I/O pin gives about 20 mA, 40 mA max. Power motors and servos from their own supply and join its ground to the Uno's GND.

Change it and keep it

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