arcade kit/Buttons and joystick/05. Press a button
Buttons and joystick · 05 of 11

Press a button

The input monitor prints the name of every button and joystick direction as it is pressed and released. It is the first program to run on any adapter, and the 30 ms wait inside it is what turns a switch's chatter into one clean press.

Run it first

Plug one button into KEY1. Leave the coin acceptor and screen off for now; the development board's USB alone is enough, because the buttons run from its supply.

  • Nano or ESP32-S3: open InputMonitor.ino in the Arduino IDE, pick the board as its first comment says, upload, and open Serial Monitor at 115200.
  • Pico or Pico 2: flash MicroPython, then save arcade.py and input_monitor.py onto the board from Thonny and run the second.
  • Zero 2 W: turn off the serial port in raspi-config, install python3-gpiozero and python3-lgpio, clone the repository and run python3 input_monitor.py from examples/zero2w.

Press and release KEY1:

KEY1 pressed
KEY1 released

Then plug in the other buttons one at a time and check each name. Do it before any of them go into a panel.

What the 30 ms is for

One press, as the pin sees it
30 ms window
Debounce window30 ms
Presses printed
1
Reported after
36 ms
One press, one event, printed about 36 ms after the contacts first touch. The examples use 30 ms, which is longer than any chatter here and shorter than anyone can notice.

Metal contacts do not close cleanly. They meet, bounce apart and meet again several times in the first few milliseconds, and a program that reported every change would print a press for each bounce.

So the monitor reports a change only once the pin has held still for 30 ms. The cost is that every press is reported 30 ms after it settles, which is shorter than anyone can notice. These are the lines that do it:

InputMonitor.ino
for (uint8_t i=0; i<14; ++i) {
  bool value = digitalRead(INPUT_PINS[i]) == HIGH;
  if (value != raw[i]) { raw[i] = value; changed[i] = ms; }
  if (value != stable[i] && ms - changed[i] >= 30) {
    stable[i] = value;
    Serial.print(NAMES[i]);
    Serial.println(value ? " pressed" : " released");
  }
}
View on GitHub · examples/arduino/InputMonitor/InputMonitor.ino @ v1.0

raw follows the pin as it bounces; stable only changes once raw has stayed the same for 30 ms. Everything the kit's examples do with a button — start a game, score a point — is built on that second value.

On a Zero 2 W

The same idea in Python, reading BCM numbers through gpiozero:

input_monitor.py
"""Run on Raspberry Pi OS. GPIO numbers are BCM, not header positions."""
from time import monotonic, sleep
from gpiozero import DigitalInputDevice

PINS = (4, 5, 6, 12, 13, 16, 17, 19, 14, 15, 20, 21, 22, 23)
NAMES = tuple("KEY%d" % n for n in range(1, 11)) + ("UP", "DOWN", "LEFT", "RIGHT")


def main():
    buttons = []
    try:
        for pin in PINS:
            buttons.append(DigitalInputDevice(pin, pull_up=False))
        raw = [b.value for b in buttons]
        stable = raw.copy()
        changed = [monotonic()] * len(buttons)
        print("Ready. Press a button or push the stick.")
        while True:
            now = monotonic()
            for i, button in enumerate(buttons):
                value = button.value
                if value != raw[i]:
                    raw[i], changed[i] = value, now
                if value != stable[i] and now - changed[i] >= 0.030:
                    stable[i] = value
                    print(NAMES[i], "pressed" if value else "released")
            sleep(0.001)
    except KeyboardInterrupt:
        pass
    finally:
        for button in buttons:
            button.close()


if __name__ == "__main__":
    main()
View on GitHub · examples/zero2w/input_monitor.py @ v1.0

The code

One sketch for both Arduino adapters: it picks the Nano or ESP32-S3 pin list from the board you select in Tools. Open Serial Monitor at 115200 and press KEY1.

InputMonitor.ino
/*  HotSwap Arcade Kit - input monitor: every button and joystick direction
    Nano:     Tools > Board Arduino Nano · Processor ATmega328P
              (try "ATmega328P (Old Bootloader)" if the upload times out)
    ESP32-S3: Tools > Board ESP32S3 Dev Module · USB CDC On Boot Enabled
              when the cable is in the board's native USB port
    Serial Monitor at 115200. Pressed reads HIGH: each input has a 10k
    pull-down on the mainboard, and the button connects it to V(MCU).
    Guide: https://learn.lonelybinary.com/manuals/arcade/press-a-button  */

#include <Arduino.h>

#if defined(ARDUINO_AVR_NANO)
const uint8_t INPUT_PINS[] = {2,3,4,5,6,7,8,9,10,11,12,A4,A3,A2};
const uint8_t INPUT_MODE = INPUT; // 10k pull-downs on the mainboard
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
const uint8_t INPUT_PINS[] = {4,5,6,7,15,16,17,18,8,40,39,38,9,47};
const uint8_t INPUT_MODE = INPUT_PULLDOWN;
#else
#error "Select classic Arduino Nano or ESP32S3 Dev Module."
#endif

const char *NAMES[] = {"KEY1", "KEY2", "KEY3", "KEY4", "KEY5",
                       "KEY6", "KEY7", "KEY8", "KEY9", "KEY10",
                       "UP", "DOWN", "LEFT", "RIGHT"};
bool raw[14], stable[14];
unsigned long changed[14];

void setup() {
  Serial.begin(115200);
  delay(1000);
  for (uint8_t i=0; i<14; ++i) {
    pinMode(INPUT_PINS[i], INPUT_MODE);
    raw[i] = stable[i] = digitalRead(INPUT_PINS[i]) == HIGH;
    changed[i] = millis();
  }
  Serial.println("Ready. Press a button or push the stick.");
}

void loop() {
  unsigned long ms = millis();
  for (uint8_t i=0; i<14; ++i) {
    bool value = digitalRead(INPUT_PINS[i]) == HIGH;
    if (value != raw[i]) { raw[i] = value; changed[i] = ms; }
    if (value != stable[i] && ms - changed[i] >= 30) {
      stable[i] = value;
      Serial.print(NAMES[i]);
      Serial.println(value ? " pressed" : " released");
    }
  }
  delay(1);
}

The pin lists are the adapter tables from One header, four adapters, in KEY1 to RIGHT order. Change a pin here and you are no longer on the kit's wiring.

View on GitHub · examples/arduino/InputMonitor/InputMonitor.ino @ v1.0

When it does not work

Nothing prints at all

Check the Serial Monitor is at 115200 and on the development board's port, not a different one. On an ESP32-S3 with the cable in its native USB socket, set USB CDC On Boot to Enabled in Tools, or nothing reaches that port.

A press prints twice

Something has shortened the debounce, or the switch is worn. The examples wait 30 ms; the figure above shows what a shorter wait does. If the stock code double-counts one button and not the others, swap that button's cable to a second button to see which is at fault.

The Nano upload times out

Many Nano-compatible boards ship with the older bootloader. In Tools, set Processor to ATmega328P (Old Bootloader) and upload again.

No module named machine, on the Pico

Thonny is running the file on your computer instead of the Pico. Set the interpreter to MicroPython (Raspberry Pi Pico) in the bottom right corner and run it again.

It works on the Zero 2 W except KEY9 and KEY10

The serial console still owns BCM14 and BCM15. Turn off the serial login shell and serial hardware in raspi-config, then reboot.

Where this goes next

Four microswitches, and why the one you wire to UP is the one nearest you.

Wire the joystick

Edit this page — content/books/arcade/press-a-button.mdx

Community

Questions about this product

See what other owners have asked, and read their solutions.

Ask a question ↗

HotSwap Arcade Kit: Buttons, Joystick and Coin Acceptor for ESP32-S3, Nano, Pico and Zero 2 W

Loading discussions…

Discuss this article

Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.

Browse Modules and blocks on the forum