Browse Source
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
14 changed files with 1384 additions and 118 deletions
@ -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. |
@ -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 |
@ -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
|
|
@ -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
|
Loading…
Reference in new issue