Skip to content

Serial programmable USB HID Gamepad using CircuitPython

License

Notifications You must be signed in to change notification settings

neildavis/teensy_hid_gamepad

Repository files navigation

teensy_hid_gamepad

Overview

A programmable USB HID Gamepad using CircuitPython with the following features:

Target Hardware

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.

Installation

  1. Ensure you have CircuitPython installed for your MCU board.

  2. Install the required libraries from the latest Adafruit CircuitPython Bundle:

    • adafruit_hid
    • adafruit_datetime
  3. Copy all of the *.py files in the root of this repository to the root of your CIRCUITPY drive/volume.

Modifying Code

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.

Physical Inputs

Both analog & digital components can be used as inputs for the gamepad:

Analog Inputs

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.

Digital Inputs

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)

Rotary Encoder Inputs

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:

  1. Digital input id for the 'Clock' (CLK) pin of the rotary encoder. This is a key into digital_ins
  2. Digital input id for the 'Data' (DT) pin of the rotary encoder. This is a key into digital_ins
  3. Button id for the rotary encoder decrement (anti-clockwise) movement.
  4. Button id for the rotary encoder increment (clockwise) movement.

USB Consumer Control

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.

Programmable Serial Interface

In additional to the physical analog and digital inputs, USB HID events can be synthesized using a programmable interface via USD CDC serial comms.

Motivation

I have created a few custom USB HID input controllers for various projects such as my:

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.

Synthesized input interface

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.

Examples

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.

Special Configuration Commands

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)