← Blog
DRIVERS · Pi INPUT

Pi GPIO Joypad Driver — BCM2835/BCM2711 Arcade HAT Support

June 22, 2026 · sigil-drivers · Sigil-Docs
drivers input pi gpio retropie arcade

pi_gpio_joypad.sg (sigil-drivers 9fe4d9b) adds a BCM2835/BCM2711 GPIO digital joypad driver — the native input layer for Pi arcade HATs and DIY joypads wired directly to GPIO header pins. Active-low buttons (pressed = pin low, floating high via pull-up resistors), SNES-style default 12-button pin mapping, sim-safe init path, snap/restore rollback support. 13 checks PASS. State at 0x9F0000. The Pi input driver stack is now complete: GPIO joypad + BT HID host + Pi BT UART transport.


What Pi GPIO joypad covers

Pi arcade HATs (e.g. Waveshare Arcade-C-HAT, Retroflag GPIcase, custom Pi0 arcade sticks) wire joystick microswitches and buttons directly to BCM GPIO pins through the 40-pin header. There is no USB, no Bluetooth, no HID stack — the game controller is the GPIO level. pi_gpio_joypad.sg reads GPLEV0 (the GPIO level register) and maps pin states to the sigilOS joypad ABI.

The active-low protocol

All buttons use a pull-up resistor (the BCM2835/BCM2711 GPIO pull-up can be configured in hardware). The idle state of each pin is logic HIGH (1) — the pull-up holds it there. When a button is pressed, it shorts the pin to GND, pulling it LOW (0). The driver inverts: button = 1 - (lev / pin_div % 2) — where lev is the GPLEV0 word and pin_div is the bit position divisor for the pin (e.g. GPIO 2 → divisor 4, GPIO 17 → divisor 131072). A 0 in GPLEV0 at that bit → button is pressed (output 1). A 1 → button is released (output 0).

Default SNES-style pin mapping

The default mapping matches the most common Pi arcade HAT convention — SNES-style face layout with L/R shoulders:

ButtonGPIO pinBit divisorNotes
Up24D-pad
Down38D-pad
Left416D-pad
Right17131072D-pad
B27134217728Face (bottom)
A224194304Face (right)
Y101024Face (left)
X9512Face (top)
L112048Left shoulder
R238388608Right shoulder
Select8256Meta
Start7128Meta

The pin map is compiled-in at driver init. Remapping is possible by changing the pin_map[] array — all downstream code uses the logical button index, not the GPIO pin number.


Driver API (surface-rows)

pi_gpio_joy_init(hw)
Initialize the joypad driver. hw=0: sim-safe mode — skip MMIO configure_pins() (no GPLEV0 MMIO on x86/QEMU, test with injected GPLEV0 words instead). hw=1 or PI_GPIO_OP_HW_INIT: real Pi mode — configure BCM pull-ups via the GPIO function select and pull-up/pull-down control registers at PBASE+0x200000 (Pi3: 0x3F200000, Pi4: 0xFE200000). Sets state base 0x9F0000.
pi_gpio_joy_read()
Read current button state. Issues peek32(GPLEV0) to read the GPIO level register. Applies active-low inversion per pin: button = 1 - (lev / pin_div % 2). Returns a 12-bit button word (Up/Down/Left/Right/B/A/Y/X/L/R/Select/Start). In sim mode (hw=0), GPLEV0 can be set by the test harness via poke32(GPLEV0, sim_val).
pi_gpio_joy_snap()
Rollback snapshot. Saves the current GPLEV0 word to the snap slot. Used by the Mode-2 SRDX rollback system to record controller state at a specific frame before applying a late input correction. The entire joypad state is one 32-bit GPLEV0 word — cheap to snap and restore.
pi_gpio_joy_rest()
Rollback restore. Restores GPLEV0 from the snap slot. Used by Mode-2 rollback to replay frames from the saved state. In sim mode, restores to the saved test value, not a real GPIO read.

Sim-safe init model

The driver follows the same sim-safe pattern as other sigilOS hardware drivers: init(0) is safe to call in x86 QEMU, where GPLEV0 MMIO does not exist. The hardware configure step (configure_pins()) is gated on hw != 0. This allows the full 13-check test suite to run on x86 without requiring actual Pi hardware:

// x86 QEMU test flow (hw=0):
pi_gpio_joy_init(0)          // skip configure_pins; set state base
poke32(GPLEV0, 0xFFFFFFF7)  // simulate GPIO 3 low (Down pressed)
st = pi_gpio_joy_read()      // active-low: bit3=0 → down=1
assert(st.down == 1)         // PASS

// Pi hardware flow (hw=1):
pi_gpio_joy_init(PI_GPIO_OP_HW_INIT)  // configure BCM pull-ups
loop: st = pi_gpio_joy_read()          // live GPLEV0 read per frame

The Pi input driver stack — now complete

DriverTransportAddressButtons/axesPASS
pi_gpio_joypad.sg BCM GPIO GPLEV0 (Pi header) 0x9F0000 12 digital buttons 13
pi_bt_hid.sg + pi_bt_uart.sg BT H4/HCI over PL011 UART → ACL→L2CAP→HID 0x9D8000 Routes to xbox_bt (16B, 4×16-bit axes) 14
sw_pro_bt.sg Bluetooth HID (057E:2009) 0x9E0000 12 buttons + 4×12-bit sticks 24
ds4_bt.sg Bluetooth HID (054C:05C4/09CC) 0x9C0000 17 buttons + gyro + accel + touchpad 21
xbox_bt.sg Bluetooth HID (045E:02FD) 0x9A0000 16 buttons + 4×16-bit axes + 2 triggers
usb-hid-gamepad USB HID 16 buttons + 4×8-bit axes

The GPIO joypad fills the last slot: wired arcade HATs and DIY builds that bypass both USB and Bluetooth entirely. With this driver, every Pi controller scenario is handled in Sigil — from a bare GPIO microswitch wired to header pins to a Switch Pro Controller over Bluetooth.

All drivers share the same rollback ABI: snap() / rest() records and restores controller state for SRDX Mode-2 rollback netplay. The GPIO joypad's state is a single 32-bit GPLEV0 word, making it the cheapest driver to snap.