diff --git a/docs/library/machine.USBDevice.rst b/docs/library/machine.USBDevice.rst new file mode 100644 index 0000000000..82897b280d --- /dev/null +++ b/docs/library/machine.USBDevice.rst @@ -0,0 +1,286 @@ +.. currentmodule:: machine +.. _machine.USBDevice: + +class USBDevice -- USB Device driver +==================================== + +.. note:: ``machine.USBDevice`` is currently only supported on the rp2 and samd + ports. + +USBDevice provides a low-level Python API for implementing USB device functions using +Python code. This low-level API assumes familiarity with the USB standard. It's +not recommended to use this API directly, instead install the high-level usbd +module from micropython-lib. + +.. warning:: This functionality is very new and the high-level usbd module is + not yet merged into micropython-lib. It can be found `here on + GitHub `_. + +Terminology +----------- + +- A "Runtime" USB device interface or driver is one which is defined using this + Python API after MicroPython initially starts up. + +- A "Built-in" USB device interface or driver is one that is compiled into the + MicroPython firmware, and is always available. Examples are USB-CDC (serial + port) which is usually enabled by default. Built-in USB-MSC (Mass Storage) is an + option on some ports. + +Lifecycle +--------- + +Managing a runtime USB interface can be tricky, especially if you are communicating +with MicroPython over a built-in USB-CDC serial port that's part of the same USB +device. + +- A MicroPython soft reset will always clear all runtime USB interfaces, which + results in the entire USB device disconnecting from the host. If MicroPython + is also providing a built-in USB-CDC serial port then this will re-appear + after the soft reset. + + This means some functions (like ``mpremote run``) that target the USB-CDC + serial port will immediately fail if a runtime USB interface is active, + because the port goes away when ``mpremote`` triggers a soft reset. The + operation should succeed on the second try, as after the soft reset there is + no more runtime USB interface. + +- To configure a runtime USB device on every boot, it's recommended to place the + configuration code in the ``boot.py`` file on the :ref:`device VFS + `. On each reset this file is executed before the USB subsystem is + initialised (and before ``main.py``), so it allows the board to come up with the runtime + USB device immediately. + +- For development or debugging, it may be convenient to connect a hardware + serial REPL and disable the built-in USB-CDC serial port entirely. Not all ports + support this (currently only ``rp2``). The custom build should be configured + with ``#define MICROPY_HW_USB_CDC (0)`` and ``#define + MICROPY_HW_ENABLE_UART_REPL (1)``. + +Constructors +------------ + +.. class:: USBDevice() + + Construct a USBDevice object. + + .. note:: This object is a singleton, each call to this constructor + returns the same object reference. + +Methods +------- + +.. method:: USBDevice.config(desc_dev, desc_cfg, desc_strs=None, open_itf_cb=None, reset_cb=None, control_xfer_cb=None, xfer_cb=None) + + Configures the ``USBDevice`` singleton object with the USB runtime device + state and callback functions: + + - ``desc_dev`` - A bytes-like object containing + the new USB device descriptor. + + - ``desc_cfg`` - A bytes-like object containing the + new USB configuration descriptor. + + - ``desc_strs`` - Optional object holding strings or bytes objects + containing USB string descriptor values. Can be a list, a dict, or any + object which supports subscript indexing with integer keys (USB string + descriptor index). + + Strings are an optional USB feature, and this parameter can be unset + (default) if no strings are referenced in the device and configuration + descriptors, or if only built-in strings should be used. + + Apart from index 0, all the string values should be plain ASCII. Index 0 + is the special "languages" USB descriptor, represented as a bytes object + with a custom format defined in the USB standard. ``None`` can be + returned at index 0 in order to use a default "English" language + descriptor. + + To fall back to providing a built-in string value for a given index, a + subscript lookup can return ``None``, raise ``KeyError``, or raise + ``IndexError``. + + - ``open_itf_cb`` - This callback is called once for each interface + or Interface Association Descriptor in response to a Set + Configuration request from the USB Host (the final stage before + the USB device is available to the host). + + The callback takes a single argument, which is a memoryview of the + interface or IAD descriptor that the host is accepting (including + all associated descriptors). It is a view into the same + ``desc_cfg`` object that was provided as a separate + argument to this function. The memoryview is only valid until the + callback function returns. + + - ``reset_cb`` - This callback is called when the USB host performs + a bus reset. The callback takes no arguments. Any in-progress + transfers will never complete. The USB host will most likely + proceed to re-enumerate the USB device by calling the descriptor + callbacks and then ``open_itf_cb()``. + + - ``control_xfer_cb`` - This callback is called one or more times + for each USB control transfer (device Endpoint 0). It takes two + arguments. + + The first argument is the control transfer stage. It is one of: + + - ``1`` for SETUP stage. + - ``2`` for DATA stage. + - ``3`` for ACK stage. + + Second argument is a memoryview to read the USB control request + data for this stage. The memoryview is only valid until the + callback function returns. + + The callback should return one of the following values: + + - ``False`` to stall the endpoint and reject the transfer. + - ``True`` to continue the transfer to the next stage. + - A buffer object to provide data for this stage of the transfer. + This should be a writable buffer for an ``OUT`` direction transfer, or a + readable buffer with data for an ``IN`` direction transfer. + + - ``xfer_cb`` - This callback is called whenever a non-control + transfer submitted by calling :func:`USBDevice.submit_xfer` completes. + + The callback has three arguments: + + 1. The Endpoint number for the completed transfer. + 2. Result value: ``True`` if the transfer succeeded, ``False`` + otherwise. + 3. Number of bytes successfully transferred. In the case of a + "short" transfer, The result is ``True`` and ``xferred_bytes`` + will be smaller than the length of the buffer submitted for the + transfer. + + .. note:: If a bus reset occurs (see :func:`USBDevice.reset`), + ``xfer_cb`` is not called for any transfers that have not + already completed. + +.. method:: USBDevice.active(self, [value] /) + + Returns the current active state of this runtime USB device as a + boolean. The runtime USB device is "active" when it is available to + interact with the host, it doesn't mean that a USB Host is actually + present. + + If the optional ``value`` argument is set to a truthy value, then + the USB device will be activated. + + If the optional ``value`` argument is set to a falsey value, then + the USB device is deactivated. While the USB device is deactivated, + it will not be detected by the USB Host. + + To simulate a disconnect and a reconnect of the USB device, call + ``active(False)`` followed by ``active(True)``. This may be + necessary if the runtime device configuration has changed, so that + the host sees the new device. + +.. attribute:: USDBD.builtin_driver + + This attribute holds the current built-in driver configuration, and must be + set to one of the ``USBDevice.BUILTIN_`` named constants defined on this object. + + By default it holds the value :data:`USBDevice.BUILTIN_NONE`. + + Runtime USB device must be inactive when setting this field. Call the + :func:`USBDevice.active` function to deactivate before setting if necessary + (and again to activate after setting). + + If this value is set to any value other than :data:`USBDevice.BUILTIN_NONE` then + the following restrictions apply to the :func:`USBDevice.config` arguments: + + - ``desc_cfg`` should begin with the built-in USB interface descriptor data + accessible via :data:`USBDevice.builtin_driver` attribute ``desc_cfg``. + Descriptors appended after the built-in configuration descriptors should use + interface, string and endpoint numbers starting from the max built-in values + defined in :data:`USBDevice.builtin_driver` attributes ``itf_max``, ``str_max`` and + ``ep_max``. + + - The ``bNumInterfaces`` field in the built-in configuration + descriptor will also need to be updated if any new interfaces + are appended to the end of ``desc_cfg``. + + - ``desc_strs`` should either be ``None`` or a list/dictionary where index + values less than ``USBDevice.builtin_driver.str_max`` are missing or have + value ``None``. This reserves those string indexes for the built-in + drivers. Placing a different string at any of these indexes overrides that + string in the built-in driver. + +.. method:: USBDevice.submit_xfer(self, ep, buffer /) + + Submit a USB transfer on endpoint number ``ep``. ``buffer`` must be + an object implementing the buffer interface, with read access for + ``IN`` endpoints and write access for ``OUT`` endpoints. + + .. note:: ``ep`` cannot be the control Endpoint number 0. Control + transfers are built up through successive executions of + ``control_xfer_cb``, see above. + + Returns ``True`` if successful, ``False`` if the transfer could not + be queued (as USB device is not configured by host, or because + another transfer is queued on this endpoint.) + + When the USB host completes the transfer, the ``xfer_cb`` callback + is called (see above). + + Raises ``OSError`` with reason ``MP_EINVAL`` If the USB device is not + active. + +.. method:: USBDevice.stall(self, ep, [stall] /) + + Calling this function gets or sets the STALL state of a device endpoint. + + ``ep`` is the number of the endpoint. + + If the optional ``stall`` parameter is set, this is a boolean flag + for the STALL state. + + The return value is the current stall state of the endpoint (before + any change made by this function). + + An endpoint that is set to STALL may remain stalled until this + function is called again, or STALL may be cleared automatically by + the USB host. + + Raises ``OSError`` with reason ``MP_EINVAL`` If the USB device is not + active. + +Constants +--------- + +.. data:: USBDevice.BUILTIN_NONE +.. data:: USBDevice.BUILTIN_DEFAULT +.. data:: USBDevice.BUILTIN_CDC +.. data:: USBDevice.BUILTIN_MSC +.. data:: USBDevice.BUILTIN_CDC_MSC + + These constant objects hold the built-in descriptor data which is + compiled into the MicroPython firmware. ``USBDevice.BUILTIN_NONE`` and + ``USBDevice.BUILTIN_DEFAULT`` are always present. Additional objects may be present + depending on the firmware build configuration and the actual built-in drivers. + + .. note:: Currently at most one of ``USBDevice.BUILTIN_CDC``, + ``USBDevice.BUILTIN_MSC`` and ``USBDevice.BUILTIN_CDC_MSC`` is defined + and will be the same object as ``USBDevice.BUILTIN_DEFAULT``. + These constants are defined to allow run-time detection of + the built-in driver (if any). Support for selecting one of + multiple built-in driver configurations may be added in the + future. + + These values are assigned to :data:`USBDevice.builtin_driver` to get/set the + built-in configuration. + + Each object contains the following read-only fields: + + - ``itf_max`` - One more than the highest bInterfaceNumber value used + in the built-in configuration descriptor. + - ``ep_max`` - One more than the highest bEndpointAddress value used + in the built-in configuration descriptor. Does not include any + ``IN`` flag bit (0x80). + - ``str_max`` - One more than the highest string descriptor index + value used by any built-in descriptor. + - ``desc_dev`` - ``bytes`` object containing the built-in USB device + descriptor. + - ``desc_cfg`` - ``bytes`` object containing the complete built-in USB + configuration descriptor. diff --git a/docs/library/machine.rst b/docs/library/machine.rst index 3f5cd6f13c..532266d1d9 100644 --- a/docs/library/machine.rst +++ b/docs/library/machine.rst @@ -265,3 +265,4 @@ Classes machine.WDT.rst machine.SD.rst machine.SDCard.rst + machine.USBDevice.rst diff --git a/extmod/extmod.cmake b/extmod/extmod.cmake index 53c7740132..957580d15d 100644 --- a/extmod/extmod.cmake +++ b/extmod/extmod.cmake @@ -18,6 +18,7 @@ set(MICROPY_SOURCE_EXTMOD ${MICROPY_EXTMOD_DIR}/machine_signal.c ${MICROPY_EXTMOD_DIR}/machine_spi.c ${MICROPY_EXTMOD_DIR}/machine_uart.c + ${MICROPY_EXTMOD_DIR}/machine_usb_device.c ${MICROPY_EXTMOD_DIR}/machine_wdt.c ${MICROPY_EXTMOD_DIR}/modbluetooth.c ${MICROPY_EXTMOD_DIR}/modframebuf.c diff --git a/extmod/extmod.mk b/extmod/extmod.mk index f0428bcd05..4f5f7d53ac 100644 --- a/extmod/extmod.mk +++ b/extmod/extmod.mk @@ -15,6 +15,7 @@ SRC_EXTMOD_C += \ extmod/machine_spi.c \ extmod/machine_timer.c \ extmod/machine_uart.c \ + extmod/machine_usb_device.c \ extmod/machine_wdt.c \ extmod/modasyncio.c \ extmod/modbinascii.c \ diff --git a/extmod/machine_usb_device.c b/extmod/machine_usb_device.c new file mode 100644 index 0000000000..69c3f38487 --- /dev/null +++ b/extmod/machine_usb_device.c @@ -0,0 +1,335 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2024 Angus Gratton + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "py/mpconfig.h" + +#if MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE + +#include "mp_usbd.h" +#include "py/mperrno.h" +#include "py/objstr.h" + +// Implements the singleton runtime USB object +// +// Currently this implementation references TinyUSB directly. + +#ifndef NO_QSTR +#include "device/usbd_pvt.h" +#endif + +#define HAS_BUILTIN_DRIVERS (MICROPY_HW_USB_CDC || MICROPY_HW_USB_MSC) + +const mp_obj_type_t machine_usb_device_type; + +static mp_obj_t usb_device_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { + (void)type; + (void)n_args; + (void)n_kw; + (void)args; + + if (MP_STATE_VM(usbd) == MP_OBJ_NULL) { + mp_obj_usb_device_t *o = m_new0(mp_obj_usb_device_t, 1); + o->base.type = &machine_usb_device_type; + o->desc_dev = mp_const_none; + o->desc_cfg = mp_const_none; + o->desc_strs = mp_const_none; + o->open_itf_cb = mp_const_none; + o->reset_cb = mp_const_none; + o->control_xfer_cb = mp_const_none; + o->xfer_cb = mp_const_none; + for (int i = 0; i < CFG_TUD_ENDPPOINT_MAX; i++) { + o->xfer_data[i][0] = mp_const_none; + o->xfer_data[i][1] = mp_const_none; + } + o->builtin_driver = MP_OBJ_FROM_PTR(&mp_type_usb_device_builtin_none); + o->active = false; // Builtin USB may be active already, but runtime is inactive + o->trigger = false; + o->control_data = MP_OBJ_TO_PTR(mp_obj_new_memoryview('B', 0, NULL)); + o->num_pend_excs = 0; + for (int i = 0; i < MP_USBD_MAX_PEND_EXCS; i++) { + o->pend_excs[i] = mp_const_none; + } + + MP_STATE_VM(usbd) = MP_OBJ_FROM_PTR(o); + } + + return MP_STATE_VM(usbd); +} + +// Utility helper to raise an error if USB device is not active +// (or if a change of active state is triggered but not processed.) +static void usb_device_check_active(mp_obj_usb_device_t *usbd) { + if (!usbd->active || usbd->trigger) { + mp_raise_OSError(MP_EINVAL); + } +} + +static mp_obj_t usb_device_submit_xfer(mp_obj_t self, mp_obj_t ep, mp_obj_t buffer) { + mp_obj_usb_device_t *usbd = (mp_obj_usb_device_t *)MP_OBJ_TO_PTR(self); + int ep_addr; + mp_buffer_info_t buf_info = { 0 }; + bool result; + + usb_device_check_active(usbd); + + // Unmarshal arguments, raises TypeError if invalid + ep_addr = mp_obj_get_int(ep); + mp_get_buffer_raise(buffer, &buf_info, ep_addr & TUSB_DIR_IN_MASK ? MP_BUFFER_READ : MP_BUFFER_RW); + + uint8_t ep_num = tu_edpt_number(ep_addr); + uint8_t ep_dir = tu_edpt_dir(ep_addr); + + if (ep_num == 0 || ep_num >= CFG_TUD_ENDPPOINT_MAX) { + // TinyUSB usbd API doesn't range check arguments, so this check avoids + // out of bounds array access, or submitting transfers on the control endpoint. + // + // This C layer doesn't otherwise keep track of which endpoints the host + // is aware of (or not). + mp_raise_ValueError("ep"); + } + + if (!usbd_edpt_claim(USBD_RHPORT, ep_addr)) { + mp_raise_OSError(MP_EBUSY); + } + + result = usbd_edpt_xfer(USBD_RHPORT, ep_addr, buf_info.buf, buf_info.len); + + if (result) { + // Store the buffer object until the transfer completes + usbd->xfer_data[ep_num][ep_dir] = buffer; + } + + return mp_obj_new_bool(result); +} +static MP_DEFINE_CONST_FUN_OBJ_3(usb_device_submit_xfer_obj, usb_device_submit_xfer); + +static mp_obj_t usb_device_active(size_t n_args, const mp_obj_t *args) { + mp_obj_usb_device_t *usbd = (mp_obj_usb_device_t *)MP_OBJ_TO_PTR(args[0]); + + bool result = usbd->active; + if (n_args == 2) { + bool value = mp_obj_is_true(args[1]); + + if (value != result) { + if (value + && !mp_usb_device_builtin_enabled(usbd) + && usbd->desc_dev == mp_const_none) { + // Only allow activating if config() has already been called to set some descriptors, or a + // built-in driver is enabled + mp_raise_OSError(MP_EINVAL); + } + + // Any change to active state is triggered here and processed + // from the TinyUSB task. + usbd->active = value; + usbd->trigger = true; + if (value) { + mp_usbd_init(); // Ensure TinyUSB has initialised by this point + } + mp_usbd_schedule_task(); + } + } + + return mp_obj_new_bool(result); +} +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(usb_device_active_obj, 1, 2, usb_device_active); + +static mp_obj_t usb_device_stall(size_t n_args, const mp_obj_t *args) { + mp_obj_usb_device_t *self = (mp_obj_usb_device_t *)MP_OBJ_TO_PTR(args[0]); + int epnum = mp_obj_get_int(args[1]); + + usb_device_check_active(self); + + mp_obj_t res = mp_obj_new_bool(usbd_edpt_stalled(USBD_RHPORT, epnum)); + + if (n_args == 3) { // Set stall state + mp_obj_t stall = args[2]; + if (mp_obj_is_true(stall)) { + usbd_edpt_stall(USBD_RHPORT, epnum); + } else { + usbd_edpt_clear_stall(USBD_RHPORT, epnum); + } + } + + return res; +} +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(usb_device_stall_obj, 2, 3, usb_device_stall); + +// Configure the singleton USB device with all of the relevant transfer and descriptor +// callbacks for dynamic devices. +static mp_obj_t usb_device_config(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + mp_obj_usb_device_t *self = (mp_obj_usb_device_t *)MP_OBJ_TO_PTR(pos_args[0]); + + enum { ARG_desc_dev, ARG_desc_cfg, ARG_desc_strs, ARG_open_itf_cb, + ARG_reset_cb, ARG_control_xfer_cb, ARG_xfer_cb, ARG_active }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_desc_dev, MP_ARG_OBJ | MP_ARG_REQUIRED }, + { MP_QSTR_desc_cfg, MP_ARG_OBJ | MP_ARG_REQUIRED }, + { MP_QSTR_desc_strs, MP_ARG_OBJ | MP_ARG_REQUIRED }, + { MP_QSTR_open_itf_cb, MP_ARG_OBJ, {.u_obj = mp_const_none} }, + { MP_QSTR_reset_cb, MP_ARG_OBJ, {.u_obj = mp_const_none} }, + { MP_QSTR_control_xfer_cb, MP_ARG_OBJ, {.u_obj = mp_const_none} }, + { MP_QSTR_xfer_cb, MP_ARG_OBJ, {.u_obj = mp_const_none} }, + }; + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + // Check descriptor arguments + mp_obj_t desc_dev = args[ARG_desc_dev].u_obj; + mp_obj_t desc_cfg = args[ARG_desc_cfg].u_obj; + mp_obj_t desc_strs = args[ARG_desc_strs].u_obj; + if (!MP_OBJ_TYPE_HAS_SLOT(mp_obj_get_type(desc_dev), buffer)) { + mp_raise_ValueError(MP_ERROR_TEXT("desc_dev")); + } + if (!MP_OBJ_TYPE_HAS_SLOT(mp_obj_get_type(desc_cfg), buffer)) { + mp_raise_ValueError(MP_ERROR_TEXT("desc_cfg")); + } + if (desc_strs != mp_const_none + && !MP_OBJ_TYPE_HAS_SLOT(mp_obj_get_type(desc_strs), subscr)) { + mp_raise_ValueError(MP_ERROR_TEXT("desc_strs")); + } + + self->desc_dev = desc_dev; + self->desc_cfg = desc_cfg; + self->desc_strs = desc_strs; + self->open_itf_cb = args[ARG_open_itf_cb].u_obj; + self->reset_cb = args[ARG_reset_cb].u_obj; + self->control_xfer_cb = args[ARG_control_xfer_cb].u_obj; + self->xfer_cb = args[ARG_xfer_cb].u_obj; + + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_KW(usb_device_config_obj, 1, usb_device_config); + +static const MP_DEFINE_BYTES_OBJ(builtin_default_desc_dev_obj, + &mp_usbd_builtin_desc_dev, sizeof(tusb_desc_device_t)); + +#if HAS_BUILTIN_DRIVERS +// BUILTIN_DEFAULT Python object holds properties of the built-in USB configuration +// (i.e. values used by the C implementation of TinyUSB devices.) +static const MP_DEFINE_BYTES_OBJ(builtin_default_desc_cfg_obj, + mp_usbd_builtin_desc_cfg, MP_USBD_BUILTIN_DESC_CFG_LEN); + +static const mp_rom_map_elem_t usb_device_builtin_default_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_itf_max), MP_OBJ_NEW_SMALL_INT(USBD_ITF_BUILTIN_MAX) }, + { MP_ROM_QSTR(MP_QSTR_ep_max), MP_OBJ_NEW_SMALL_INT(USBD_EP_BUILTIN_MAX) }, + { MP_ROM_QSTR(MP_QSTR_str_max), MP_OBJ_NEW_SMALL_INT(USBD_STR_BUILTIN_MAX) }, + { MP_ROM_QSTR(MP_QSTR_desc_dev), MP_ROM_PTR(&builtin_default_desc_dev_obj) }, + { MP_ROM_QSTR(MP_QSTR_desc_cfg), MP_ROM_PTR(&builtin_default_desc_cfg_obj) }, +}; +static MP_DEFINE_CONST_DICT(usb_device_builtin_default_dict, usb_device_builtin_default_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + mp_type_usb_device_builtin_default, + MP_QSTR_BUILTIN_DEFAULT, + MP_TYPE_FLAG_NONE, + locals_dict, &usb_device_builtin_default_dict + ); +#endif // HAS_BUILTIN_DRIVERS + +// BUILTIN_NONE holds properties for no enabled built-in USB device support +static const mp_rom_map_elem_t usb_device_builtin_none_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_itf_max), MP_OBJ_NEW_SMALL_INT(0) }, + { MP_ROM_QSTR(MP_QSTR_ep_max), MP_OBJ_NEW_SMALL_INT(0) }, + { MP_ROM_QSTR(MP_QSTR_str_max), MP_OBJ_NEW_SMALL_INT(1) }, + { MP_ROM_QSTR(MP_QSTR_desc_dev), MP_ROM_PTR(&builtin_default_desc_dev_obj) }, + { MP_ROM_QSTR(MP_QSTR_desc_cfg), mp_const_empty_bytes }, +}; +static MP_DEFINE_CONST_DICT(usb_device_builtin_none_dict, usb_device_builtin_none_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + mp_type_usb_device_builtin_none, + MP_QSTR_BUILTIN_NONE, + MP_TYPE_FLAG_NONE, + locals_dict, &usb_device_builtin_none_dict + ); + +static const mp_rom_map_elem_t usb_device_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_config), MP_ROM_PTR(&usb_device_config_obj) }, + { MP_ROM_QSTR(MP_QSTR_submit_xfer), MP_ROM_PTR(&usb_device_submit_xfer_obj) }, + { MP_ROM_QSTR(MP_QSTR_active), MP_ROM_PTR(&usb_device_active_obj) }, + { MP_ROM_QSTR(MP_QSTR_stall), MP_ROM_PTR(&usb_device_stall_obj) }, + + // Built-in driver constants + { MP_ROM_QSTR(MP_QSTR_BUILTIN_NONE), MP_ROM_PTR(&mp_type_usb_device_builtin_none) }, + + #if !HAS_BUILTIN_DRIVERS + // No builtin-in drivers, so BUILTIN_DEFAULT is BUILTIN_NONE + { MP_ROM_QSTR(MP_QSTR_BUILTIN_DEFAULT), MP_ROM_PTR(&mp_type_usb_device_builtin_none) }, + #else + { MP_ROM_QSTR(MP_QSTR_BUILTIN_DEFAULT), MP_ROM_PTR(&mp_type_usb_device_builtin_default) }, + + // Specific driver constant names are to support future switching of built-in drivers, + // but currently only one is present and it maps directly to BUILTIN_DEFAULT + #if MICROPY_HW_USB_CDC && !MICROPY_HW_USB_MSC + { MP_ROM_QSTR(MP_QSTR_BUILTIN_CDC), MP_ROM_PTR(&mp_type_usb_device_builtin_default) }, + #endif + #if MICROPY_HW_USB_MSC && !MICROPY_HW_USB_CDC + { MP_ROM_QSTR(MP_QSTR_BUILTIN_MSC), MP_ROM_PTR(&mp_type_usb_device_builtin_default) }, + #endif + #if MICROPY_HW_USB_CDC && MICROPY_HW_USB_MSC + { MP_ROM_QSTR(MP_QSTR_BUILTIN_CDC_MSC), MP_ROM_PTR(&mp_type_usb_device_builtin_default) }, + #endif + #endif // !HAS_BUILTIN_DRIVERS +}; +static MP_DEFINE_CONST_DICT(usb_device_locals_dict, usb_device_locals_dict_table); + +static void usb_device_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { + mp_obj_usb_device_t *self = MP_OBJ_TO_PTR(self_in); + if (dest[0] == MP_OBJ_NULL) { + // Load attribute. + if (attr == MP_QSTR_builtin_driver) { + dest[0] = self->builtin_driver; + } else { + // Continue lookup in locals_dict. + dest[1] = MP_OBJ_SENTINEL; + } + } else if (dest[1] != MP_OBJ_NULL) { + // Store attribute. + if (attr == MP_QSTR_builtin_driver) { + if (self->active) { + mp_raise_OSError(MP_EINVAL); // Need to deactivate first + } + // Note: this value should be one of the BUILTIN_nnn constants, + // but not checked here to save code size in a low level API + self->builtin_driver = dest[1]; + dest[0] = MP_OBJ_NULL; + } + } +} + +MP_DEFINE_CONST_OBJ_TYPE( + machine_usb_device_type, + MP_QSTR_USBDevice, + MP_TYPE_FLAG_NONE, + make_new, usb_device_make_new, + locals_dict, &usb_device_locals_dict, + attr, &usb_device_attr + ); + +MP_REGISTER_ROOT_POINTER(mp_obj_t usbd); + +#endif diff --git a/extmod/modmachine.c b/extmod/modmachine.c index d7e82b124d..2a7e315bbb 100644 --- a/extmod/modmachine.c +++ b/extmod/modmachine.c @@ -232,6 +232,9 @@ static const mp_rom_map_elem_t machine_module_globals_table[] = { #if MICROPY_PY_MACHINE_UART { MP_ROM_QSTR(MP_QSTR_UART), MP_ROM_PTR(&machine_uart_type) }, #endif + #if MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE + { MP_ROM_QSTR(MP_QSTR_USBDevice), MP_ROM_PTR(&machine_usb_device_type) }, + #endif #if MICROPY_PY_MACHINE_WDT { MP_ROM_QSTR(MP_QSTR_WDT), MP_ROM_PTR(&machine_wdt_type) }, #endif diff --git a/extmod/modmachine.h b/extmod/modmachine.h index e6b08b3fc9..7c16ed302e 100644 --- a/extmod/modmachine.h +++ b/extmod/modmachine.h @@ -213,6 +213,7 @@ extern const mp_obj_type_t machine_signal_type; extern const mp_obj_type_t machine_spi_type; extern const mp_obj_type_t machine_timer_type; extern const mp_obj_type_t machine_uart_type; +extern const mp_obj_type_t machine_usbd_type; extern const mp_obj_type_t machine_wdt_type; #if MICROPY_PY_MACHINE_SOFTI2C @@ -230,6 +231,10 @@ extern const mp_machine_spi_p_t mp_machine_soft_spi_p; extern const mp_obj_dict_t mp_machine_spi_locals_dict; #endif +#if MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE +extern const mp_obj_type_t machine_usb_device_type; +#endif + #if defined(MICROPY_MACHINE_MEM_GET_READ_ADDR) uintptr_t MICROPY_MACHINE_MEM_GET_READ_ADDR(mp_obj_t addr_o, uint align); #endif diff --git a/py/runtime.c b/py/runtime.c index f7e0abdb46..1836f5d92a 100644 --- a/py/runtime.c +++ b/py/runtime.c @@ -171,6 +171,10 @@ void mp_init(void) { MP_STATE_VM(bluetooth) = MP_OBJ_NULL; #endif + #if MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE + MP_STATE_VM(usbd) = MP_OBJ_NULL; + #endif + #if MICROPY_PY_THREAD_GIL mp_thread_mutex_init(&MP_STATE_VM(gil_mutex)); #endif diff --git a/shared/tinyusb/mp_usbd.c b/shared/tinyusb/mp_usbd.c index 74b3f07492..436314dcde 100644 --- a/shared/tinyusb/mp_usbd.c +++ b/shared/tinyusb/mp_usbd.c @@ -24,42 +24,42 @@ * THE SOFTWARE. */ -#include - #include "py/mpconfig.h" -#include "py/runtime.h" #if MICROPY_HW_ENABLE_USBDEV +#include "mp_usbd.h" + #ifndef NO_QSTR -#include "tusb.h" // TinyUSB is not available when running the string preprocessor #include "device/dcd.h" -#include "device/usbd.h" -#include "device/usbd_pvt.h" #endif -// TinyUSB task function wrapper, as scheduled from the USB IRQ -static void mp_usbd_task_callback(mp_sched_node_t *node); - -extern void __real_dcd_event_handler(dcd_event_t const *event, bool in_isr); +#if !MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE void mp_usbd_task(void) { tud_task_ext(0, false); } +void mp_usbd_task_callback(mp_sched_node_t *node) { + (void)node; + mp_usbd_task(); +} + +#endif // !MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE + +extern void __real_dcd_event_handler(dcd_event_t const *event, bool in_isr); + // If -Wl,--wrap=dcd_event_handler is passed to the linker, then this wrapper // will be called and allows MicroPython to schedule the TinyUSB task when // dcd_event_handler() is called from an ISR. TU_ATTR_FAST_FUNC void __wrap_dcd_event_handler(dcd_event_t const *event, bool in_isr) { - static mp_sched_node_t usbd_task_node; - __real_dcd_event_handler(event, in_isr); - mp_sched_schedule_node(&usbd_task_node, mp_usbd_task_callback); + mp_usbd_schedule_task(); } -static void mp_usbd_task_callback(mp_sched_node_t *node) { - (void)node; - mp_usbd_task(); +TU_ATTR_FAST_FUNC void mp_usbd_schedule_task(void) { + static mp_sched_node_t usbd_task_node; + mp_sched_schedule_node(&usbd_task_node, mp_usbd_task_callback); } void mp_usbd_hex_str(char *out_str, const uint8_t *bytes, size_t bytes_len) { @@ -72,4 +72,4 @@ void mp_usbd_hex_str(char *out_str, const uint8_t *bytes, size_t bytes_len) { out_str[hex_len] = 0; } -#endif +#endif // MICROPY_HW_ENABLE_USBDEV diff --git a/shared/tinyusb/mp_usbd.h b/shared/tinyusb/mp_usbd.h index 89f8bf0ee9..ef32348451 100644 --- a/shared/tinyusb/mp_usbd.h +++ b/shared/tinyusb/mp_usbd.h @@ -27,25 +27,110 @@ #ifndef MICROPY_INCLUDED_SHARED_TINYUSB_MP_USBD_H #define MICROPY_INCLUDED_SHARED_TINYUSB_MP_USBD_H +#include "py/mpconfig.h" + +#if MICROPY_HW_ENABLE_USBDEV + #include "py/obj.h" +#include "py/objarray.h" +#include "py/runtime.h" + +#ifndef NO_QSTR #include "tusb.h" +#include "device/dcd.h" +#endif -static inline void mp_usbd_init(void) { - // Currently this is a thin wrapper around tusb_init(), however - // runtime USB support will require this to be extended. - tusb_init(); -} - -// Call this to explicitly run the TinyUSB device task. +// Run the TinyUSB device task void mp_usbd_task(void); +// Schedule a call to mp_usbd_task(), even if no USB interrupt has occurred +void mp_usbd_schedule_task(void); + // Function to be implemented in port code. // Can write a string up to MICROPY_HW_USB_DESC_STR_MAX characters long, plus terminating byte. extern void mp_usbd_port_get_serial_number(char *buf); -// Most ports need to write a hexadecimal serial number from a byte array, this +// Most ports need to write a hexadecimal serial number from a byte array. This // is a helper function for this. out_str must be long enough to hold a string of total // length (2 * bytes_len + 1) (including NUL terminator). void mp_usbd_hex_str(char *out_str, const uint8_t *bytes, size_t bytes_len); +// Length of built-in configuration descriptor +#define MP_USBD_BUILTIN_DESC_CFG_LEN (TUD_CONFIG_DESC_LEN + \ + (CFG_TUD_CDC ? (TUD_CDC_DESC_LEN) : 0) + \ + (CFG_TUD_MSC ? (TUD_MSC_DESC_LEN) : 0) \ + ) + +// Built-in USB device and configuration descriptor values +extern const tusb_desc_device_t mp_usbd_builtin_desc_dev; +extern const uint8_t mp_usbd_builtin_desc_cfg[MP_USBD_BUILTIN_DESC_CFG_LEN]; + +void mp_usbd_task_callback(mp_sched_node_t *node); + +#if MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE +void mp_usbd_deinit(void); +void mp_usbd_init(void); + +const char *mp_usbd_runtime_string_cb(uint8_t index); + +// Maximum number of pending exceptions per single TinyUSB task execution +#define MP_USBD_MAX_PEND_EXCS 2 + +typedef struct { + mp_obj_base_t base; + + mp_obj_t desc_dev; // Device descriptor bytes + mp_obj_t desc_cfg; // Configuration descriptor bytes + mp_obj_t desc_strs; // List/dict/similar to look up string descriptors by index + + // Runtime device driver callback functions + mp_obj_t open_itf_cb; + mp_obj_t reset_cb; + mp_obj_t control_xfer_cb; + mp_obj_t xfer_cb; + + mp_obj_t builtin_driver; // Points to one of mp_type_usb_device_builtin_nnn + + bool active; // Has the user set the USB device active? + bool trigger; // Has the user requested the active state change (or re-activate)? + + // Temporary pointers for xfer data in progress on each endpoint + // Ensuring they aren't garbage collected until the xfer completes + mp_obj_t xfer_data[CFG_TUD_ENDPPOINT_MAX][2]; + + // Pointer to a memoryview that is reused to refer to various pieces of + // control transfer data that are pushed to USB control transfer + // callbacks. Python code can't rely on the memoryview contents + // to remain valid after the callback returns! + mp_obj_array_t *control_data; + + // Pointers to exceptions thrown inside Python callbacks. See + // usbd_callback_function_n(). + mp_uint_t num_pend_excs; + mp_obj_t pend_excs[MP_USBD_MAX_PEND_EXCS]; +} mp_obj_usb_device_t; + +// Built-in constant objects, possible values of builtin_driver +// +// (Currently not possible to change built-in drivers at runtime, just enable/disable.) +extern const mp_obj_type_t mp_type_usb_device_builtin_default; +extern const mp_obj_type_t mp_type_usb_device_builtin_none; + +// Return true if any built-in driver is enabled +inline static bool mp_usb_device_builtin_enabled(const mp_obj_usb_device_t *usbd) { + return usbd->builtin_driver != MP_OBJ_FROM_PTR(&mp_type_usb_device_builtin_none); +} + +#else // Static USBD drivers only + +static inline void mp_usbd_init(void) { + // Without runtime USB support, this can be a thin wrapper wrapper around tusb_init() + extern bool tusb_init(void); + tusb_init(); +} + +#endif + +#endif // MICROPY_HW_ENABLE_USBDEV + #endif // MICROPY_INCLUDED_SHARED_TINYUSB_USBD_H diff --git a/shared/tinyusb/mp_usbd_descriptor.c b/shared/tinyusb/mp_usbd_descriptor.c index 72a2652179..be3473b6b9 100644 --- a/shared/tinyusb/mp_usbd_descriptor.c +++ b/shared/tinyusb/mp_usbd_descriptor.c @@ -31,12 +31,11 @@ #include "tusb.h" #include "mp_usbd.h" -#include "mp_usbd_internal.h" #define USBD_CDC_CMD_MAX_SIZE (8) #define USBD_CDC_IN_OUT_MAX_SIZE ((CFG_TUD_MAX_SPEED == OPT_MODE_HIGH_SPEED) ? 512 : 64) -const tusb_desc_device_t mp_usbd_desc_device_static = { +const tusb_desc_device_t mp_usbd_builtin_desc_dev = { .bLength = sizeof(tusb_desc_device_t), .bDescriptorType = TUSB_DESC_DEVICE, .bcdUSB = 0x0200, @@ -53,8 +52,8 @@ const tusb_desc_device_t mp_usbd_desc_device_static = { .bNumConfigurations = 1, }; -const uint8_t mp_usbd_desc_cfg_static[USBD_STATIC_DESC_LEN] = { - TUD_CONFIG_DESCRIPTOR(1, USBD_ITF_STATIC_MAX, USBD_STR_0, USBD_STATIC_DESC_LEN, +const uint8_t mp_usbd_builtin_desc_cfg[MP_USBD_BUILTIN_DESC_CFG_LEN] = { + TUD_CONFIG_DESCRIPTOR(1, USBD_ITF_BUILTIN_MAX, USBD_STR_0, MP_USBD_BUILTIN_DESC_CFG_LEN, 0, USBD_MAX_POWER_MA), #if CFG_TUD_CDC @@ -69,51 +68,68 @@ const uint8_t mp_usbd_desc_cfg_static[USBD_STATIC_DESC_LEN] = { const uint16_t *tud_descriptor_string_cb(uint8_t index, uint16_t langid) { char serial_buf[MICROPY_HW_USB_DESC_STR_MAX + 1]; // Includes terminating NUL byte static uint16_t desc_wstr[MICROPY_HW_USB_DESC_STR_MAX + 1]; // Includes prefix uint16_t - const char *desc_str; + const char *desc_str = NULL; uint16_t desc_len; - switch (index) { - case 0: - desc_wstr[1] = 0x0409; // supported language is English - desc_len = 4; - break; - case USBD_STR_SERIAL: - // TODO: make a port-specific serial number callback - mp_usbd_port_get_serial_number(serial_buf); - desc_str = serial_buf; - break; - case USBD_STR_MANUF: - desc_str = MICROPY_HW_USB_MANUFACTURER_STRING; - break; - case USBD_STR_PRODUCT: - desc_str = MICROPY_HW_USB_PRODUCT_FS_STRING; - break; - #if CFG_TUD_CDC - case USBD_STR_CDC: - desc_str = MICROPY_HW_USB_CDC_INTERFACE_STRING; - break; - #endif - #if CFG_TUD_MSC - case USBD_STR_MSC: - desc_str = MICROPY_HW_USB_MSC_INTERFACE_STRING; - break; - #endif - default: - desc_str = NULL; - } + #if MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE + desc_str = mp_usbd_runtime_string_cb(index); + #endif - if (index != 0) { + if (index == 0) { + // String descriptor 0 is special, see USB 2.0 section 9.6.7 String + // + // Expect any runtime value in desc_str to be a fully formed descriptor if (desc_str == NULL) { - return NULL; // Will STALL the endpoint + desc_str = "\x04\x03\x09\x04"; // Descriptor for "English" } + if (desc_str[0] < sizeof(desc_wstr)) { + memcpy(desc_wstr, desc_str, desc_str[0]); + return desc_wstr; + } + return NULL; // Descriptor length too long (or malformed), stall endpoint + } - // Convert from narrow string to wide string - desc_len = 2; - for (int i = 0; i < MICROPY_HW_USB_DESC_STR_MAX && desc_str[i] != 0; i++) { - desc_wstr[1 + i] = desc_str[i]; - desc_len += 2; + // Otherwise, generate a "UNICODE" string descriptor from the C string + + if (desc_str == NULL) { + // Fall back to the "static" string + switch (index) { + case USBD_STR_SERIAL: + mp_usbd_port_get_serial_number(serial_buf); + desc_str = serial_buf; + break; + case USBD_STR_MANUF: + desc_str = MICROPY_HW_USB_MANUFACTURER_STRING; + break; + case USBD_STR_PRODUCT: + desc_str = MICROPY_HW_USB_PRODUCT_FS_STRING; + break; + #if CFG_TUD_CDC + case USBD_STR_CDC: + desc_str = MICROPY_HW_USB_CDC_INTERFACE_STRING; + break; + #endif + #if CFG_TUD_MSC + case USBD_STR_MSC: + desc_str = MICROPY_HW_USB_MSC_INTERFACE_STRING; + break; + #endif + default: + break; } } + + if (desc_str == NULL) { + return NULL; // No string, STALL the endpoint + } + + // Convert from narrow string to wide string + desc_len = 2; + for (int i = 0; i < MICROPY_HW_USB_DESC_STR_MAX && desc_str[i] != 0; i++) { + desc_wstr[1 + i] = desc_str[i]; + desc_len += 2; + } + // first byte is length (including header), second byte is string type desc_wstr[0] = (TUSB_DESC_STRING << 8) | desc_len; @@ -121,13 +137,21 @@ const uint16_t *tud_descriptor_string_cb(uint8_t index, uint16_t langid) { } +#if !MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE + const uint8_t *tud_descriptor_device_cb(void) { - return (const void *)&mp_usbd_desc_device_static; + return (const void *)&mp_usbd_builtin_desc_dev; } const uint8_t *tud_descriptor_configuration_cb(uint8_t index) { (void)index; - return mp_usbd_desc_cfg_static; + return mp_usbd_builtin_desc_cfg; } -#endif +#else + +// If runtime device support is enabled, descriptor callbacks are implemented in usbd.c + +#endif // !MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE + +#endif // MICROPY_HW_ENABLE_USBDEV diff --git a/shared/tinyusb/mp_usbd_internal.h b/shared/tinyusb/mp_usbd_internal.h deleted file mode 100644 index 8b8f50bae8..0000000000 --- a/shared/tinyusb/mp_usbd_internal.h +++ /dev/null @@ -1,34 +0,0 @@ -/* - * This file is part of the MicroPython project, http://micropython.org/ - * - * The MIT License (MIT) - * - * Copyright (c) 2022 Angus Gratton - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -#ifndef MICROPY_INCLUDED_SHARED_TINYUSB_MP_USBD_INTERNAL_H -#define MICROPY_INCLUDED_SHARED_TINYUSB_MP_USBD_INTERNAL_H -#include "tusb.h" - -// Static USB device descriptor values -extern const tusb_desc_device_t mp_usbd_desc_device_static; -extern const uint8_t mp_usbd_desc_cfg_static[USBD_STATIC_DESC_LEN]; - -#endif // MICROPY_INCLUDED_SHARED_TINYUSB_MP_USBD_INTERNAL_H diff --git a/shared/tinyusb/mp_usbd_runtime.c b/shared/tinyusb/mp_usbd_runtime.c new file mode 100644 index 0000000000..a46c0f85e6 --- /dev/null +++ b/shared/tinyusb/mp_usbd_runtime.c @@ -0,0 +1,554 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2022 Blake W. Felt + * Copyright (c) 2022-2023 Angus Gratton + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include + +#include "mp_usbd.h" +#include "py/mpconfig.h" +#include "py/mperrno.h" +#include "py/mphal.h" +#include "py/obj.h" +#include "py/objarray.h" +#include "py/objstr.h" +#include "py/runtime.h" + +#if MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE + +#ifndef NO_QSTR +#include "tusb.h" // TinyUSB is not available when running the string preprocessor +#include "device/dcd.h" +#include "device/usbd.h" +#include "device/usbd_pvt.h" +#endif + +static bool in_usbd_task; // Flags if mp_usbd_task() is processing already + +// Some top-level functions that manage global TinyUSB USBD state, not the +// singleton object visible to Python +static void mp_usbd_disconnect(mp_obj_usb_device_t *usbd); +static void mp_usbd_task_inner(void); + +// Pend an exception raise in a USBD callback to print when safe. +// +// We can't raise any exceptions out of the TinyUSB task, as it may still need +// to do some state cleanup. +// +// The requirement for this becomes very similar to +// mp_call_function_x_protected() for interrupts, but it's more restrictive: if +// the C-based USB-CDC serial port is in use, we can't print from inside a +// TinyUSB callback as it might try to recursively call into TinyUSB to flush +// the CDC port and make room. Therefore, we have to store the exception and +// print it as we exit the TinyUSB task. +// +// (Worse, a single TinyUSB task can process multiple callbacks and therefore generate +// multiple exceptions...) +static void usbd_pend_exception(mp_obj_t exception) { + mp_obj_usb_device_t *usbd = MP_OBJ_TO_PTR(MP_STATE_VM(usbd)); + assert(usbd != NULL); + if (usbd->num_pend_excs < MP_USBD_MAX_PEND_EXCS) { + usbd->pend_excs[usbd->num_pend_excs] = exception; + } + usbd->num_pend_excs++; +} + +// Call a Python function from inside a TinyUSB callback. +// +// Handles any exception using usbd_pend_exception() +static mp_obj_t usbd_callback_function_n(mp_obj_t fun, size_t n_args, const mp_obj_t *args) { + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + mp_obj_t ret = mp_call_function_n_kw(fun, n_args, 0, args); + nlr_pop(); + return ret; + } else { + usbd_pend_exception(MP_OBJ_FROM_PTR(nlr.ret_val)); + return MP_OBJ_NULL; + } +} + +// Return a pointer to the data inside a Python buffer provided in a callback +static void *usbd_get_buffer_in_cb(mp_obj_t obj, mp_uint_t flags) { + mp_buffer_info_t buf_info; + if (obj == mp_const_none) { + // This is only if the user somehow + return NULL; + } else if (mp_get_buffer(obj, &buf_info, flags)) { + return buf_info.buf; + } else { + mp_obj_t exc = mp_obj_new_exception_msg(&mp_type_TypeError, + MP_ERROR_TEXT("object with buffer protocol required")); + usbd_pend_exception(exc); + return NULL; + } +} + +const uint8_t *tud_descriptor_device_cb(void) { + mp_obj_usb_device_t *usbd = MP_OBJ_TO_PTR(MP_STATE_VM(usbd)); + const void *result = NULL; + if (usbd) { + result = usbd_get_buffer_in_cb(usbd->desc_dev, MP_BUFFER_READ); + } + return result ? result : &mp_usbd_builtin_desc_dev; +} + +const uint8_t *tud_descriptor_configuration_cb(uint8_t index) { + (void)index; + mp_obj_usb_device_t *usbd = MP_OBJ_TO_PTR(MP_STATE_VM(usbd)); + const void *result = NULL; + if (usbd) { + result = usbd_get_buffer_in_cb(usbd->desc_cfg, MP_BUFFER_READ); + } + return result ? result : &mp_usbd_builtin_desc_cfg; +} + +const char *mp_usbd_runtime_string_cb(uint8_t index) { + mp_obj_usb_device_t *usbd = MP_OBJ_TO_PTR(MP_STATE_VM(usbd)); + nlr_buf_t nlr; + + if (usbd == NULL || usbd->desc_strs == mp_const_none) { + return NULL; + } + + if (nlr_push(&nlr) == 0) { + mp_obj_t res = mp_obj_subscr(usbd->desc_strs, mp_obj_new_int(index), MP_OBJ_SENTINEL); + nlr_pop(); + if (res != mp_const_none) { + return usbd_get_buffer_in_cb(res, MP_BUFFER_READ); + } + } else { + mp_obj_t exception = MP_OBJ_FROM_PTR(nlr.ret_val); + if (!(mp_obj_is_type(exception, &mp_type_KeyError) || mp_obj_is_type(exception, &mp_type_IndexError))) { + // Don't print KeyError or IndexError, allowing dicts or lists to have missing entries. + // but log any more exotic errors that pop up + usbd_pend_exception(exception); + } + } + + return NULL; +} + +bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) { + return false; // Currently no support for Vendor control transfers on the Python side +} + +// Generic "runtime device" TinyUSB class driver, delegates everything to Python callbacks + +static void runtime_dev_init(void) { +} + +static void runtime_dev_reset(uint8_t rhport) { + mp_obj_usb_device_t *usbd = MP_OBJ_TO_PTR(MP_STATE_VM(usbd)); + if (!usbd) { + return; + } + + for (int epnum = 0; epnum < CFG_TUD_ENDPPOINT_MAX; epnum++) { + for (int dir = 0; dir < 2; dir++) { + usbd->xfer_data[epnum][dir] = mp_const_none; + } + } + + if (mp_obj_is_callable(usbd->reset_cb)) { + usbd_callback_function_n(usbd->reset_cb, 0, NULL); + } +} + +// Calculate how many interfaces TinyUSB expects us to claim from +// driver open(). +// +// Annoyingly, the calling function (process_set_config() in TinyUSB) knows +// this but doesn't pass the information to us. +// +// The answer is: +// - If an Interface Association Descriptor (IAD) is immediately before itf_desc +// in the configuration descriptor, then claim all of the associated interfaces. +// - Otherwise, claim exactly one interface +// +// Relying on the implementation detail that itf_desc is a pointer inside the +// tud_descriptor_configuration_cb() result. Therefore, we can iterate through +// from the beginning to check for an IAD immediately preceding it. +// +// Returns the number of associated interfaces to claim. +static uint8_t _runtime_dev_count_itfs(tusb_desc_interface_t const *itf_desc) { + const tusb_desc_configuration_t *cfg_desc = (const void *)tud_descriptor_configuration_cb(0); + const uint8_t *p_desc = (const void *)cfg_desc; + const uint8_t *p_end = p_desc + cfg_desc->wTotalLength; + assert(p_desc <= itf_desc && itf_desc < p_end); + while (p_desc != (const void *)itf_desc && p_desc < p_end) { + const uint8_t *next = tu_desc_next(p_desc); + + if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE_ASSOCIATION + && next == (const void *)itf_desc) { + const tusb_desc_interface_assoc_t *desc_iad = (const void *)p_desc; + return desc_iad->bInterfaceCount; + } + p_desc = next; + } + return 1; // No IAD found +} + +// Scan the other descriptors after these interface(s) to find the total associated length to claim +// from driver open(). +// +// Total number of interfaces to scan for is assoc_itf_count. +// +// Opens any associated endpoints so they can have transfers submitted against them. +// +// Returns the total number of descriptor bytes to claim. +static uint16_t _runtime_dev_claim_itfs(tusb_desc_interface_t const *itf_desc, uint8_t assoc_itf_count, uint16_t max_len) { + const uint8_t *p_desc = (const void *)itf_desc; + const uint8_t *p_end = p_desc + max_len; + while (p_desc < p_end) { + if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) { + if (assoc_itf_count > 0) { + // Claim this interface descriptor + assoc_itf_count--; + } else { + // This is the end of the previous interface's associated descriptors + break; + } + } else if (tu_desc_type(p_desc) == TUSB_DESC_ENDPOINT) { + // Open any endpoints that we come across + if (tu_desc_type(p_desc) == TUSB_DESC_ENDPOINT) { + bool r = usbd_edpt_open(USBD_RHPORT, (const void *)p_desc); + if (!r) { + mp_obj_t exc = mp_obj_new_exception_arg1(&mp_type_OSError, MP_OBJ_NEW_SMALL_INT(MP_ENODEV)); + usbd_pend_exception(exc); + break; + } + } + } + p_desc = tu_desc_next(p_desc); + } + return p_desc - (const uint8_t *)itf_desc; +} + +// TinyUSB "Application driver" open callback. Called when the USB host sets +// configuration. Returns number of bytes to claim from descriptors pointed to +// by itf_desc. +// +// This is a little fiddly as it's called before any compiled-in "static" +// TinyUSB drivers, but we don't want to override those. +// +// Also, TinyUSB expects us to know how many interfaces to claim for each time +// this function is called, and will behave unexpectedly if we claim the wrong +// number of interfaces. However, unlike a "normal" USB driver we don't know at +// compile time how many interfaces we've implemented. Instead, we have to look +// back through the configuration descriptor to figure this out. +static uint16_t runtime_dev_open(uint8_t rhport, tusb_desc_interface_t const *itf_desc, uint16_t max_len) { + mp_obj_usb_device_t *usbd = MP_OBJ_TO_PTR(MP_STATE_VM(usbd)); + + // Runtime USB isn't initialised + if (!usbd) { + return 0; + } + + // If TinyUSB built-in drivers are enabled, don't claim any interface in the static range + if (mp_usb_device_builtin_enabled(usbd) && itf_desc->bInterfaceNumber < USBD_ITF_BUILTIN_MAX) { + return 0; + } + + // Determine the total descriptor length of the interface(s) we are going to claim + uint8_t assoc_itf_count = _runtime_dev_count_itfs(itf_desc); + uint16_t claim_len = _runtime_dev_claim_itfs(itf_desc, assoc_itf_count, max_len); + + // Call the Python callback to allow the driver to start working with these interface(s) + + if (mp_obj_is_callable(usbd->open_itf_cb)) { + // Repurpose the control_data memoryview to point into itf_desc for this one call + usbd->control_data->items = (void *)itf_desc; + usbd->control_data->len = claim_len; + mp_obj_t args[] = { MP_OBJ_FROM_PTR(usbd->control_data) }; + usbd_callback_function_n(usbd->open_itf_cb, 1, args); + usbd->control_data->len = 0; + usbd->control_data->items = NULL; + } + + return claim_len; +} + +static bool runtime_dev_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) { + mp_obj_t cb_res = mp_const_false; + mp_obj_usb_device_t *usbd = MP_OBJ_TO_PTR(MP_STATE_VM(usbd)); + tusb_dir_t dir = request->bmRequestType_bit.direction; + mp_buffer_info_t buf_info; + + if (!usbd) { + return false; + } + + if (mp_obj_is_callable(usbd->control_xfer_cb)) { + usbd->control_data->items = (void *)request; + usbd->control_data->len = sizeof(tusb_control_request_t); + mp_obj_t args[] = { + mp_obj_new_int(stage), + MP_OBJ_FROM_PTR(usbd->control_data), + }; + cb_res = usbd_callback_function_n(usbd->control_xfer_cb, MP_ARRAY_SIZE(args), args); + usbd->control_data->items = NULL; + usbd->control_data->len = 0; + + if (cb_res == MP_OBJ_NULL) { + // Exception occurred in the callback handler, stall this transfer + cb_res = mp_const_false; + } + } + + // Check if callback returned any data to submit + if (mp_get_buffer(cb_res, &buf_info, dir == TUSB_DIR_IN ? MP_BUFFER_READ : MP_BUFFER_RW)) { + bool result = tud_control_xfer(USBD_RHPORT, + request, + buf_info.buf, + buf_info.len); + + if (result) { + // Keep buffer object alive until the transfer completes + usbd->xfer_data[0][dir] = cb_res; + } + + return result; + } else { + // Expect True or False to stall or continue + + if (stage == CONTROL_STAGE_ACK) { + // Allow data to be GCed once it's no longer in use + usbd->xfer_data[0][dir] = mp_const_none; + } + return mp_obj_is_true(cb_res); + } +} + +static bool runtime_dev_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { + mp_obj_t ep = mp_obj_new_int(ep_addr); + mp_obj_t cb_res = mp_const_false; + mp_obj_usb_device_t *usbd = MP_OBJ_TO_PTR(MP_STATE_VM(usbd)); + if (!usbd) { + return false; + } + + if (mp_obj_is_callable(usbd->xfer_cb)) { + mp_obj_t args[] = { + ep, + MP_OBJ_NEW_SMALL_INT(result), + MP_OBJ_NEW_SMALL_INT(xferred_bytes), + }; + cb_res = usbd_callback_function_n(usbd->xfer_cb, MP_ARRAY_SIZE(args), args); + } + + // Clear any xfer_data for this endpoint + usbd->xfer_data[tu_edpt_number(ep_addr)][tu_edpt_dir(ep_addr)] = mp_const_none; + + return cb_res != MP_OBJ_NULL && mp_obj_is_true(cb_res); +} + +static usbd_class_driver_t const _runtime_dev_driver = +{ + #if CFG_TUSB_DEBUG >= 2 + .name = "runtime_dev", + #endif + .init = runtime_dev_init, + .reset = runtime_dev_reset, + .open = runtime_dev_open, + .control_xfer_cb = runtime_dev_control_xfer_cb, + .xfer_cb = runtime_dev_xfer_cb, + .sof = NULL +}; + +usbd_class_driver_t const *usbd_app_driver_get_cb(uint8_t *driver_count) { + *driver_count = 1; + return &_runtime_dev_driver; +} + + +// Functions below here (named mp_usbd_xyz) apply to the whole TinyUSB C-based subsystem +// and not necessarily the USBD singleton object (named usbd_xyz). + +// To support soft reset clearing USB runtime state, we manage three TinyUSB states: +// +// - "Not initialised" - tusb_inited() returns false, no USB at all). Only way +// back to this state is hard reset. +// +// - "Activated" - tusb_inited() returns true, USB device "connected" at device +// end and available to host. +// +// - "Deactivated" - tusb_inited() returns true, but USB device "disconnected" +// at device end and host can't see it. + + +// Top-level USB device subsystem init. +// +// Makes an on-demand call to mp_usbd_activate(), if USB is needed. +// +// This is called on any soft reset after boot.py runs, or on demand if the +// user activates USB and it hasn't activated yet. +void mp_usbd_init(void) { + mp_obj_usb_device_t *usbd = MP_OBJ_TO_PTR(MP_STATE_VM(usbd)); + bool need_usb; + + if (usbd == NULL) { + // No runtime USB device + #if CFG_TUD_CDC || CFG_TUD_MSC + // Builtin drivers are available, so initialise as defaults + need_usb = true; + #else + // No static drivers, nothing to initialise + need_usb = false; + #endif + } else { + // Otherwise, initialise based on whether runtime USB is active + need_usb = usbd->active; + } + + if (need_usb) { + tusb_init(); // Safe to call redundantly + tud_connect(); // Reconnect if mp_usbd_deinit() has disconnected + } +} + +// Top-level USB device deinit. +// +// This variant is called from soft reset, NULLs out the USB device +// singleton instance from MP_STATE_VM, and disconnects the port. +void mp_usbd_deinit(void) { + mp_obj_usb_device_t *usbd = MP_OBJ_TO_PTR(MP_STATE_VM(usbd)); + MP_STATE_VM(usbd) = MP_OBJ_NULL; + if (usbd) { + // Disconnect if a runtime USB device was active + mp_usbd_disconnect(usbd); + } +} + +// Thin wrapper around tud_disconnect() that tells TinyUSB all endpoints +// have stalled, to prevent it getting confused if a transfer is in progress. +static void mp_usbd_disconnect(mp_obj_usb_device_t *usbd) { + if (!tusb_inited()) { + return; // TinyUSB hasn't initialised + } + + if (usbd) { + // There might be USB transfers in progress right now, so need to stall any live + // endpoints + // + // TODO: figure out if we really need this + for (int epnum = 0; epnum < CFG_TUD_ENDPPOINT_MAX; epnum++) { + for (int dir = 0; dir < 2; dir++) { + if (usbd->xfer_data[epnum][dir] != mp_const_none) { + usbd_edpt_stall(USBD_RHPORT, tu_edpt_addr(epnum, dir)); + usbd->xfer_data[epnum][dir] = mp_const_none; + } + } + } + } + + #if MICROPY_HW_USB_CDC + // Ensure no pending static CDC writes, as these can cause TinyUSB to crash + tud_cdc_write_clear(); + #endif + + bool was_connected = tud_connected(); + tud_disconnect(); + if (was_connected) { + mp_hal_delay_ms(50); // TODO: Always??? + } +} + +// Thjs callback is queued by mp_usbd_schedule_task() to process USB later. +void mp_usbd_task_callback(mp_sched_node_t *node) { + if (tud_inited() && !in_usbd_task) { + mp_usbd_task_inner(); + } + // If in_usbd_task is set, it means something else has already manually called + // mp_usbd_task() (most likely: C-based USB-CDC serial port). Now the MP + // scheduler is running inside there and triggering this callback. It's OK + // to skip, the already-running outer TinyUSB task will process all pending + // events before it returns. +} + +// Task function can be called manually to force processing of USB events +// (mostly from USB-CDC serial port when blocking.) +void mp_usbd_task(void) { + if (in_usbd_task) { + // If this exception triggers, it means a USB callback tried to do + // something that itself became blocked on TinyUSB (most likely: read or + // write from a C-based USB-CDC serial port.) + mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("TinyUSB callback can't recurse")); + } + + mp_usbd_task_inner(); +} + +static void mp_usbd_task_inner(void) { + in_usbd_task = true; + + tud_task_ext(0, false); + + mp_obj_usb_device_t *usbd = MP_OBJ_TO_PTR(MP_STATE_VM(usbd)); + + // Check for a triggered change to/from active state + if (usbd && usbd->trigger) { + if (usbd->active) { + if (tud_connected()) { + // If a SETUP packet has been received, first disconnect + // and wait for the host to recognise this and trigger a bus reset. + // + // Effectively this forces it to re-enumerate the device. + mp_usbd_disconnect(usbd); + } + tud_connect(); + } else { + mp_usbd_disconnect(usbd); + } + usbd->trigger = false; + } + + in_usbd_task = false; + + if (usbd) { + // Print any exceptions that were raised by Python callbacks + // inside tud_task_ext(). See usbd_callback_function_n. + + // As printing exceptions to USB-CDC may recursively call mp_usbd_task(), + // first copy out the pending data to the local stack + mp_uint_t num_pend_excs = usbd->num_pend_excs; + mp_obj_t pend_excs[MP_USBD_MAX_PEND_EXCS]; + for (mp_uint_t i = 0; i < MIN(MP_USBD_MAX_PEND_EXCS, num_pend_excs); i++) { + pend_excs[i] = usbd->pend_excs[i]; + usbd->pend_excs[i] = mp_const_none; + } + usbd->num_pend_excs = 0; + + // Now print the exceptions stored from this mp_usbd_task() call + for (mp_uint_t i = 0; i < MIN(MP_USBD_MAX_PEND_EXCS, num_pend_excs); i++) { + mp_obj_print_exception(&mp_plat_print, pend_excs[i]); + } + if (num_pend_excs > MP_USBD_MAX_PEND_EXCS) { + mp_printf(&mp_plat_print, "%u additional exceptions in USB callbacks\n", + num_pend_excs - MP_USBD_MAX_PEND_EXCS); + } + } +} + +#endif // MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE diff --git a/shared/tinyusb/tusb_config.h b/shared/tinyusb/tusb_config.h index 266cb88cc2..c5644e3d58 100644 --- a/shared/tinyusb/tusb_config.h +++ b/shared/tinyusb/tusb_config.h @@ -31,6 +31,10 @@ #if MICROPY_HW_ENABLE_USBDEV +#ifndef MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE +#define MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE 0 +#endif + #ifndef MICROPY_HW_USB_MANUFACTURER_STRING #define MICROPY_HW_USB_MANUFACTURER_STRING "MicroPython" #endif @@ -86,12 +90,9 @@ #define CFG_TUD_MSC_BUFSIZE (MICROPY_FATFS_MAX_SS) #endif -// Define static descriptor size and interface count based on the above config +#define USBD_RHPORT (0) // Currently only one port is supported -#define USBD_STATIC_DESC_LEN (TUD_CONFIG_DESC_LEN + \ - (CFG_TUD_CDC ? (TUD_CDC_DESC_LEN) : 0) + \ - (CFG_TUD_MSC ? (TUD_MSC_DESC_LEN) : 0) \ - ) +// Define built-in interface, string and endpoint numbering based on the above config #define USBD_STR_0 (0x00) #define USBD_STR_MANUF (0x01) @@ -126,19 +127,19 @@ #endif // CFG_TUD_CDC #endif // CFG_TUD_MSC -/* Limits of statically defined USB interfaces, endpoints, strings */ +/* Limits of builtin USB interfaces, endpoints, strings */ #if CFG_TUD_MSC -#define USBD_ITF_STATIC_MAX (USBD_ITF_MSC + 1) -#define USBD_STR_STATIC_MAX (USBD_STR_MSC + 1) -#define USBD_EP_STATIC_MAX (EPNUM_MSC_OUT + 1) +#define USBD_ITF_BUILTIN_MAX (USBD_ITF_MSC + 1) +#define USBD_STR_BUILTIN_MAX (USBD_STR_MSC + 1) +#define USBD_EP_BUILTIN_MAX (EPNUM_MSC_OUT + 1) #elif CFG_TUD_CDC -#define USBD_ITF_STATIC_MAX (USBD_ITF_CDC + 2) -#define USBD_STR_STATIC_MAX (USBD_STR_CDC + 1) -#define USBD_EP_STATIC_MAX (((EPNUM_CDC_EP_IN)&~TUSB_DIR_IN_MASK) + 1) +#define USBD_ITF_BUILTIN_MAX (USBD_ITF_CDC + 2) +#define USBD_STR_BUILTIN_MAX (USBD_STR_CDC + 1) +#define USBD_EP_BUILTIN_MAX (((USBD_CDC_EP_IN)&~TUSB_DIR_IN_MASK) + 1) #else // !CFG_TUD_MSC && !CFG_TUD_CDC -#define USBD_ITF_STATIC_MAX (0) -#define USBD_STR_STATIC_MAX (0) -#define USBD_EP_STATIC_MAX (0) +#define USBD_ITF_BUILTIN_MAX (0) +#define USBD_STR_BUILTIN_MAX (0) +#define USBD_EP_BUILTIN_MAX (0) #endif #endif // MICROPY_HW_ENABLE_USBDEV