Skip to content

Commit b11565c

Browse files
authored
Merge pull request #37 from loopj/esp32-hid-examples
Add USB HID examples for ESP32-S3
2 parents c462a4c + 76ccbc8 commit b11565c

13 files changed

Lines changed: 507 additions & 0 deletions

File tree

‎.github/workflows/esp32_examples.yml‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ jobs:
1818
example: [gcn-host, gcn-target, n64-host, n64-target]
1919
target: [esp32c3, esp32s3, esp32c6, esp32h2]
2020
include:
21+
# USB HID needs native USB-OTG, so these examples are ESP32-S3 only
22+
- idf: release-v6.0
23+
example: gcn-adapter-usb-hid
24+
target: esp32s3
25+
- idf: release-v6.0
26+
example: n64-adapter-usb-hid
27+
target: esp32s3
2128
# Also exercise the ESP-IDF v5 compatibility path
2229
- idf: release-v5.5
2330
example: n64-host
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/build/
2+
sdkconfig
3+
sdkconfig.old
4+
dependencies.lock
5+
managed_components/
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
cmake_minimum_required(VERSION 3.16)
2+
3+
# libjoybus is pulled in as a local component-manager dependency (see main/idf_component.yml).
4+
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
5+
project(gcn-adapter-usb-hid)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
idf_component_register(
2+
SRCS "main.c"
3+
INCLUDE_DIRS "."
4+
REQUIRES libjoybus esp_tinyusb)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
dependencies:
2+
libjoybus:
3+
path: ../../../..
4+
espressif/esp_tinyusb: "^2.0.0"
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
#include <esp_timer.h>
2+
3+
#include <joybus/joybus.h>
4+
#include <joybus/backend/esp32.h>
5+
6+
#include "tinyusb.h"
7+
#include "tinyusb_default_config.h"
8+
#include "class/hid/hid_device.h"
9+
10+
// Change this define to match your hardware setup
11+
#define JOYBUS_GPIO 1
12+
13+
// How often to poll the controller and send a USB report
14+
#define POLL_INTERVAL_MS 1
15+
16+
// USB identity (VID/PID from https://pid.codes)
17+
#define USB_VENDOR_ID 0x1209
18+
#define USB_PRODUCT_ID 0x5750
19+
#define USB_HID_EP_IN 0x81
20+
21+
// Joybus instance
22+
static struct joybus_esp32 esp32_bus;
23+
static struct joybus *bus = JOYBUS(&esp32_bus);
24+
25+
// Joybus state
26+
static struct joybus_id id;
27+
static struct joybus_gcn_controller_state input;
28+
static struct joybus_gcn_controller_state origin;
29+
30+
// Polling mode
31+
enum { POLL_MODE_IDENTIFY, POLL_MODE_READ };
32+
static uint8_t poll_mode = POLL_MODE_IDENTIFY;
33+
34+
// Device descriptor
35+
static const tusb_desc_device_t device_descriptor = {
36+
.bLength = sizeof(tusb_desc_device_t),
37+
.bDescriptorType = TUSB_DESC_DEVICE,
38+
.bcdUSB = 0x0200,
39+
.bDeviceClass = 0x00,
40+
.bDeviceSubClass = 0x00,
41+
.bDeviceProtocol = 0x00,
42+
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
43+
.idVendor = USB_VENDOR_ID,
44+
.idProduct = USB_PRODUCT_ID,
45+
.bcdDevice = 0x0100,
46+
.iManufacturer = 0x01,
47+
.iProduct = 0x02,
48+
.iSerialNumber = 0x00,
49+
.bNumConfigurations = 0x01,
50+
};
51+
52+
// HID report descriptor: a standard gamepad
53+
static const uint8_t hid_report_descriptor[] = {
54+
TUD_HID_REPORT_DESC_GAMEPAD(),
55+
};
56+
57+
// Configuration descriptor: a single HID interface
58+
static const uint8_t configuration_descriptor[] = {
59+
TUD_CONFIG_DESCRIPTOR(1, 1, 0, TUD_CONFIG_DESC_LEN + TUD_HID_DESC_LEN, 0x00, 100),
60+
TUD_HID_DESCRIPTOR(0, 0, HID_ITF_PROTOCOL_NONE, sizeof(hid_report_descriptor), USB_HID_EP_IN,
61+
CFG_TUD_HID_EP_BUFSIZE, 1),
62+
};
63+
64+
// String descriptors
65+
static const char *string_descriptor[] = {
66+
(const char[]){0x09, 0x04}, // 0: English (0x0409)
67+
"libjoybus", // 1: Manufacturer
68+
"GameCube Adapter", // 2: Product
69+
};
70+
71+
// Return the HID report descriptor to the USB stack
72+
uint8_t const *tud_hid_descriptor_report_cb(uint8_t instance)
73+
{
74+
return hid_report_descriptor;
75+
}
76+
77+
// The host never requests reports from us, so this stalls the request
78+
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer,
79+
uint16_t reqlen)
80+
{
81+
return 0;
82+
}
83+
84+
// The host never sends reports to us, so this is unused
85+
void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, const uint8_t *buffer,
86+
uint16_t bufsize)
87+
{
88+
}
89+
90+
// Joybus read origin callback
91+
static void joybus_read_origin_cb(struct joybus *bus, int status, void *user_data)
92+
{
93+
// Return to identify mode on any error
94+
if (status < 0)
95+
poll_mode = POLL_MODE_IDENTIFY;
96+
}
97+
98+
// Joybus identify callback
99+
static void joybus_identify_cb(struct joybus *bus, int status, void *user_data)
100+
{
101+
// Stay in identify mode on any Joybus error
102+
if (status < 0)
103+
return;
104+
105+
// Check it's a GameCube controller
106+
uint16_t type = id.type;
107+
if (!(type & JOYBUS_TYPE_GCN_DEVICE))
108+
return;
109+
110+
// Check we've received data if it's a wireless controller
111+
if ((type & JOYBUS_TYPE_GCN_WIRELESS) && !(type & JOYBUS_TYPE_GCN_WIRELESS_RECEIVED))
112+
return;
113+
114+
// Check it's a standard controller
115+
if (!(type & JOYBUS_TYPE_GCN_STANDARD))
116+
return;
117+
118+
// Read the origin, then move to polling for input
119+
joybus_gcn_read_origin_async(bus, &origin, joybus_read_origin_cb, NULL);
120+
poll_mode = POLL_MODE_READ;
121+
}
122+
123+
// Joybus GCN read callback
124+
static void joybus_read_cb(struct joybus *bus, int status, void *user_data)
125+
{
126+
// Return to identify mode on any error
127+
if (status < 0) {
128+
poll_mode = POLL_MODE_IDENTIFY;
129+
return;
130+
}
131+
132+
// Refetch the origin if the controller asks for it
133+
if (input.buttons & JOYBUS_GCN_NEED_ORIGIN)
134+
joybus_gcn_read_origin_async(bus, &origin, joybus_read_origin_cb, NULL);
135+
}
136+
137+
// Clamp an axis value based on the origin and expected resting position
138+
static inline uint8_t clamp_axis(uint8_t value, uint8_t origin, uint8_t resting)
139+
{
140+
int adjusted = value + (resting - origin);
141+
if (adjusted < 0)
142+
adjusted = 0;
143+
else if (adjusted > 255)
144+
adjusted = 255;
145+
146+
return adjusted;
147+
}
148+
149+
// Map a GameCube analog stick value (0-255) to a HID axis value (-128 to 127)
150+
static inline int8_t get_stick(uint8_t value, uint8_t origin)
151+
{
152+
return clamp_axis(value, origin, 128) - 128;
153+
}
154+
155+
// Map a GameCube trigger value (0-255) to a HID axis value (-128 to 127)
156+
static inline int8_t get_trigger(uint8_t value, uint8_t origin)
157+
{
158+
return clamp_axis(value, origin, 0) - 128;
159+
}
160+
161+
// Map GameCube buttons to HID gamepad buttons
162+
static inline uint32_t get_buttons(const struct joybus_gcn_controller_state *input)
163+
{
164+
uint32_t buttons = 0;
165+
166+
buttons |= (input->buttons & JOYBUS_GCN_BUTTON_A) ? GAMEPAD_BUTTON_A : 0;
167+
buttons |= (input->buttons & JOYBUS_GCN_BUTTON_B) ? GAMEPAD_BUTTON_B : 0;
168+
buttons |= (input->buttons & JOYBUS_GCN_BUTTON_X) ? GAMEPAD_BUTTON_X : 0;
169+
buttons |= (input->buttons & JOYBUS_GCN_BUTTON_Y) ? GAMEPAD_BUTTON_Y : 0;
170+
buttons |= (input->buttons & JOYBUS_GCN_BUTTON_START) ? GAMEPAD_BUTTON_START : 0;
171+
buttons |= (input->buttons & JOYBUS_GCN_BUTTON_Z) ? GAMEPAD_BUTTON_Z : 0;
172+
buttons |= (input->buttons & JOYBUS_GCN_BUTTON_L) ? GAMEPAD_BUTTON_TL : 0;
173+
buttons |= (input->buttons & JOYBUS_GCN_BUTTON_R) ? GAMEPAD_BUTTON_TR : 0;
174+
175+
return buttons;
176+
}
177+
178+
// Map the GameCube D-pad to a HID hat value
179+
static inline uint8_t get_hat(const struct joybus_gcn_controller_state *input)
180+
{
181+
bool up = input->buttons & JOYBUS_GCN_BUTTON_UP;
182+
bool down = input->buttons & JOYBUS_GCN_BUTTON_DOWN;
183+
bool left = input->buttons & JOYBUS_GCN_BUTTON_LEFT;
184+
bool right = input->buttons & JOYBUS_GCN_BUTTON_RIGHT;
185+
186+
if (up && right)
187+
return GAMEPAD_HAT_UP_RIGHT;
188+
if (down && right)
189+
return GAMEPAD_HAT_DOWN_RIGHT;
190+
if (down && left)
191+
return GAMEPAD_HAT_DOWN_LEFT;
192+
if (up && left)
193+
return GAMEPAD_HAT_UP_LEFT;
194+
if (up)
195+
return GAMEPAD_HAT_UP;
196+
if (right)
197+
return GAMEPAD_HAT_RIGHT;
198+
if (down)
199+
return GAMEPAD_HAT_DOWN;
200+
if (left)
201+
return GAMEPAD_HAT_LEFT;
202+
203+
return GAMEPAD_HAT_CENTERED;
204+
}
205+
206+
// Send the latest input state as a USB HID gamepad report
207+
static void send_hid_report(void)
208+
{
209+
tud_hid_gamepad_report(0, get_stick(input.stick_x, origin.stick_x), get_stick(input.stick_y, origin.stick_y),
210+
get_stick(input.substick_x, origin.substick_x), get_stick(input.substick_y, origin.substick_y),
211+
get_trigger(input.trigger_left, origin.trigger_left),
212+
get_trigger(input.trigger_right, origin.trigger_right), get_hat(&input), get_buttons(&input));
213+
}
214+
215+
// Poll the controller and send a USB report at regular intervals
216+
static void poll_task(void *arg)
217+
{
218+
switch (poll_mode) {
219+
case POLL_MODE_IDENTIFY:
220+
joybus_identify_async(bus, &id, joybus_identify_cb, NULL);
221+
break;
222+
223+
case POLL_MODE_READ:
224+
joybus_gcn_read_async(bus, JOYBUS_GCN_ANALOG_MODE_3, JOYBUS_GCN_MOTOR_STOP, &input, joybus_read_cb, NULL);
225+
226+
// Send the latest input state to the USB host
227+
if (tud_hid_ready())
228+
send_hid_report();
229+
break;
230+
}
231+
}
232+
233+
void app_main(void)
234+
{
235+
// Initialize Joybus as a host
236+
joybus_esp32_init(&esp32_bus, joybus_esp32_config_default(JOYBUS_GPIO));
237+
joybus_enable(bus, JOYBUS_MODE_HOST);
238+
239+
// Initialize TinyUSB with our gamepad descriptors
240+
tinyusb_config_t tusb_cfg = TINYUSB_DEFAULT_CONFIG();
241+
tusb_cfg.descriptor.device = &device_descriptor;
242+
tusb_cfg.descriptor.full_speed_config = configuration_descriptor;
243+
tusb_cfg.descriptor.string = string_descriptor;
244+
tusb_cfg.descriptor.string_count = sizeof(string_descriptor) / sizeof(string_descriptor[0]);
245+
ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg));
246+
247+
// Poll for Joybus data and send USB reports at regular intervals
248+
const esp_timer_create_args_t poll_args = {.callback = poll_task, .name = "poll"};
249+
esp_timer_handle_t poll_timer;
250+
esp_timer_create(&poll_args, &poll_timer);
251+
esp_timer_start_periodic(poll_timer, POLL_INTERVAL_MS * 1000);
252+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# USB HID needs native USB-OTG, so build for the ESP32-S3 (idf.py set-target esp32s3).
2+
CONFIG_ESP_CONSOLE_UART_DEFAULT=y
3+
CONFIG_TINYUSB_HID_COUNT=1
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/build/
2+
sdkconfig
3+
sdkconfig.old
4+
dependencies.lock
5+
managed_components/
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
cmake_minimum_required(VERSION 3.16)
2+
3+
# libjoybus is pulled in as a local component-manager dependency (see main/idf_component.yml).
4+
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
5+
project(n64-adapter-usb-hid)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
idf_component_register(
2+
SRCS "main.c"
3+
INCLUDE_DIRS "."
4+
REQUIRES libjoybus esp_tinyusb)

0 commit comments

Comments
 (0)