Skip to content

Instantly share code, notes, and snippets.

@urish
Last active June 21, 2025 13:50
Show Gist options
  • Save urish/25aa821dc23b8299ce878c7236b11489 to your computer and use it in GitHub Desktop.
Save urish/25aa821dc23b8299ce878c7236b11489 to your computer and use it in GitHub Desktop.
gamepad_pmod_circuitpython_pio
import usb_hid
from joystick_xl.hid import create_joystick
# enable default CircuitPython USB HID devices as well as JoystickXL
usb_hid.enable(
(
usb_hid.Device.KEYBOARD,
usb_hid.Device.MOUSE,
usb_hid.Device.CONSUMER_CONTROL,
create_joystick(axes=2, buttons=8, hats=1),
)
)
"""
Tiny Tapeout Gamepad Pmod Driver for CircuitPython
https://github.com/psychogenic/gamepad-pmod
DATA -> GP19 (gamepad_data) –- sampled by PIO
CLOCK -> GP18 (gamepad_clk) –- edge-detect inside PIO
LATCH -> GP17 (gamepad_latch) –- read in plain Python
Protocol:
* While LATCH is low the pad shifts out 24 bits, MSB first, one bit
per rising edge of CLOCK.
* When LATCH goes low→high the frame is considered complete and the
current 24-bit word is printed.
"""
import array
import board
import rp2pio
import adafruit_pioasm
from joystick_xl.joystick import Joystick
# Pin mapping
LATCH_GPIO = 17
CLOCK_GPIO = 18
DATA_GPIO = 19
LATCH_PIN = board.GP17
# Gamepad buttons
GAMEPAD_BUTTONS = ['L', 'R', 'X', 'A', 'Right', 'Left', 'Down', 'Up', 'Start', 'Select', 'Y', 'B']
GP_L = 1 << 0
GP_R = 1 << 1
GP_X = 1 << 2
GP_A = 1 << 3
GP_RIGHT = 1 << 4
GP_LEFT = 1 << 5
GP_DOWN = 1 << 6
GP_UP = 1 << 7
GP_START = 1 << 8
GP_SELECT = 1 << 9
GP_Y = 1 << 10
GP_B = 1 << 11
# PIO program
PIO_SRC = f"""
.program gamepad_sniff
frame_wait:
wait 0 gpio {LATCH_GPIO}
set x, 23 ; need 24 clock cycles
mov isr, null ; clear previous data
bitloop: ; ---- shift 24 bits while latch is LOW ----
wait 1 gpio {CLOCK_GPIO} ; clk ↑
mov osr, pins
out null, 2 ; Discard clock / latch values
in osr, 1 ; sample DATA
wait 0 gpio {CLOCK_GPIO} ; clk ↓
jmp x-- bitloop ; repeat 24×
; ---------- wait for latch HIGH (end-of-frame) ------------------
wait 1 gpio {LATCH_GPIO}
push ; send 24-bit word to FIFO
jmp frame_wait ; capture next frame
"""
assembled = adafruit_pioasm.assemble(PIO_SRC)
# ---------------- State machine ---------------------------------------------
sm = rp2pio.StateMachine(
assembled,
frequency=2_000_000, # state-machine clock – plenty for ~1 MHz CLK
first_in_pin=LATCH_PIN, # DATA is the one pin read by `IN PINS,1`
in_pin_count=3,
in_shift_right=False, # shift MSB-first
)
def decode_message(value: int):
result = []
for i, btn in enumerate(GAMEPAD_BUTTONS):
if value & (1 << i):
result.append(btn)
return result
def map_hat_directions(value: int):
if value & GP_UP:
if value & GP_LEFT:
return 7
if value & GP_RIGHT:
return 1
return 0
if value & GP_DOWN:
if value & GP_LEFT:
return 5
if value & GP_RIGHT:
return 3
return 4
if value & GP_RIGHT:
return 2
if value & GP_LEFT:
return 6
return 8
buf = array.array('I', [0])
js = Joystick()
print("Ready – waiting for frames …")
while True:
sm.readinto(buf)
value = buf[0]
js.update_button((0, value & GP_START))
js.update_button((1, value & GP_SELECT))
js.update_hat((0, map_hat_directions(value)))
print(f"{value:024b} 0x{value:06X} {decode_message(value)}")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment