Browse Source

extmod/machine_usb_device: Add support for Python USB devices.

This new machine-module driver provides a "USBDevice" singleton object and
a shim TinyUSB "runtime" driver that delegates the descriptors and all of
the TinyUSB callbacks to Python functions.  This allows writing arbitrary
USB devices in pure Python.  It's also possible to have a base built-in
USB device implemented in C (eg CDC, or CDC+MSC) and a Python USB device
added on top of that.

This work was funded through GitHub Sponsors.

Signed-off-by: Angus Gratton <angus@redyak.com.au>
pull/9497/head
Angus Gratton 12 months ago
committed by Damien George
parent
commit
9d0d262be0
  1. 286
      docs/library/machine.USBDevice.rst
  2. 1
      docs/library/machine.rst
  3. 1
      extmod/extmod.cmake
  4. 1
      extmod/extmod.mk
  5. 335
      extmod/machine_usb_device.c
  6. 3
      extmod/modmachine.c
  7. 5
      extmod/modmachine.h
  8. 4
      py/runtime.c
  9. 34
      shared/tinyusb/mp_usbd.c
  10. 101
      shared/tinyusb/mp_usbd.h
  11. 112
      shared/tinyusb/mp_usbd_descriptor.c
  12. 34
      shared/tinyusb/mp_usbd_internal.h
  13. 554
      shared/tinyusb/mp_usbd_runtime.c
  14. 31
      shared/tinyusb/tusb_config.h

286
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 <https://github.com/micropython/micropython-lib/pull/558>`_.
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
<filesystem>`. 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.

1
docs/library/machine.rst

@ -265,3 +265,4 @@ Classes
machine.WDT.rst
machine.SD.rst
machine.SDCard.rst
machine.USBDevice.rst

1
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

1
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 \

335
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

3
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

5
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

4
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

34
shared/tinyusb/mp_usbd.c

@ -24,42 +24,42 @@
* THE SOFTWARE.
*/
#include <stdlib.h>
#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

101
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 "tusb.h"
#include "py/objarray.h"
#include "py/runtime.h"
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();
}
#ifndef NO_QSTR
#include "tusb.h"
#include "device/dcd.h"
#endif
// 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

112
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

34
shared/tinyusb/mp_usbd_internal.h

@ -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

554
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 <stdlib.h>
#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

31
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

Loading…
Cancel
Save