Build a Retro Game Console with Raspberry Pi, RetroPie, and a GPIO Controller
RetroPie works fine with an off-the-shelf USB gamepad, but the fun part is wiring a real set of arcade buttons straight into a Raspberry Pi’s GPIO pins, then writing the software layer that makes the system see those presses as arrow keys and A/B buttons. This is the trickiest step in building a DIY arcade bartop or cabinet from scratch.
How it works
- Each button or microswitch on an arcade joystick is a simple switch wired between a GPIO pin and ground.
- The Raspberry Pi enables an internal pull-up resistor (
pull_up_down=GPIO.PUD_UP) on each pin, so it reads HIGH when idle and LOW when a press grounds that pin. - A background Python daemon continuously polls the GPIO pins, detects a falling edge (a press) after debouncing, and emits the matching keyboard event through
uinput. - RetroPie/EmulationStation receives these emulated keyboard events exactly as if a real key was pressed, with no changes needed to any game configuration.
Parts list
- A Raspberry Pi (3B+ or newer recommended for smooth emulation of later-generation systems)
- Arcade-style microswitch buttons and a joystick, 8 or more
- Female-female jumper wires or crimped spade connectors to wire buttons to GPIO
- A 16GB+ SD card flashed with RetroPie
- A cabinet enclosure or a drilled wood/acrylic panel to mount the buttons (however elaborate you want to get)
Sample code
#!/usr/bin/env python3
import RPi.GPIO as GPIO
import time
from uinput import Device
import uinput
# Mapping: GPIO pin -> emulated keyboard key
BUTTON_MAP = {
17: uinput.KEY_UP,
27: uinput.KEY_DOWN,
22: uinput.KEY_Z, # A button -> Z key (EmulationStation's default)
23: uinput.KEY_X, # B button -> X key
}
DEBOUNCE_TIME = 0.03 # 30ms
GPIO.setmode(GPIO.BCM)
for pin in BUTTON_MAP:
GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
device = Device(list(BUTTON_MAP.values()))
previous_state = {pin: GPIO.HIGH for pin in BUTTON_MAP}
print("Listening for button presses... Press Ctrl+C to quit")
try:
while True:
for pin, key in BUTTON_MAP.items():
current_state = GPIO.input(pin)
# Falling edge: was HIGH, now LOW => button was just pressed
if previous_state[pin] == GPIO.HIGH and current_state == GPIO.LOW:
time.sleep(DEBOUNCE_TIME)
if GPIO.input(pin) == GPIO.LOW: # re-confirm after debounce
device.emit(key, 1) # key down
print(f"GPIO{pin} button pressed")
# Rising edge: button was just released
if previous_state[pin] == GPIO.LOW and current_state == GPIO.HIGH:
device.emit(key, 0) # key up
previous_state[pin] = current_state
time.sleep(0.005) # poll every 5ms, fast enough to feel instant
except KeyboardInterrupt:
GPIO.cleanup()
Install
python3-uinput(sudo apt install python3-uinput) and load theuinputkernel module (sudo modprobe uinput, adding it to/etc/modulesso it loads on boot). If you’d rather not roll your own daemon,Adafruit-Retrogameormk_arcade_joystick_rpido exactly this through a kernel driver instead, with no separate Python process to run.
Common pitfalls
- Skipping debounce on the microswitches turns a single press into several rapid-fire presses — the
time.sleep(DEBOUNCE_TIME)plus re-confirmation above is a simple software debounce that’s plenty for typical arcade microswitches. - Running the Python daemon manually with
python3 script.pyand then closing the terminal kills it — set it up as asystemdservice so it starts automatically on boot. - Wiring 3.3V instead of GND to a button pin leaves that GPIO reading a false HIGH, or in the worst case can damage the pin — always double-check with a multimeter before powering up.
- EmulationStation needs these virtual keys registered in
es_input.cfg— use the input configuration screen in the RetroPie interface and press each real button to have it record itself automatically.
Where to go from here
Add a rotary potentiometer or encoder as a physical volume knob, or wire up a small SPI display to show info about the running game separately from the main TV output — both build on the same GPIO-reading approach used here.
Comments