/* libusb/usb_common.c - Common framework for implementing multiple USB devices at once * * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> * SPDX-License-Identifier: AGPL-3.0-or-later */ #include <stdint.h> /* for uint{n}_t types */ #include <stddef.h> /* for size_t */ #include <string.h> /* memcpy() */ #include <stdlib.h> /* for malloc(), realloc(), reallocarray() */ #include <tusb.h> /* for various tusb_*_t types */ #include <libmisc/assert.h> #define LOG_NAME USB_COMMON #include <libmisc/log.h> #include <libusb/tusb_helpers.h> /* for LANGID_*, TU_UTF16() */ #include <libusb/usb_common.h> #include "config.h" /* Globals ********************************************************************/ uint8_t cfgnum_std = 0; static struct { uint8_t configc; uint8_t **configv; uint16_t *serial; uint8_t serial_bytelen; } globals = {0}; /* Strings ********************************************************************/ /** * Return a pointer to the USB "String Descriptor" (see USB 2.0 §9.6.7 * "String"). */ uint16_t const *tud_descriptor_string_cb(uint8_t strid, uint16_t langid) { static struct TU_ATTR_PACKED { uint8_t bLength; uint8_t bDescriptorType; uint16_t bString[(0xFF-2)/2]; } desc; TU_VERIFY_STATIC(sizeof desc == (0xFF/2)*2, "incorrect size"); static uint16_t const langids[] = { LANGID_EN_US, }; size_t bytelen = 0; if (strid == 0) { memcpy(desc.bString, langids, sizeof langids); bytelen = sizeof langids; } else { #define CONST_STR(str) bytelen = ((sizeof (str))-1); memcpy(desc.bString, (str), ((sizeof (str))-1)); switch (langid) { case LANGID_EN_US: switch (strid) { case STRID_MANUF: CONST_STR(TU_UTF16("Umorpha Systems")); break; case STRID_PRODUCT: CONST_STR(TU_UTF16("SBC-Harness Keyboard")); break; case STRID_CFG: CONST_STR(TU_UTF16("Standard Configuration")); break; case STRID_KBD_IFC: CONST_STR(TU_UTF16("Keyboard Interface")); break; case STRID_SERIAL: bytelen = globals.serial_bytelen; memcpy(desc.bString, globals.serial, bytelen); break; default: debugf("GET STRING: unknown string id=%"PRIu8, strid); return NULL; } break; default: debugf("GET STRING: unknown LANGID=%"PRIx16, langid); return NULL; } } assert(bytelen <= sizeof desc.bString); desc.bLength = bytelen + 2; desc.bDescriptorType = TUSB_DESC_STRING; return (uint16_t *)&desc; } /* Main entrypoints ***********************************************************/ void usb_common_earlyinit(uint16_t *serial, uint8_t serial_bytelen) { globals.serial = serial; globals.serial_bytelen = serial_bytelen; if (cfgnum_std) return; cfgnum_std = usb_add_config( STRID_CFG, /* iConfiguration ; Index of string descriptor describing this configuration */ TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, /* bmAttributes ; Bitmap of flags (self-powered and remote-wakeup are the only flags defined in USB 2.0) */ 100); /* bMaxPower (in mA) ; Maximum power consumption of the device when in this configuration */ } void usb_common_lateinit(void) { tud_init(CONFIG_USB_COMMON_RHPORT); } COROUTINE usb_common_cr(void *_arg) { (void) _arg; cr_begin(); for (;;) { tud_task(); cr_yield(); } cr_end(); } /* Main utilities *************************************************************/ uint8_t usb_add_config(uint8_t iConfiguration, uint8_t bmAttributes, uint8_t bMaxPower_mA) { uint8_t *desc; globals.configv = reallocarray(globals.configv, ++globals.configc, sizeof globals.configv[0]); desc = globals.configv[globals.configc-1] = malloc(TUD_CONFIG_DESC_LEN); memcpy(desc, (uint8_t[]){ /* USB configuration descriptor header (USB 2.0 §9.6.4 "Configuration") */ TUD_CONFIG_DESCRIPTOR( globals.configc, /* bConfigurationValue */ 0, /* bNumInterfaces (will be incremented by usb_add_interface() */ iConfiguration, /* iConfiguration */ TUD_CONFIG_DESC_LEN, /* wTotalLength (will be incremented by usb_add_interface() */ bmAttributes, /* bmAttributes */ bMaxPower_mA+1), /* bMaxPower (+1 because tusb just does n/2 instead of (n+1)/2) */ }, TUD_CONFIG_DESC_LEN); return globals.configc; } uint8_t usb_add_interface(uint8_t cfg_num, uint16_t ifc_len, uint8_t *ifc_dat) { assert(cfg_num > 0 && cfg_num <= globals.configc); assert(ifc_len >= 3); assert(ifc_dat); uint8_t *desc = globals.configv[cfg_num-1]; // wTotalLength uint16_t total_len = TU_U16(desc[3], desc[2]) + ifc_len; desc[3] = TU_U16_HIGH(total_len); desc[2] = TU_U16_LOW(total_len); // bNumInterfaces ifc_dat[3] = desc[4]++; desc = globals.configv[cfg_num-1] = realloc(desc, total_len); memcpy(&desc[total_len-ifc_len], ifc_dat, ifc_len); return ifc_dat[3]; } /** * Return a pointer to the USB "Device Descriptor" (see USB 2.0 §9.6.1 * "Device") as a byte-array. */ uint8_t const *tud_descriptor_device_cb(void) { /* Our device descriptor is static, so just declare it as a * static const. */ static tusb_desc_device_t desc = { .bLength = sizeof(tusb_desc_device_t), .bDescriptorType = TUSB_DESC_DEVICE, .bcdUSB = 0x200, /* USB 2.0 */ /* Use the "base device class", which means to use information * from interface descriptors instead of a global device * descriptor. */ .bDeviceClass = 0x00, .bDeviceSubClass = 0x00, .bDeviceProtocol = 0x00, .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, .idVendor = 0xCAFE, /* Vendor ID (assigned by the USB-IF) */ .idProduct = 0x4001, /* Product ID (assigned by the manufacturer) */ .bcdDevice = 0x0100, /* Device release number */ .iManufacturer = STRID_MANUF, /* Index of string descriptor describing manufacturer */ .iProduct = STRID_PRODUCT, /* Index of string descriptor describing product */ .iSerialNumber = STRID_SERIAL, /* Index of string descriptor describing the device's serial number */ .bNumConfigurations = 0, /* Number of possible configurations */ }; desc.bNumConfigurations = globals.configc; return (uint8_t const *) &desc; } /** * Return a pointer to the USB config "Configuration Descriptor" (see * USB 2.0 §9.6.4 "Configuration") for the given index. */ uint8_t const *tud_descriptor_configuration_cb(uint8_t index) { if (index >= globals.configc) return NULL; return globals.configv[index]; }