This demonstrates a CircuitPython driver for reading XInput USB gamepad events using the Adafruit USB Host BFF (MAX3421E) and a QT Py ESP32-S3 dev board.
Assemble the Hardware
If you are unfamiliar with soldering stacking headers, you might want to read:
For this build, I included an otherwise unused microSD card BFF board because it adds some mechanical stability. Also, I might want to use SD cards later. If you don't care about SD cards, adjust these instructions accordingly (you might want to select different headers).
Getting Ready
There are three mildly tricky things about assembling the hardware for this project:
It's easy to assemble Qt Py boards in the wrong orientation. It will help to check the learn guides and pay close attention to the silk screen marks.
You need to carefully cut the longer header strips into pairs of headers that are 7 positions long. For the stacking headers and female headers, you can pull out one of the pins and carefully cut the plastic with your flush cutters. Once you separate a longer strip into two pieces, you can clean up the edges by nibbling away excess material with the flush cutters.
The USB Host BFF has a jumper on
A0which connects to the USB host port 5V power enable line. To solder the jumper closed, you will need to orient your headers such that you don't block the jumper with plastic. I accomplished that by using downward-pointing female headers on my ESP32-S3 board, then putting stacking headers on my USB host board with the pins coming out on the same side as the5VENjumper.
Order of Soldering
Assemble the microSD BFF with male header pins on a breadboard and solder the headers in place
Remove the microSD BFF from the breadboard and put a row of stacking header onto each row of the microSD board's header pins
Being careful of board orientation, put the USB Host BFF onto the pins of the stacking headers. Clamp the microSD and USB Host BFF sandwich in a vise, then solder the USB Host board's pins. Be sure to solder the
A0jumper closed, but try not to make the blob of solder taller than necessary (to avoid mechanical interference with the headers).(optional: trim stacking header pins with flush cutters to match the length of regular header pins)
Put a row of female header onto each row of the USB Host board's stacking header pins
Being careful of board orientation, put the QT PY ESP32-S3 board on to the pins of the female headers and solder them in place
Smoke Test and Final Assembly
Try plugging your board into a USB charger to make sure the LEDs light up
If the LEDs light up, unplug the USB power cable, then plug your USB OTG host cable into the USB Host BFF's USB port
Secure the QT Py board stack and otg cable to a Tamiya Universal Plate with cable ties. Trim the ends of the cable ties with your flush cutters.
Plug the USB gamepad into the otg cable's USB A port
Running the Gamepad Decoder
The next section has instructions for updating CircuitPython and copying the project bundle code to your CIRCUITPY drive. But, for now, this is a preview of what the code looks like when it runs.
To see output from code.py, you will need to
connect to the serial console
of your Qt Py board.
When code.py starts with a supported gamepad connected (so far 8BitDo SN30 Pro
USB is the only one I've confirmed to work), it takes several seconds to reset the USB
port and initialize the MAX3421E USB host controller chip. Once that's done, you
should see a message like this:
Auto-reload is on. Simply save files over USB to run them or enter REPL to disable. code.py output: Resetting USB bus... USB Host Ready Looking for USB gamepads... ... Found an XInput gamepad (045e:028e)... setting configuration ( 0, 0) ( 0, 0)
NOTE: USB hot plugging may be unreliable. Based on my limited testing, it
seems like there may be bugs in my driver code or in the CircuitPython
max3421e package. I got the best results when I plugged the gamepad in before
powering up.
If you press buttons and move the sticks, you should see additional output with decoded USB reports for gamepad events (XInput protocol) that like this:
( 0, 0) ( 0, 0) dUp ( 0, 0) ( 0, 0) ( 0, 0) ( 0, 0) dUp ( 0, 0) ( 0, 0) ( 0, 0) ( 0, 0) dDn ( 0, 0) ( 0, 0) ( 0, 0) ( 0, 0) dDn ( 0, 0) ( 0, 0) ( 0, 0) ( 0, 0) dL ( 0, 0) ( 0, 0) ( 0, 0) ( 0, 0) dR ( 0, 0) ( 0, 0) ( 0, 0) ( 0, 0) dL ( 0, 0) ( 0, 0) ( 0, 0) ( 0, 0) dR ( 0, 0) ( 0, 0) ( 0, 0) ( 0, 0) B ( 0, 0) ( 0, 0) ( 0, 0) ( 0, 0) A ( 0, 0) ( 0, 0) ( 0, 0) ( 0, 0) Start ( 0, 0) ( 0, 0) ( 0, 0) ( 0, 0) Select ( 0, 0) ( 0, 0) ( -1792, 0) ( 0, 0) (-22784, 0) ( 0, 0) (-32768, 2304) ( 0, 0) (-26624, 32767) ( 0, 0) ( -1024, 32767) ( 0, 0) ( 32767, 24832) ( 0, 0) ( 32767, -1024) ( 0, 0) ( 768,-32768) ( 0, 0) (-15360,-29440) ( 0, 0) ( 0, 0) ( 0, 0) ( 0, 0) (-15616,-18688) ( 0, 0) (-32768,-32768) ( 0, 0) (-32768, 2304) ( 0, 0) (-19456, 32767) ( 0, 0) ( -2560, 32767) ( 0, 0) ( 23296, 31488) ( 0, 0) ( 32767,-16128) ( 0, 0) ( -2560,-32768) ( 0, 0) (-22016,-22016) ( 0, 0) ( 0, 0)
The pairs of numbers in parentheses are (X, Y) coordinates for the left and
right joysticks. The text labels on the right are printed for edge-triggered
transitions (not-pressed to pressed) of the gamepad's buttons. Lines with all
zero coordinates and no button labels get printed when the last button is
released. To see how it works, check out the code and comments in code.py.
Updating CircuitPython
NOTE: To update CircuitPython on the ESP32-S3 with 2MB PSRAM and 4MB Flash, you need to use the .BIN file (combination bootloader and CircuitPython core)
Download the CircuitPython 9.1.1 .BIN file from the Adafruit QT Py ESP32-S3 4MB Flash/2MB PSRAM page on circuitpython.org
Follow the instructions in the Web Serial ESPTool section of the "CircuitPython on ESP32 Quick Start" learn guide to update your board with CircuitPython 9.1.1. First erasing the board's contents, then programming it with the .BIN file. (CAUTION: the normal UF2 file method does not work on this board because it does not have a large enough flash drive to hold the CircuitPython UF2 file)
CircuitPython Code
To copy the project bundle files to your CIRCUITPY drive:
Download the project bundle .zip file using the button below.
Expand the zip file by opening it, or use
unzipin a Terminal. You should end up with a folder named prox-sensor-encoder-menu, which should contain aREADME.txtfile and aCircuitPython 9.xfolder.Open the CircuitPython 9.x folder and copy all of its contents to your CIRCUITPY drive.
To learn more about copying libraries to your CIRCUITPY drive, check out the CircuitPython Libraries section of the Welcome to CircuitPython! learn guide.
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: Copyright 2024 Sam Blenny
#
# usbgamepad
#
# Hardware:
# - Adafruit QT Py S3 with 2MB PSRAM (#5700)
# - Adafruit USB Host BFF for QT Py or Xiao with MAX3421E (#5956)
# - Adafruit microSD Card BFF Add-On for QT Py and Xiao (#5683)
# - 8BitDo SN30 Pro USB gamepad
#
# Pinouts:
# ESP32S3 MAX3421E microSD
# A0 5VEN
# A1 CS
# A2 IRQ
# TX CS
# SCK SCK SCK
# MI MISO MISO
# MO MOSI MOSI
#
from board import A0, A1, A2, NEOPIXEL, NEOPIXEL_POWER, SPI, TX
from digitalio import DigitalInOut, Direction
import gc
from max3421e import Max3421E
from neopixel_write import neopixel_write
from struct import unpack
from sys import stdout
from time import sleep
from usb import core
# Gamepad button bitmask constants
BTN = {
'dUp': 0x0001,
'dDn': 0x0002,
'dL': 0x0004,
'dR': 0x0008,
'Start': 0x0010,
'Select': 0x0020,
'LHat': 0x0040,
'RHat': 0x0080,
'L': 0x0100,
'R': 0x0200,
'Home': 0x0400,
'B': 0x1000,
'A': 0x2000,
'Y': 0x4000,
'X': 0x8000,
}
def decode(btn, L2, R2):
# Decode the button bitfield along with L2 and R2
names = []
for k in sorted(BTN):
v = BTN[k]
if btn & v:
names.append(k)
if L2:
names.append("L2")
if R2:
names.append("R2")
return " ".join(names)
def start_xpad(device):
# Initialize gamepad and poll for input changes, print updates
interface = 0
prev14 = bytearray(14)
buf64 = bytearray(64)
# Make sure CircuitPython core is not claiming the device
if device.is_kernel_driver_active(interface):
print("detaching kernel driver")
device.detach_kernel_driver(interface)
# Make sure that configuration is set
try:
print("setting configuration")
device.set_configuration()
except core.USBError as e:
print(e)
# ==========================================================
# == weird bug workaround: omitting this hangs the device ==
_ = dir(device)
# ==========================================================
# Initial reads may give old data, so drain gamepad's buffer
for _ in range(8):
try:
_ = device.read(0x81, buf64)
except core.USBError as e:
if e.errno != 75:
raise e
# Start polling for input events
while True:
sleep(0.025) # aim for 30 Hz (8 ms for 2 endpoint reads)
# For some gamepads, first read after not having polled for
# a while will usually give a "[Errno 75] Overflow"
# exception. But, a second read immediately after the error
# response normally works. For other gamepads (e.g.
# non-wireless), the first read may return a sucessful
# response.
try:
n = device.read(0x81, buf64) # type is array.array('B')
except core.USBError as e:
if e.errno != 75:
raise e
n = device.read(0x81, buf64)
if n < 14:
# skip unexpected responses (looking for a 20 byte report)
continue
buf14 = buf64[:14]
if buf14 != prev14:
# Unpack normal responses
prev14[:] = buf14
(btn, L2, R2, LX, LY, RX, RY) = unpack('<HBBhhhh', buf14[2:14])
print("(%6d,%6d) (%6d,%6d) " % (LX, LY, RX, RY),
decode(btn, L2, R2))
def find_and_connect():
# Attempt to establish a gamepad connection
print("Looking for USB gamepads...")
while True:
gamepad = core.find(idVendor=0x045e, idProduct=0x028e)
if gamepad:
print("\nFound an XInput gamepad (045e:028e)...")
sleep(1) # Wait briefly to let adapter and USB bus settle
try:
return start_xpad(gamepad)
except core.USBError as e:
if e.errno == 19:
# 19 = "No such device (it may have been disconnected)"
print("[Gamepad disconnected]")
else:
print(e)
return {"lost": True}
else:
# If no gamepads are connected, retry at 1 s intervals
print(".", end='')
sleep(1)
def main():
gc.collect()
# Set Neopixel to dim magenta
npx = DigitalInOut(NEOPIXEL)
npxPow = DigitalInOut(NEOPIXEL_POWER)
npxPow.direction = Direction.OUTPUT
npxPow.value = True
neopixel_write(npx, bytearray([0,1,1]))
# Cycle MAX3421E USB port power
print("Resetting USB bus...")
# Turn off USB host port 5V power output
usbEn = DigitalInOut(A0)
usbEn.direction = Direction.OUTPUT
usbEn.value = False
# Begin initializing MAX3421E USB host chip
spi = SPI()
usbHost = Max3421E(spi, chip_select=A1, irq=A2)
# Wait for 5V caps to discharge and MAX3421E to initialize
sleep(2)
# Turn USB host port 5V power back on
usbEn.direction = Direction.INPUT
sleep(1.5)
print("USB Host Ready")
# MAIN EVENT LOOP
# Establish and maintain a gamepad connection
while True:
find_and_connect() # only returns if connection is lost
# Let USB bus settle for a bit after a lost connection
sleep(1.5)
gc.collect()
main()
This page (USB Host Gamepad Decoder) was last updated on August 07, 2024.
Text editor powered by tinymce.