소스 검색

Zephyr port: XInput vendor class + per-mode selective class registration

- usb_xinput_class.cpp: Zephyr custom class for the Xbox/XInput interface
  (FF/5D/01 + magic 0x21 blob + interrupt IN/OUT), 20-byte report via
  opk_xinput_send; OUT rumble packet -> hapticSteamRumble. Single controller
  (multi-instance XInput is a follow-up).
- mode_xinput.cpp: route xinputSend(slot 0) to the Zephyr class instead of the
  unused TinyUSB endpoint path.
- usb_device_setup.cpp: register only the classes the active mode used, via a
  blocklist computed from opk_hid_claimed()/opk_want_webusb()/opk_xinput_want().
  This gives per-mode interface sets (clean-PS stays single-HID) and keeps
  within the nRF52840's 8 IN-endpoint budget.

Builds green. Class instance names (hid_N/cdc_acm_0/opk_*) and the endpoint
budget per mode are the key things to confirm on hardware.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Jari 4 달 전
부모
커밋
0f7ca07313

+ 1 - 0
OpenPuckZephyr/CMakeLists.txt

@@ -32,6 +32,7 @@ target_sources(app PRIVATE
   src/compat/usb_glue.cpp           # Zephyr USBD backing for the TinyUSB shim
   src/compat/usb_device_setup.cpp   # usbd_context (HID pool + CDC) bring-up
   src/compat/usb_webusb_class.cpp   # WebUSB vendor class (config panel pipe)
+  src/compat/usb_xinput_class.cpp   # XInput (Xbox) vendor class
 
   # ported firmware modules
   src/config.cpp

+ 26 - 2
OpenPuckZephyr/src/compat/usb_device_setup.cpp

@@ -20,6 +20,10 @@
 LOG_MODULE_REGISTER(opk_usbd, LOG_LEVEL_INF);
 
 extern "C" void opk_usb_msg(const enum usbd_msg_type type);
+// per-mode class-selection inputs (set by the shim / modes before attach)
+extern "C" int opk_hid_claimed(void);
+extern "C" bool opk_want_webusb(void);
+extern "C" bool opk_xinput_want(void);
 
 USBD_DEVICE_DEFINE(opk_usbd, DEVICE_DT_GET(DT_NODELABEL(zephyr_udc0)), 0x28DE,
 		   0x1304);
@@ -123,8 +127,28 @@ static int do_setup(void)
 		return err;
 	}
 
-	// Register every class instance from devicetree (HID pool + board CDC ACM).
-	err = usbd_register_all_classes(&opk_usbd, USBD_SPEED_FS, 1, NULL);
+	// Register only the classes this mode actually used. Zephyr's descriptor
+	// set is fixed once enabled, so per-mode interface selection happens here
+	// (the reboot-to-reenumerate concession). The nRF52840 has 8 IN endpoints,
+	// so registering every class at once would also overflow the budget.
+	// Names are the USBD_DEFINE_CLASS instance names: HID pool = hid_0..hid_4,
+	// the board CDC console = cdc_acm_0, plus our opk_webusb_0 / opk_xinput_0.
+	static const char *const HIDN[] = { "hid_0", "hid_1", "hid_2", "hid_3",
+					    "hid_4" };
+	const char *block[8];
+	int bi = 0;
+	int hc = opk_hid_claimed();
+	for (int i = hc; i < 5; i++)
+		block[bi++] = HIDN[i]; // unused HID pool nodes
+	if (!opk_want_webusb()) {
+		block[bi++] = "opk_webusb_0";
+		block[bi++] = "cdc_acm_0"; // CDC rides with the panel (clean-PS drops both)
+	}
+	if (!opk_xinput_want())
+		block[bi++] = "opk_xinput_0";
+	block[bi] = NULL;
+
+	err = usbd_register_all_classes(&opk_usbd, USBD_SPEED_FS, 1, block);
 	if (err) {
 		LOG_ERR("register classes failed (%d)", err);
 		return err;

+ 13 - 0
OpenPuckZephyr/src/compat/usb_glue.cpp

@@ -335,11 +335,24 @@ extern "C" int opk_webusb_read(void);
 extern "C" uint32_t opk_webusb_write(const void *data, uint32_t n);
 extern "C" void opk_webusb_flush(void);
 
+static bool s_want_webusb;
+extern "C" bool opk_want_webusb(void)
+{
+	return s_want_webusb;
+}
+extern "C" int opk_hid_claimed(void)
+{
+	return s_hid_claimed;
+}
+
 Adafruit_USBD_WebUSB::Adafruit_USBD_WebUSB()
 {
 }
 bool Adafruit_USBD_WebUSB::begin()
 {
+	// The firmware calls this exactly for modes that expose the panel (i.e.
+	// not the clean-PS modes); used to gate per-mode class registration.
+	s_want_webusb = true;
 	return true;
 }
 bool Adafruit_USBD_WebUSB::connected()

+ 185 - 0
OpenPuckZephyr/src/compat/usb_xinput_class.cpp

@@ -0,0 +1,185 @@
+// usb_xinput_class.cpp -- Zephyr custom class for the Xbox 360 (XInput) interface.
+//
+// XInput is a vendor interface (class 0xFF / sub 0x5D / proto 0x01) carrying a
+// magic 0x21 descriptor blob plus an interrupt IN/OUT endpoint pair, serving the
+// 20-byte XInput report. mode_xinput.cpp builds the report and rumble decode;
+// this class is the Zephyr USB plumbing (the TinyUSB class driver in that file
+// is unused under Zephyr).
+//
+// Single interface (one controller). Multi-controller XInput (one interface per
+// connected pad) is a follow-up — see ARCHITECTURE.zephyr.md.
+#include <zephyr/usb/usbd.h>
+#include <zephyr/drivers/usb/udc.h>
+#include <zephyr/sys/byteorder.h>
+#include <zephyr/sys/atomic.h>
+#include <zephyr/logging/log.h>
+#include <string.h>
+
+LOG_MODULE_REGISTER(opk_xinput, LOG_LEVEL_ERR);
+
+#define XI_IN_EP 0x83
+#define XI_OUT_EP 0x03
+#define XI_ENABLED 0
+
+NET_BUF_POOL_FIXED_DEFINE(xi_pool, 4, 32, sizeof(struct udc_buf_info), NULL);
+static atomic_t xi_state;
+static struct usbd_class_data *xi_cd;
+
+// rumble decode lives in mode_xinput; it provides this (weak no-op fallback).
+extern "C" __attribute__((weak)) void opk_xinput_rumble(uint8_t big, uint8_t sml)
+{
+	(void)big;
+	(void)sml;
+}
+
+// The XInput descriptor: 9-byte interface (FF/5D/01) + 17-byte magic 0x21 blob
+// (embeds the endpoint addresses) + interrupt IN + interrupt OUT. Endpoint
+// addresses are fixed so the magic blob stays consistent with the descriptors.
+struct xi_desc {
+	struct usb_if_descriptor if0;
+	uint8_t magic[17];
+	struct usb_ep_descriptor in_ep;
+	struct usb_ep_descriptor out_ep;
+	struct usb_desc_header nil;
+} __packed;
+
+static struct xi_desc xi_desc_0 = {
+	.if0 = {
+		.bLength = sizeof(struct usb_if_descriptor),
+		.bDescriptorType = USB_DESC_INTERFACE,
+		.bInterfaceNumber = 0,
+		.bAlternateSetting = 0,
+		.bNumEndpoints = 2,
+		.bInterfaceClass = 0xFF,
+		.bInterfaceSubClass = 0x5D,
+		.bInterfaceProtocol = 0x01,
+		.iInterface = 0,
+	},
+	.magic = { 0x11, 0x21, 0x00, 0x01, 0x01, 0x25, XI_IN_EP, 0x14, 0x00,
+		   0x00, 0x00, 0x00, 0x13, XI_OUT_EP, 0x08, 0x00, 0x00 },
+	.in_ep = {
+		.bLength = sizeof(struct usb_ep_descriptor),
+		.bDescriptorType = USB_DESC_ENDPOINT,
+		.bEndpointAddress = XI_IN_EP,
+		.bmAttributes = USB_EP_TYPE_INTERRUPT,
+		.wMaxPacketSize = sys_cpu_to_le16(0x20),
+		.bInterval = 1,
+	},
+	.out_ep = {
+		.bLength = sizeof(struct usb_ep_descriptor),
+		.bDescriptorType = USB_DESC_ENDPOINT,
+		.bEndpointAddress = XI_OUT_EP,
+		.bmAttributes = USB_EP_TYPE_INTERRUPT,
+		.wMaxPacketSize = sys_cpu_to_le16(0x20),
+		.bInterval = 8,
+	},
+	.nil = { .bLength = 0, .bDescriptorType = 0 },
+};
+
+const static struct usb_desc_header *xi_fs_desc_0[] = {
+	(struct usb_desc_header *)&xi_desc_0.if0,
+	(struct usb_desc_header *)&xi_desc_0.magic,
+	(struct usb_desc_header *)&xi_desc_0.in_ep,
+	(struct usb_desc_header *)&xi_desc_0.out_ep,
+	(struct usb_desc_header *)&xi_desc_0.nil,
+};
+
+static struct net_buf *xi_alloc(uint8_t ep)
+{
+	struct net_buf *buf = net_buf_alloc(&xi_pool, K_NO_WAIT);
+	if (!buf)
+		return NULL;
+	struct udc_buf_info *bi = udc_get_buf_info(buf);
+	memset(bi, 0, sizeof(*bi));
+	bi->ep = ep;
+	return buf;
+}
+
+static void xi_queue_out(struct usbd_class_data *c)
+{
+	struct net_buf *buf = xi_alloc(XI_OUT_EP);
+	if (buf && usbd_ep_enqueue(c, buf))
+		net_buf_unref(buf);
+}
+
+static int xi_request(struct usbd_class_data *c, struct net_buf *buf, int err)
+{
+	struct usbd_context *ctx = usbd_class_get_ctx(c);
+	struct udc_buf_info *bi = (struct udc_buf_info *)net_buf_user_data(buf);
+
+	if (err == 0 && bi->ep == XI_OUT_EP) {
+		// XInput rumble packet: [00][08][00][big][small]...
+		if (buf->len >= 5 && buf->data[0] == 0x00 && buf->data[1] == 0x08)
+			opk_xinput_rumble(buf->data[3], buf->data[4]);
+		usbd_ep_buf_free(ctx, buf);
+		if (atomic_test_bit(&xi_state, XI_ENABLED))
+			xi_queue_out(c);
+		return 0;
+	}
+	usbd_ep_buf_free(ctx, buf);
+	return 0;
+}
+
+static void *xi_get_desc(struct usbd_class_data *const c,
+			 const enum usbd_speed speed)
+{
+	(void)c;
+	(void)speed;
+	return xi_fs_desc_0;
+}
+static void xi_enable(struct usbd_class_data *const c)
+{
+	xi_cd = c;
+	if (!atomic_test_and_set_bit(&xi_state, XI_ENABLED))
+		xi_queue_out(c);
+}
+static void xi_disable(struct usbd_class_data *const c)
+{
+	(void)c;
+	atomic_clear_bit(&xi_state, XI_ENABLED);
+}
+static int xi_init(struct usbd_class_data *c)
+{
+	xi_cd = c;
+	return 0;
+}
+
+static struct usbd_class_api xi_api = {
+	.request = xi_request,
+	.enable = xi_enable,
+	.disable = xi_disable,
+	.init = xi_init,
+	.get_desc = xi_get_desc,
+};
+
+USBD_DEFINE_CLASS(opk_xinput_0, &xi_api, NULL, NULL);
+
+// ---- bridge for mode_xinput ----
+// Whether the XInput interface should be registered this boot (Xbox mode only).
+static bool s_xi_want;
+extern "C" void opk_xinput_begin(void)
+{
+	s_xi_want = true;
+}
+extern "C" bool opk_xinput_want(void)
+{
+	return s_xi_want;
+}
+extern "C" bool opk_xinput_ready(void)
+{
+	return atomic_test_bit(&xi_state, XI_ENABLED);
+}
+extern "C" bool opk_xinput_send(const uint8_t *rep, uint16_t n)
+{
+	if (!xi_cd || !opk_xinput_ready())
+		return false;
+	struct net_buf *buf = xi_alloc(XI_IN_EP);
+	if (!buf)
+		return false;
+	net_buf_add_mem(buf, rep, n > 32 ? 32 : n);
+	if (usbd_ep_enqueue(xi_cd, buf)) {
+		net_buf_unref(buf);
+		return false;
+	}
+	return true;
+}

+ 19 - 9
OpenPuckZephyr/src/mode_xinput.cpp

@@ -19,6 +19,17 @@
 #include <Adafruit_TinyUSB.h>
 #include <Arduino.h>
 #include <string.h>
+
+// Zephyr XInput custom class bridge (usb_xinput_class.cpp).
+extern "C" bool opk_xinput_ready(void);
+extern "C" bool opk_xinput_send(const uint8_t *rep, uint16_t n);
+extern "C" void opk_xinput_begin(void);
+// rumble decode hook the class calls on an OUT packet (-> Steam rumble relay).
+extern "C" void opk_xinput_rumble(uint8_t big, uint8_t sml)
+{
+	hapticSteamRumble((uint16_t)big * 257u, (uint16_t)sml * 257u, 0);
+}
+
 // custom class-driver API for the XInput interface
 extern "C" {
 #include "device/usbd_pvt.h"
@@ -229,12 +240,12 @@ static Adafruit_USBD_XInput g_xinput[NSLOT];
 static void xinputSend(uint8_t slot, uint16_t buttons, uint8_t lt, uint8_t rt,
 		       int16_t lx, int16_t ly, int16_t rx, int16_t ry)
 {
-	if (slot >= NSLOT || !g_xiSlot[slot].inUse)
+	// Zephyr port: a single XInput interface (controller 0) via the Zephyr
+	// custom class (usb_xinput_class.cpp). The TinyUSB per-slot endpoint
+	// bookkeeping (g_xiSlot) is unused here.
+	if (slot != 0 || !opk_xinput_ready())
 		return;
-	XiSlot &S = g_xiSlot[slot];
-	if (!tud_mounted() || S.epIn == 0 || usbd_edpt_busy(0, S.epIn))
-		return;
-	uint8_t *r = S.inBuf;
+	uint8_t r[20];
 	r[0] = 0x00;
 	r[1] = 0x14;
 	r[2] = buttons & 0xFF;
@@ -250,10 +261,7 @@ static void xinputSend(uint8_t slot, uint16_t buttons, uint8_t lt, uint8_t rt,
 	r[12] = ry & 0xFF;
 	r[13] = ry >> 8;
 	memset(r + 14, 0, 6);
-	if (usbd_edpt_claim(0, S.epIn)) {
-		if (!usbd_edpt_xfer(0, S.epIn, r, 20))
-			usbd_edpt_release(0, S.epIn);
-	}
+	opk_xinput_send(r, 20);
 }
 
 // ===================== right-pad mouse interface =====================
@@ -463,6 +471,8 @@ void XboxController::beginPool()
 	g_mouse.begin();
 	for (int s = 0; s < NSLOT; s++)
 		g_xinput[s].setStringDescriptor("Controller");
+	// Zephyr: request the XInput vendor class be registered this boot.
+	opk_xinput_begin();
 }
 void XboxController::mountSlots(uint8_t k)
 {