Text Editor
Adafruit recommends using the Mu editor for editing your CircuitPython code. You can get more info in this guide.
Alternatively, you can use any text editor that saves simple text files.
Download the Project Bundle
Your project will use a specific set of CircuitPython libraries, and the code.py file. To get everything you need, click on the Download Project Bundle link below, and uncompress the .zip file.
Drag the contents of the uncompressed bundle directory onto your board's CIRCUITPY drive, replacing any existing files or directories with the same names, and adding any new ones that are necessary. The CIRCUITPY drive appears when you plug the KB2040 into the computer via USB.
# SPDX-FileCopyrightText: 2023 Robert Dale Smith for Adafruit Industries # # SPDX-License-Identifier: MIT # USB HID descriptor for generic DirectInput compatible gamepad. import usb_hid # This is only one example of a gamepad descriptor, and may not suit your needs. GAMEPAD_REPORT_DESCRIPTOR = bytes(( 0x05, 0x01, # USAGE_PAGE (Generic Desktop) 0x09, 0x05, # USAGE (Gamepad) 0xa1, 0x01, # COLLECTION (Application) 0x15, 0x00, # LOGICAL_MINIMUM (0) 0x25, 0x01, # LOGICAL_MAXIMUM (1) 0x35, 0x00, # PHYSICAL_MINIMUM (0) 0x45, 0x01, # PHYSICAL_MAXIMUM (1) 0x75, 0x01, # REPORT_SIZE (1) 0x95, 0x0e, # REPORT_COUNT (14) 0x05, 0x09, # USAGE_PAGE (Button) 0x19, 0x01, # USAGE_MINIMUM (Button 1) 0x29, 0x0e, # USAGE_MAXIMUM (Button 14) 0x81, 0x02, # INPUT (Data,Var,Abs) 0x95, 0x02, # REPORT_COUNT (3) 0x81, 0x01, # INPUT (Cnst,Ary,Abs) 0x05, 0x01, # USAGE_PAGE (Generic Desktop) 0x25, 0x07, # LOGICAL_MAXIMUM (7) 0x46, 0x3b, 0x01, # PHYSICAL_MAXIMUM (315) 0x75, 0x04, # REPORT_SIZE (4) 0x95, 0x01, # REPORT_COUNT (1) 0x65, 0x14, # UNIT (Eng Rot:Angular Pos) 0x09, 0x39, # USAGE (Hat switch) 0x81, 0x42, # INPUT (Data,Var,Abs,Null) 0x65, 0x00, # UNIT (None) 0x95, 0x01, # REPORT_COUNT (1) 0x81, 0x01, # INPUT (Cnst,Ary,Abs) 0x26, 0xff, 0x00, # LOGICAL_MAXIMUM (255) 0x46, 0xff, 0x00, # PHYSICAL_MAXIMUM (255) 0x09, 0x30, # USAGE (X) 0x09, 0x31, # USAGE (Y) 0x09, 0x32, # USAGE (Z) 0x09, 0x35, # USAGE (Rz) 0x75, 0x08, # REPORT_SIZE (8) 0x95, 0x04, # REPORT_COUNT (6) 0x81, 0x02, # INPUT (Data,Var,Abs) 0x06, 0x00, 0xff, # USAGE_PAGE (Vendor Specific) 0x09, 0x20, # Unknown 0x09, 0x21, # Unknown 0x09, 0x22, # Unknown 0x09, 0x23, # Unknown 0x09, 0x24, # Unknown 0x09, 0x25, # Unknown 0x09, 0x26, # Unknown 0x09, 0x27, # Unknown 0x09, 0x28, # Unknown 0x09, 0x29, # Unknown 0x09, 0x2a, # Unknown 0x09, 0x2b, # Unknown 0x95, 0x0c, # REPORT_COUNT (12) 0x81, 0x02, # INPUT (Data,Var,Abs) 0x0a, 0x21, 0x26, # Unknown 0x95, 0x08, # REPORT_COUNT (8) 0xb1, 0x02, # FEATURE (Data,Var,Abs) 0xc0 # END_COLLECTION )) gamepad = usb_hid.Device( report_descriptor=GAMEPAD_REPORT_DESCRIPTOR, usage_page=0x01, # Generic Desktop Control usage=0x05, # Gamepad report_ids=(0,), # Descriptor uses report ID 0. in_report_lengths=(19,), # This gamepad sends 19 bytes in its report. out_report_lengths=(0,), # It does not receive any reports. ) usb_hid.enable((gamepad,))
# SPDX-FileCopyrightText: 2023 Robert Dale Smith for Adafruit Industries # # SPDX-License-Identifier: MIT # Simple Super Nintendo controller to standard USB HID gamepad with DirectInput button mapping. # Tested on KB2040 import time import board import digitalio import usb_hid # Update the SNES Controller pins based on your input LATCH_PIN = board.D6 CLOCK_PIN = board.D5 DATA_PIN = board.D7 # Set up pins for SNES Controller latch = digitalio.DigitalInOut(LATCH_PIN) latch.direction = digitalio.Direction.OUTPUT clock = digitalio.DigitalInOut(CLOCK_PIN) clock.direction = digitalio.Direction.OUTPUT data = digitalio.DigitalInOut(DATA_PIN) data.direction = digitalio.Direction.INPUT data.pull = digitalio.Pull.UP # pull-up as a default # SNES to USB button mapping buttonmap = { "B": (0, 0, 0x2), # Button 1 "Y": (1, 0, 0x1), # Button 3 "Select": (2, 1, 0x01), # Button 9 "Start": (3, 1, 0x02), # Button 10 "Up": (4, 0, 0), # D-pad North "Down": (5, 0, 0), # D-pad South "Left": (6, 0, 0), # D-pad East "Right": (7, 0, 0), # D-pad West "A": (8, 0, 0x4), # Button 2 "X": (9, 0, 0x8), # Button 4 "L": (10, 0, 0x10), # Button 5 "R": (11, 0, 0x20) # Button 6 } # D-pad buttons to hat-switch value dpad_state = { "Up": 0, "Down": 0, "Left": 0, "Right": 0, } hat_map = { (0, 0, 0, 0): 0x08, # Released (1, 0, 0, 0): 0x00, # N (1, 1, 0, 0): 0x01, # NE (0, 1, 0, 0): 0x02, # E (0, 1, 0, 1): 0x03, # SE (0, 0, 0, 1): 0x04, # S (0, 0, 1, 1): 0x05, # SW (0, 0, 1, 0): 0x06, # W (1, 0, 1, 0): 0x07, # NW } def read_snes_controller(): data_bits = [] latch.value = True time.sleep(0.000012) # 12µs latch.value = False for _ in range(16): time.sleep(0.000006) # Wait 6µs data_bits.append(data.value) clock.value = True time.sleep(0.000006) # 6µs clock.value = False time.sleep(0.000006) # 6µs return data_bits # Find the gamepad device in the usb_hid devices gamepad_device = None for device in usb_hid.devices: if device.usage_page == 0x01 and device.usage == 0x05: gamepad_device = device break if gamepad_device is not None: print("Gamepad device found!") else: print("Gamepad device not found.") report = bytearray(19) report[2] = 0x08 # default released hat switch value report[3] = 127 # default x center value report[4] = 127 # default y center value report[5] = 127 # default z center value report[6] = 127 # default rz center value prev_report = bytearray(report) while True: button_state = read_snes_controller() all_buttons = list(buttonmap.keys()) for idx, button in enumerate(all_buttons): index, byte_index, button_value = buttonmap[button] is_pressed = not button_state [index] # True if button is pressed if button in dpad_state: # If it's a direction button dpad_state[button] = 1 if is_pressed else 0 else: if is_pressed: report[byte_index] |= button_value else: # not pressed, reset button state report[byte_index] &= ~button_value # SOCD (up priority and neutral horizontal) if (dpad_state["Up"] and dpad_state["Down"]): dpad_state["Down"] = 0 if (dpad_state["Left"] and dpad_state["Right"]): dpad_state["Left"] = 0 dpad_state["Right"] = 0 # Extract the dpad_state to a tuple and get the corresponding hat value dpad_tuple = (dpad_state["Up"], dpad_state["Right"], dpad_state["Left"], dpad_state["Down"]) report[2] = hat_map[dpad_tuple] # Assuming report[2] is the byte for the hat switch if prev_report != report: gamepad_device.send_report(report) prev_report[:] = report time.sleep(0.1)
How It Works
The code uses the digitalio
library to send the SNES console latch/clock signals and to read the controller button data wire. Then the usb_hid
library is used to send standard USB HID Gamepad reports when buttons are pressed or released.
USB HID Gamepad Descriptor
First, the boot.py file is needed to predefine the USB descriptor to appear to the computer as gamepad at boot time. Within this file the usb_hid
library is imported and a custom USB HID gamepad descriptor is defined. This descriptor follows a common pattern of DirectInput compatible USB controllers, making it easily compatible with Windows, Mac, and Linux operating systems.
The USB Gamepad descriptor defined has a size of 19 bytes.
- 1st and 2nd bytes contain the 14 button bits. (1 = pressed, 0 = released)
- 3rd byte contains a 4-bit hat switch value (d-pad)
- 4th-7th bytes are the 8-bit analog axis values X, Y, Z, Rz (left/right analog stick)
- The remaining bytes contain compatibility feature flags and vendor specific values.
Libraries
The meat of our program lives within the code.py file. It starts with the importing the necessary libraries.
import time import board import digitalio import usb_hid
Digital In Out
Next, to define the controller's Latch, Clock, and Data GPIO pin numbers. Then to create an instance of the DigitalInOut
class for each pin and set the pin directions.
Pulsing the Latch wire output high will signal to the controller the start of the next read cycle. Then the controller will wait for a sequence of Clock output pulses. Upon each clock high/low, the controller will respond with the next button bit that we will read from the data input. (0 = pressed, 1 = released)
LATCH_PIN = board.D6 CLOCK_PIN = board.D5 DATA_PIN = board.D7 # Set up pins for SNES Controller latch = digitalio.DigitalInOut(LATCH_PIN) latch.direction = digitalio.Direction.OUTPUT clock = digitalio.DigitalInOut(CLOCK_PIN) clock.direction = digitalio.Direction.OUTPUT data = digitalio.DigitalInOut(DATA_PIN) data.direction = digitalio.Direction.INPUT data.pull = digitalio.Pull.UP # pull-up as a default
SNES to USB Mapping
Next, to define the buttonmap
object that will be used to translate from the SNES controller button data read in to the USB HID report output byte and bit positions.
The buttons are defined in the order they are read in from the SNES controller. For example, SNES bit 0 is "B", bit 1 is "Y", bit 2 is "Select", etc.
"ButtonName": (SNES bit index, HID byte index, HID bit index)
# SNES to USB button mapping buttonmap = { "B": (0, 0, 0x2), # Button 1 "Y": (1, 0, 0x1), # Button 3 "Select": (2, 1, 0x01), # Button 9 "Start": (3, 1, 0x02), # Button 10 "Up": (4, 0, 0), # D-pad North "Down": (5, 0, 0), # D-pad South "Left": (6, 0, 0), # D-pad East "Right": (7, 0, 0), # D-pad West "A": (8, 0, 0x4), # Button 2 "X": (9, 0, 0x8), # Button 4 "L": (10, 0, 0x10), # Button 5 "R": (11, 0, 0x20) # Button 6 }
D-pad to Hat-switch State
Standard USB HID gamepads use a hat-switch value for directional pad button inputs. We will use the dpad_state
object to store the current d-pad directions. Then the hat_map
is used to convert that state directly to the corresponding hat-switch directional value.
# D-pad buttons to hat-switch value dpad_state = { "Up": 0, "Down": 0, "Left": 0, "Right": 0, } hat_map = { (0, 0, 0, 0): 0x08, # Released (1, 0, 0, 0): 0x00, # N (1, 1, 0, 0): 0x01, # NE (0, 1, 0, 0): 0x02, # E (0, 1, 0, 1): 0x03, # SE (0, 0, 0, 1): 0x04, # S (0, 0, 1, 1): 0x05, # SW (0, 0, 1, 0): 0x06, # W (1, 0, 1, 0): 0x07, # NW }
SNES Controller Reading
Next, we'll define the read_snes_controller()
function that will be used to poll the controller by pulsing the latch and clock outputs while reading the data input.
The function will read and return 16-bits from the controller. The first 12-bits each represent a button, while the remaining 4-bits are the controller ID (all high).
def read_snes_controller(): data_bits = [] latch.value = True time.sleep(0.000012) # 12µs latch.value = False for _ in range(16): time.sleep(0.000006) # Wait 6µs data_bits.append(data.value) clock.value = True time.sleep(0.000006) # 6µs clock.value = False time.sleep(0.000006) # 6µs return data_bits
USB HID Gamepad Device
Next, we'll define gamepad_device
with the usb_hid device predefined at boot within the boot.py file.
# Find the gamepad device in the usb_hid devices gamepad_device = None for device in usb_hid.devices: if device.usage_page == 0x01 and device.usage == 0x05: gamepad_device = device break
USB HID Output Report
Finally, we'll need to create an array of 19 bytes called report
. Set the hat-switch byte to its default value of 0x08
. Also set the unused analog values to the default center position of 127
.
Then create a prev_report
array for keeping track of when the output report needs to be updated.
report = bytearray(19) report[2] = 0x08 # default released hat switch value report[3] = 127 # default x center value report[4] = 127 # default y center value report[5] = 127 # default z center value report[6] = 127 # default rz center value prev_report = bytearray(report)
Main Loop
The main loop first reads in the SNES controller button_state
. Then each possible button state bit is checked and the corresponding USB HID output report bits are toggled depending on whether it is pressed or released. (report[0] or report[1]
)
Once the buttons have been processed, the dpad_state
will be SOCD cleaned. Meaning we will clean up opposing button direction presses to avoid them being output.
Then the dpad_state
is translated to the hat-switch value and set to the third report byte. (report[2]
)
Finally whenever the report data has changed, send the USB HID output report data with the send_report(report)
function.
while True: button_state = read_snes_controller() all_buttons = list(buttonmap.keys()) for idx, button in enumerate(all_buttons): index, byte_index, button_value = buttonmap[button] is_pressed = not button_state [index] # True if button is pressed if button in dpad_state: # If it's a direction button dpad_state[button] = 1 if is_pressed else 0 else: if is_pressed: report[byte_index] |= button_value else: # not pressed, reset button state report[byte_index] &= ~button_value # SOCD (up priority and neutral horizontal) if (dpad_state["Up"] and dpad_state["Down"]): dpad_state["Down"] = 0 if (dpad_state["Left"] and dpad_state["Right"]): dpad_state["Left"] = 0 dpad_state["Right"] = 0 # Extract the dpad_state to a tuple and get the corresponding hat value dpad_tuple = (dpad_state["Up"], dpad_state["Right"], dpad_state["Left"], dpad_state["Down"]) report[2] = hat_map[dpad_tuple] # Assuming report[2] is the byte for the hat switch if prev_report != report: gamepad_device.send_report(report) prev_report[:] = report time.sleep(0.1)
Text editor powered by tinymce.