A programmable USB HID Gamepad using CircuitPython with the following features:
- Up to 16 digital inputs (GPIO) can be attached and mapped to USB HID Gamepad buttons.
- Up to 4 analog inputs (16-bit ADC) can be attached and mapped to two USB HID Gamepad joystick axes.
- Rotary Encoder inputs mapped to gamepad button or volume button pairs
- USB Consumer Control support for volume up/down/mute and power off.
- Programmable HID responses via USB CDC Serial comms using the same port as USB HID.
- Programmable dynamic re-mapping of digital inputs to buttons and analog inputs to joystick axis
- Single command automated configuration for EmulationStation and RetroArch as used in e.g. RetroPie.
- Open and hackable.
This project was originally developed for a Teensy 4.0 MCU (hence the name), but is now targeted at RP2040 development boards like Pimoroni Tiny2040.
It should work on any MCU with enough ADC & GPIO inputs and a working CircuitPython port including USB HID & CDC serial support. Some minor changes for board I/O pins etc. may be required.
-
Ensure you have CircuitPython installed for your MCU board.
-
Install the required libraries from the latest Adafruit CircuitPython Bundle:
adafruit_hid
adafruit_datetime
-
Copy all of the
*.py
files in the root of this repository to the root of yourCIRCUITPY
drive/volume.
If you need to modify any of the code, note that in code.py
the default 'auto-reload on save' functionality has been disabled by this line:
# Disable auto reload
supervisor.runtime.autoreload = False
To reload after saving, use the
CircuitPython serial console and press
CTRL+C
to interrupt the running program, and then CTRL+D
to reload. Or just delete/comment out the line above.
Both analog & digital components can be used as inputs for the gamepad:
By default, two gamepad analog joysticks (with two axes each) are enabled on four ADC inputs (A0
-A3
) as found on the
Pimoroni Tiny2040 board.
These can be changed and/or removed in these sections of code in config.py
:
# These are the default mappings of analog axes for joysticks:
default_joystick_pins = {
'x' : 'a0',
'y' : 'a1',
'z' : 'a2',
'r_z' : 'a3',
}
The 'x
' & 'y
' axes correspond to the left analog joystick, whilst 'z
' & 'r_z
' correspond to the right analog joystick. The values are keys into the available analog axes defined in analog_ins
.
You are free to modify these but just be sure they match up in both dictionaries:
analog_ins: dict[str, Pin ] = {
'a0' : board.A0,
'a1' : board.A1,
'a2' : board.A2,
'a3' : board.A3,
}
Remove unwanted entries and/or map to alternative inputs on your board as required.
Up to 16 buttons and 3 volume controls can be mapped to GPIO inputs on the board.
By default, since the
Pimoroni Tiny2040
board has only 8 GPIO left (excluding the pins used as ADC for the analog joysticks)
only 6 gamepad buttons and 2 volume control buttons are mapped.
These can be changed and/or removed in these sections of code in config.py
:
# These are the default mappings of buttons to digital inputs
default_button_pins = {
BUTTON_VOL_UP : 'd0',
BUTTON_VOL_DOWN : 'd1',
BUTTON_START : 'd2',
BUTTON_SELECT : 'd3',
BUTTON_SOUTH_B : 'd4',
BUTTON_WEST_Y : 'd5',
BUTTON_EAST_A : 'd6',
BUTTON_NORTH_X : 'd7',
}
The keys identify the gamepad button action.
The values are keys into the available digital inputs defined in digital_ins
.
You are free to modify these but just be sure they match up in both dictionaries:
digital_ins: dict[str, Pin] = {
'd0' : board.GP0,
'd1' : board.GP1,
'd2' : board.GP2,
'd3' : board.GP3,
'd4' : board.GP4,
'd5' : board.GP5,
'd6' : board.GP6,
'd7' : board.GP7,
}
The complete set of valid button keys can also be seen in config.py
:
# Enumerate all our digital io inputs as HID button IDs (0-15)
BUTTON_WEST_Y = 0
BUTTON_SOUTH_B = 1
BUTTON_EAST_A = 2
BUTTON_NORTH_X = 3
BUTTON_SHOULDER_L = 4
BUTTON_SHOULDER_R = 5
BUTTON_TRIGGER_L = 6
BUTTON_TRIGGER_R = 7
BUTTON_SELECT = 8
BUTTON_START = 9
BUTTON_THUMB_L = 10
BUTTON_THUMB_R = 11
BUTTON_HAT_UP = 12
BUTTON_HAT_DOWN = 13
BUTTON_HAT_LEFT = 14
BUTTON_HAT_RIGHT = 15
BUTTON_MAX = BUTTON_HAT_RIGHT
# CC Volume handled by buttons outside gamepad button range
BUTTON_VOL_UP = ConsumerControlCode.VOLUME_INCREMENT
BUTTON_VOL_DOWN = ConsumerControlCode.VOLUME_DECREMENT
BUTTON_VOL_MUTE = ConsumerControlCode.MUTE
BUTTON_POWER = CC_POWER_CODE
The names should be self explanatory. Note that 'Hat' is USB HID speak for what is commonly referred to as a 'D-Pad'
The numeric values 0-15 are those reported as Gamepad button IDs in the USB HID reports. These have been assigned by reverse engineering of a Logitech F310 controller - which mimics an Xbox 360 controller - using Gamepad test software such as this. Note that the 'X' & 'Y' pair and the 'A' & 'B' pair are arranged in opposite order to the commonly used SNES controller. If this is an issue, just swap them around.
The values for volume are assigned to
adafruit_hid.consumer_control_code.ConsumerControlCode
values for convenience, given that they do not clash with the gamepad button range (0-15)
An arbitrary number of rotary encoders can be used to map onto pairs of digital inputs (one digital input each for clockwise/ant-clockwise)
By default no rotary encoders are configured. However the code in config.py
includes a commented-out example for a rotary encoder on inputs d0
and d1
to control volume:
default_rotary_encoder_pins: dict[str: (str, str, int, int)] = {
'rot_vol': ('d0', 'd1', BUTTON_VOL_DOWN, BUTTON_VOL_UP),
}
The key is arbitrary, but must not conflict with any of the joystick axes (x
, y
, z
, r_z
)
The positional arguments in the tuple value are as follows:
- Digital input id for the 'Clock' (CLK) pin of the rotary encoder. This is a key into
digital_ins
- Digital input id for the 'Data' (DT) pin of the rotary encoder. This is a key into
digital_ins
- Button id for the rotary encoder decrement (anti-clockwise) movement.
- Button id for the rotary encoder increment (clockwise) movement.
In addition to the USB HID Gamepad functionality, limited USB Consumer Control functions are supported. These are currently limited to:
- Volume Up (Increment)
- Volume Down (Decrement)
- Volume Mute (Toggle)
- Power Off
The volume commands can be mapped to digital GPIO inputs as described above.
The 'power off' functionality is achieved by holding the 'start' button for a period of time
as determined in the code by this constant in config.py
:
# Holding 'Start' button for this period will send a Power Off command
START_BUTTON_HOLD_FOR_SHUTDOWN_SECS = 3
This functionality is also dependent on the host operating system acting upon the USB CC codes sent by the device. Most modern desktop OS like Windows, macOS & Linux desktop distros will support this.
Some 'bare bones' Linux distros without a GUI desktop environment may need additional software to enable this functionality, e.g. Raspberry Pi OS Lite. In these cases, my daemon project may work.
In additional to the physical analog and digital inputs, USB HID events can be synthesized using a programmable interface via USD CDC serial comms.
I have created a few custom USB HID input controllers for various projects such as my:
- Tiger / Grandstand / Sega - After Burner - Tabletop Arcade Conversion
- Tomy Demon Driver (1978) vs Sega Monaco GP (1979)
These projects commonly have significantly fewer inputs than a full dual-shock style gamepad. They also run on platforms using software such as RetroPie, RetroArch and EmulationStation. These software include useful features to simplify configuration of input devices by producing controller inputs in response to visual prompts. However, they can assume a full gamepad input set which is not available on my custom controller. By synthesizing these events, we can make use of these convenient tools without having to resort to editing config files manually.
The programmable serial interface makes use of the second 'data' serial device offered by CircuitPython to receive input. See Adafruit's docs to learn how to identify the appropriate serial (or 'COM') port for your OS.
You can make use of this interface using any commonly available serial terminal emulator software. Popular text-base ones include Minicom or Screen. There are also GUI alternatives such as PuTTY. A large (but non-exhaustive) list can be found here.
The interface accepts commands as a line of text terminated by a CR (0xD
) or LF (0xA
).
Each line may contain an arbitrary number of name=value
pairs separated by a semi-colon.
The available commands are listed in the following table:
Command Name (e.g.) | Valid Values (e.g.) | Description |
---|---|---|
btn{N} (e.g. btn1 ) |
1 |
Press (and release) button N |
x , y , z , r_z |
-16327 - 16327 |
Set joystick axes analog values |
vol |
-1 , 1 , mute |
Volume. 1 increments, -1 decrements, mute toggles 'mute' |
{digital input} (e.g. d0 ) |
{button id} (e.g. 9 == 'Start ') |
[Re]Map a digital input to a button ID |
{analog input} (e.g. a0 ) |
{joystick axis} (e.g. r_z ) |
[Re]Map an analog input to a joystick axis |
hold |
+ve floating point values | Time in seconds to hold the controls at specified values |
pre |
+ve floating point values | Time in seconds to wait before synthesizing the inputs |
post |
+ve floating point values | Time in seconds to wait after synthesizing the inputs |
By default, the specified input values are held for half a second. This can be changed by use of
the hold
command.
Command string | Actions |
---|---|
'btn1=1;btn5=1;x=-16327 ' |
Press buttons 1 & 5 and set left analog stick x-axis full left. |
'r_z=8000 ' |
Move right analog joystick y-axis approx half way down. |
'btn=1;hold=5 ' |
Press button 3 and hold it for five seconds. |
'vol=-1;post=2.5 ' |
Decrement volume and wait for 2.5 seconds before processing any other events or commands. |
'd0=9;a3=y ' |
Remap digital input d0 to button number 9 (Start ) and remap analog input a3 to left joystick y axis. |
In addition to the generic programmable serial interface described above specific commands are available to automate particular softwares' configuration procedures. These commands are not 'compoundable' with the generic commands above and must be entered alone.
- '
conf_es
' : Performs a full input configuration sequence for EmulationStation (Main Menu -> Configure Input) - '
conf_ra
' : Performs a full input configuration sequence for RetroArch (Main Menu -> Settings -> Input -> Port N Controls -> Set All Controls)