From 5d831595b01744fddaf5b08dee5379708f3035f0 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Wed, 30 Sep 2026 10:16:43 -0500 Subject: [PATCH 1/8] usb host bulk in support --- locale/circuitpython.pot | 13 +- .../adafruit_fruit_jam/mpconfigboard.mk | 3 + ports/raspberrypi/supervisor/port.c | 7 + shared-bindings/usb/core/Device.c | 88 +++++++++ shared-bindings/usb/core/Device.h | 7 + shared-module/usb/core/Device.c | 172 ++++++++++++++++++ shared-module/usb/core/Device.h | 2 + 7 files changed, 288 insertions(+), 4 deletions(-) diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index 54cd9c3dbb5..79293639f5f 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -797,7 +797,7 @@ msgstr "" #: shared-bindings/i2cioexpander/IOPin.c shared-bindings/mipidsi/Display.c #: shared-bindings/pwmio/PWMOut.c shared-bindings/supervisor/__init__.c #: shared-module/aurora_epaper/aurora_framebuffer.c -#: shared-module/lvfontio/OnDiskFont.c +#: shared-module/lvfontio/OnDiskFont.c shared-module/usb/core/Device.c msgid "Invalid %q" msgstr "" @@ -941,7 +941,7 @@ msgstr "" msgid "No pull up found on SDA or SCL; check your wiring" msgstr "" -#: ports/atmel-samd/common-hal/busio/UART.c +#: ports/atmel-samd/common-hal/busio/UART.c shared-bindings/usb/core/Device.c msgid "%q must be power of 2" msgstr "" @@ -1023,6 +1023,7 @@ msgstr "" #: ports/espressif/common-hal/espidf/__init__.c #: ports/raspberrypi/common-hal/picogame/Display.c #: shared-bindings/picogame/Canvas.c shared-bindings/picogame/__init__.c +#: shared-module/usb/core/Device.c msgid "Operation or feature not supported" msgstr "" @@ -1108,7 +1109,7 @@ msgstr "" #: ports/espressif/bindings/espnow/ESPNow.c #: ports/espressif/common-hal/espulp/ULP.c #: shared-module/memorymonitor/AllocationAlarm.c -#: shared-module/memorymonitor/AllocationSize.c +#: shared-module/memorymonitor/AllocationSize.c shared-module/usb/core/Device.c msgid "Already running" msgstr "" @@ -1543,7 +1544,7 @@ msgstr "" #: ports/raspberrypi/common-hal/usb_host/Port.c #: shared-bindings/digitalio/DigitalInOut.c shared-bindings/emmcio/EMMC.c #: shared-bindings/i2cioexpander/IOPin.c shared-bindings/microcontroller/Pin.c -#: shared-module/max3421e/Max3421E.c +#: shared-module/max3421e/Max3421E.c shared-module/usb/core/Device.c msgid "%q in use" msgstr "" @@ -4602,6 +4603,10 @@ msgstr "" msgid "No configuration set" msgstr "" +#: shared-module/usb/core/Device.c +msgid "No continuous capture running" +msgstr "" + #: shared-module/usb_hid/Device.c msgid "USB busy" msgstr "" diff --git a/ports/raspberrypi/boards/adafruit_fruit_jam/mpconfigboard.mk b/ports/raspberrypi/boards/adafruit_fruit_jam/mpconfigboard.mk index 76dd0da5f40..545d699617c 100644 --- a/ports/raspberrypi/boards/adafruit_fruit_jam/mpconfigboard.mk +++ b/ports/raspberrypi/boards/adafruit_fruit_jam/mpconfigboard.mk @@ -24,3 +24,6 @@ CFLAGS += -DCFG_TUH_ENUMERATION_BUFSIZE=4096 # is only allocated when CIRCUITPY_USB_HOST_ISO_BUFFER_SIZE is set in # settings.toml. CFLAGS += -DPIO_USB_HOST_ISOCHRONOUS=1 + +# USB host bulk input, for SDR devices +CFLAGS += -DPIO_USB_HOST_BULK_STREAM=1 diff --git a/ports/raspberrypi/supervisor/port.c b/ports/raspberrypi/supervisor/port.c index 34e9fc159fd..152097cd149 100644 --- a/ports/raspberrypi/supervisor/port.c +++ b/ports/raspberrypi/supervisor/port.c @@ -23,6 +23,10 @@ #include "shared-bindings/microcontroller/__init__.h" #include "shared-bindings/rtc/__init__.h" +#if CIRCUITPY_USB_HOST +#include "shared-bindings/usb/core/Device.h" +#endif + #if CIRCUITPY_AUDIOCORE #include "audio_dma.h" #endif @@ -476,6 +480,9 @@ safe_mode_t port_init(void) { } void reset_port(void) { + #if CIRCUITPY_USB_HOST + usb_core_device_reset(); + #endif #if CIRCUITPY_BUSIO reset_uart(); #endif diff --git a/shared-bindings/usb/core/Device.c b/shared-bindings/usb/core/Device.c index bc6c6296899..d6d9808041b 100644 --- a/shared-bindings/usb/core/Device.c +++ b/shared-bindings/usb/core/Device.c @@ -195,6 +195,90 @@ static mp_obj_t usb_core_device_set_configuration(size_t n_args, const mp_obj_t } MP_DEFINE_CONST_FUN_OBJ_KW(usb_core_device_set_configuration_obj, 1, usb_core_device_set_configuration); +//| def start_bulk_in(self, endpoint: int, buffer_size: int = 65536) -> None: +//| """Start continuous full-speed bulk IN capture into internal SRAM. +//| +//| The USB host polls the endpoint every frame, independently of Python, +//| and stores the data in a ring of ``buffer_size`` bytes, a power of two +//| from 4096 to 65536. A full ring drops the newest packets and counts +//| them in `bulk_in_lost_packets`. NAKs and duplicate packets are not +//| errors. A stalled endpoint stops capture. Only one capture may run at +//| a time, and it keeps this Device alive until stopped, disconnected, +//| deinitialized or reset. +//| +//| Only available with the PIO USB host. +//| """ +//| ... +//| +static mp_obj_t usb_core_device_start_bulk_in(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_endpoint, ARG_buffer_size }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_endpoint, MP_ARG_REQUIRED | MP_ARG_INT }, + { MP_QSTR_buffer_size, MP_ARG_INT, {.u_int = 65536} }, + }; + usb_core_device_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + check_for_deinit(self); + 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); + // IN endpoint addresses 1-15; endpoint 0 is control only. + mp_int_t endpoint = mp_arg_validate_int_range(args[ARG_endpoint].u_int, 0x81, 0x8F, MP_QSTR_endpoint); + mp_int_t buffer_size = mp_arg_validate_int_range(args[ARG_buffer_size].u_int, 4096, 65536, MP_QSTR_buffer_size); + if ((buffer_size & (buffer_size - 1)) != 0) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("%q must be power of 2"), MP_QSTR_buffer_size); + } + common_hal_usb_core_device_start_bulk_in(self, endpoint, buffer_size); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_KW(usb_core_device_start_bulk_in_obj, 2, usb_core_device_start_bulk_in); + +//| def read_bulk_into(self, buffer: WriteableBuffer) -> int: +//| """Copy captured bulk IN bytes into ``buffer`` without waiting. +//| +//| Returns the number of bytes copied, possibly zero. Raises `USBError` +//| once capture has stopped, for example because the device stalled or +//| was unplugged, and every byte captured before that has been read.""" +//| ... +//| +static mp_obj_t usb_core_device_read_bulk_into(mp_obj_t self_in, mp_obj_t buffer_in) { + usb_core_device_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + mp_buffer_info_t buffer; + mp_get_buffer_raise(buffer_in, &buffer, MP_BUFFER_WRITE); + return mp_obj_new_int(common_hal_usb_core_device_read_bulk_into(self, buffer.buf, buffer.len)); +} +MP_DEFINE_CONST_FUN_OBJ_2(usb_core_device_read_bulk_into_obj, usb_core_device_read_bulk_into); + +//| bulk_in_lost_packets: int +//| """Packets dropped since `start_bulk_in` because the ring was full, meaning +//| `read_bulk_into` was not called often enough. Zero means the captured +//| stream has no gaps. The count stays readable after capture stops, until +//| the next `start_bulk_in`. (read-only)""" +//| +static mp_obj_t usb_core_device_obj_get_bulk_in_lost_packets(mp_obj_t self_in) { + usb_core_device_obj_t *self = MP_OBJ_TO_PTR(self_in); + return mp_obj_new_int_from_uint(common_hal_usb_core_device_get_bulk_in_lost_packets(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(usb_core_device_get_bulk_in_lost_packets_obj, usb_core_device_obj_get_bulk_in_lost_packets); + +MP_PROPERTY_GETTER(usb_core_device_bulk_in_lost_packets_obj, + (mp_obj_t)&usb_core_device_get_bulk_in_lost_packets_obj); + +//| def stop_bulk_in(self) -> None: +//| """Stop capture and free its SRAM once the USB host has let go of it. +//| +//| If stopping times out, the memory stays allocated and this raises +//| `USBTimeoutError`; call it again to retry. Calling it when no capture is +//| running does nothing. Setting a configuration or an interface on the +//| device also stops its capture. +//| """ +//| ... +//| +static mp_obj_t usb_core_device_stop_bulk_in(mp_obj_t self_in) { + common_hal_usb_core_device_stop_bulk_in(MP_OBJ_TO_PTR(self_in)); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_1(usb_core_device_stop_bulk_in_obj, usb_core_device_stop_bulk_in); + //| def write(self, endpoint: int, data: ReadableBuffer, timeout: Optional[int] = None) -> int: //| """Write data to a specific endpoint on the device. //| @@ -397,6 +481,10 @@ static const mp_rom_map_elem_t usb_core_device_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_speed), MP_ROM_PTR(&usb_core_device_speed_obj) }, { MP_ROM_QSTR(MP_QSTR_set_configuration), MP_ROM_PTR(&usb_core_device_set_configuration_obj) }, + { MP_ROM_QSTR(MP_QSTR_start_bulk_in), MP_ROM_PTR(&usb_core_device_start_bulk_in_obj) }, + { MP_ROM_QSTR(MP_QSTR_read_bulk_into), MP_ROM_PTR(&usb_core_device_read_bulk_into_obj) }, + { MP_ROM_QSTR(MP_QSTR_bulk_in_lost_packets), MP_ROM_PTR(&usb_core_device_bulk_in_lost_packets_obj) }, + { MP_ROM_QSTR(MP_QSTR_stop_bulk_in), MP_ROM_PTR(&usb_core_device_stop_bulk_in_obj) }, { MP_ROM_QSTR(MP_QSTR_write), MP_ROM_PTR(&usb_core_device_write_obj) }, { MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&usb_core_device_read_obj) }, { MP_ROM_QSTR(MP_QSTR_ctrl_transfer), MP_ROM_PTR(&usb_core_device_ctrl_transfer_obj) }, diff --git a/shared-bindings/usb/core/Device.h b/shared-bindings/usb/core/Device.h index 51338f61228..d7540468470 100644 --- a/shared-bindings/usb/core/Device.h +++ b/shared-bindings/usb/core/Device.h @@ -25,6 +25,7 @@ mp_obj_t common_hal_usb_core_device_get_port_numbers(usb_core_device_obj_t *self mp_int_t common_hal_usb_core_device_get_speed(usb_core_device_obj_t *self); void common_hal_usb_core_device_set_configuration(usb_core_device_obj_t *self, mp_int_t configuration); +bool common_hal_usb_core_device_open_endpoint(usb_core_device_obj_t *self, mp_int_t endpoint); mp_int_t common_hal_usb_core_device_write(usb_core_device_obj_t *self, mp_int_t endpoint, const uint8_t *buffer, mp_int_t len, mp_int_t timeout); mp_int_t common_hal_usb_core_device_read(usb_core_device_obj_t *self, mp_int_t endpoint, uint8_t *buffer, mp_int_t len, mp_int_t timeout, bool raise_on_timeout); mp_int_t common_hal_usb_core_device_ctrl_transfer(usb_core_device_obj_t *self, @@ -35,3 +36,9 @@ mp_int_t common_hal_usb_core_device_ctrl_transfer(usb_core_device_obj_t *self, bool common_hal_usb_core_device_is_kernel_driver_active(usb_core_device_obj_t *self, mp_int_t interface); void common_hal_usb_core_device_detach_kernel_driver(usb_core_device_obj_t *self, mp_int_t interface); void common_hal_usb_core_device_attach_kernel_driver(usb_core_device_obj_t *self, mp_int_t interface); + +void usb_core_device_reset(void); +void common_hal_usb_core_device_start_bulk_in(usb_core_device_obj_t *self, mp_int_t endpoint, mp_int_t buffer_size); +mp_int_t common_hal_usb_core_device_read_bulk_into(usb_core_device_obj_t *self, uint8_t *buffer, mp_int_t length); +uint32_t common_hal_usb_core_device_get_bulk_in_lost_packets(usb_core_device_obj_t *self); +void common_hal_usb_core_device_stop_bulk_in(usb_core_device_obj_t *self); diff --git a/shared-module/usb/core/Device.c b/shared-module/usb/core/Device.c index a3bb449e87e..fb71103ffd2 100644 --- a/shared-module/usb/core/Device.c +++ b/shared-module/usb/core/Device.c @@ -21,6 +21,76 @@ #include "supervisor/shared/tick.h" #include "supervisor/usb.h" +// Continuous bulk IN capture needs the RP2 PIO host built with bulk streams. +#if CFG_TUH_RPI_PIO_USB && defined(PIO_USB_HOST_BULK_STREAM) && PIO_USB_HOST_BULK_STREAM +#define USB_CORE_BULK_CAPTURE (1) +#include "pio_usb_bulk_stream.h" +#else +#define USB_CORE_BULK_CAPTURE (0) +#endif + +#if USB_CORE_BULK_CAPTURE +MP_REGISTER_ROOT_POINTER(mp_obj_t usb_bulk_in_owner); +static pio_usb_bulk_ring_t *_bulk_ring; +static uint8_t *_bulk_storage; +static uint8_t _bulk_device_address; +static uint8_t _bulk_endpoint; + +static usb_core_device_obj_t *_bulk_owner(void) { + return MP_STATE_VM(usb_bulk_in_owner) == MP_OBJ_NULL ? NULL : MP_OBJ_TO_PTR(MP_STATE_VM(usb_bulk_in_owner)); +} + +// No Python allocation here: also called on disconnect and VM reset. On a +// timeout both SRAM allocations are kept until the host core lets go of them. +static bool _bulk_stop(bool abandon_owner) { + if (_bulk_ring == NULL) { + return true; + } + usb_core_device_obj_t *owner = _bulk_owner(); + bool stopped = pio_usb_host_bulk_stream_stop(_bulk_ring, 100000); + if (owner != NULL) { + owner->bulk_in_lost_packets = _bulk_ring->stats.overrun_packets; + } + if (stopped) { + port_free(_bulk_storage); + port_free(_bulk_ring); + _bulk_storage = NULL; + _bulk_ring = NULL; + } + if (stopped || abandon_owner) { + MP_STATE_VM(usb_bulk_in_owner) = MP_OBJ_NULL; + } + return stopped; +} + +static void _bulk_stop_device(uint8_t device_address) { + if (_bulk_ring != NULL && _bulk_device_address == device_address && !_bulk_stop(false)) { + mp_raise_usb_core_USBTimeoutError(); + } +} + +static void _bulk_check_endpoint(usb_core_device_obj_t *self, mp_int_t endpoint) { + if (_bulk_ring != NULL && _bulk_device_address == self->device_address && _bulk_endpoint == endpoint) { + mp_raise_usb_core_USBError(MP_ERROR_TEXT("%q in use"), MP_QSTR_endpoint); + } +} +#else +static void _bulk_stop_device(uint8_t device_address) { + (void)device_address; +} + +static void _bulk_check_endpoint(usb_core_device_obj_t *self, mp_int_t endpoint) { + (void)self; + (void)endpoint; +} +#endif + +void usb_core_device_reset(void) { + #if USB_CORE_BULK_CAPTURE + _bulk_stop(true); + #endif +} + // Track what device numbers are mounted. We can't use tuh_ready() because it is // true before enumeration completes and TinyUSB drivers are started. static size_t _mounted_devices = 0; @@ -31,6 +101,11 @@ void tuh_mount_cb(uint8_t dev_addr) { void tuh_umount_cb(uint8_t dev_addr) { _mounted_devices &= ~(1 << dev_addr); + #if USB_CORE_BULK_CAPTURE + if (_bulk_ring != NULL && _bulk_device_address == dev_addr) { + _bulk_stop(true); + } + #endif } static xfer_result_t _xfer_result; @@ -70,6 +145,7 @@ bool common_hal_usb_core_device_construct(usb_core_device_obj_t *self, uint8_t d } self->device_address = device_address; self->first_langid = 0; + self->bulk_in_lost_packets = 0; _xfer_result = XFER_RESULT_INVALID; return true; } @@ -82,6 +158,7 @@ void common_hal_usb_core_device_deinit(usb_core_device_obj_t *self) { if (common_hal_usb_core_device_deinited(self)) { return; } + _bulk_stop_device(self->device_address); size_t open_size = sizeof(self->open_endpoints); for (size_t i = 0; i < open_size; i++) { if (self->open_endpoints[i] != 0) { @@ -377,6 +454,7 @@ mp_int_t common_hal_usb_core_device_get_speed(usb_core_device_obj_t *self) { void common_hal_usb_core_device_set_configuration(usb_core_device_obj_t *self, mp_int_t configuration) { // We assume that the config index is one less than the value. uint8_t config_index = configuration - 1; + _bulk_stop_device(self->device_address); // Get the configuration descriptor and cache it. We'll use it later to open // endpoints. @@ -460,7 +538,95 @@ static bool _open_endpoint(usb_core_device_obj_t *self, mp_int_t endpoint) { return open; } +bool common_hal_usb_core_device_open_endpoint(usb_core_device_obj_t *self, mp_int_t endpoint) { + return _open_endpoint(self, endpoint); +} + +void common_hal_usb_core_device_start_bulk_in(usb_core_device_obj_t *self, mp_int_t endpoint, mp_int_t buffer_size) { + #if USB_CORE_BULK_CAPTURE + if (_bulk_ring != NULL) { + if (_bulk_owner() != NULL) { + mp_raise_usb_core_USBError(MP_ERROR_TEXT("Already running")); + } + if (!_bulk_stop(true)) { + mp_raise_usb_core_USBTimeoutError(); + } + } + tuh_bus_info_t bus_info; + if (!tuh_bus_info_get(self->device_address, &bus_info) || bus_info.rhport < 1 || bus_info.speed != TUSB_SPEED_FULL) { + mp_raise_usb_core_USBError(MP_ERROR_TEXT("Operation or feature not supported")); + } + if (!_open_endpoint(self, endpoint)) { + mp_raise_usb_core_USBError(MP_ERROR_TEXT("Invalid %q"), MP_QSTR_endpoint); + } + pio_usb_bulk_ring_t *ring = port_malloc(sizeof(*ring), true); + if (ring == NULL) { + mp_raise_msg(&mp_type_MemoryError, MP_ERROR_TEXT("Could not allocate DMA capable buffer")); + } + memset(ring, 0, sizeof(*ring)); + uint8_t *storage = port_malloc(buffer_size, true); + if (storage == NULL) { + port_free(ring); + mp_raise_msg(&mp_type_MemoryError, MP_ERROR_TEXT("Could not allocate DMA capable buffer")); + } + // TinyUSB numbers the PIO root ports from 1. + if (!pio_usb_host_bulk_stream_start(bus_info.rhport - 1, self->device_address, endpoint, + ring, storage, buffer_size)) { + port_free(storage); + port_free(ring); + mp_raise_usb_core_USBError(MP_ERROR_TEXT("Invalid %q"), MP_QSTR_endpoint); + } + _bulk_ring = ring; + _bulk_storage = storage; + _bulk_device_address = self->device_address; + _bulk_endpoint = endpoint; + self->bulk_in_lost_packets = 0; + // Keep the Device alive until explicit stop, disconnect, deinit or VM reset. + MP_STATE_VM(usb_bulk_in_owner) = MP_OBJ_FROM_PTR(self); + #else + mp_raise_NotImplementedError(MP_ERROR_TEXT("Operation or feature not supported")); + #endif +} + +mp_int_t common_hal_usb_core_device_read_bulk_into(usb_core_device_obj_t *self, uint8_t *buffer, mp_int_t length) { + #if USB_CORE_BULK_CAPTURE + if (_bulk_ring == NULL || _bulk_owner() != self) { + mp_raise_usb_core_USBError(MP_ERROR_TEXT("No continuous capture running")); + } + uint32_t count = pio_usb_host_bulk_stream_read(_bulk_ring, buffer, length); + // A STALL or unplug detaches the ring by itself; report it once drained. + if (count == 0 && !_bulk_ring->active) { + mp_raise_usb_core_USBError(MP_ERROR_TEXT("No continuous capture running")); + } + return count; + #else + mp_raise_NotImplementedError(MP_ERROR_TEXT("Operation or feature not supported")); + #endif +} + +uint32_t common_hal_usb_core_device_get_bulk_in_lost_packets(usb_core_device_obj_t *self) { + #if USB_CORE_BULK_CAPTURE + if (_bulk_ring != NULL && _bulk_owner() == self) { + self->bulk_in_lost_packets = _bulk_ring->stats.overrun_packets; + } + return self->bulk_in_lost_packets; + #else + mp_raise_NotImplementedError(MP_ERROR_TEXT("Operation or feature not supported")); + #endif +} + +void common_hal_usb_core_device_stop_bulk_in(usb_core_device_obj_t *self) { + #if USB_CORE_BULK_CAPTURE + if (_bulk_ring != NULL && _bulk_owner() == self && !_bulk_stop(false)) { + mp_raise_usb_core_USBTimeoutError(); + } + #else + mp_raise_NotImplementedError(MP_ERROR_TEXT("Operation or feature not supported")); + #endif +} + mp_int_t common_hal_usb_core_device_write(usb_core_device_obj_t *self, mp_int_t endpoint, const uint8_t *buffer, mp_int_t len, mp_int_t timeout) { + _bulk_check_endpoint(self, endpoint); if (!_open_endpoint(self, endpoint)) { mp_raise_usb_core_USBError(NULL); return 0; @@ -492,6 +658,7 @@ mp_int_t common_hal_usb_core_device_write(usb_core_device_obj_t *self, mp_int_t } mp_int_t common_hal_usb_core_device_read(usb_core_device_obj_t *self, mp_int_t endpoint, uint8_t *buffer, mp_int_t len, mp_int_t timeout, bool raise_on_timeout) { + _bulk_check_endpoint(self, endpoint); if (!_open_endpoint(self, endpoint)) { mp_raise_usb_core_USBError(NULL); return 0; @@ -530,6 +697,11 @@ mp_int_t common_hal_usb_core_device_ctrl_transfer(usb_core_device_obj_t *self, mp_int_t bmRequestType, mp_int_t bRequest, mp_int_t wValue, mp_int_t wIndex, uint8_t *buffer, mp_int_t len, mp_int_t timeout) { + // Stop capture before the device changes or disables its streaming endpoint. + if (len == 0 && ((bmRequestType == 0x01 && bRequest == TUSB_REQ_SET_INTERFACE) || + (bmRequestType == 0x00 && bRequest == TUSB_REQ_SET_CONFIGURATION))) { + _bulk_stop_device(self->device_address); + } // Timeout is in ms. #if !CIRCUITPY_ALL_MEMORY_DMA_CAPABLE diff --git a/shared-module/usb/core/Device.h b/shared-module/usb/core/Device.h index c7392a04ba7..131806ace77 100644 --- a/shared-module/usb/core/Device.h +++ b/shared-module/usb/core/Device.h @@ -15,4 +15,6 @@ typedef struct { uint8_t *configuration_descriptor; // Contains the length of the all descriptors. uint8_t open_endpoints[8]; uint16_t first_langid; + // Continuous bulk IN capture losses, kept after stop until the next start. + uint32_t bulk_in_lost_packets; } usb_core_device_obj_t; From 06e458a65c0987ecf5a2ebc0d3d0c7d7172ef38f Mon Sep 17 00:00:00 2001 From: foamyguy Date: Wed, 30 Sep 2026 12:48:43 -0500 Subject: [PATCH 2/8] usb_host_bulk module --- locale/circuitpython.pot | 12 ++++-- ports/raspberrypi/Makefile | 5 +++ .../adafruit_fruit_jam/mpconfigboard.mk | 3 ++ py/circuitpy_defns.mk | 5 +++ py/circuitpy_mpconfig.h | 5 +++ py/circuitpy_mpconfig.mk | 4 ++ shared-bindings/usb/core/Device.h | 1 + shared-module/usb/core/Device.c | 42 +++++++++++++++++++ 8 files changed, 74 insertions(+), 3 deletions(-) diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index d249c88fc7c..5ba42795cb4 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -915,7 +915,8 @@ msgstr "" #: ports/mimxrt10xx/common-hal/pwmio/PWMOut.c #: ports/raspberrypi/bindings/picodvi/Framebuffer.c #: ports/raspberrypi/bindings/rp2pio/StateMachine.c -#: ports/raspberrypi/common-hal/picodvi/Framebuffer_RP2040.c py/argcheck.c +#: ports/raspberrypi/common-hal/picodvi/Framebuffer_RP2040.c +#: ports/raspberrypi/common-hal/usb_host_bulk/InStream.c py/argcheck.c #: shared-bindings/digitalio/DigitalInOut.c #: shared-bindings/epaperdisplay/EPaperDisplay.c #: shared-bindings/i2cioexpander/IOPin.c shared-bindings/mipidsi/Display.c @@ -1066,6 +1067,7 @@ msgid "No pull up found on SDA or SCL; check your wiring" msgstr "" #: ports/atmel-samd/common-hal/busio/UART.c +#: shared-bindings/usb_host_bulk/InStream.c msgid "%q must be power of 2" msgstr "" @@ -1146,6 +1148,7 @@ msgstr "" #: ports/atmel-samd/common-hal/picogame/Display.c #: ports/espressif/common-hal/espidf/__init__.c #: ports/raspberrypi/common-hal/picogame/Display.c +#: ports/raspberrypi/common-hal/usb_host_bulk/InStream.c #: shared-bindings/picogame/Canvas.c shared-bindings/picogame/__init__.c msgid "Operation or feature not supported" msgstr "" @@ -1231,6 +1234,7 @@ msgstr "" #: ports/espressif/bindings/espnow/ESPNow.c #: ports/espressif/common-hal/espulp/ULP.c +#: ports/raspberrypi/common-hal/usb_host_bulk/InStream.c #: shared-module/memorymonitor/AllocationAlarm.c #: shared-module/memorymonitor/AllocationSize.c msgid "Already running" @@ -1667,7 +1671,7 @@ msgstr "" #: ports/raspberrypi/common-hal/usb_host/Port.c #: shared-bindings/digitalio/DigitalInOut.c shared-bindings/emmcio/EMMC.c #: shared-bindings/i2cioexpander/IOPin.c shared-bindings/microcontroller/Pin.c -#: shared-module/max3421e/Max3421E.c +#: shared-module/max3421e/Max3421E.c shared-module/usb/core/Device.c msgid "%q in use" msgstr "" @@ -1721,7 +1725,9 @@ msgstr "" msgid "pop from an empty PulseIn" msgstr "" -#: ports/espressif/common-hal/qspibus/QSPIBus.c shared-module/usb/core/Device.c +#: ports/espressif/common-hal/qspibus/QSPIBus.c +#: ports/raspberrypi/common-hal/usb_host_bulk/InStream.c +#: shared-module/usb/core/Device.c msgid "Could not allocate DMA capable buffer" msgstr "" diff --git a/ports/raspberrypi/Makefile b/ports/raspberrypi/Makefile index f961664a081..997cd4681a7 100755 --- a/ports/raspberrypi/Makefile +++ b/ports/raspberrypi/Makefile @@ -614,6 +614,11 @@ SRC_C += \ INC += \ -isystem lib/Pico-PIO-USB/src +ifeq ($(CIRCUITPY_USB_HOST_BULK),1) +# Pico-PIO-USB bulk IN ring, polled from the host frame handler. +CFLAGS += -DPIO_USB_HOST_BULK_STREAM=1 +endif + # Core1 posts USB events while core0 may hold the TinyUSB queue mutex. The # contended wait path calls these two flash-resident SDK functions, and core1 # must not execute from flash. Replace them with RAM-resident implementations diff --git a/ports/raspberrypi/boards/adafruit_fruit_jam/mpconfigboard.mk b/ports/raspberrypi/boards/adafruit_fruit_jam/mpconfigboard.mk index 76dd0da5f40..f105f7436eb 100644 --- a/ports/raspberrypi/boards/adafruit_fruit_jam/mpconfigboard.mk +++ b/ports/raspberrypi/boards/adafruit_fruit_jam/mpconfigboard.mk @@ -24,3 +24,6 @@ CFLAGS += -DCFG_TUH_ENUMERATION_BUFSIZE=4096 # is only allocated when CIRCUITPY_USB_HOST_ISO_BUFFER_SIZE is set in # settings.toml. CFLAGS += -DPIO_USB_HOST_ISOCHRONOUS=1 + +# USB host bulk input, for SDR devices +CIRCUITPY_USB_HOST_BULK = 1 diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index 8fe4f6da7df..45b66986af3 100755 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -471,6 +471,9 @@ endif ifeq ($(CIRCUITPY_USB_HOST),1) SRC_PATTERNS += usb_host/% endif +ifeq ($(CIRCUITPY_USB_HOST_BULK),1) +SRC_PATTERNS += usb_host_bulk/% +endif ifeq ($(CIRCUITPY_USB_MIDI),1) SRC_PATTERNS += usb_midi/% endif @@ -610,6 +613,8 @@ SRC_COMMON_HAL_ALL = \ spitarget/__init__.c \ usb_host/__init__.c \ usb_host/Port.c \ + usb_host_bulk/__init__.c \ + usb_host_bulk/InStream.c \ watchdog/WatchDogMode.c \ watchdog/WatchDogTimer.c \ watchdog/__init__.c \ diff --git a/py/circuitpy_mpconfig.h b/py/circuitpy_mpconfig.h index e7e8ea9f2fb..934e0924851 100644 --- a/py/circuitpy_mpconfig.h +++ b/py/circuitpy_mpconfig.h @@ -597,6 +597,11 @@ void background_callback_run_all(void); #define CIRCUITPY_USB_HOST_HIGH_SPEED 0 #endif +// usb_host_bulk takes a usb.core.Device and uses TinyUSB host calls. +#if CIRCUITPY_USB_HOST_BULK && !(CIRCUITPY_USB_HOST && CIRCUITPY_PYUSB) +#error CIRCUITPY_USB_HOST_BULK requires CIRCUITPY_USB_HOST and CIRCUITPY_PYUSB +#endif + // If the port requires certain USB endpoint numbers, define these in mpconfigport.h. #ifndef USB_CDC_EP_NUM_NOTIFICATION diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index d3de1d32bb6..3b030cce52b 100755 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -775,6 +775,10 @@ CFLAGS += -DCIRCUITPY_USB_HOST=$(CIRCUITPY_USB_HOST) CIRCUITPY_PYUSB ?= $(call enable-if-any,$(CIRCUITPY_USB_HOST) $(CIRCUITPY_MAX3421E)) CFLAGS += -DCIRCUITPY_PYUSB=$(CIRCUITPY_PYUSB) +# Continuous bulk transfers on PIO USB host ports. Opt-in per board. +CIRCUITPY_USB_HOST_BULK ?= 0 +CFLAGS += -DCIRCUITPY_USB_HOST_BULK=$(CIRCUITPY_USB_HOST_BULK) + CIRCUITPY_USB_KEYBOARD_WORKFLOW ?= $(call enable-if-any,$(CIRCUITPY_USB_HOST) $(CIRCUITPY_MAX3421E)) CFLAGS += -DCIRCUITPY_USB_KEYBOARD_WORKFLOW=$(CIRCUITPY_USB_KEYBOARD_WORKFLOW) diff --git a/shared-bindings/usb/core/Device.h b/shared-bindings/usb/core/Device.h index 51338f61228..5814469240a 100644 --- a/shared-bindings/usb/core/Device.h +++ b/shared-bindings/usb/core/Device.h @@ -25,6 +25,7 @@ mp_obj_t common_hal_usb_core_device_get_port_numbers(usb_core_device_obj_t *self mp_int_t common_hal_usb_core_device_get_speed(usb_core_device_obj_t *self); void common_hal_usb_core_device_set_configuration(usb_core_device_obj_t *self, mp_int_t configuration); +bool common_hal_usb_core_device_open_endpoint(usb_core_device_obj_t *self, mp_int_t endpoint); mp_int_t common_hal_usb_core_device_write(usb_core_device_obj_t *self, mp_int_t endpoint, const uint8_t *buffer, mp_int_t len, mp_int_t timeout); mp_int_t common_hal_usb_core_device_read(usb_core_device_obj_t *self, mp_int_t endpoint, uint8_t *buffer, mp_int_t len, mp_int_t timeout, bool raise_on_timeout); mp_int_t common_hal_usb_core_device_ctrl_transfer(usb_core_device_obj_t *self, diff --git a/shared-module/usb/core/Device.c b/shared-module/usb/core/Device.c index a3bb449e87e..18c1728276c 100644 --- a/shared-module/usb/core/Device.c +++ b/shared-module/usb/core/Device.c @@ -21,6 +21,28 @@ #include "supervisor/shared/tick.h" #include "supervisor/usb.h" +#if CIRCUITPY_USB_HOST_BULK +#include "shared-bindings/usb_host_bulk/InStream.h" +#else +static inline void usb_host_bulk_stop_device(uint8_t device_address) { + (void)device_address; +} + +static inline bool usb_host_bulk_endpoint_busy(uint8_t device_address, uint8_t endpoint) { + (void)device_address; + (void)endpoint; + return false; +} + +static inline void usb_host_bulk_device_gone(uint8_t device_address) { + (void)device_address; +} + +static inline void usb_host_bulk_device_deinit(usb_core_device_obj_t *device) { + (void)device; +} +#endif + // Track what device numbers are mounted. We can't use tuh_ready() because it is // true before enumeration completes and TinyUSB drivers are started. static size_t _mounted_devices = 0; @@ -31,6 +53,7 @@ void tuh_mount_cb(uint8_t dev_addr) { void tuh_umount_cb(uint8_t dev_addr) { _mounted_devices &= ~(1 << dev_addr); + usb_host_bulk_device_gone(dev_addr); } static xfer_result_t _xfer_result; @@ -82,6 +105,7 @@ void common_hal_usb_core_device_deinit(usb_core_device_obj_t *self) { if (common_hal_usb_core_device_deinited(self)) { return; } + usb_host_bulk_device_deinit(self); size_t open_size = sizeof(self->open_endpoints); for (size_t i = 0; i < open_size; i++) { if (self->open_endpoints[i] != 0) { @@ -377,6 +401,7 @@ mp_int_t common_hal_usb_core_device_get_speed(usb_core_device_obj_t *self) { void common_hal_usb_core_device_set_configuration(usb_core_device_obj_t *self, mp_int_t configuration) { // We assume that the config index is one less than the value. uint8_t config_index = configuration - 1; + usb_host_bulk_stop_device(self->device_address); // Get the configuration descriptor and cache it. We'll use it later to open // endpoints. @@ -460,7 +485,18 @@ static bool _open_endpoint(usb_core_device_obj_t *self, mp_int_t endpoint) { return open; } +bool common_hal_usb_core_device_open_endpoint(usb_core_device_obj_t *self, mp_int_t endpoint) { + return _open_endpoint(self, endpoint); +} + +static void _check_endpoint_free(usb_core_device_obj_t *self, mp_int_t endpoint) { + if (usb_host_bulk_endpoint_busy(self->device_address, endpoint)) { + mp_raise_usb_core_USBError(MP_ERROR_TEXT("%q in use"), MP_QSTR_endpoint); + } +} + mp_int_t common_hal_usb_core_device_write(usb_core_device_obj_t *self, mp_int_t endpoint, const uint8_t *buffer, mp_int_t len, mp_int_t timeout) { + _check_endpoint_free(self, endpoint); if (!_open_endpoint(self, endpoint)) { mp_raise_usb_core_USBError(NULL); return 0; @@ -492,6 +528,7 @@ mp_int_t common_hal_usb_core_device_write(usb_core_device_obj_t *self, mp_int_t } mp_int_t common_hal_usb_core_device_read(usb_core_device_obj_t *self, mp_int_t endpoint, uint8_t *buffer, mp_int_t len, mp_int_t timeout, bool raise_on_timeout) { + _check_endpoint_free(self, endpoint); if (!_open_endpoint(self, endpoint)) { mp_raise_usb_core_USBError(NULL); return 0; @@ -530,6 +567,11 @@ mp_int_t common_hal_usb_core_device_ctrl_transfer(usb_core_device_obj_t *self, mp_int_t bmRequestType, mp_int_t bRequest, mp_int_t wValue, mp_int_t wIndex, uint8_t *buffer, mp_int_t len, mp_int_t timeout) { + // Stop a stream before the device changes or disables its endpoint. + if (len == 0 && ((bmRequestType == 0x01 && bRequest == TUSB_REQ_SET_INTERFACE) || + (bmRequestType == 0x00 && bRequest == TUSB_REQ_SET_CONFIGURATION))) { + usb_host_bulk_stop_device(self->device_address); + } // Timeout is in ms. #if !CIRCUITPY_ALL_MEMORY_DMA_CAPABLE From 396e568d43ad7086ff824729d37a3957712a991a Mon Sep 17 00:00:00 2001 From: foamyguy Date: Wed, 30 Sep 2026 13:14:50 -0500 Subject: [PATCH 3/8] refactor to usb_host_bulk module with InStream class instead of changes to usb.core --- locale/circuitpython.pot | 195 +++++++++--- .../common-hal/usb_host_bulk/InStream.c | 191 ++++++++++++ .../common-hal/usb_host_bulk/InStream.h | 23 ++ .../common-hal/usb_host_bulk/__init__.c | 7 + ports/raspberrypi/supervisor/port.c | 7 - shared-bindings/usb/core/Device.c | 88 ------ shared-bindings/usb/core/Device.h | 6 - shared-bindings/usb_host_bulk/InStream.c | 280 ++++++++++++++++++ shared-bindings/usb_host_bulk/InStream.h | 43 +++ shared-bindings/usb_host_bulk/__init__.c | 36 +++ shared-bindings/usb_host_bulk/__init__.h | 7 + shared-module/usb/core/Device.h | 2 - 12 files changed, 747 insertions(+), 138 deletions(-) create mode 100644 ports/raspberrypi/common-hal/usb_host_bulk/InStream.c create mode 100644 ports/raspberrypi/common-hal/usb_host_bulk/InStream.h create mode 100644 ports/raspberrypi/common-hal/usb_host_bulk/__init__.c create mode 100644 shared-bindings/usb_host_bulk/InStream.c create mode 100644 shared-bindings/usb_host_bulk/InStream.h create mode 100644 shared-bindings/usb_host_bulk/__init__.c create mode 100644 shared-bindings/usb_host_bulk/__init__.h diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index 79293639f5f..ff48f2c64d1 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -100,6 +100,10 @@ msgstr "" msgid "ndarray length overflows" msgstr "" +#: extmod/ulab/code/ndarray.c +msgid "maximum number of dimensions is " +msgstr "" + #: extmod/ulab/code/ndarray.c msgid "cannot convert complex type" msgstr "" @@ -154,11 +158,23 @@ msgid "operation is not supported for given type" msgstr "" #: extmod/ulab/code/ndarray.c -msgid "shape must be integer or tuple of integers" +msgid "keyword argument must be tuple of integers" msgstr "" -#: extmod/ulab/code/ndarray.c extmod/ulab/code/numpy/random/random.c -msgid "maximum number of dimensions is " +#: extmod/ulab/code/ndarray.c +msgid "too many axes specified" +msgstr "" + +#: extmod/ulab/code/ndarray.c +msgid "axis index out of bounds" +msgstr "" + +#: extmod/ulab/code/ndarray.c +msgid "repeated indices" +msgstr "" + +#: extmod/ulab/code/ndarray.c +msgid "shape must be integer or tuple of integers" msgstr "" #: extmod/ulab/code/ndarray.c @@ -225,6 +241,31 @@ msgstr "" msgid "not implemented for complex dtype" msgstr "" +#: extmod/ulab/code/numpy/compare.c extmod/ulab/code/user/user.c +#: shared-bindings/_eve/__init__.c +msgid "input must be an ndarray" +msgstr "" + +#: extmod/ulab/code/numpy/compare.c +msgid "object too deep for desired array" +msgstr "" + +#: extmod/ulab/code/numpy/compare.c +msgid "cannot cast array data from dtype" +msgstr "" + +#: extmod/ulab/code/numpy/compare.c +msgid "minlength must not be negative" +msgstr "" + +#: extmod/ulab/code/numpy/compare.c +msgid "the weights and list don't have the same length" +msgstr "" + +#: extmod/ulab/code/numpy/compare.c +msgid "cannot cast weigths to float" +msgstr "" + #: extmod/ulab/code/numpy/compare.c extmod/ulab/code/numpy/create.c #: extmod/ulab/code/numpy/io/io.c extmod/ulab/code/numpy/transform.c #: extmod/ulab/code/numpy/vector.c @@ -259,7 +300,7 @@ msgstr "" msgid "wrong axis specified" msgstr "" -#: extmod/ulab/code/numpy/create.c +#: extmod/ulab/code/numpy/create.c extmod/ulab/code/utils/utils.c msgid "input arrays are not compatible" msgstr "" @@ -271,6 +312,45 @@ msgstr "" msgid "number of points must be at least 2" msgstr "" +#: extmod/ulab/code/numpy/create.c +msgid "indexing should be 'xy' or 'ij'" +msgstr "" + +#: extmod/ulab/code/numpy/create.c +msgid "indexing must be 'xy' or 'ij'" +msgstr "" + +#: extmod/ulab/code/numpy/create.c +msgid "too many input arrays" +msgstr "" + +#: extmod/ulab/code/numpy/create.c extmod/ulab/code/numpy/numerical.c +#: extmod/ulab/code/numpy/transform.c +msgid "arguments must be ndarrays" +msgstr "" + +#: extmod/ulab/code/numpy/create.c +msgid "arguments must be 1D arrays" +msgstr "" + +#: extmod/ulab/code/numpy/create.c +msgid "input is not an array" +msgstr "" + +#: extmod/ulab/code/numpy/create.c extmod/ulab/code/numpy/numerical.c +#: ports/espressif/common-hal/pulseio/PulseIn.c +#: shared-bindings/bitmaptools/__init__.c +msgid "index out of range" +msgstr "" + +#: extmod/ulab/code/numpy/create.c +msgid "mode should be raise, wrap or clip" +msgstr "" + +#: extmod/ulab/code/numpy/create.c +msgid "index must not be negative" +msgstr "" + #: extmod/ulab/code/numpy/create.c msgid "offset must be non-negative and no greater than buffer length" msgstr "" @@ -291,7 +371,7 @@ msgstr "" msgid "FFT is implemented for linear arrays only" msgstr "" -#: extmod/ulab/code/numpy/fft/fft_tools.c +#: extmod/ulab/code/numpy/fft/fft_tools.c extmod/ulab/code/utils/utils.c msgid "input array length must be power of 2" msgstr "" @@ -400,10 +480,6 @@ msgstr "" msgid "axis too long" msgstr "" -#: extmod/ulab/code/numpy/numerical.c extmod/ulab/code/numpy/transform.c -msgid "arguments must be ndarrays" -msgstr "" - #: extmod/ulab/code/numpy/numerical.c msgid "cross is defined for 1D arrays of length 3" msgstr "" @@ -412,12 +488,6 @@ msgstr "" msgid "diff argument must be an ndarray" msgstr "" -#: extmod/ulab/code/numpy/numerical.c extmod/ulab/code/ulab_tools.c -#: ports/espressif/common-hal/pulseio/PulseIn.c -#: shared-bindings/bitmaptools/__init__.c -msgid "index out of range" -msgstr "" - #: extmod/ulab/code/numpy/numerical.c msgid "differentiation order out of range" msgstr "" @@ -463,10 +533,10 @@ msgid "argument must be None, an integer or a tuple of integers" msgstr "" #: extmod/ulab/code/numpy/random/random.c -msgid "shape must be None, and integer or a tuple of integers" +msgid "shape must be None, an integer or a tuple of integers" msgstr "" -#: extmod/ulab/code/numpy/random/random.c +#: extmod/ulab/code/numpy/random/random.c extmod/ulab/code/ulab_tools.c msgid "out has wrong type" msgstr "" @@ -478,7 +548,7 @@ msgstr "" msgid "size must match out.shape when used together" msgstr "" -#: extmod/ulab/code/numpy/random/random.c +#: extmod/ulab/code/numpy/random/random.c extmod/ulab/code/ulab_tools.c msgid "output array must be contiguous" msgstr "" @@ -502,7 +572,7 @@ msgstr "" msgid "dimensions do not match" msgstr "" -#: extmod/ulab/code/numpy/vector.c +#: extmod/ulab/code/numpy/vector.c extmod/ulab/code/utils/utils.c msgid "out must be an ndarray" msgstr "" @@ -538,7 +608,7 @@ msgstr "" msgid "input dtype must be float or complex" msgstr "" -#: extmod/ulab/code/numpy/vector.c +#: extmod/ulab/code/numpy/vector.c extmod/ulab/code/scipy/integrate/integrate.c msgid "first argument must be a callable" msgstr "" @@ -546,18 +616,36 @@ msgstr "" msgid "wrong output type" msgstr "" -#: extmod/ulab/code/scipy/linalg/linalg.c -msgid "first two arguments must be ndarrays" +#: extmod/ulab/code/scipy/integrate/integrate.c +#, c-format +msgid "can't convert arg %d from %s to float" msgstr "" -#: extmod/ulab/code/scipy/linalg/linalg.c extmod/ulab/code/user/user.c -msgid "input must be a dense ndarray" +#: extmod/ulab/code/scipy/integrate/integrate.c +msgid "levels needs to be a positive integer" msgstr "" +#: extmod/ulab/code/scipy/integrate/integrate.c +msgid "steps needs to be a positive integer" +msgstr "" + +#: extmod/ulab/code/scipy/integrate/integrate.c #: extmod/ulab/code/scipy/optimize/optimize.c msgid "first argument must be a function" msgstr "" +#: extmod/ulab/code/scipy/integrate/integrate.c +msgid "order needs to be a positive integer" +msgstr "" + +#: extmod/ulab/code/scipy/linalg/linalg.c +msgid "first two arguments must be ndarrays" +msgstr "" + +#: extmod/ulab/code/scipy/linalg/linalg.c extmod/ulab/code/user/user.c +msgid "input must be a dense ndarray" +msgstr "" + #: extmod/ulab/code/scipy/optimize/optimize.c msgid "function has the same sign at the ends of interval" msgstr "" @@ -622,8 +710,16 @@ msgstr "" msgid "input must be square matrix" msgstr "" -#: extmod/ulab/code/user/user.c shared-bindings/_eve/__init__.c -msgid "input must be an ndarray" +#: extmod/ulab/code/ulab_tools.c +msgid "out array has wrong dtype" +msgstr "" + +#: extmod/ulab/code/ulab_tools.c +msgid "out array has wrong dimension" +msgstr "" + +#: extmod/ulab/code/ulab_tools.c +msgid "out array has wrong shape" msgstr "" #: extmod/ulab/code/utils/utils.c @@ -638,6 +734,34 @@ msgstr "" msgid "out array is too small" msgstr "" +#: extmod/ulab/code/utils/utils.c +msgid "spectrogram is defined for ndarrays only" +msgstr "" + +#: extmod/ulab/code/utils/utils.c +msgid "spectrogram is implemented for 1D arrays only" +msgstr "" + +#: extmod/ulab/code/utils/utils.c +msgid "out array must be a 1D array of float type" +msgstr "" + +#: extmod/ulab/code/utils/utils.c +msgid "input and out arrays must have same length" +msgstr "" + +#: extmod/ulab/code/utils/utils.c +msgid "scratchpad must be an ndarray" +msgstr "" + +#: extmod/ulab/code/utils/utils.c +msgid "scratchpad must be a 1D dense float array" +msgstr "" + +#: extmod/ulab/code/utils/utils.c +msgid "scratchpad must be twice as long as input" +msgstr "" + #: extmod/vfs_fat.c py/moderrno.c msgid "Read-only filesystem" msgstr "" @@ -791,13 +915,14 @@ msgstr "" #: ports/mimxrt10xx/common-hal/pwmio/PWMOut.c #: ports/raspberrypi/bindings/picodvi/Framebuffer.c #: ports/raspberrypi/bindings/rp2pio/StateMachine.c -#: ports/raspberrypi/common-hal/picodvi/Framebuffer_RP2040.c py/argcheck.c +#: ports/raspberrypi/common-hal/picodvi/Framebuffer_RP2040.c +#: ports/raspberrypi/common-hal/usb_host_bulk/InStream.c py/argcheck.c #: shared-bindings/digitalio/DigitalInOut.c #: shared-bindings/epaperdisplay/EPaperDisplay.c #: shared-bindings/i2cioexpander/IOPin.c shared-bindings/mipidsi/Display.c #: shared-bindings/pwmio/PWMOut.c shared-bindings/supervisor/__init__.c #: shared-module/aurora_epaper/aurora_framebuffer.c -#: shared-module/lvfontio/OnDiskFont.c shared-module/usb/core/Device.c +#: shared-module/lvfontio/OnDiskFont.c msgid "Invalid %q" msgstr "" @@ -942,6 +1067,7 @@ msgid "No pull up found on SDA or SCL; check your wiring" msgstr "" #: ports/atmel-samd/common-hal/busio/UART.c shared-bindings/usb/core/Device.c +#: shared-bindings/usb_host_bulk/InStream.c msgid "%q must be power of 2" msgstr "" @@ -1022,8 +1148,8 @@ msgstr "" #: ports/atmel-samd/common-hal/picogame/Display.c #: ports/espressif/common-hal/espidf/__init__.c #: ports/raspberrypi/common-hal/picogame/Display.c +#: ports/raspberrypi/common-hal/usb_host_bulk/InStream.c #: shared-bindings/picogame/Canvas.c shared-bindings/picogame/__init__.c -#: shared-module/usb/core/Device.c msgid "Operation or feature not supported" msgstr "" @@ -1108,8 +1234,9 @@ msgstr "" #: ports/espressif/bindings/espnow/ESPNow.c #: ports/espressif/common-hal/espulp/ULP.c +#: ports/raspberrypi/common-hal/usb_host_bulk/InStream.c #: shared-module/memorymonitor/AllocationAlarm.c -#: shared-module/memorymonitor/AllocationSize.c shared-module/usb/core/Device.c +#: shared-module/memorymonitor/AllocationSize.c msgid "Already running" msgstr "" @@ -1598,7 +1725,9 @@ msgstr "" msgid "pop from an empty PulseIn" msgstr "" -#: ports/espressif/common-hal/qspibus/QSPIBus.c shared-module/usb/core/Device.c +#: ports/espressif/common-hal/qspibus/QSPIBus.c +#: ports/raspberrypi/common-hal/usb_host_bulk/InStream.c +#: shared-module/usb/core/Device.c msgid "Could not allocate DMA capable buffer" msgstr "" @@ -4603,10 +4732,6 @@ msgstr "" msgid "No configuration set" msgstr "" -#: shared-module/usb/core/Device.c -msgid "No continuous capture running" -msgstr "" - #: shared-module/usb_hid/Device.c msgid "USB busy" msgstr "" diff --git a/ports/raspberrypi/common-hal/usb_host_bulk/InStream.c b/ports/raspberrypi/common-hal/usb_host_bulk/InStream.c new file mode 100644 index 00000000000..7032eda6acf --- /dev/null +++ b/ports/raspberrypi/common-hal/usb_host_bulk/InStream.c @@ -0,0 +1,191 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include + +#include "shared-bindings/usb_host_bulk/InStream.h" + +#include "hardware/sync.h" +#include "py/runtime.h" +#include "shared-bindings/usb/core/__init__.h" +#include "shared-bindings/usb/core/Device.h" +#include "supervisor/port_heap.h" +#include "tusb.h" + +#include "pio_usb_bulk_stream.h" + +#define STOP_TIMEOUT_US (100000) + +// The stream the host is polling, if any. A weak link: the Python object owns +// the ring, and its finaliser clears this. +static usb_host_bulk_instream_obj_t *_active; + +// A ring whose stop timed out in a finaliser. The host core may still write +// to it, so it is kept until a later stop succeeds. +static pio_usb_bulk_ring_t *_orphan_ring; +static uint8_t *_orphan_storage; + +static bool _stop_orphan(void) { + if (_orphan_ring == NULL) { + return true; + } + if (!pio_usb_host_bulk_stream_stop(_orphan_ring, STOP_TIMEOUT_US)) { + return false; + } + port_free(_orphan_storage); + port_free(_orphan_ring); + _orphan_storage = NULL; + _orphan_ring = NULL; + return true; +} + +// Detach the ring from the host but keep it, so that bytes captured before +// the stop can still be read. No Python allocation: also used on unplug. +static bool _stop(usb_host_bulk_instream_obj_t *self) { + if (!pio_usb_host_bulk_stream_stop(self->ring, STOP_TIMEOUT_US)) { + return false; + } + if (_active == self) { + _active = NULL; + } + return true; +} + +void common_hal_usb_host_bulk_instream_construct(usb_host_bulk_instream_obj_t *self, + usb_core_device_obj_t *device, uint8_t endpoint, uint32_t buffer_size) { + if (!_stop_orphan()) { + mp_raise_usb_core_USBTimeoutError(); + } + if (_active != NULL) { + if (_active->ring->active) { + mp_raise_RuntimeError(MP_ERROR_TEXT("Already running")); + } + // It ended on its own and keeps its ring until it is deinited. + _active = NULL; + } + tuh_bus_info_t bus_info; + if (!tuh_bus_info_get(device->device_address, &bus_info) || bus_info.rhport < 1 || + bus_info.speed != TUSB_SPEED_FULL) { + mp_raise_usb_core_USBError(MP_ERROR_TEXT("Operation or feature not supported")); + } + if (!common_hal_usb_core_device_open_endpoint(device, endpoint)) { + mp_raise_usb_core_USBError(MP_ERROR_TEXT("Invalid %q"), MP_QSTR_endpoint); + } + // The host core writes both from the frame handler, so they must be in SRAM. + pio_usb_bulk_ring_t *ring = port_malloc(sizeof(*ring), true); + if (ring == NULL) { + mp_raise_msg(&mp_type_MemoryError, MP_ERROR_TEXT("Could not allocate DMA capable buffer")); + } + memset(ring, 0, sizeof(*ring)); + uint8_t *storage = port_malloc(buffer_size, true); + if (storage == NULL) { + port_free(ring); + mp_raise_msg(&mp_type_MemoryError, MP_ERROR_TEXT("Could not allocate DMA capable buffer")); + } + // TinyUSB numbers the PIO root ports from 1. + if (!pio_usb_host_bulk_stream_start(bus_info.rhport - 1, device->device_address, endpoint, + ring, storage, buffer_size)) { + port_free(storage); + port_free(ring); + mp_raise_usb_core_USBError(MP_ERROR_TEXT("Invalid %q"), MP_QSTR_endpoint); + } + self->device = MP_OBJ_FROM_PTR(device); + self->ring = ring; + self->storage = storage; + self->lost_packets = 0; + self->device_address = device->device_address; + self->endpoint = endpoint; + _active = self; +} + +bool common_hal_usb_host_bulk_instream_deinited(usb_host_bulk_instream_obj_t *self) { + return self->device == MP_OBJ_NULL; +} + +// Uses only this object's own fields: in a finaliser, the Device may already +// have been swept. +void common_hal_usb_host_bulk_instream_deinit(usb_host_bulk_instream_obj_t *self, bool abandon) { + if (common_hal_usb_host_bulk_instream_deinited(self)) { + return; + } + bool stopped = _stop(self); + if (!stopped && !abandon) { + mp_raise_usb_core_USBTimeoutError(); + } + self->lost_packets = self->ring->stats.overrun_packets; + if (stopped) { + port_free(self->storage); + port_free(self->ring); + } else if (_stop_orphan()) { + _orphan_ring = self->ring; + _orphan_storage = self->storage; + } + // Otherwise two rings are stuck. Leak this one rather than free memory + // the host core may still write to. + if (_active == self) { + _active = NULL; + } + self->ring = NULL; + self->storage = NULL; + self->device = MP_OBJ_NULL; +} + +uint32_t common_hal_usb_host_bulk_instream_read(usb_host_bulk_instream_obj_t *self, uint8_t *data, uint32_t len) { + return pio_usb_host_bulk_stream_read(self->ring, data, len); +} + +uint32_t common_hal_usb_host_bulk_instream_get_in_waiting(usb_host_bulk_instream_obj_t *self) { + pio_usb_bulk_ring_t *ring = self->ring; + uint32_t const read = ring->read_pos; + uint32_t const written = ring->write_pos; + __dmb(); + uint32_t const available = written - read; + return available > ring->capacity ? 0 : available; +} + +uint32_t common_hal_usb_host_bulk_instream_get_lost_packets(usb_host_bulk_instream_obj_t *self) { + if (self->ring != NULL) { + return self->ring->stats.overrun_packets; + } + return self->lost_packets; +} + +bool common_hal_usb_host_bulk_instream_get_ended(usb_host_bulk_instream_obj_t *self) { + bool ended = !self->ring->active; + // Clearing active is the host's last access, so later reads of the ring + // see every byte it wrote. + __dmb(); + return ended; +} + +void common_hal_usb_host_bulk_instream_reset_input_buffer(usb_host_bulk_instream_obj_t *self) { + // read_pos belongs to the consumer, so moving it is safe while capturing. + self->ring->read_pos = self->ring->write_pos; +} + +void usb_host_bulk_stop_device(uint8_t device_address) { + if (_active != NULL && _active->device_address == device_address && !_stop(_active)) { + mp_raise_usb_core_USBTimeoutError(); + } +} + +bool usb_host_bulk_endpoint_busy(uint8_t device_address, uint8_t endpoint) { + return _active != NULL && _active->device_address == device_address && + _active->endpoint == endpoint && _active->ring->active; +} + +void usb_host_bulk_device_deinit(usb_core_device_obj_t *device) { + if (_active != NULL && _active->device == MP_OBJ_FROM_PTR(device)) { + _stop(_active); + } +} + +void usb_host_bulk_device_gone(uint8_t device_address) { + // The host core also detaches the ring by itself once it sees the unplug. + if (_active != NULL && _active->device_address == device_address) { + _stop(_active); + } +} diff --git a/ports/raspberrypi/common-hal/usb_host_bulk/InStream.h b/ports/raspberrypi/common-hal/usb_host_bulk/InStream.h new file mode 100644 index 00000000000..84a81d3ed03 --- /dev/null +++ b/ports/raspberrypi/common-hal/usb_host_bulk/InStream.h @@ -0,0 +1,23 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include + +#include "py/obj.h" + +#include "pio_usb_bulk_stream.h" + +typedef struct { + mp_obj_base_t base; + mp_obj_t device; // keeps the usb.core.Device alive; NULL once deinited + pio_usb_bulk_ring_t *ring; // NULL once freed + uint8_t *storage; + uint32_t lost_packets; // latched when the ring is freed + uint8_t device_address; // copies, so deinit never touches `device` + uint8_t endpoint; +} usb_host_bulk_instream_obj_t; diff --git a/ports/raspberrypi/common-hal/usb_host_bulk/__init__.c b/ports/raspberrypi/common-hal/usb_host_bulk/__init__.c new file mode 100644 index 00000000000..81be9d0b79d --- /dev/null +++ b/ports/raspberrypi/common-hal/usb_host_bulk/__init__.c @@ -0,0 +1,7 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +// Nothing diff --git a/ports/raspberrypi/supervisor/port.c b/ports/raspberrypi/supervisor/port.c index 152097cd149..34e9fc159fd 100644 --- a/ports/raspberrypi/supervisor/port.c +++ b/ports/raspberrypi/supervisor/port.c @@ -23,10 +23,6 @@ #include "shared-bindings/microcontroller/__init__.h" #include "shared-bindings/rtc/__init__.h" -#if CIRCUITPY_USB_HOST -#include "shared-bindings/usb/core/Device.h" -#endif - #if CIRCUITPY_AUDIOCORE #include "audio_dma.h" #endif @@ -480,9 +476,6 @@ safe_mode_t port_init(void) { } void reset_port(void) { - #if CIRCUITPY_USB_HOST - usb_core_device_reset(); - #endif #if CIRCUITPY_BUSIO reset_uart(); #endif diff --git a/shared-bindings/usb/core/Device.c b/shared-bindings/usb/core/Device.c index d6d9808041b..bc6c6296899 100644 --- a/shared-bindings/usb/core/Device.c +++ b/shared-bindings/usb/core/Device.c @@ -195,90 +195,6 @@ static mp_obj_t usb_core_device_set_configuration(size_t n_args, const mp_obj_t } MP_DEFINE_CONST_FUN_OBJ_KW(usb_core_device_set_configuration_obj, 1, usb_core_device_set_configuration); -//| def start_bulk_in(self, endpoint: int, buffer_size: int = 65536) -> None: -//| """Start continuous full-speed bulk IN capture into internal SRAM. -//| -//| The USB host polls the endpoint every frame, independently of Python, -//| and stores the data in a ring of ``buffer_size`` bytes, a power of two -//| from 4096 to 65536. A full ring drops the newest packets and counts -//| them in `bulk_in_lost_packets`. NAKs and duplicate packets are not -//| errors. A stalled endpoint stops capture. Only one capture may run at -//| a time, and it keeps this Device alive until stopped, disconnected, -//| deinitialized or reset. -//| -//| Only available with the PIO USB host. -//| """ -//| ... -//| -static mp_obj_t usb_core_device_start_bulk_in(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { - enum { ARG_endpoint, ARG_buffer_size }; - static const mp_arg_t allowed_args[] = { - { MP_QSTR_endpoint, MP_ARG_REQUIRED | MP_ARG_INT }, - { MP_QSTR_buffer_size, MP_ARG_INT, {.u_int = 65536} }, - }; - usb_core_device_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); - check_for_deinit(self); - 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); - // IN endpoint addresses 1-15; endpoint 0 is control only. - mp_int_t endpoint = mp_arg_validate_int_range(args[ARG_endpoint].u_int, 0x81, 0x8F, MP_QSTR_endpoint); - mp_int_t buffer_size = mp_arg_validate_int_range(args[ARG_buffer_size].u_int, 4096, 65536, MP_QSTR_buffer_size); - if ((buffer_size & (buffer_size - 1)) != 0) { - mp_raise_ValueError_varg(MP_ERROR_TEXT("%q must be power of 2"), MP_QSTR_buffer_size); - } - common_hal_usb_core_device_start_bulk_in(self, endpoint, buffer_size); - return mp_const_none; -} -MP_DEFINE_CONST_FUN_OBJ_KW(usb_core_device_start_bulk_in_obj, 2, usb_core_device_start_bulk_in); - -//| def read_bulk_into(self, buffer: WriteableBuffer) -> int: -//| """Copy captured bulk IN bytes into ``buffer`` without waiting. -//| -//| Returns the number of bytes copied, possibly zero. Raises `USBError` -//| once capture has stopped, for example because the device stalled or -//| was unplugged, and every byte captured before that has been read.""" -//| ... -//| -static mp_obj_t usb_core_device_read_bulk_into(mp_obj_t self_in, mp_obj_t buffer_in) { - usb_core_device_obj_t *self = MP_OBJ_TO_PTR(self_in); - check_for_deinit(self); - mp_buffer_info_t buffer; - mp_get_buffer_raise(buffer_in, &buffer, MP_BUFFER_WRITE); - return mp_obj_new_int(common_hal_usb_core_device_read_bulk_into(self, buffer.buf, buffer.len)); -} -MP_DEFINE_CONST_FUN_OBJ_2(usb_core_device_read_bulk_into_obj, usb_core_device_read_bulk_into); - -//| bulk_in_lost_packets: int -//| """Packets dropped since `start_bulk_in` because the ring was full, meaning -//| `read_bulk_into` was not called often enough. Zero means the captured -//| stream has no gaps. The count stays readable after capture stops, until -//| the next `start_bulk_in`. (read-only)""" -//| -static mp_obj_t usb_core_device_obj_get_bulk_in_lost_packets(mp_obj_t self_in) { - usb_core_device_obj_t *self = MP_OBJ_TO_PTR(self_in); - return mp_obj_new_int_from_uint(common_hal_usb_core_device_get_bulk_in_lost_packets(self)); -} -MP_DEFINE_CONST_FUN_OBJ_1(usb_core_device_get_bulk_in_lost_packets_obj, usb_core_device_obj_get_bulk_in_lost_packets); - -MP_PROPERTY_GETTER(usb_core_device_bulk_in_lost_packets_obj, - (mp_obj_t)&usb_core_device_get_bulk_in_lost_packets_obj); - -//| def stop_bulk_in(self) -> None: -//| """Stop capture and free its SRAM once the USB host has let go of it. -//| -//| If stopping times out, the memory stays allocated and this raises -//| `USBTimeoutError`; call it again to retry. Calling it when no capture is -//| running does nothing. Setting a configuration or an interface on the -//| device also stops its capture. -//| """ -//| ... -//| -static mp_obj_t usb_core_device_stop_bulk_in(mp_obj_t self_in) { - common_hal_usb_core_device_stop_bulk_in(MP_OBJ_TO_PTR(self_in)); - return mp_const_none; -} -MP_DEFINE_CONST_FUN_OBJ_1(usb_core_device_stop_bulk_in_obj, usb_core_device_stop_bulk_in); - //| def write(self, endpoint: int, data: ReadableBuffer, timeout: Optional[int] = None) -> int: //| """Write data to a specific endpoint on the device. //| @@ -481,10 +397,6 @@ static const mp_rom_map_elem_t usb_core_device_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_speed), MP_ROM_PTR(&usb_core_device_speed_obj) }, { MP_ROM_QSTR(MP_QSTR_set_configuration), MP_ROM_PTR(&usb_core_device_set_configuration_obj) }, - { MP_ROM_QSTR(MP_QSTR_start_bulk_in), MP_ROM_PTR(&usb_core_device_start_bulk_in_obj) }, - { MP_ROM_QSTR(MP_QSTR_read_bulk_into), MP_ROM_PTR(&usb_core_device_read_bulk_into_obj) }, - { MP_ROM_QSTR(MP_QSTR_bulk_in_lost_packets), MP_ROM_PTR(&usb_core_device_bulk_in_lost_packets_obj) }, - { MP_ROM_QSTR(MP_QSTR_stop_bulk_in), MP_ROM_PTR(&usb_core_device_stop_bulk_in_obj) }, { MP_ROM_QSTR(MP_QSTR_write), MP_ROM_PTR(&usb_core_device_write_obj) }, { MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&usb_core_device_read_obj) }, { MP_ROM_QSTR(MP_QSTR_ctrl_transfer), MP_ROM_PTR(&usb_core_device_ctrl_transfer_obj) }, diff --git a/shared-bindings/usb/core/Device.h b/shared-bindings/usb/core/Device.h index d7540468470..5814469240a 100644 --- a/shared-bindings/usb/core/Device.h +++ b/shared-bindings/usb/core/Device.h @@ -36,9 +36,3 @@ mp_int_t common_hal_usb_core_device_ctrl_transfer(usb_core_device_obj_t *self, bool common_hal_usb_core_device_is_kernel_driver_active(usb_core_device_obj_t *self, mp_int_t interface); void common_hal_usb_core_device_detach_kernel_driver(usb_core_device_obj_t *self, mp_int_t interface); void common_hal_usb_core_device_attach_kernel_driver(usb_core_device_obj_t *self, mp_int_t interface); - -void usb_core_device_reset(void); -void common_hal_usb_core_device_start_bulk_in(usb_core_device_obj_t *self, mp_int_t endpoint, mp_int_t buffer_size); -mp_int_t common_hal_usb_core_device_read_bulk_into(usb_core_device_obj_t *self, uint8_t *buffer, mp_int_t length); -uint32_t common_hal_usb_core_device_get_bulk_in_lost_packets(usb_core_device_obj_t *self); -void common_hal_usb_core_device_stop_bulk_in(usb_core_device_obj_t *self); diff --git a/shared-bindings/usb_host_bulk/InStream.c b/shared-bindings/usb_host_bulk/InStream.c new file mode 100644 index 00000000000..4047fda68bf --- /dev/null +++ b/shared-bindings/usb_host_bulk/InStream.c @@ -0,0 +1,280 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include "py/objproperty.h" +#include "py/runtime.h" +#include "py/stream.h" + +#include "shared/runtime/context_manager_helpers.h" +#include "shared-bindings/usb/core/Device.h" +#include "shared-bindings/usb_host_bulk/InStream.h" +#include "shared-bindings/util.h" + +//| class InStream: +//| """Continuously receive from a full-speed bulk IN endpoint in the background. +//| +//| The USB host polls the endpoint every frame, independently of Python, and stores +//| the payload in a ring of ``buffer_size`` bytes in internal SRAM. +//| +//| Reads never block. To wait for data, wrap the stream with ``asyncio.StreamReader`` +//| or poll `in_waiting`. +//| +//| Only one stream can run at a time. +//| +//| Synchronous example:: +//| +//| import usb_host_bulk +//| +//| with usb_host_bulk.InStream(device, 0x81) as stream: +//| while True: +//| n = stream.readinto(buf) +//| if n is None: +//| continue # nothing waiting yet +//| if n == 0: +//| break # the device stalled or was unplugged +//| process(buf, n) +//| print("lost packets:", stream.lost_packets) +//| +//| asyncio example:: +//| +//| import asyncio +//| import usb_host_bulk +//| +//| async def receive(device): +//| with usb_host_bulk.InStream(device, 0x81) as stream: +//| reader = asyncio.StreamReader(stream) +//| buf = bytearray(8192) +//| while True: +//| n = await reader.readinto(buf) +//| if not n: +//| break # the device stalled or was unplugged +//| process(buf, n) +//| +//| ``asyncio.StreamReader(stream)`` is a CircuitPython and MicroPython idiom. Use +//| ``readinto`` rather than ``readexactly`` at high data rates, because +//| ``readexactly`` allocates on every call. +//| """ +//| +//| def __init__( +//| self, device: usb.core.Device, endpoint: int, *, buffer_size: int = 32768 +//| ) -> None: +//| """Open ``endpoint`` on ``device`` and start capturing immediately. +//| +//| :param usb.core.Device device: a configured device (call ``set_configuration()`` first) +//| :param int endpoint: bEndpointAddress of a bulk IN endpoint, 0x81 to 0x8F, with +//| a maximum packet size of 64 bytes or less +//| :param int buffer_size: ring size in bytes, a power of two from 4096 to 65536 +//| """ +//| ... +//| +static mp_obj_t usb_host_bulk_instream_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + enum { ARG_device, ARG_endpoint, ARG_buffer_size }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_device, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_endpoint, MP_ARG_REQUIRED | MP_ARG_INT }, + { MP_QSTR_buffer_size, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 32768} }, + }; + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + mp_obj_t device_obj = mp_arg_validate_type(args[ARG_device].u_obj, &usb_core_device_type, MP_QSTR_device); + usb_core_device_obj_t *device = MP_OBJ_TO_PTR(device_obj); + if (common_hal_usb_core_device_deinited(device)) { + raise_deinited_error(); + } + // IN endpoint addresses 1-15; endpoint 0 is control only. + mp_int_t endpoint = mp_arg_validate_int_range(args[ARG_endpoint].u_int, 0x81, 0x8F, MP_QSTR_endpoint); + mp_int_t buffer_size = mp_arg_validate_int_range(args[ARG_buffer_size].u_int, 4096, 65536, MP_QSTR_buffer_size); + if ((buffer_size & (buffer_size - 1)) != 0) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("%q must be power of 2"), MP_QSTR_buffer_size); + } + + usb_host_bulk_instream_obj_t *self = mp_obj_malloc_with_finaliser(usb_host_bulk_instream_obj_t, &usb_host_bulk_instream_type); + common_hal_usb_host_bulk_instream_construct(self, device, endpoint, buffer_size); + return MP_OBJ_FROM_PTR(self); +} + +static void check_for_deinit(usb_host_bulk_instream_obj_t *self) { + if (common_hal_usb_host_bulk_instream_deinited(self)) { + raise_deinited_error(); + } +} + +//| def readinto(self, buf: WriteableBuffer, nbytes: Optional[int] = None) -> Optional[int]: +//| """Copy waiting bytes into ``buf``, at most ``nbytes`` of them if given. +//| +//| :return: the number of bytes copied, ``None`` if nothing is waiting yet, or 0 +//| once capture has ended and every captured byte has been read +//| :rtype: int or None""" +//| ... +//| +//| def read(self, nbytes: Optional[int] = None) -> Optional[bytes]: +//| """Same as `readinto`, but allocates and returns ``bytes``: ``None`` if nothing +//| is waiting yet, ``b""`` once capture has ended and every byte has been read.""" +//| ... +//| + +// Standard stream methods, implemented in py/stream.c on top of this. +static mp_uint_t usb_host_bulk_instream_read_stream(mp_obj_t self_in, void *buf, mp_uint_t size, int *errcode) { + usb_host_bulk_instream_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + // Look at ended first: the host detaches the ring only after its last + // write, so a stream seen ended here has all of its data visible below. + bool ended = common_hal_usb_host_bulk_instream_get_ended(self); + uint32_t count = common_hal_usb_host_bulk_instream_read(self, buf, size); + if (count > 0 || ended) { + return count; + } + *errcode = MP_EAGAIN; + return MP_STREAM_ERROR; +} + +static mp_uint_t usb_host_bulk_instream_ioctl(mp_obj_t self_in, mp_uint_t request, uintptr_t arg, int *errcode) { + usb_host_bulk_instream_obj_t *self = MP_OBJ_TO_PTR(self_in); + if (request == MP_STREAM_CLOSE) { + common_hal_usb_host_bulk_instream_deinit(self, false); + return 0; + } + check_for_deinit(self); + if (request == MP_STREAM_POLL) { + mp_uint_t flags = arg; + mp_uint_t ret = 0; + // An ended stream is readable (it returns EOF) so that waiters wake up. + bool ended = common_hal_usb_host_bulk_instream_get_ended(self); + if ((flags & MP_STREAM_POLL_RD) && + (ended || common_hal_usb_host_bulk_instream_get_in_waiting(self) > 0)) { + ret |= MP_STREAM_POLL_RD; + } + if (ended) { + ret |= MP_STREAM_POLL_HUP; + } + return ret; + } + *errcode = MP_EINVAL; + return MP_STREAM_ERROR; +} + +//| in_waiting: int +//| """Bytes waiting in the ring. (read-only)""" +//| +static mp_obj_t usb_host_bulk_instream_obj_get_in_waiting(mp_obj_t self_in) { + usb_host_bulk_instream_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_int_from_uint(common_hal_usb_host_bulk_instream_get_in_waiting(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(usb_host_bulk_instream_get_in_waiting_obj, usb_host_bulk_instream_obj_get_in_waiting); + +MP_PROPERTY_GETTER(usb_host_bulk_instream_in_waiting_obj, + (mp_obj_t)&usb_host_bulk_instream_get_in_waiting_obj); + +//| lost_packets: int +//| """Packets dropped because the ring was full, meaning reads were not frequent +//| enough. Zero means the captured stream has no gaps. Readable after `deinit`. +//| (read-only)""" +//| +static mp_obj_t usb_host_bulk_instream_obj_get_lost_packets(mp_obj_t self_in) { + usb_host_bulk_instream_obj_t *self = MP_OBJ_TO_PTR(self_in); + return mp_obj_new_int_from_uint(common_hal_usb_host_bulk_instream_get_lost_packets(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(usb_host_bulk_instream_get_lost_packets_obj, usb_host_bulk_instream_obj_get_lost_packets); + +MP_PROPERTY_GETTER(usb_host_bulk_instream_lost_packets_obj, + (mp_obj_t)&usb_host_bulk_instream_get_lost_packets_obj); + +//| ended: bool +//| """True once capture stopped on its own: the device stalled the endpoint or was +//| unplugged, or the device was reconfigured. Bytes captured before that can still +//| be read. (read-only)""" +//| +static mp_obj_t usb_host_bulk_instream_obj_get_ended(mp_obj_t self_in) { + usb_host_bulk_instream_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_bool(common_hal_usb_host_bulk_instream_get_ended(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(usb_host_bulk_instream_get_ended_obj, usb_host_bulk_instream_obj_get_ended); + +MP_PROPERTY_GETTER(usb_host_bulk_instream_ended_obj, + (mp_obj_t)&usb_host_bulk_instream_get_ended_obj); + +//| def reset_input_buffer(self) -> None: +//| """Discard every byte waiting in the ring.""" +//| ... +//| +static mp_obj_t usb_host_bulk_instream_obj_reset_input_buffer(mp_obj_t self_in) { + usb_host_bulk_instream_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + common_hal_usb_host_bulk_instream_reset_input_buffer(self); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(usb_host_bulk_instream_reset_input_buffer_obj, usb_host_bulk_instream_obj_reset_input_buffer); + +//| def deinit(self) -> None: +//| """Stop capture and free the ring. Raises `usb.core.USBTimeoutError` if the host +//| does not let go of the ring in time; call it again to retry.""" +//| ... +//| +static mp_obj_t usb_host_bulk_instream_obj_deinit(mp_obj_t self_in) { + usb_host_bulk_instream_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_usb_host_bulk_instream_deinit(self, false); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(usb_host_bulk_instream_deinit_obj, usb_host_bulk_instream_obj_deinit); + +// A finaliser cannot raise, so it never waits for a retry. +static mp_obj_t usb_host_bulk_instream_obj___del__(mp_obj_t self_in) { + usb_host_bulk_instream_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_usb_host_bulk_instream_deinit(self, true); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(usb_host_bulk_instream___del___obj, usb_host_bulk_instream_obj___del__); + +//| def __enter__(self) -> InStream: +//| """No-op used by Context Managers.""" +//| ... +//| +// Provided by context manager helper. + +//| def __exit__(self) -> None: +//| """Automatically deinitializes when exiting a context. See +//| :ref:`lifetime-and-contextmanagers` for more info.""" +//| ... +//| +//| +// Provided by context manager helper. + +static const mp_rom_map_elem_t usb_host_bulk_instream_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&usb_host_bulk_instream___del___obj) }, + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&usb_host_bulk_instream_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) }, + { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&default___exit___obj) }, + + // Standard stream methods. + { MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&mp_stream_read_obj) }, + { MP_ROM_QSTR(MP_QSTR_readinto), MP_ROM_PTR(&mp_stream_readinto_obj) }, + + { MP_ROM_QSTR(MP_QSTR_reset_input_buffer), MP_ROM_PTR(&usb_host_bulk_instream_reset_input_buffer_obj) }, + + // Properties + { MP_ROM_QSTR(MP_QSTR_in_waiting), MP_ROM_PTR(&usb_host_bulk_instream_in_waiting_obj) }, + { MP_ROM_QSTR(MP_QSTR_lost_packets), MP_ROM_PTR(&usb_host_bulk_instream_lost_packets_obj) }, + { MP_ROM_QSTR(MP_QSTR_ended), MP_ROM_PTR(&usb_host_bulk_instream_ended_obj) }, +}; +static MP_DEFINE_CONST_DICT(usb_host_bulk_instream_locals_dict, usb_host_bulk_instream_locals_dict_table); + +static const mp_stream_p_t usb_host_bulk_instream_stream_p = { + .read = usb_host_bulk_instream_read_stream, + .ioctl = usb_host_bulk_instream_ioctl, + .is_text = false, +}; + +MP_DEFINE_CONST_OBJ_TYPE( + usb_host_bulk_instream_type, + MP_QSTR_InStream, + MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS, + make_new, usb_host_bulk_instream_make_new, + locals_dict, &usb_host_bulk_instream_locals_dict, + protocol, &usb_host_bulk_instream_stream_p + ); diff --git a/shared-bindings/usb_host_bulk/InStream.h b/shared-bindings/usb_host_bulk/InStream.h new file mode 100644 index 00000000000..a574afd5ef0 --- /dev/null +++ b/shared-bindings/usb_host_bulk/InStream.h @@ -0,0 +1,43 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include + +#include "common-hal/usb_host_bulk/InStream.h" +#include "shared-module/usb/core/Device.h" + +extern const mp_obj_type_t usb_host_bulk_instream_type; + +void common_hal_usb_host_bulk_instream_construct(usb_host_bulk_instream_obj_t *self, + usb_core_device_obj_t *device, uint8_t endpoint, uint32_t buffer_size); +// Raises USBTimeoutError, and stays usable, if the host keeps the ring. From a +// finaliser, pass abandon=true instead: the memory is then kept aside and the +// next stream retries the stop. +void common_hal_usb_host_bulk_instream_deinit(usb_host_bulk_instream_obj_t *self, bool abandon); +bool common_hal_usb_host_bulk_instream_deinited(usb_host_bulk_instream_obj_t *self); +uint32_t common_hal_usb_host_bulk_instream_read(usb_host_bulk_instream_obj_t *self, uint8_t *data, uint32_t len); +uint32_t common_hal_usb_host_bulk_instream_get_in_waiting(usb_host_bulk_instream_obj_t *self); +uint32_t common_hal_usb_host_bulk_instream_get_lost_packets(usb_host_bulk_instream_obj_t *self); +bool common_hal_usb_host_bulk_instream_get_ended(usb_host_bulk_instream_obj_t *self); +void common_hal_usb_host_bulk_instream_reset_input_buffer(usb_host_bulk_instream_obj_t *self); + +// Hooks for usb.core.Device, which must not leave a stream polling an endpoint +// it reconfigures, reads, writes or closes. + +// Stops a stream from device_address. Raises USBTimeoutError if the host does +// not let go of the ring in time. +void usb_host_bulk_stop_device(uint8_t device_address); +// True while a stream is polling this endpoint. +bool usb_host_bulk_endpoint_busy(uint8_t device_address, uint8_t endpoint); +// Stops a stream from device_address if it can. Never raises or allocates, so +// it is safe from finalisers and TinyUSB callbacks. +void usb_host_bulk_device_gone(uint8_t device_address); +// Stops a stream opened through this Device object, but not one opened +// through another Device for the same address. Never raises or allocates. +void usb_host_bulk_device_deinit(usb_core_device_obj_t *device); diff --git a/shared-bindings/usb_host_bulk/__init__.c b/shared-bindings/usb_host_bulk/__init__.c new file mode 100644 index 00000000000..7cb3e4f58ac --- /dev/null +++ b/shared-bindings/usb_host_bulk/__init__.c @@ -0,0 +1,36 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include "py/obj.h" +#include "py/runtime.h" + +#include "shared-bindings/usb_host_bulk/__init__.h" +#include "shared-bindings/usb_host_bulk/InStream.h" + +//| """Continuous bulk transfers on USB host ports +//| +//| This module keeps an endpoint busy in the background so that +//| continuously-streaming devices, such as RTL2832U SDR dongles, never overflow. +//| Devices are still found and configured with `usb.core`; `usb_host` manages the +//| host ports themselves. +//| +//| Available only on boards built with the PIO USB host and this feature enabled. +//| """ +//| + +static const mp_rom_map_elem_t usb_host_bulk_module_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_usb_host_bulk) }, + { MP_ROM_QSTR(MP_QSTR_InStream), MP_ROM_PTR(&usb_host_bulk_instream_type) }, +}; + +static MP_DEFINE_CONST_DICT(usb_host_bulk_module_globals, usb_host_bulk_module_globals_table); + +const mp_obj_module_t usb_host_bulk_module = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&usb_host_bulk_module_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_usb_host_bulk, usb_host_bulk_module); diff --git a/shared-bindings/usb_host_bulk/__init__.h b/shared-bindings/usb_host_bulk/__init__.h new file mode 100644 index 00000000000..779b49ffd8d --- /dev/null +++ b/shared-bindings/usb_host_bulk/__init__.h @@ -0,0 +1,7 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once diff --git a/shared-module/usb/core/Device.h b/shared-module/usb/core/Device.h index 131806ace77..c7392a04ba7 100644 --- a/shared-module/usb/core/Device.h +++ b/shared-module/usb/core/Device.h @@ -15,6 +15,4 @@ typedef struct { uint8_t *configuration_descriptor; // Contains the length of the all descriptors. uint8_t open_endpoints[8]; uint16_t first_langid; - // Continuous bulk IN capture losses, kept after stop until the next start. - uint32_t bulk_in_lost_packets; } usb_core_device_obj_t; From 2bbf9549519cc4a6c547ef1b6e55a62538ca02b9 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Wed, 30 Sep 2026 13:56:31 -0500 Subject: [PATCH 4/8] fix stubs --- shared-bindings/usb_host_bulk/InStream.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shared-bindings/usb_host_bulk/InStream.c b/shared-bindings/usb_host_bulk/InStream.c index 4047fda68bf..e9ceeec784a 100644 --- a/shared-bindings/usb_host_bulk/InStream.c +++ b/shared-bindings/usb_host_bulk/InStream.c @@ -13,6 +13,9 @@ #include "shared-bindings/usb_host_bulk/InStream.h" #include "shared-bindings/util.h" +//| import usb.core +//| +//| //| class InStream: //| """Continuously receive from a full-speed bulk IN endpoint in the background. //| From 910505192141df456e89d6f6bd33fb71a2cd7b46 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Thu, 1 Oct 2026 13:31:13 -0500 Subject: [PATCH 5/8] remove ended, make examples stand-alone runnable. --- shared-bindings/usb_host_bulk/InStream.c | 94 +++++++++++++++--------- 1 file changed, 61 insertions(+), 33 deletions(-) diff --git a/shared-bindings/usb_host_bulk/InStream.c b/shared-bindings/usb_host_bulk/InStream.c index e9ceeec784a..c7aa28d3867 100644 --- a/shared-bindings/usb_host_bulk/InStream.c +++ b/shared-bindings/usb_host_bulk/InStream.c @@ -29,36 +29,77 @@ //| //| Synchronous example:: //| +//| import time +//| import usb.core //| import usb_host_bulk //| +//| # Replace these with your device's vendor and product IDs. +//| device = usb.core.find(idVendor=0x0BDA, idProduct=0x2838) +//| if device is None: +//| raise RuntimeError("USB device not found") +//| device.set_configuration() +//| # Many devices only start sending after device-specific setup, such as +//| # vendor requests made with device.ctrl_transfer(). Do that here. +//| +//| buf = bytearray(8192) +//| received = 0 +//| report_at = time.monotonic() + 1 //| with usb_host_bulk.InStream(device, 0x81) as stream: //| while True: -//| n = stream.readinto(buf) -//| if n is None: -//| continue # nothing waiting yet +//| n = stream.readinto(buf) # None while nothing is waiting //| if n == 0: //| break # the device stalled or was unplugged -//| process(buf, n) -//| print("lost packets:", stream.lost_packets) +//| if n: +//| received += n # process buf[:n] here +//| if time.monotonic() >= report_at: +//| print("received", received, "bytes, lost packets:", stream.lost_packets) +//| report_at += 1 //| //| asyncio example:: //| //| import asyncio +//| from asyncio import StreamReader +//| import usb.core //| import usb_host_bulk //| -//| async def receive(device): +//| # Replace these with your device's vendor and product IDs. +//| device = usb.core.find(idVendor=0x0BDA, idProduct=0x2838) +//| if device is None: +//| raise RuntimeError("USB device not found") +//| device.set_configuration() +//| # Many devices only start sending after device-specific setup, such as +//| # vendor requests made with device.ctrl_transfer(). Do that here. +//| +//| received = 0 +//| +//| async def receive(stream): +//| global received +//| reader = StreamReader(stream) +//| buf = bytearray(8192) +//| while True: +//| n = await reader.readinto(buf) +//| if not n: +//| break # the device stalled or was unplugged +//| received += n # process buf[:n] here +//| +//| async def report(stream): +//| while True: +//| await asyncio.sleep(1) +//| print("received", received, "bytes, lost packets:", stream.lost_packets) +//| +//| async def main(): //| with usb_host_bulk.InStream(device, 0x81) as stream: -//| reader = asyncio.StreamReader(stream) -//| buf = bytearray(8192) -//| while True: -//| n = await reader.readinto(buf) -//| if not n: -//| break # the device stalled or was unplugged -//| process(buf, n) -//| -//| ``asyncio.StreamReader(stream)`` is a CircuitPython and MicroPython idiom. Use -//| ``readinto`` rather than ``readexactly`` at high data rates, because -//| ``readexactly`` allocates on every call. +//| reporter = asyncio.create_task(report(stream)) +//| await receive(stream) +//| reporter.cancel() +//| +//| asyncio.run(main()) +//| +//| ``asyncio.StreamReader(stream)`` is a CircuitPython and MicroPython idiom. Import +//| ``StreamReader`` before starting the stream: asyncio loads it on first use, which +//| can take long enough to overflow the ring. Use ``readinto`` rather than +//| ``readexactly`` at high data rates, because ``readexactly`` allocates on every +//| call. //| """ //| //| def __init__( @@ -109,6 +150,9 @@ static void check_for_deinit(usb_host_bulk_instream_obj_t *self) { //| def readinto(self, buf: WriteableBuffer, nbytes: Optional[int] = None) -> Optional[int]: //| """Copy waiting bytes into ``buf``, at most ``nbytes`` of them if given. //| +//| Capture ends on its own when the device stalls the endpoint, is unplugged, or +//| is reconfigured. Bytes captured before that can still be read. +//| //| :return: the number of bytes copied, ``None`` if nothing is waiting yet, or 0 //| once capture has ended and every captured byte has been read //| :rtype: int or None""" @@ -187,21 +231,6 @@ MP_DEFINE_CONST_FUN_OBJ_1(usb_host_bulk_instream_get_lost_packets_obj, usb_host_ MP_PROPERTY_GETTER(usb_host_bulk_instream_lost_packets_obj, (mp_obj_t)&usb_host_bulk_instream_get_lost_packets_obj); -//| ended: bool -//| """True once capture stopped on its own: the device stalled the endpoint or was -//| unplugged, or the device was reconfigured. Bytes captured before that can still -//| be read. (read-only)""" -//| -static mp_obj_t usb_host_bulk_instream_obj_get_ended(mp_obj_t self_in) { - usb_host_bulk_instream_obj_t *self = MP_OBJ_TO_PTR(self_in); - check_for_deinit(self); - return mp_obj_new_bool(common_hal_usb_host_bulk_instream_get_ended(self)); -} -MP_DEFINE_CONST_FUN_OBJ_1(usb_host_bulk_instream_get_ended_obj, usb_host_bulk_instream_obj_get_ended); - -MP_PROPERTY_GETTER(usb_host_bulk_instream_ended_obj, - (mp_obj_t)&usb_host_bulk_instream_get_ended_obj); - //| def reset_input_buffer(self) -> None: //| """Discard every byte waiting in the ring.""" //| ... @@ -263,7 +292,6 @@ static const mp_rom_map_elem_t usb_host_bulk_instream_locals_dict_table[] = { // Properties { MP_ROM_QSTR(MP_QSTR_in_waiting), MP_ROM_PTR(&usb_host_bulk_instream_in_waiting_obj) }, { MP_ROM_QSTR(MP_QSTR_lost_packets), MP_ROM_PTR(&usb_host_bulk_instream_lost_packets_obj) }, - { MP_ROM_QSTR(MP_QSTR_ended), MP_ROM_PTR(&usb_host_bulk_instream_ended_obj) }, }; static MP_DEFINE_CONST_DICT(usb_host_bulk_instream_locals_dict, usb_host_bulk_instream_locals_dict_table); From edd194ca5751da6114ec68b3ddf51991a58f7857 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Fri, 2 Oct 2026 08:31:23 -0500 Subject: [PATCH 6/8] remove orphan ring logic, support one stream per endpoint --- .../common-hal/usb_host_bulk/InStream.c | 105 ++++++++++-------- shared-bindings/usb_host_bulk/InStream.c | 3 +- shared-bindings/usb_host_bulk/InStream.h | 14 +-- 3 files changed, 66 insertions(+), 56 deletions(-) diff --git a/ports/raspberrypi/common-hal/usb_host_bulk/InStream.c b/ports/raspberrypi/common-hal/usb_host_bulk/InStream.c index 7032eda6acf..db47cc48531 100644 --- a/ports/raspberrypi/common-hal/usb_host_bulk/InStream.c +++ b/ports/raspberrypi/common-hal/usb_host_bulk/InStream.c @@ -16,30 +16,20 @@ #include "tusb.h" #include "pio_usb_bulk_stream.h" +#include "pio_usb_configuration.h" #define STOP_TIMEOUT_US (100000) -// The stream the host is polling, if any. A weak link: the Python object owns -// the ring, and its finaliser clears this. -static usb_host_bulk_instream_obj_t *_active; +// The streams the host is polling, at most one per endpoint. Weak links: each +// Python object owns its ring, and its finaliser clears its slot. +static usb_host_bulk_instream_obj_t *_active[PIO_USB_EP_POOL_CNT]; -// A ring whose stop timed out in a finaliser. The host core may still write -// to it, so it is kept until a later stop succeeds. -static pio_usb_bulk_ring_t *_orphan_ring; -static uint8_t *_orphan_storage; - -static bool _stop_orphan(void) { - if (_orphan_ring == NULL) { - return true; - } - if (!pio_usb_host_bulk_stream_stop(_orphan_ring, STOP_TIMEOUT_US)) { - return false; +static void _forget(usb_host_bulk_instream_obj_t *self) { + for (size_t i = 0; i < MP_ARRAY_SIZE(_active); i++) { + if (_active[i] == self) { + _active[i] = NULL; + } } - port_free(_orphan_storage); - port_free(_orphan_ring); - _orphan_storage = NULL; - _orphan_ring = NULL; - return true; } // Detach the ring from the host but keep it, so that bytes captured before @@ -48,23 +38,28 @@ static bool _stop(usb_host_bulk_instream_obj_t *self) { if (!pio_usb_host_bulk_stream_stop(self->ring, STOP_TIMEOUT_US)) { return false; } - if (_active == self) { - _active = NULL; - } + _forget(self); return true; } void common_hal_usb_host_bulk_instream_construct(usb_host_bulk_instream_obj_t *self, usb_core_device_obj_t *device, uint8_t endpoint, uint32_t buffer_size) { - if (!_stop_orphan()) { - mp_raise_usb_core_USBTimeoutError(); - } - if (_active != NULL) { - if (_active->ring->active) { + size_t slot = MP_ARRAY_SIZE(_active); + for (size_t i = 0; i < MP_ARRAY_SIZE(_active); i++) { + usb_host_bulk_instream_obj_t *other = _active[i]; + if (other != NULL && !other->ring->active) { + // It ended on its own and keeps its ring until it is deinited. + _active[i] = other = NULL; + } + if (other == NULL) { + slot = i; + } else if (other->device_address == device->device_address && + other->endpoint == endpoint) { mp_raise_RuntimeError(MP_ERROR_TEXT("Already running")); } - // It ended on its own and keeps its ring until it is deinited. - _active = NULL; + } + if (slot == MP_ARRAY_SIZE(_active)) { + mp_raise_RuntimeError_varg(MP_ERROR_TEXT("Too many %q"), MP_QSTR_InStream); } tuh_bus_info_t bus_info; if (!tuh_bus_info_get(device->device_address, &bus_info) || bus_info.rhport < 1 || @@ -98,7 +93,7 @@ void common_hal_usb_host_bulk_instream_construct(usb_host_bulk_instream_obj_t *s self->lost_packets = 0; self->device_address = device->device_address; self->endpoint = endpoint; - _active = self; + _active[slot] = self; } bool common_hal_usb_host_bulk_instream_deinited(usb_host_bulk_instream_obj_t *self) { @@ -119,15 +114,10 @@ void common_hal_usb_host_bulk_instream_deinit(usb_host_bulk_instream_obj_t *self if (stopped) { port_free(self->storage); port_free(self->ring); - } else if (_stop_orphan()) { - _orphan_ring = self->ring; - _orphan_storage = self->storage; - } - // Otherwise two rings are stuck. Leak this one rather than free memory - // the host core may still write to. - if (_active == self) { - _active = NULL; } + // Otherwise the host core is not running frames (a stop is honoured within + // one frame), so leak the ring rather than free memory it may still own. + _forget(self); self->ring = NULL; self->storage = NULL; self->device = MP_OBJ_NULL; @@ -166,26 +156,45 @@ void common_hal_usb_host_bulk_instream_reset_input_buffer(usb_host_bulk_instream self->ring->read_pos = self->ring->write_pos; } +// Stops every stream from device_address, or every stream opened through +// device if it is not NULL. Returns false if any of them timed out. +static bool _stop_matching(uint8_t device_address, usb_core_device_obj_t *device) { + bool stopped = true; + for (size_t i = 0; i < MP_ARRAY_SIZE(_active); i++) { + usb_host_bulk_instream_obj_t *stream = _active[i]; + if (stream == NULL) { + continue; + } + if (device != NULL ? stream->device == MP_OBJ_FROM_PTR(device) : + stream->device_address == device_address) { + stopped = _stop(stream) && stopped; + } + } + return stopped; +} + void usb_host_bulk_stop_device(uint8_t device_address) { - if (_active != NULL && _active->device_address == device_address && !_stop(_active)) { + if (!_stop_matching(device_address, NULL)) { mp_raise_usb_core_USBTimeoutError(); } } bool usb_host_bulk_endpoint_busy(uint8_t device_address, uint8_t endpoint) { - return _active != NULL && _active->device_address == device_address && - _active->endpoint == endpoint && _active->ring->active; + for (size_t i = 0; i < MP_ARRAY_SIZE(_active); i++) { + usb_host_bulk_instream_obj_t *stream = _active[i]; + if (stream != NULL && stream->device_address == device_address && + stream->endpoint == endpoint && stream->ring->active) { + return true; + } + } + return false; } void usb_host_bulk_device_deinit(usb_core_device_obj_t *device) { - if (_active != NULL && _active->device == MP_OBJ_FROM_PTR(device)) { - _stop(_active); - } + _stop_matching(0, device); } void usb_host_bulk_device_gone(uint8_t device_address) { - // The host core also detaches the ring by itself once it sees the unplug. - if (_active != NULL && _active->device_address == device_address) { - _stop(_active); - } + // The host core also detaches the rings by itself once it sees the unplug. + _stop_matching(device_address, NULL); } diff --git a/shared-bindings/usb_host_bulk/InStream.c b/shared-bindings/usb_host_bulk/InStream.c index c7aa28d3867..85eb3377a60 100644 --- a/shared-bindings/usb_host_bulk/InStream.c +++ b/shared-bindings/usb_host_bulk/InStream.c @@ -25,7 +25,8 @@ //| Reads never block. To wait for data, wrap the stream with ``asyncio.StreamReader`` //| or poll `in_waiting`. //| -//| Only one stream can run at a time. +//| One stream per endpoint can run. They share the bandwidth of +//| their host port. //| //| Synchronous example:: //| diff --git a/shared-bindings/usb_host_bulk/InStream.h b/shared-bindings/usb_host_bulk/InStream.h index a574afd5ef0..155f7152bc5 100644 --- a/shared-bindings/usb_host_bulk/InStream.h +++ b/shared-bindings/usb_host_bulk/InStream.h @@ -17,8 +17,8 @@ extern const mp_obj_type_t usb_host_bulk_instream_type; void common_hal_usb_host_bulk_instream_construct(usb_host_bulk_instream_obj_t *self, usb_core_device_obj_t *device, uint8_t endpoint, uint32_t buffer_size); // Raises USBTimeoutError, and stays usable, if the host keeps the ring. From a -// finaliser, pass abandon=true instead: the memory is then kept aside and the -// next stream retries the stop. +// finaliser, pass abandon=true instead: the memory is then leaked rather than +// freed while the host may still write to it. void common_hal_usb_host_bulk_instream_deinit(usb_host_bulk_instream_obj_t *self, bool abandon); bool common_hal_usb_host_bulk_instream_deinited(usb_host_bulk_instream_obj_t *self); uint32_t common_hal_usb_host_bulk_instream_read(usb_host_bulk_instream_obj_t *self, uint8_t *data, uint32_t len); @@ -30,14 +30,14 @@ void common_hal_usb_host_bulk_instream_reset_input_buffer(usb_host_bulk_instream // Hooks for usb.core.Device, which must not leave a stream polling an endpoint // it reconfigures, reads, writes or closes. -// Stops a stream from device_address. Raises USBTimeoutError if the host does -// not let go of the ring in time. +// Stops every stream from device_address. Raises USBTimeoutError if the host +// does not let go of a ring in time. void usb_host_bulk_stop_device(uint8_t device_address); // True while a stream is polling this endpoint. bool usb_host_bulk_endpoint_busy(uint8_t device_address, uint8_t endpoint); -// Stops a stream from device_address if it can. Never raises or allocates, so -// it is safe from finalisers and TinyUSB callbacks. +// Stops every stream from device_address that it can. Never raises or +// allocates, so it is safe from finalisers and TinyUSB callbacks. void usb_host_bulk_device_gone(uint8_t device_address); -// Stops a stream opened through this Device object, but not one opened +// Stops the streams opened through this Device object, but not ones opened // through another Device for the same address. Never raises or allocates. void usb_host_bulk_device_deinit(usb_core_device_obj_t *device); From f3669cdfbaf70df9639fcc29b6384873fc1e5c64 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Fri, 2 Oct 2026 14:38:21 -0500 Subject: [PATCH 7/8] remove ended, change deinit logic, move unread bytes to VM heap, update comment --- .../common-hal/usb_host_bulk/InStream.c | 120 +++++++++++++----- .../common-hal/usb_host_bulk/InStream.h | 5 +- shared-bindings/usb_host_bulk/InStream.c | 35 ++--- shared-bindings/usb_host_bulk/InStream.h | 11 +- 4 files changed, 117 insertions(+), 54 deletions(-) diff --git a/ports/raspberrypi/common-hal/usb_host_bulk/InStream.c b/ports/raspberrypi/common-hal/usb_host_bulk/InStream.c index db47cc48531..608e4ed33cd 100644 --- a/ports/raspberrypi/common-hal/usb_host_bulk/InStream.c +++ b/ports/raspberrypi/common-hal/usb_host_bulk/InStream.c @@ -20,8 +20,8 @@ #define STOP_TIMEOUT_US (100000) -// The streams the host is polling, at most one per endpoint. Weak links: each -// Python object owns its ring, and its finaliser clears its slot. +// Store the streams the host is polling, at most one per endpoint, so that if +// a device disappears we can deinit all streams polling it. static usb_host_bulk_instream_obj_t *_active[PIO_USB_EP_POOL_CNT]; static void _forget(usb_host_bulk_instream_obj_t *self) { @@ -42,13 +42,51 @@ static bool _stop(usb_host_bulk_instream_obj_t *self) { return true; } +static uint32_t _ring_in_waiting(pio_usb_bulk_ring_t *ring) { + uint32_t const read = ring->read_pos; + uint32_t const written = ring->write_pos; + __dmb(); + uint32_t const available = written - read; + return available > ring->capacity ? 0 : available; +} + +static void _free_ring(usb_host_bulk_instream_obj_t *self) { + self->lost_packets = self->ring->stats.overrun_packets; + port_free(self->storage); + port_free(self->ring); + self->ring = NULL; + self->storage = NULL; + self->device = MP_OBJ_NULL; +} + +// Finish deiniting a stream whose ring the host has let go of: move the +// unread bytes to the VM heap, which the GC cleans up, and free the ring. +// Returns false, leaving the stream as it was, if there is no room for them. +static bool _retire(usb_host_bulk_instream_obj_t *self, bool raise_on_no_memory) { + uint32_t remaining = _ring_in_waiting(self->ring); + if (remaining > 0) { + uint8_t *leftover = raise_on_no_memory ? + m_malloc_without_collect(remaining) : + m_malloc_maybe_without_collect(remaining); + if (leftover == NULL) { + return false; + } + pio_usb_host_bulk_stream_read(self->ring, leftover, remaining); + self->leftover = leftover; + self->leftover_len = remaining; + self->leftover_pos = 0; + } + _free_ring(self); + return true; +} + void common_hal_usb_host_bulk_instream_construct(usb_host_bulk_instream_obj_t *self, usb_core_device_obj_t *device, uint8_t endpoint, uint32_t buffer_size) { size_t slot = MP_ARRAY_SIZE(_active); for (size_t i = 0; i < MP_ARRAY_SIZE(_active); i++) { usb_host_bulk_instream_obj_t *other = _active[i]; if (other != NULL && !other->ring->active) { - // It ended on its own and keeps its ring until it is deinited. + // It ended on its own, and finishes deiniting when next used. _active[i] = other = NULL; } if (other == NULL) { @@ -90,6 +128,9 @@ void common_hal_usb_host_bulk_instream_construct(usb_host_bulk_instream_obj_t *s self->device = MP_OBJ_FROM_PTR(device); self->ring = ring; self->storage = storage; + self->leftover = NULL; + self->leftover_len = 0; + self->leftover_pos = 0; self->lost_packets = 0; self->device_address = device->device_address; self->endpoint = endpoint; @@ -97,43 +138,66 @@ void common_hal_usb_host_bulk_instream_construct(usb_host_bulk_instream_obj_t *s } bool common_hal_usb_host_bulk_instream_deinited(usb_host_bulk_instream_obj_t *self) { + if (self->device != MP_OBJ_NULL && !self->ring->active) { + // Clearing active is the host's last access, so the ring now holds + // every byte it wrote. + __dmb(); + _retire(self, false); + } return self->device == MP_OBJ_NULL; } // Uses only this object's own fields: in a finaliser, the Device may already // have been swept. void common_hal_usb_host_bulk_instream_deinit(usb_host_bulk_instream_obj_t *self, bool abandon) { - if (common_hal_usb_host_bulk_instream_deinited(self)) { + if (self->device == MP_OBJ_NULL) { return; } - bool stopped = _stop(self); - if (!stopped && !abandon) { - mp_raise_usb_core_USBTimeoutError(); + if (!_stop(self)) { + if (!abandon) { + mp_raise_usb_core_USBTimeoutError(); + } + // The host core is not running frames (a stop is honoured within one + // frame), so leak the ring rather than free memory it may still own. + self->lost_packets = self->ring->stats.overrun_packets; + _forget(self); + self->ring = NULL; + self->storage = NULL; + self->device = MP_OBJ_NULL; + return; } - self->lost_packets = self->ring->stats.overrun_packets; - if (stopped) { - port_free(self->storage); - port_free(self->ring); + if (abandon) { + // Being collected, so nothing will read the bytes. + _free_ring(self); + } else { + // On MemoryError the stopped stream stays readable until drained. + _retire(self, true); } - // Otherwise the host core is not running frames (a stop is honoured within - // one frame), so leak the ring rather than free memory it may still own. - _forget(self); - self->ring = NULL; - self->storage = NULL; - self->device = MP_OBJ_NULL; } uint32_t common_hal_usb_host_bulk_instream_read(usb_host_bulk_instream_obj_t *self, uint8_t *data, uint32_t len) { - return pio_usb_host_bulk_stream_read(self->ring, data, len); + if (self->ring != NULL) { + return pio_usb_host_bulk_stream_read(self->ring, data, len); + } + uint32_t count = MIN(len, self->leftover_len - self->leftover_pos); + if (count > 0) { + memcpy(data, self->leftover + self->leftover_pos, count); + self->leftover_pos += count; + } + if (self->leftover != NULL && self->leftover_pos == self->leftover_len) { + m_del(uint8_t, self->leftover, self->leftover_len); + self->leftover = NULL; + self->leftover_len = 0; + self->leftover_pos = 0; + } + return count; } uint32_t common_hal_usb_host_bulk_instream_get_in_waiting(usb_host_bulk_instream_obj_t *self) { - pio_usb_bulk_ring_t *ring = self->ring; - uint32_t const read = ring->read_pos; - uint32_t const written = ring->write_pos; - __dmb(); - uint32_t const available = written - read; - return available > ring->capacity ? 0 : available; + if (self->ring != NULL) { + return _ring_in_waiting(self->ring); + } + return self->leftover_len - self->leftover_pos; } uint32_t common_hal_usb_host_bulk_instream_get_lost_packets(usb_host_bulk_instream_obj_t *self) { @@ -143,14 +207,6 @@ uint32_t common_hal_usb_host_bulk_instream_get_lost_packets(usb_host_bulk_instre return self->lost_packets; } -bool common_hal_usb_host_bulk_instream_get_ended(usb_host_bulk_instream_obj_t *self) { - bool ended = !self->ring->active; - // Clearing active is the host's last access, so later reads of the ring - // see every byte it wrote. - __dmb(); - return ended; -} - void common_hal_usb_host_bulk_instream_reset_input_buffer(usb_host_bulk_instream_obj_t *self) { // read_pos belongs to the consumer, so moving it is safe while capturing. self->ring->read_pos = self->ring->write_pos; diff --git a/ports/raspberrypi/common-hal/usb_host_bulk/InStream.h b/ports/raspberrypi/common-hal/usb_host_bulk/InStream.h index 84a81d3ed03..bc2f299fdaf 100644 --- a/ports/raspberrypi/common-hal/usb_host_bulk/InStream.h +++ b/ports/raspberrypi/common-hal/usb_host_bulk/InStream.h @@ -15,8 +15,11 @@ typedef struct { mp_obj_base_t base; mp_obj_t device; // keeps the usb.core.Device alive; NULL once deinited - pio_usb_bulk_ring_t *ring; // NULL once freed + pio_usb_bulk_ring_t *ring; // port heap; NULL once freed uint8_t *storage; + uint8_t *leftover; // VM heap: bytes unread at deinit; NULL once read + uint32_t leftover_len; + uint32_t leftover_pos; uint32_t lost_packets; // latched when the ring is freed uint8_t device_address; // copies, so deinit never touches `device` uint8_t endpoint; diff --git a/shared-bindings/usb_host_bulk/InStream.c b/shared-bindings/usb_host_bulk/InStream.c index 85eb3377a60..4fa5a0db440 100644 --- a/shared-bindings/usb_host_bulk/InStream.c +++ b/shared-bindings/usb_host_bulk/InStream.c @@ -151,8 +151,9 @@ static void check_for_deinit(usb_host_bulk_instream_obj_t *self) { //| def readinto(self, buf: WriteableBuffer, nbytes: Optional[int] = None) -> Optional[int]: //| """Copy waiting bytes into ``buf``, at most ``nbytes`` of them if given. //| -//| Capture ends on its own when the device stalls the endpoint, is unplugged, or -//| is reconfigured. Bytes captured before that can still be read. +//| Capture ends when the stream is deinited, which also happens on its own when +//| the device stalls the endpoint, is unplugged, or is reconfigured. Bytes captured +//| before that can still be read. //| //| :return: the number of bytes copied, ``None`` if nothing is waiting yet, or 0 //| once capture has ended and every captured byte has been read @@ -168,12 +169,11 @@ static void check_for_deinit(usb_host_bulk_instream_obj_t *self) { // Standard stream methods, implemented in py/stream.c on top of this. static mp_uint_t usb_host_bulk_instream_read_stream(mp_obj_t self_in, void *buf, mp_uint_t size, int *errcode) { usb_host_bulk_instream_obj_t *self = MP_OBJ_TO_PTR(self_in); - check_for_deinit(self); - // Look at ended first: the host detaches the ring only after its last - // write, so a stream seen ended here has all of its data visible below. - bool ended = common_hal_usb_host_bulk_instream_get_ended(self); + // Look at deinited first: a stream seen deinited here has all of its data + // visible below, so an empty read means the end. + bool deinited = common_hal_usb_host_bulk_instream_deinited(self); uint32_t count = common_hal_usb_host_bulk_instream_read(self, buf, size); - if (count > 0 || ended) { + if (count > 0 || deinited) { return count; } *errcode = MP_EAGAIN; @@ -186,17 +186,16 @@ static mp_uint_t usb_host_bulk_instream_ioctl(mp_obj_t self_in, mp_uint_t reques common_hal_usb_host_bulk_instream_deinit(self, false); return 0; } - check_for_deinit(self); if (request == MP_STREAM_POLL) { mp_uint_t flags = arg; mp_uint_t ret = 0; - // An ended stream is readable (it returns EOF) so that waiters wake up. - bool ended = common_hal_usb_host_bulk_instream_get_ended(self); + // A deinited stream is readable (it returns EOF) so that waiters wake up. + bool deinited = common_hal_usb_host_bulk_instream_deinited(self); if ((flags & MP_STREAM_POLL_RD) && - (ended || common_hal_usb_host_bulk_instream_get_in_waiting(self) > 0)) { + (deinited || common_hal_usb_host_bulk_instream_get_in_waiting(self) > 0)) { ret |= MP_STREAM_POLL_RD; } - if (ended) { + if (deinited) { ret |= MP_STREAM_POLL_HUP; } return ret; @@ -206,11 +205,10 @@ static mp_uint_t usb_host_bulk_instream_ioctl(mp_obj_t self_in, mp_uint_t reques } //| in_waiting: int -//| """Bytes waiting in the ring. (read-only)""" +//| """Bytes waiting to be read, including those left after `deinit`. (read-only)""" //| static mp_obj_t usb_host_bulk_instream_obj_get_in_waiting(mp_obj_t self_in) { usb_host_bulk_instream_obj_t *self = MP_OBJ_TO_PTR(self_in); - check_for_deinit(self); return mp_obj_new_int_from_uint(common_hal_usb_host_bulk_instream_get_in_waiting(self)); } MP_DEFINE_CONST_FUN_OBJ_1(usb_host_bulk_instream_get_in_waiting_obj, usb_host_bulk_instream_obj_get_in_waiting); @@ -233,7 +231,8 @@ MP_PROPERTY_GETTER(usb_host_bulk_instream_lost_packets_obj, (mp_obj_t)&usb_host_bulk_instream_get_lost_packets_obj); //| def reset_input_buffer(self) -> None: -//| """Discard every byte waiting in the ring.""" +//| """Discard every byte waiting in the ring. Raises `ValueError` once the stream +//| is deinited, because no more data will come.""" //| ... //| static mp_obj_t usb_host_bulk_instream_obj_reset_input_buffer(mp_obj_t self_in) { @@ -245,8 +244,10 @@ static mp_obj_t usb_host_bulk_instream_obj_reset_input_buffer(mp_obj_t self_in) static MP_DEFINE_CONST_FUN_OBJ_1(usb_host_bulk_instream_reset_input_buffer_obj, usb_host_bulk_instream_obj_reset_input_buffer); //| def deinit(self) -> None: -//| """Stop capture and free the ring. Raises `usb.core.USBTimeoutError` if the host -//| does not let go of the ring in time; call it again to retry.""" +//| """Stop capture and free the ring. Bytes already captured can still be read: +//| they move to a buffer that is freed once they have all been read. Raises +//| `usb.core.USBTimeoutError` if the host does not let go of the ring in time; +//| call it again to retry.""" //| ... //| static mp_obj_t usb_host_bulk_instream_obj_deinit(mp_obj_t self_in) { diff --git a/shared-bindings/usb_host_bulk/InStream.h b/shared-bindings/usb_host_bulk/InStream.h index 155f7152bc5..96bb7128a5f 100644 --- a/shared-bindings/usb_host_bulk/InStream.h +++ b/shared-bindings/usb_host_bulk/InStream.h @@ -16,15 +16,18 @@ extern const mp_obj_type_t usb_host_bulk_instream_type; void common_hal_usb_host_bulk_instream_construct(usb_host_bulk_instream_obj_t *self, usb_core_device_obj_t *device, uint8_t endpoint, uint32_t buffer_size); -// Raises USBTimeoutError, and stays usable, if the host keeps the ring. From a -// finaliser, pass abandon=true instead: the memory is then leaked rather than -// freed while the host may still write to it. +// Stops capture and moves the unread bytes to the VM heap, where they stay +// readable. Raises USBTimeoutError, and stays usable, if the host keeps the +// ring. From a finaliser, pass abandon=true instead: nothing is allocated, the +// unread bytes are dropped, and the ring is leaked rather than freed while the +// host may still write to it. void common_hal_usb_host_bulk_instream_deinit(usb_host_bulk_instream_obj_t *self, bool abandon); +// A stream the host ended by itself (stall, unplug, reconfiguration) is deinited +// here, so this may allocate: call it only from the VM. bool common_hal_usb_host_bulk_instream_deinited(usb_host_bulk_instream_obj_t *self); uint32_t common_hal_usb_host_bulk_instream_read(usb_host_bulk_instream_obj_t *self, uint8_t *data, uint32_t len); uint32_t common_hal_usb_host_bulk_instream_get_in_waiting(usb_host_bulk_instream_obj_t *self); uint32_t common_hal_usb_host_bulk_instream_get_lost_packets(usb_host_bulk_instream_obj_t *self); -bool common_hal_usb_host_bulk_instream_get_ended(usb_host_bulk_instream_obj_t *self); void common_hal_usb_host_bulk_instream_reset_input_buffer(usb_host_bulk_instream_obj_t *self); // Hooks for usb.core.Device, which must not leave a stream polling an endpoint From 17f25f8e8239fcfdf80fba3a05a3ad66b6d66037 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 6 Oct 2026 12:07:10 -0500 Subject: [PATCH 8/8] remove gone, simplify deinit --- .../common-hal/usb_host_bulk/InStream.c | 74 ++++++------------- .../common-hal/usb_host_bulk/InStream.h | 2 +- shared-bindings/usb_host_bulk/InStream.c | 32 +++----- shared-bindings/usb_host_bulk/InStream.h | 22 +++--- shared-module/usb/core/Device.c | 6 +- 5 files changed, 45 insertions(+), 91 deletions(-) diff --git a/ports/raspberrypi/common-hal/usb_host_bulk/InStream.c b/ports/raspberrypi/common-hal/usb_host_bulk/InStream.c index 608e4ed33cd..6cfcb6ea442 100644 --- a/ports/raspberrypi/common-hal/usb_host_bulk/InStream.c +++ b/ports/raspberrypi/common-hal/usb_host_bulk/InStream.c @@ -18,8 +18,6 @@ #include "pio_usb_bulk_stream.h" #include "pio_usb_configuration.h" -#define STOP_TIMEOUT_US (100000) - // Store the streams the host is polling, at most one per endpoint, so that if // a device disappears we can deinit all streams polling it. static usb_host_bulk_instream_obj_t *_active[PIO_USB_EP_POOL_CNT]; @@ -34,12 +32,11 @@ static void _forget(usb_host_bulk_instream_obj_t *self) { // Detach the ring from the host but keep it, so that bytes captured before // the stop can still be read. No Python allocation: also used on unplug. -static bool _stop(usb_host_bulk_instream_obj_t *self) { - if (!pio_usb_host_bulk_stream_stop(self->ring, STOP_TIMEOUT_US)) { - return false; +// This waits about a millisecond at most. +static void _stop(usb_host_bulk_instream_obj_t *self) { + while (!pio_usb_host_bulk_stream_stop(self->ring, 1000)) { } _forget(self); - return true; } static uint32_t _ring_in_waiting(pio_usb_bulk_ring_t *ring) { @@ -59,15 +56,15 @@ static void _free_ring(usb_host_bulk_instream_obj_t *self) { self->device = MP_OBJ_NULL; } -// Finish deiniting a stream whose ring the host has let go of: move the -// unread bytes to the VM heap, which the GC cleans up, and free the ring. -// Returns false, leaving the stream as it was, if there is no room for them. -static bool _retire(usb_host_bulk_instream_obj_t *self, bool raise_on_no_memory) { +// Finish deiniting a stream that ended without deinit(), because the +// device stalled the endpoint, was unplugged or was reconfigured: +// move the unread bytes to the VM heap, which the GC cleans up, and +// free the ring. Returns false, leaving the stream as it was, if +// there is no room for them. +static bool _retire(usb_host_bulk_instream_obj_t *self) { uint32_t remaining = _ring_in_waiting(self->ring); if (remaining > 0) { - uint8_t *leftover = raise_on_no_memory ? - m_malloc_without_collect(remaining) : - m_malloc_maybe_without_collect(remaining); + uint8_t *leftover = m_malloc_maybe_without_collect(remaining); if (leftover == NULL) { return false; } @@ -142,37 +139,21 @@ bool common_hal_usb_host_bulk_instream_deinited(usb_host_bulk_instream_obj_t *se // Clearing active is the host's last access, so the ring now holds // every byte it wrote. __dmb(); - _retire(self, false); + _retire(self); } return self->device == MP_OBJ_NULL; } -// Uses only this object's own fields: in a finaliser, the Device may already -// have been swept. -void common_hal_usb_host_bulk_instream_deinit(usb_host_bulk_instream_obj_t *self, bool abandon) { - if (self->device == MP_OBJ_NULL) { - return; - } - if (!_stop(self)) { - if (!abandon) { - mp_raise_usb_core_USBTimeoutError(); - } - // The host core is not running frames (a stop is honoured within one - // frame), so leak the ring rather than free memory it may still own. - self->lost_packets = self->ring->stats.overrun_packets; - _forget(self); - self->ring = NULL; - self->storage = NULL; - self->device = MP_OBJ_NULL; - return; - } - if (abandon) { - // Being collected, so nothing will read the bytes. +// Also the finaliser +void common_hal_usb_host_bulk_instream_deinit(usb_host_bulk_instream_obj_t *self) { + if (self->device != MP_OBJ_NULL) { + _stop(self); _free_ring(self); - } else { - // On MemoryError the stopped stream stays readable until drained. - _retire(self, true); } + // Drop the bytes left by an ended stream. The GC frees their buffer. + self->leftover = NULL; + self->leftover_len = 0; + self->leftover_pos = 0; } uint32_t common_hal_usb_host_bulk_instream_read(usb_host_bulk_instream_obj_t *self, uint8_t *data, uint32_t len) { @@ -213,9 +194,8 @@ void common_hal_usb_host_bulk_instream_reset_input_buffer(usb_host_bulk_instream } // Stops every stream from device_address, or every stream opened through -// device if it is not NULL. Returns false if any of them timed out. -static bool _stop_matching(uint8_t device_address, usb_core_device_obj_t *device) { - bool stopped = true; +// device if it is not NULL. +static void _stop_matching(uint8_t device_address, usb_core_device_obj_t *device) { for (size_t i = 0; i < MP_ARRAY_SIZE(_active); i++) { usb_host_bulk_instream_obj_t *stream = _active[i]; if (stream == NULL) { @@ -223,16 +203,13 @@ static bool _stop_matching(uint8_t device_address, usb_core_device_obj_t *device } if (device != NULL ? stream->device == MP_OBJ_FROM_PTR(device) : stream->device_address == device_address) { - stopped = _stop(stream) && stopped; + _stop(stream); } } - return stopped; } void usb_host_bulk_stop_device(uint8_t device_address) { - if (!_stop_matching(device_address, NULL)) { - mp_raise_usb_core_USBTimeoutError(); - } + _stop_matching(device_address, NULL); } bool usb_host_bulk_endpoint_busy(uint8_t device_address, uint8_t endpoint) { @@ -249,8 +226,3 @@ bool usb_host_bulk_endpoint_busy(uint8_t device_address, uint8_t endpoint) { void usb_host_bulk_device_deinit(usb_core_device_obj_t *device) { _stop_matching(0, device); } - -void usb_host_bulk_device_gone(uint8_t device_address) { - // The host core also detaches the rings by itself once it sees the unplug. - _stop_matching(device_address, NULL); -} diff --git a/ports/raspberrypi/common-hal/usb_host_bulk/InStream.h b/ports/raspberrypi/common-hal/usb_host_bulk/InStream.h index bc2f299fdaf..632298842bf 100644 --- a/ports/raspberrypi/common-hal/usb_host_bulk/InStream.h +++ b/ports/raspberrypi/common-hal/usb_host_bulk/InStream.h @@ -17,7 +17,7 @@ typedef struct { mp_obj_t device; // keeps the usb.core.Device alive; NULL once deinited pio_usb_bulk_ring_t *ring; // port heap; NULL once freed uint8_t *storage; - uint8_t *leftover; // VM heap: bytes unread at deinit; NULL once read + uint8_t *leftover; // VM heap: bytes unread when the host ended the stream uint32_t leftover_len; uint32_t leftover_pos; uint32_t lost_packets; // latched when the ring is freed diff --git a/shared-bindings/usb_host_bulk/InStream.c b/shared-bindings/usb_host_bulk/InStream.c index 4fa5a0db440..803581805c3 100644 --- a/shared-bindings/usb_host_bulk/InStream.c +++ b/shared-bindings/usb_host_bulk/InStream.c @@ -151,18 +151,18 @@ static void check_for_deinit(usb_host_bulk_instream_obj_t *self) { //| def readinto(self, buf: WriteableBuffer, nbytes: Optional[int] = None) -> Optional[int]: //| """Copy waiting bytes into ``buf``, at most ``nbytes`` of them if given. //| -//| Capture ends when the stream is deinited, which also happens on its own when -//| the device stalls the endpoint, is unplugged, or is reconfigured. Bytes captured -//| before that can still be read. +//| Capture ends when the stream is deinited, which drops any bytes not yet read. +//| It also ends on its own when the device stalls the endpoint, is unplugged, or is +//| reconfigured. Bytes captured before that can still be read. //| //| :return: the number of bytes copied, ``None`` if nothing is waiting yet, or 0 -//| once capture has ended and every captured byte has been read +//| once capture has ended and nothing is left to read //| :rtype: int or None""" //| ... //| //| def read(self, nbytes: Optional[int] = None) -> Optional[bytes]: //| """Same as `readinto`, but allocates and returns ``bytes``: ``None`` if nothing -//| is waiting yet, ``b""`` once capture has ended and every byte has been read.""" +//| is waiting yet, ``b""`` once capture has ended and nothing is left to read.""" //| ... //| @@ -183,7 +183,7 @@ static mp_uint_t usb_host_bulk_instream_read_stream(mp_obj_t self_in, void *buf, static mp_uint_t usb_host_bulk_instream_ioctl(mp_obj_t self_in, mp_uint_t request, uintptr_t arg, int *errcode) { usb_host_bulk_instream_obj_t *self = MP_OBJ_TO_PTR(self_in); if (request == MP_STREAM_CLOSE) { - common_hal_usb_host_bulk_instream_deinit(self, false); + common_hal_usb_host_bulk_instream_deinit(self); return 0; } if (request == MP_STREAM_POLL) { @@ -205,7 +205,8 @@ static mp_uint_t usb_host_bulk_instream_ioctl(mp_obj_t self_in, mp_uint_t reques } //| in_waiting: int -//| """Bytes waiting to be read, including those left after `deinit`. (read-only)""" +//| """Bytes waiting to be read, including those left when capture ended on its own. +//| (read-only)""" //| static mp_obj_t usb_host_bulk_instream_obj_get_in_waiting(mp_obj_t self_in) { usb_host_bulk_instream_obj_t *self = MP_OBJ_TO_PTR(self_in); @@ -244,27 +245,16 @@ static mp_obj_t usb_host_bulk_instream_obj_reset_input_buffer(mp_obj_t self_in) static MP_DEFINE_CONST_FUN_OBJ_1(usb_host_bulk_instream_reset_input_buffer_obj, usb_host_bulk_instream_obj_reset_input_buffer); //| def deinit(self) -> None: -//| """Stop capture and free the ring. Bytes already captured can still be read: -//| they move to a buffer that is freed once they have all been read. Raises -//| `usb.core.USBTimeoutError` if the host does not let go of the ring in time; -//| call it again to retry.""" +//| """Stop capture and free the ring, dropping any bytes not yet read.""" //| ... //| static mp_obj_t usb_host_bulk_instream_obj_deinit(mp_obj_t self_in) { usb_host_bulk_instream_obj_t *self = MP_OBJ_TO_PTR(self_in); - common_hal_usb_host_bulk_instream_deinit(self, false); + common_hal_usb_host_bulk_instream_deinit(self); return mp_const_none; } static MP_DEFINE_CONST_FUN_OBJ_1(usb_host_bulk_instream_deinit_obj, usb_host_bulk_instream_obj_deinit); -// A finaliser cannot raise, so it never waits for a retry. -static mp_obj_t usb_host_bulk_instream_obj___del__(mp_obj_t self_in) { - usb_host_bulk_instream_obj_t *self = MP_OBJ_TO_PTR(self_in); - common_hal_usb_host_bulk_instream_deinit(self, true); - return mp_const_none; -} -static MP_DEFINE_CONST_FUN_OBJ_1(usb_host_bulk_instream___del___obj, usb_host_bulk_instream_obj___del__); - //| def __enter__(self) -> InStream: //| """No-op used by Context Managers.""" //| ... @@ -280,7 +270,7 @@ static MP_DEFINE_CONST_FUN_OBJ_1(usb_host_bulk_instream___del___obj, usb_host_bu // Provided by context manager helper. static const mp_rom_map_elem_t usb_host_bulk_instream_locals_dict_table[] = { - { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&usb_host_bulk_instream___del___obj) }, + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&usb_host_bulk_instream_deinit_obj) }, { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&usb_host_bulk_instream_deinit_obj) }, { MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) }, { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&default___exit___obj) }, diff --git a/shared-bindings/usb_host_bulk/InStream.h b/shared-bindings/usb_host_bulk/InStream.h index 96bb7128a5f..f62cbc413fc 100644 --- a/shared-bindings/usb_host_bulk/InStream.h +++ b/shared-bindings/usb_host_bulk/InStream.h @@ -16,14 +16,13 @@ extern const mp_obj_type_t usb_host_bulk_instream_type; void common_hal_usb_host_bulk_instream_construct(usb_host_bulk_instream_obj_t *self, usb_core_device_obj_t *device, uint8_t endpoint, uint32_t buffer_size); -// Stops capture and moves the unread bytes to the VM heap, where they stay -// readable. Raises USBTimeoutError, and stays usable, if the host keeps the -// ring. From a finaliser, pass abandon=true instead: nothing is allocated, the -// unread bytes are dropped, and the ring is leaked rather than freed while the -// host may still write to it. -void common_hal_usb_host_bulk_instream_deinit(usb_host_bulk_instream_obj_t *self, bool abandon); -// A stream the host ended by itself (stall, unplug, reconfiguration) is deinited -// here, so this may allocate: call it only from the VM. +// Stops capture, waits for the host to let go of the ring, frees it and drops +// the unread bytes. It is also the finaliser. +void common_hal_usb_host_bulk_instream_deinit(usb_host_bulk_instream_obj_t *self); +// A stream that ended without deinit(), because the device stalled the +// endpoint, was unplugged or was reconfigured, is deinited here, keeping its +// unread bytes readable on the VM heap, so this may allocate: call it only +// from the VM. bool common_hal_usb_host_bulk_instream_deinited(usb_host_bulk_instream_obj_t *self); uint32_t common_hal_usb_host_bulk_instream_read(usb_host_bulk_instream_obj_t *self, uint8_t *data, uint32_t len); uint32_t common_hal_usb_host_bulk_instream_get_in_waiting(usb_host_bulk_instream_obj_t *self); @@ -33,14 +32,11 @@ void common_hal_usb_host_bulk_instream_reset_input_buffer(usb_host_bulk_instream // Hooks for usb.core.Device, which must not leave a stream polling an endpoint // it reconfigures, reads, writes or closes. -// Stops every stream from device_address. Raises USBTimeoutError if the host -// does not let go of a ring in time. +// Stops every stream from device_address. Their unread bytes stay readable. +// Never raises or allocates, so it is safe from TinyUSB callbacks. void usb_host_bulk_stop_device(uint8_t device_address); // True while a stream is polling this endpoint. bool usb_host_bulk_endpoint_busy(uint8_t device_address, uint8_t endpoint); -// Stops every stream from device_address that it can. Never raises or -// allocates, so it is safe from finalisers and TinyUSB callbacks. -void usb_host_bulk_device_gone(uint8_t device_address); // Stops the streams opened through this Device object, but not ones opened // through another Device for the same address. Never raises or allocates. void usb_host_bulk_device_deinit(usb_core_device_obj_t *device); diff --git a/shared-module/usb/core/Device.c b/shared-module/usb/core/Device.c index 18c1728276c..9c177c057a9 100644 --- a/shared-module/usb/core/Device.c +++ b/shared-module/usb/core/Device.c @@ -34,10 +34,6 @@ static inline bool usb_host_bulk_endpoint_busy(uint8_t device_address, uint8_t e return false; } -static inline void usb_host_bulk_device_gone(uint8_t device_address) { - (void)device_address; -} - static inline void usb_host_bulk_device_deinit(usb_core_device_obj_t *device) { (void)device; } @@ -53,7 +49,7 @@ void tuh_mount_cb(uint8_t dev_addr) { void tuh_umount_cb(uint8_t dev_addr) { _mounted_devices &= ~(1 << dev_addr); - usb_host_bulk_device_gone(dev_addr); + usb_host_bulk_stop_device(dev_addr); } static xfer_result_t _xfer_result;