diff --git a/micropython/usbd/device.py b/micropython/usbd/device.py new file mode 100644 index 000000000..d7b329721 --- /dev/null +++ b/micropython/usbd/device.py @@ -0,0 +1,633 @@ +# MicroPython USB device module +# MIT license; Copyright (c) 2022 Angus Gratton +from micropython import const +import machine +import ustruct + +## +## Constants that are used by consumers of this module +## +## (TODO: decide if this is too expensive on code size) + +EP_OUT_FLAG = const(1 << 7) + +# Control transfer stages +STAGE_IDLE = const(0) +STAGE_SETUP = const(1) +STAGE_DATA = const(2) +STAGE_ACK = const(3) + +# TinyUSB xfer_result_t enum +RESULT_SUCCESS = const(0) +RESULT_FAILED = const(1) +RESULT_STALLED = const(2) +RESULT_TIMEOUT = const(3) +RESULT_INVALID = const(4) + +## +## Constants used only inside this module +## + +# USB descriptor types +_STD_DESC_DEVICE_TYPE = const(0x1) +_STD_DESC_CONFIG_TYPE = const(0x2) +_STD_DESC_STRING_TYPE = const(0x3) +_STD_DESC_INTERFACE_TYPE = const(0x4) +_STD_DESC_ENDPOINT_TYPE = const(0x5) +_STD_DESC_INTERFACE_ASSOC = const(0xB) + +# Standard USB descriptor lengths +_STD_DESC_CONFIG_LEN = const(9) +_STD_DESC_INTERFACE_LEN = const(9) +_STD_DESC_ENDPOINT_LEN = const(7) + +# Standard control request bmRequest fields, can extract by calling split_bmRequestType() +_REQ_RECIPIENT_DEVICE = const(0x0) +_REQ_RECIPIENT_INTERFACE = const(0x1) +_REQ_RECIPIENT_ENDPOINT = const(0x2) +_REQ_RECIPIENT_OTHER = const(0x3) + +REQ_TYPE_STANDARD = const(0x0) +REQ_TYPE_CLASS = const(0x1) +REQ_TYPE_VENDOR = const(0x2) +REQ_TYPE_RESERVED = const(0x3) + +# Offsets into the standard configuration descriptor, to fixup +_OFFS_CONFIG_iConfiguration = const(6) + + +# Singleton _USBDevice instance +_inst = None + + +def get(): + """Access the singleton instance of the MicroPython _USBDevice object.""" + global _inst + if not _inst: + _inst = _USBDevice() + return _inst + + +class _USBDevice: + """Class that implements the Python parts of the MicroPython USBDevice. + + This object represents any interfaces on the USB device that are implemented + in Python, and also allows disabling the 'static' USB interfaces that are + implemented in Python (if include_static property is set to False). + + Should be accessed via the singleton getter module function get(), + not instantiated directly.. + """ + + def __init__(self): + self._eps = ( + {} + ) # Mapping from each endpoint to a tuple of (interface, Optional(transfer callback)) + self._itfs = [] # Interfaces + self.include_static = True # Include static devices when enumerating? + + # Device properties, set non-NULL to override static values + self.manufacturer_str = None + self.product_str = None + self.serial_str = None + self.id_vendor = None + self.id_product = None + self.device_class = None + self.device_subclass = None + self.device_protocol = None + self.bcd_device = None + + # Configuration properties + self.config_str = None + self.max_power_ma = 50 + + self._strs = self._get_device_strs() + + usbd = self._usbd = machine.USBD() + usbd.init( + descriptor_device_cb=self._descriptor_device_cb, + descriptor_config_cb=self._descriptor_config_cb, + descriptor_string_cb=self._descriptor_string_cb, + open_driver_cb=self._open_driver_cb, + control_xfer_cb=self._control_xfer_cb, + xfer_cb=self._xfer_cb, + ) + + def add_interface(self, itf): + """Add an instance of USBInterface to the USBDevice. + + The next time USB is reenumerated (by calling .reenumerate() or + otherwise), this interface will appear to the host. + + """ + self._itfs.append(itf) + + def remove_interface(self, itf): + """Remove an instance of USBInterface from the USBDevice. + + If the USB device is currently enumerated to a host, and in particular + if any endpoint transfers are pending, then this may cause it to + misbehave as these transfers are not cancelled. + + """ + self._itfs.remove(itf) + + def reenumerate(self): + """Disconnect the USB device and then reconnect it, causing the host to reenumerate it. + + Any open USB interfaces (for example USB-CDC serial connection) will be temporarily terminated. + + This is the only way to change the composition of an existing USB device. + """ + self._usbd.reenumerate() + + def _descriptor_device_cb(self): + """Singleton callback from TinyUSB to read the USB device descriptor. + + This function will build a new device descriptor based on the 'static' + USB device values compiled into MicroPython, but many values can be + optionally overriden by setting properties of this object. + + """ + FMT = "= 0 # index shouldn't be in the static range + try: + return self._itfs[index] + except IndexError: + return None # host has old mappings for interfaces + + def _descriptor_config_cb(self): + """Singleton callback from TinyUSB to read the configuration descriptor. + + Each time this function is called (in response to a GET DESCRIPTOR - + CONFIGURATION request from the host), it rebuilds the full configuration + descriptor and also the list of strings stored in self._strs. + + This normally only happens during enumeration, but may happen more than + once (the host will first ask for a minimum length descriptor, and then + use the length field request to request the whole thing). + + """ + static = self._usbd.static + + # Rebuild the _strs list as we build the configuration descriptor + strs = self._get_device_strs() + + if self.include_static: + desc = bytearray(static.desc_cfg) + else: + desc = bytearray(_STD_DESC_CONFIG_LEN) + + self._eps = {} # rebuild endpoint mapping as we enumerate each interface + itf_idx = static.itf_max + ep_addr = static.ep_max + str_idx = static.str_max + len(strs) + for itf in self._itfs: + # Get the endpoint descriptors first so we know how many endpoints there are + ep_desc, ep_strs, ep_addrs = itf.get_endpoint_descriptors(ep_addr, str_idx) + strs += ep_strs + str_idx += len(ep_strs) + + # Now go back and get the interface descriptor + itf_desc, itf_strs = itf.get_itf_descriptor(len(ep_addrs), itf_idx, str_idx) + desc += itf_desc + strs += itf_strs + itf_idx += 1 + str_idx += len(itf_strs) + + desc += ep_desc + for e in ep_addrs: + self._eps[e] = (itf, None) # no pending transfer + # TODO: check if always incrementing leaves too many gaps + ep_addr = max((e & ~80) + 1, e) + + self._write_configuration_descriptor(desc) + + self._strs = strs + return desc + + def _write_configuration_descriptor(self, desc): + """Utility function to update the Standard Configuration Descriptor + header supplied in the argument with values based on the current state + of the device. + + See USB 2.0 specification section 9.6.3 p264 for details. + + Currently only one configuration per device is supported. + + """ + bmAttributes = ( + (1 << 7) # Reserved + | (0 if self.max_power_ma else (1 << 6)) # Self-Powered + # Remote Wakeup not currently supported + ) + + iConfiguration = self._get_str_index(self.config_str) + if self.include_static and not iConfiguration: + iConfiguration = desc[_OFFS_CONFIG_iConfiguration] + + bNumInterfaces = self._usbd.static.itf_max if self.include_static else 0 + bNumInterfaces += len(self._itfs) + + ustruct.pack_into( + "= 0 + ) # Shouldn't get any calls here where index is less than first dynamic string index + try: + return self._strs[index] + except IndexError: + return None + + def _open_driver_cb(self, interface_desc_view): + """Singleton callback from TinyUSB custom class driver""" + pass + + def _submit_xfer(self, ep_addr, data, done_cb=None): + """Singleton function to submit a USB transfer (of any type except control). + + Generally, drivers should call USBInterface.submit_xfer() instead. See that function for documentation + about the possible parameter values. + """ + itf, cb = self._eps[ep_addr] + if cb: + raise RuntimeError(f"Pending xfer on EP {ep_addr}") + if self._usbd.submit_xfer(ep_addr, data): + self._eps[ep_addr] = (itf, done_cb) + return True + return False + + def _xfer_cb(self, ep_addr, result, xferred_bytes): + """Singleton callback from TinyUSB custom class driver when a transfer completes.""" + try: + itf, cb = self._eps[ep_addr] + self._eps[ep_addr] = (itf, None) + except KeyError: + cb = None + if cb: + cb(ep_addr, result, xferred_bytes) + + def _control_xfer_cb(self, stage, request): + """Singleton callback from TinyUSB custom class driver when a control + transfer is in progress. + + stage determines appropriate responses (possible values STAGE_SETUP, + STAGE_DATA, STAGE_ACK). + + The TinyUSB class driver framework only calls this function for + particular types of control transfer, other standard control transfers + are handled by TinyUSB itself. + + """ + bmRequestType, _, _, wIndex, _ = request + recipient, _, _ = split_bmRequestType(bmRequestType) + + itf = None + result = None + + if recipient == _REQ_RECIPIENT_DEVICE: + itf = self._get_interface(wIndex & 0xFFFF) + if itf: + result = itf.handle_device_control_xfer(stage, request) + elif recipient == _REQ_RECIPIENT_INTERFACE: + itf = self._get_interface(wIndex & 0xFFFF) + if itf: + result = itf.handle_interface_control_xfer(stage, request) + elif recipient == _REQ_RECIPIENT_ENDPOINT: + ep_num = wIndex & 0xFFFF + try: + itf, _ = self._eps[ep_num] + except KeyError: + pass + if itf: + result = itf.handle_endpoint_control_xfer(stage, request) + + if not itf: + # At time this code was written, only the control transfers shown above are passed to the + # class driver callback. See invoke_class_control() in tinyusb usbd.c + print(f"Unexpected control request type {bmRequestType:#x}") + return False + + # Accept the following possible replies from handle_NNN_control_xfer(): + # + # True - Continue transfer, no data + # False - STALL transfer + # Object with buffer interface - submit this data for the control transfer + if type(result) == bool: + return result + + return self._usbd.control_xfer(request, result) + + +class USBInterface: + """Abstract base class to implement a USBInterface (and associated endpoints) in Python""" + + def __init__( + self, + bInterfaceClass=0xFF, + bInterfaceSubClass=0, + bInterfaceProtocol=0xFF, + interface_str=None, + ): + """Create a new USBInterface object. Optionally can set bInterfaceClass, + bInterfaceSubClass, bInterfaceProtocol values to specify the interface + type. Can also optionally set a string descriptor value interface_str to describe this + interface. + + The defaults are to set 'vendor' class and protocol values, the host + will not attempt to use any standard class driver to talk to this + interface. + + """ + # Defaults set "vendor" class and protocol + self.bInterfaceClass = bInterfaceClass + self.bInterfaceSubClass = bInterfaceSubClass + self.bInterfaceProtocol = bInterfaceProtocol + self.interface_str = interface_str + + def get_itf_descriptor(self, num_eps, itf_idx, str_idx): + """Return the interface descriptor binary data and associated other + descriptors for the interface (not including endpoint descriptors), plus + associated string descriptor data. + + For most types of USB interface, this function doesn't need to be + overriden. Only override if you need to append interface-specific + descriptors before the first endpoint descriptor. To return an Interface + Descriptor Association, on the first interface this function should + return the IAD descriptor followed by the Interface descriptor. + + Parameters: + + - num_eps - number of endpoints in the interface, as returned by + get_endpoint_descriptors() which is actually called before this + function. + + - itf_idx - Interface index number for this interface. + + - str_idx - First string index number to assign for any string + descriptor indexes included in the result. + + Result: + + Should be a 2-tuple: + + - Interface descriptor binary data, to return as part of the + configuration descriptor. + + - List of any strings referenced in the interface descriptor data + (indexes in the descriptor data should start from 'str_idx'.) + + See USB 2.0 specification section 9.6.5 p267 for standard interface descriptors. + + """ + desc = ustruct.pack( + "<" + "B" * _STD_DESC_INTERFACE_LEN, + _STD_DESC_INTERFACE_LEN, # bLength + _STD_DESC_INTERFACE_TYPE, # bDescriptorType + itf_idx, # bInterfaceNumber + 0, # bAlternateSetting, not currently supported + num_eps, + self.bInterfaceClass, + self.bInterfaceSubClass, + self.bInterfaceProtocol, + str_idx if self.interface_str else 0, # iInterface + ) + strs = [self.interface_str] if self.interface_str else [] + + return (desc, strs) + + def get_endpoint_descriptors(self, ep_addr, str_idx): + """Similar to get_itf_descriptor, returns descriptors for any endpoints + in this interface, plus associated other configuration descriptor data. + + The base class returns no endpoints, so usually this is overriden in the subclass. + + This function is called any time the host asks for a configuration + descriptor. It is actually called before get_itf_descriptor(), so that + the number of endpoints is known. + + Parameters: + + - ep_addr - Address for this endpoint, without any EP_OUT_FLAG (0x80) bit set. + - str_idx - Index to use for the first string descriptor in the result, if any. + + Result: + + Should be a 3-tuple: + + - Endpoint descriptor binary data and associated other descriptors for + the endpoint, to return as part of the configuration descriptor. + + - List of any strings referenced in the descriptor data (indexes in the + descriptor data should start from 'str_idx'.) + + - List of endpoint addresses referenced in the descriptor data (should + start from ep_addr, optionally with the EP_OUT_FLAG bit set.) + + """ + return (b"", [], []) + + def handle_device_control_xfer(self, stage, request): + """Control transfer callback. Override to handle a non-standard device + control transfer where bmRequestType Recipient is Device, Type is + REQ_TYPE_CLASS, and the lower byte of wIndex indicates this interface. + + (See USB 2.0 specification 9.4 Standard Device Requests, p250). + + This particular request type seems pretty uncommon for a device class + driver to need to handle, most hosts will not send this so most + implementations won't need to override it. + + Parameters: + + - stage is one of STAGE_SETUP, STAGE_DATA, STAGE_ACK. + - request is a tuple of (bmRequestType, bRequest, wValue, wIndex, wLength), as per USB 2.0 specification 9.3 USB Device Requests, p250. + + The function can call split_bmRequestType() to split bmRequestType into (Recipient, Type, Direction). + + Result: + + - True to continue the request + - False to STALL the endpoint + - A buffer interface object to provide a buffer to the host as part of the transfer, if possible. + + """ + return False + + def handle_interface_control_xfer(self, stage, request): + """Control transfer callback. Override to handle a device control + transfer where bmRequestType Recipient is Interface, and the lower byte + of wIndex indicates this interface. + + (See USB 2.0 specification 9.4 Standard Device Requests, p250). + + bmRequestType Type field may have different values. It's not necessary + to handle the mandatory Standard requests (bmRequestType Type == + REQ_TYPE_STANDARD), if the driver returns False in these cases then + TinyUSB will provide the necessary responses. + + See handle_device_control_xfer() for a description of the arguments and possible return values. + + """ + return False + + def handle_endpoint_control_xfer(self, stage, request): + """Control transfer callback. Override to handle a device + control transfer where bmRequestType Recipient is Endpoint and + the lower byte of wIndex indicates an endpoint address associated with this interface. + + bmRequestType Type will generally have any value except + REQ_TYPE_STANDARD, as Standard endpoint requests are handled by + TinyUSB. The exception is the the Standard "Set Feature" request. This + is handled by Tiny USB but also passed through to the driver in case it + needs to change any internal state, but most drivers can ignore and + return False in this case. + + (See USB 2.0 specification 9.4 Standard Device Requests, p250). + + See handle_device_control_xfer() for a description of the parameters and possible return values. + + """ + return False + + def submit_xfer(self, ep_addr, data, done_cb=None): + """Submit a USB transfer (of any type except control) + + Parameters: + + - ep_addr. Address of the endpoint to submit the transfer on. Caller is + responsible for ensuring that ep_addr is correct and belongs to this + interface. Only one transfer can be active at a time on each endpoint. + + - data. Buffer containing data to send, or for data to be read into + (depending on endpoint direction). + + - done_cb. Optional callback function for when the transfer + completes. The callback is called with arguments (ep_addr, result, + xferred_bytes) where result is one of xfer_result_t enum (see top of + this file), and xferred_bytes is an integer. + + """ + return get()._submit_xfer(ep_addr, data, done_cb) + + +def endpoint_descriptor(bEndpointAddress, bmAttributes, wMaxPacketSize, bInterval=1): + """Utility function to generate a standard Endpoint descriptor bytes object, with + the properties specified in the parameter list. + + See USB 2.0 specification section 9.6.6 Endpoint p269 + + As well as a numeric value, bmAttributes can be a string value to represent + common endpoint types: "control", "bulk", "interrupt". + + """ + bmAttributes = {"control": 0, "bulk": 2, "interrupt": 3}.get( + bmAttributes, bmAttributes + ) + return ustruct.pack( + "> 5) & 0x03, + (bmRequestType >> 7) & 0x01, + ) diff --git a/micropython/usbd/hid.py b/micropython/usbd/hid.py new file mode 100644 index 000000000..3f7e54696 --- /dev/null +++ b/micropython/usbd/hid.py @@ -0,0 +1,242 @@ +# MicroPython USB hid module +# MIT license; Copyright (c) 2022 Angus Gratton +from device import ( + USBInterface, + EP_OUT_FLAG, + endpoint_descriptor, + split_bmRequestType, + STAGE_SETUP, + REQ_TYPE_STANDARD, + REQ_TYPE_CLASS, +) +from micropython import const +import ustruct + +_DESC_HID_TYPE = const(0x21) +_DESC_REPORT_TYPE = const(0x22) +_DESC_PHYSICAL_TYPE = const(0x23) + +_INTERFACE_CLASS = const(0x03) +_INTERFACE_SUBCLASS_NONE = const(0x00) +_INTERFACE_SUBCLASS_BOOT = const(0x01) + +_INTERFACE_PROTOCOL_NONE = const(0x00) +_INTERFACE_PROTOCOL_KEYBOARD = const(0x01) +_INTERFACE_PROTOCOL_MOUSE = const(0x02) + +# bRequest values for HID control requests +_REQ_CONTROL_GET_REPORT = const(0x01) +_REQ_CONTROL_GET_IDLE = const(0x02) +_REQ_CONTROL_GET_PROTOCOL = const(0x03) +_REQ_CONTROL_GET_DESCRIPTOR = const(0x06) +_REQ_CONTROL_SET_REPORT = const(0x09) +_REQ_CONTROL_SET_IDLE = const(0x0A) +_REQ_CONTROL_SET_PROTOCOL = const(0x0B) + + +class HIDInterface(USBInterface): + """ Abstract base class to implement a USB device HID interface in Python. """ + def __init__( + self, + report_descriptor, + extra_descriptors=[], + protocol=_INTERFACE_PROTOCOL_NONE, + interface_str=None, + ): + """Construct a new HID interface. + + - report_descriptor is the only mandatory argument, which is the binary + data consisting of the HID Report Descriptor. See Device Class + Definition for Human Interface Devices (HID) v1.11 section 6.2.2 Report + Descriptor, p23. + + - extra_descriptors is an optional argument holding additional HID descriptors, to append after the mandatory report descriptor. Most HID devices do not use these. + + - protocol can be set to a specific value as per HID v1.11 section 4.3 Protocols, p9. + + - interface_str is an optional string descriptor to associate with the HID USB interface. + + """ + super().__init__( + _INTERFACE_CLASS, _INTERFACE_SUBCLASS_NONE, protocol, interface_str + ) + self.extra_descriptors = extra_descriptors + self.report_descriptor = report_descriptor + self._int_ep = None # set during enumeration + + def get_report(self): + return False + + def send_report(self, report_data): + """ Helper function to send a HID report in the typical USB interrupt endpoint associated with a HID interface. """ + return self.submit_xfer(self._int_ep, report_data) + + def get_endpoint_descriptors(self, ep_addr, str_idx): + """Return the typical single USB interrupt endpoint descriptor associated with a HID interface. + + As per HID v1.11 section 7.1 Standard Requests, return the contents of + the standard HID descriptor before the associated endpoint descriptor. + + """ + desc = self.get_hid_descriptor() + ep_addr |= EP_OUT_FLAG + desc += endpoint_descriptor(ep_addr, "interrupt", 8, 8) + self.idle_rate = 0 + self.protocol = 0 + self._int_ep = ep_addr + return (desc, [], [ep_addr]) + + def get_hid_descriptor(self): + """ Generate a full USB HID descriptor from the object's report descriptor and optional + additional descriptors. + + See HID Specification Version 1.1, Section 6.2.1 HID Descriptor p22 + """ + result = ustruct.pack( + "> 8 + if desc_type == _DESC_HID_TYPE: + return self.get_hid_descriptor() + if desc_type == _DESC_REPORT_TYPE: + return self.report_descriptor + elif req_type == REQ_TYPE_CLASS: + # HID Spec p50: 7.2 Class-Specific Requests + if bRequest == _REQ_CONTROL_GET_REPORT: + return False # Unsupported for now + if bRequest == _REQ_CONTROL_GET_IDLE: + return bytes([self.idle_rate]) + if bRequest == _REQ_CONTROL_GET_PROTOCOL: + return bytes([self.protocol]) + if bRequest == _REQ_CONTROL_SET_IDLE: + self.idle_rate = wValue >> 8 + return b"" + if bRequest == _REQ_CONTROL_SET_PROTOCOL: + self.protocol = wValue + return b"" + return False # Unsupported + + +# Basic 3-button mouse HID Report Descriptor. +# This is cribbed from Appendix E.10 of the HID v1.11 document. +_MOUSE_REPORT_DESC = bytes( + [ + 0x05, + 0x01, # Usage Page (Generic Desktop) + 0x09, + 0x02, # Usage (Mouse) + 0xA1, + 0x01, # Collection (Application) + 0x09, + 0x01, # Usage (Pointer) + 0xA1, + 0x00, # Collection (Physical) + 0x05, + 0x09, # Usage Page (Buttons) + 0x19, + 0x01, # Usage Minimum (01), + 0x29, + 0x03, # Usage Maximun (03), + 0x15, + 0x00, # Logical Minimum (0), + 0x25, + 0x01, # Logical Maximum (1), + 0x95, + 0x03, # Report Count (3), + 0x75, + 0x01, # Report Size (1), + 0x81, + 0x02, # Input (Data, Variable, Absolute), ;3 button bits + 0x95, + 0x01, # Report Count (1), + 0x75, + 0x05, # Report Size (5), + 0x81, + 0x01, # Input (Constant), ;5 bit padding + 0x05, + 0x01, # Usage Page (Generic Desktop), + 0x09, + 0x30, # Usage (X), + 0x09, + 0x31, # Usage (Y), + 0x15, + 0x81, # Logical Minimum (-127), + 0x25, + 0x7F, # Logical Maximum (127), + 0x75, + 0x08, # Report Size (8), + 0x95, + 0x02, # Report Count (2), + 0x81, + 0x06, # Input (Data, Variable, Relative), ;2 position bytes (X & Y) + 0xC0, # End Collection, + 0xC0, # End Collection + ] +) + + +class MouseInterface(HIDInterface): + """ Very basic synchronous USB mouse HID interface """ + def __init__(self): + super().__init__( + _MOUSE_REPORT_DESC, + protocol=_INTERFACE_PROTOCOL_MOUSE, + interface_str="MP Mouse!", + ) + self._l = False # Left button + self._m = False # Middle button + self._r = False # Right button + + def send_report(self, dx=0, dy=0): + # TODO: this bytes object gets constructed each time a report is sent, + # may make more sense to assign it once? + report = ustruct.pack("BBB", (1 << 0) if self._l else 0 | + (1 << 1) if self._r else 0 | + (1 << 2) if self._m else 0, + dx, dy) + super().send_report(report) + + def click_left(self, down=True): + self._l = down + self.send_report() + + def click_middle(self, down=True): + self._m = down + self.send_report() + + def click_right(self, down=True): + self._r = down + self.send_report() + + def move_by(self, dx, dy): + # dx, dy are -127, 127 in range + self.send_report(dx, dy)