If you are building a raspberry pi retro video game console in 2026, flashing an SD card with Batocera or RetroPie is only half the battle. The real satisfaction comes from wiring a custom arcade control panel directly to the GPIO header, eliminating USB adapter latency and giving you total control over the hardware. This guide walks through the exact board selection, physical wiring, and the Python bridge required to translate microswitch closures into MAME-compatible keystrokes on modern Raspberry Pi OS.
The Hardware Decision Tree: Which Pi Board to Choose
Do not just default to the newest board. Thermal throttling, OS compatibility, and emulation targets dictate your hardware. Use this decision path to select your board:
| If your primary target is... | And your constraint is... | Choose this board variant |
|---|---|---|
| 8-bit / 16-bit (NES, SNES, Genesis) | Ultra-low budget, portable handheld | Raspberry Pi Zero 2 W |
| Up to PS1 / Dreamcast / N64 | Passive cooling, low power draw | Raspberry Pi 4 Model B (4GB) |
| GameCube, Saturn, PS2, modern indie | Maximum performance, active cooling | Raspberry Pi 5 (4GB) |
Parts List & GPIO Pin Mapping
Avoid cheap, no-name microswitches; they will double-trigger and ruin your Street Fighter combos. Here is the exact spec sheet for a 1-player, 8-button arcade harness.
Spec Sheet: Arcade Components
| Component | Exact Variant / Model | Estimated Cost |
|---|---|---|
| Compute Board | Raspberry Pi 5 (4GB) with Active Cooler | $65 |
| Joystick | SuzoHapp Compact II (4/8-way switchable) | $28 |
| Pushbuttons | Sanwa OBSF-30 (24mm microswitch, snap-in) | $4 ea |
| Wiring Harness | 40-pin female-to-female Dupont ribbon (12") | $6 |
| Connectors | 0.110" (2.8mm) fully insulated quick disconnects | $10/pk |
GPIO Pin Mapping Table (BCM Numbering)
We map to BCM (Broadcom) pins, not physical pin numbers. We intentionally avoid BCM 2 and 3 (hardwired I2C pull-ups that cause ghosting) and BCM 14/15 (default UART serial pins).
| Function | BCM GPIO | Physical Pin | Keyboard Mapping Target |
|---|---|---|---|
| Joystick Up | 17 | 11 | Up Arrow |
| Joystick Down | 27 | 13 | Down Arrow |
| Joystick Left | 22 | 15 | Left Arrow |
| Joystick Right | 23 | 16 | Right Arrow |
| Button 1 (LP) | 24 | 18 | Left Ctrl |
| Button 2 (MP) | 25 | 22 | Left Alt |
| Button 3 (HP) | 5 | 29 | Spacebar |
| Button 4 (LK) | 6 | 31 | Left Shift |
| Start | 16 | 36 | Enter |
| Select / Coin | 26 | 37 | Backspace |
Bench Wiring: Crimping and Ground Daisy-Chains
The most common point of failure in custom arcade builds is the ground wiring. Do not run individual ground wires from every switch back to the Pi. Instead, use a daisy-chain ground loop.
- Prep the Microswitches: Slide a 0.110" female quick disconnect onto the NC (Normally Closed) terminal of every Sanwa button and joystick microswitch. Never use the NO (Normally Open) terminal for active-low GPIO reads.
- Daisy-Chain the Grounds: Crimp a single continuous 18 AWG stranded wire looping through the COM (Common) terminal of every switch. Use a daisy-chain crimp connector or solder a pigtail at each terminal.
- Terminate Ground to Pi: Connect the end of your ground loop to Physical Pin 6 (GND) on the Raspberry Pi 5 header using a Dupont connector.
- Wire the Signal Pins: Run individual wires from the NC terminal of each switch to the corresponding BCM GPIO pin mapped in the table above.
- Verify Continuity: Before plugging in the Pi, use a multimeter in continuity mode. Press a button and verify you get a near-zero ohm reading between the NC terminal and the Dupont end.
The GPIO-to-Keystroke Python Bridge
Because the Raspberry Pi 5 uses the RP1 chip, legacy GPIO libraries relying on /dev/mem will fail. This script uses gpiozero with the lgpio pin factory, and pynput to inject keystrokes into the OS. This code targets the Raspberry Pi 5 (4GB) running Raspberry Pi OS Bookworm.
Prerequisites: Run sudo apt install python3-gpiozero python3-rpi-lgpio python3-pynput
import os
import sys
from gpiozero import Button
from signal import pause
from pynput.keyboard import Key, Controller
# Force lgpio pin factory for Pi 5 / Bookworm compatibility
os.environ['GPIOZERO_PIN_FACTORY'] = 'lgpio'
keyboard = Controller()
# Pin Definitions (BCM mapping from table above)
PIN_MAP = {
17: Key.up, 27: Key.down, 22: Key.left, 23: Key.right,
24: Key.ctrl_l, 25: Key.alt_l, 5: Key.space, 6: Key.shift_l,
16: Key.enter, 26: Key.backspace
}
buttons = {}
def press_action(key):
try:
keyboard.press(key)
except Exception as e:
print(f'Press error: {e}')
def release_action(key):
try:
keyboard.release(key)
except Exception as e:
print(f'Release error: {e}')
try:
for pin, key in PIN_MAP.items():
# pull_up=True means pin reads HIGH normally, LOW when button grounds it
# bounce_time=0.01 handles mechanical switch chatter (10ms debounce)
btn = Button(pin, pull_up=True, bounce_time=0.01)
btn.when_pressed = lambda k=key: press_action(k)
btn.when_released = lambda k=key: release_action(k)
buttons[pin] = btn
print('Arcade GPIO bridge active. Press Ctrl+C to exit.')
pause()
except KeyboardInterrupt:
print('Exiting gracefully...')
sys.exit(0)
except Exception as e:
print(f'Fatal GPIO Error: {e}')
sys.exit(1)
Debugging: BadPinFactory and Ghost Inputs
When migrating from Pi 4 to Pi 5, or moving from Bullseye to Bookworm, you will inevitably hit GPIO permission or factory errors. Here is how to diagnose them.
Exact Error: gpiozero.exc.BadPinFactory: Unable to load any default pin factory!
Ranked Causes & Fixes:
- Missing lgpio backend (Most Likely): The Pi 5 requires the
lgpioC library. Fix:sudo apt install python3-rpi-lgpio. - Running in a restricted Docker container: Docker blocks access to
/dev/gpiochip0. Fix: Pass--device /dev/gpiochip0to your docker run command.
Exact Error: OSError: [Errno 13] Permission denied: '/dev/uinput'
This occurs when pynput tries to inject keystrokes but lacks kernel input permissions.
Fix: Add your user to the input group and reboot: sudo usermod -aG input $USER
The First Three Things to Check When Inputs Fail or Ghost
- Ground Continuity: Put your multimeter in continuity mode. Check the resistance between the COM terminal of a button and Physical Pin 6 on the Pi. It must read < 1 ohm. If it reads higher, your daisy-chain crimp is loose.
- Interface Conflicts: Run
sudo raspi-config-> Interface Options. Ensure I2C, SPI, and Serial Port are disabled. If I2C is enabled, BCM 2 and 3 will fight your button pulls. - Switch Bounce: If pressing 'Button 1' registers as two rapid presses, your microswitch spring is fatigued. Increase the
bounce_timeparameter in the Python script from0.01to0.03(30ms).
Extending or Simplifying Your Build
Depending on your timeline and budget, you may want to pivot from this raw GPIO approach.
How to Simplify (The Pre-Made Route)
If you do not want to write Python daemons or manage systemd services, abandon the raw GPIO approach and buy a Mausberry Arcade GPIO Board or the Adafruit RetroGame HAT. These boards feature an onboard ATtiny microcontroller that handles the polling, debouncing, and USB HID translation in hardware. You simply wire the switches to the HAT, plug it into USB, and the OS sees it as a standard Xbox 360 controller. It costs about $25 more but saves hours of software debugging.
How to Extend (Adding Analog Spinners and Trackballs)
GPIO is strictly digital (HIGH/LOW). To add a trackball or an analog spinner (rotary encoder) for games like Arkanoid or Centipede, you cannot use standard pushbutton pins. Instead, wire a USB optical encoder (like the Ultimarc Ultra-Stik) or use a dedicated I2C rotary encoder module. For pure GPIO tracking of a quadrature rotary encoder, you will need to swap gpiozero.Button for gpiozero.RotaryEncoder and map the A/B phase outputs to integer increments passed to a virtual mouse via pynput.mouse.
For deeper integration with emulator frontends, consult the Batocera official controller documentation to map your injected keystrokes to specific RetroArch core inputs, and review the gpiozero API documentation for advanced event threading.






