Skip to content

Commit 23c79df

Browse files
authored
Merge pull request #2625 from RobertDaleSmith/super-nintendo-usb-controller
Cool stuff!
2 parents 5a7d086 + 956b146 commit 23c79df

File tree

2 files changed

+202
-0
lines changed

2 files changed

+202
-0
lines changed

Super_Nintendo_USB_Controller/boot.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# SPDX-FileCopyrightText: 2023 Robert Dale Smith for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: Unlicense
4+
5+
import usb_hid
6+
7+
# This is only one example of a gamepad descriptor, and may not suit your needs.
8+
GAMEPAD_REPORT_DESCRIPTOR = bytes((
9+
0x05, 0x01, # USAGE_PAGE (Generic Desktop)
10+
0x09, 0x05, # USAGE (Gamepad)
11+
0xa1, 0x01, # COLLECTION (Application)
12+
0x15, 0x00, # LOGICAL_MINIMUM (0)
13+
0x25, 0x01, # LOGICAL_MAXIMUM (1)
14+
0x35, 0x00, # PHYSICAL_MINIMUM (0)
15+
0x45, 0x01, # PHYSICAL_MAXIMUM (1)
16+
0x75, 0x01, # REPORT_SIZE (1)
17+
0x95, 0x0e, # REPORT_COUNT (14)
18+
0x05, 0x09, # USAGE_PAGE (Button)
19+
0x19, 0x01, # USAGE_MINIMUM (Button 1)
20+
0x29, 0x0e, # USAGE_MAXIMUM (Button 14)
21+
0x81, 0x02, # INPUT (Data,Var,Abs)
22+
0x95, 0x02, # REPORT_COUNT (3)
23+
0x81, 0x01, # INPUT (Cnst,Ary,Abs)
24+
0x05, 0x01, # USAGE_PAGE (Generic Desktop)
25+
0x25, 0x07, # LOGICAL_MAXIMUM (7)
26+
0x46, 0x3b, 0x01, # PHYSICAL_MAXIMUM (315)
27+
0x75, 0x04, # REPORT_SIZE (4)
28+
0x95, 0x01, # REPORT_COUNT (1)
29+
0x65, 0x14, # UNIT (Eng Rot:Angular Pos)
30+
0x09, 0x39, # USAGE (Hat switch)
31+
0x81, 0x42, # INPUT (Data,Var,Abs,Null)
32+
0x65, 0x00, # UNIT (None)
33+
0x95, 0x01, # REPORT_COUNT (1)
34+
0x81, 0x01, # INPUT (Cnst,Ary,Abs)
35+
0x26, 0xff, 0x00, # LOGICAL_MAXIMUM (255)
36+
0x46, 0xff, 0x00, # PHYSICAL_MAXIMUM (255)
37+
0x09, 0x30, # USAGE (X)
38+
0x09, 0x31, # USAGE (Y)
39+
0x09, 0x32, # USAGE (Z)
40+
0x09, 0x35, # USAGE (Rz)
41+
0x75, 0x08, # REPORT_SIZE (8)
42+
0x95, 0x04, # REPORT_COUNT (6)
43+
0x81, 0x02, # INPUT (Data,Var,Abs)
44+
0x06, 0x00, 0xff, # USAGE_PAGE (Vendor Specific)
45+
0x09, 0x20, # Unknown
46+
0x09, 0x21, # Unknown
47+
0x09, 0x22, # Unknown
48+
0x09, 0x23, # Unknown
49+
0x09, 0x24, # Unknown
50+
0x09, 0x25, # Unknown
51+
0x09, 0x26, # Unknown
52+
0x09, 0x27, # Unknown
53+
0x09, 0x28, # Unknown
54+
0x09, 0x29, # Unknown
55+
0x09, 0x2a, # Unknown
56+
0x09, 0x2b, # Unknown
57+
0x95, 0x0c, # REPORT_COUNT (12)
58+
0x81, 0x02, # INPUT (Data,Var,Abs)
59+
0x0a, 0x21, 0x26, # Unknown
60+
0x95, 0x08, # REPORT_COUNT (8)
61+
0xb1, 0x02, # FEATURE (Data,Var,Abs)
62+
0xc0 # END_COLLECTION
63+
))
64+
65+
gamepad = usb_hid.Device(
66+
report_descriptor=GAMEPAD_REPORT_DESCRIPTOR,
67+
usage_page=0x01, # Generic Desktop Control
68+
usage=0x05, # Gamepad
69+
report_ids=(0,), # Descriptor uses report ID 0.
70+
in_report_lengths=(19,), # This gamepad sends 6 bytes in its report.
71+
out_report_lengths=(0,), # It does not receive any reports.
72+
)
73+
74+
usb_hid.enable((gamepad,))

Super_Nintendo_USB_Controller/code.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# SPDX-FileCopyrightText: 2023 Robert Dale Smith for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
# Simple Super Nintendo controller to standard USB HID gamepad with DirectInput button mapping.
5+
# Tested on KB2040
6+
7+
import time
8+
import board
9+
import digitalio
10+
import usb_hid
11+
12+
# Update the SNES Controller pins based on your input
13+
LATCH_PIN = board.D6
14+
CLOCK_PIN = board.D5
15+
DATA_PIN = board.D7
16+
17+
# Set up pins for SNES Controller
18+
latch = digitalio.DigitalInOut(LATCH_PIN)
19+
latch.direction = digitalio.Direction.OUTPUT
20+
21+
clock = digitalio.DigitalInOut(CLOCK_PIN)
22+
clock.direction = digitalio.Direction.OUTPUT
23+
24+
data = digitalio.DigitalInOut(DATA_PIN)
25+
data.direction = digitalio.Direction.INPUT
26+
data.pull = digitalio.Pull.UP # pull-up as a default
27+
28+
# SNES to USB button mapping
29+
buttonmap = {
30+
"B": (0, 0, 0x2), # Button 1
31+
"Y": (1, 0, 0x1), # Button 3
32+
"Select": (2, 1, 0x01), # Button 9
33+
"Start": (3, 1, 0x02), # Button 10
34+
"Up": (4, 0, 0), # D-pad North
35+
"Down": (5, 0, 0), # D-pad South
36+
"Left": (6, 0, 0), # D-pad East
37+
"Right": (7, 0, 0), # D-pad West
38+
"A": (8, 0, 0x4), # Button 2
39+
"X": (9, 0, 0x8), # Button 4
40+
"L": (10, 0, 0x10), # Button 5
41+
"R": (11, 0, 0x20) # Button 6
42+
}
43+
44+
# D-pad buttons to hat-switch value
45+
dpad_state = {
46+
"Up": 0,
47+
"Down": 0,
48+
"Left": 0,
49+
"Right": 0,
50+
}
51+
52+
hat_map = {
53+
(0, 0, 0, 0): 0x08, # Released
54+
(1, 0, 0, 0): 0x00, # N
55+
(1, 1, 0, 0): 0x01, # NE
56+
(0, 1, 0, 0): 0x02, # E
57+
(0, 1, 0, 1): 0x03, # SE
58+
(0, 0, 0, 1): 0x04, # S
59+
(0, 0, 1, 1): 0x05, # SW
60+
(0, 0, 1, 0): 0x06, # W
61+
(1, 0, 1, 0): 0x07, # NW
62+
}
63+
64+
def read_snes_controller():
65+
data_bits = []
66+
latch.value = True
67+
time.sleep(0.000012) # 12µs
68+
latch.value = False
69+
70+
for _ in range(16):
71+
time.sleep(0.000006) # Wait 6µs
72+
data_bits.append(data.value)
73+
74+
clock.value = True
75+
time.sleep(0.000006) # 6µs
76+
clock.value = False
77+
time.sleep(0.000006) # 6µs
78+
79+
return data_bits
80+
81+
# Find the gamepad device in the usb_hid devices
82+
gamepad_device = None
83+
for device in usb_hid.devices:
84+
if device.usage_page == 0x01 and device.usage == 0x05:
85+
gamepad_device = device
86+
break
87+
88+
if gamepad_device is not None:
89+
print("Gamepad device found!")
90+
else:
91+
print("Gamepad device not found.")
92+
93+
report = bytearray(19)
94+
report[2] = 0x08 # default released hat switch value
95+
prev_report = bytearray(report)
96+
97+
while True:
98+
button_state = read_snes_controller()
99+
all_buttons = list(buttonmap.keys())
100+
101+
for idx, button in enumerate(all_buttons):
102+
index, byte_index, button_value = buttonmap[button]
103+
is_pressed = not button_state [index] # True if button is pressed
104+
105+
if button in dpad_state: # If it's a direction button
106+
dpad_state[button] = 1 if is_pressed else 0
107+
else:
108+
if is_pressed:
109+
report[byte_index] |= button_value
110+
else: # not pressed, reset button state
111+
report[byte_index] &= ~button_value
112+
113+
# SOCD (up priority and neutral horizontal)
114+
if (dpad_state["Up"] and dpad_state["Down"]):
115+
dpad_state["Down"] = 0
116+
if (dpad_state["Left"] and dpad_state["Right"]):
117+
dpad_state["Left"] = 0
118+
dpad_state["Right"] = 0
119+
120+
# Extract the dpad_state to a tuple and get the corresponding hat value
121+
dpad_tuple = (dpad_state["Up"], dpad_state["Right"], dpad_state["Left"], dpad_state["Down"])
122+
report[2] = hat_map[dpad_tuple] # Assuming report[2] is the byte for the hat switch
123+
124+
if prev_report != report:
125+
gamepad_device.send_report(report)
126+
prev_report[:] = report
127+
128+
time.sleep(0.1)

0 commit comments

Comments
 (0)