diff --git a/modules/usbhost/micropython.cmake b/modules/usbhost/micropython.cmake index fd7ae1e5..3e3a23ab 100644 --- a/modules/usbhost/micropython.cmake +++ b/modules/usbhost/micropython.cmake @@ -51,9 +51,10 @@ target_include_directories(usermod_usbhost INTERFACE if(ESP_PLATFORM) idf_component_get_property(usbhost_tusb_lib espressif__tinyusb COMPONENT_LIB) if(usbhost_tusb_lib) - # TinyUSB's device controller and core in IRAM (src/usbhost_isr_iram.lf), - # so machine.USBDevice's streaming endpoints keep running through a - # flash write. + # TinyUSB's device-side interrupt path in IRAM (src/usbhost_isr_iram.lf), + # so cache misses don't delay machine.USBDevice's streaming endpoints + # while a display or anything else keeps the cache busy. Only that + # path: the rest of TinyUSB stays in flash. __ldgen_add_fragment_files("${USBHOST_SRC_DIR}/usbhost_isr_iram.lf") endif() # The IDF defines ESP_PLATFORM for its own components but not for a user C diff --git a/modules/usbhost/src/mod_usbhost.c b/modules/usbhost/src/mod_usbhost.c index 33b4fd2f..607bc3bb 100644 --- a/modules/usbhost/src/mod_usbhost.c +++ b/modules/usbhost/src/mod_usbhost.c @@ -61,10 +61,21 @@ // 1 kHz event stream over that window is ~1500 events. Full HID/MIDI traffic // at that rate through a stall this long is a pathological case rather than a // working one, so the ring holds a generous fraction of it and reports -// overflow instead of pretending. 256 records is 2 KB at 8 bytes each. +// overflow instead of pretending. 256 records is 3 KB at 12 bytes each. #define USBHOST_EVENT_CAPACITY (256) -static usbhost_event_t usbhost_event_slots[USBHOST_EVENT_CAPACITY]; +// The ring's records are taken from the heap the first time a host starts, +// PSRAM first, and kept from then on: internal RAM is what Wi-Fi, BLE and a +// display's DMA need, and a firmware that never starts a host spends nothing. +// Only the host task writes them and only the interpreter reads them, neither +// from an interrupt, so PSRAM serves. Without the engine nothing is ever +// queued, and a one-record ring (which holds nothing) stands in. +#if USBHOST_HAVE_HOST +#include "esp_heap_caps.h" +static usbhost_event_t *usbhost_event_slots; +#else +static usbhost_event_t usbhost_event_none[1]; +#endif static usbhost_ringbuf_t usbhost_events; static bool usbhost_host_running = false; @@ -80,7 +91,26 @@ static void usbhost_arm_soft_reset(void); void usbhost_host_emit(const usbhost_event_t *event); #endif void usbhost_host_emit(const usbhost_event_t *event) { - usbhost_rb_push(&usbhost_events, event); + if (usbhost_events.slots != NULL) { + usbhost_rb_push(&usbhost_events, event); + } +} + +// The ring's records, ready for usbhost_rb_init(); raises MemoryError if the +// heap can't give them. +static void usbhost_events_reserve(void) { + #if USBHOST_HAVE_HOST + if (usbhost_event_slots == NULL) { + usbhost_event_slots = heap_caps_calloc_prefer(USBHOST_EVENT_CAPACITY, sizeof(usbhost_event_t), 2, + MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_DEFAULT); + if (usbhost_event_slots == NULL) { + mp_raise_msg(&mp_type_MemoryError, MP_ERROR_TEXT("no memory for the USB host's event queue")); + } + } + usbhost_rb_init(&usbhost_events, usbhost_event_slots, USBHOST_EVENT_CAPACITY); + #else + usbhost_rb_init(&usbhost_events, usbhost_event_none, 1); + #endif } static const struct { @@ -183,7 +213,7 @@ static mp_obj_t usbhost_host_start(size_t n_args, const mp_obj_t *pos_args, mp_m // running host must be a no-op, not a wipe of queued events -- learned // when the NUCLEO's attach events vanished under an idempotent restart. if (!usbhost_host_running) { - usbhost_rb_init(&usbhost_events, usbhost_event_slots, USBHOST_EVENT_CAPACITY); + usbhost_events_reserve(); } #if USBHOST_HAVE_HOST if (!usbhost_host_running) { diff --git a/modules/usbhost/src/usbhost_host_stream.c b/modules/usbhost/src/usbhost_host_stream.c index a128b811..90273fcc 100644 --- a/modules/usbhost/src/usbhost_host_stream.c +++ b/modules/usbhost/src/usbhost_host_stream.c @@ -41,6 +41,7 @@ #include "freertos/FreeRTOS.h" #include "freertos/task.h" +#include "esp_heap_caps.h" #include "esp_rom_sys.h" #include "esp_timer.h" #include "usb/usb_host.h" @@ -82,15 +83,22 @@ typedef struct { volatile usbhost_stream_stats_t st; } usbhost_stream_t; -static usbhost_stream_t usbhost_streams[USBHOST_HOST_STREAMS]; +// The slots are taken from the heap when the first stream opens, PSRAM first, +// and kept from then on, so a firmware that never streams spends no internal +// RAM on them. Nothing reads them from an interrupt: the library calls +// usbhost_stream_cb() from whoever pumps the client's events. The packets are +// in the transfers, which come from DMA-capable internal RAM +// (usbhost_stream_open_locked()). NULL until the first stream. +static usbhost_stream_t *usbhost_streams; // The producers' gate on each playback stream's ring: the interpreter's // writes and a C sink's, from any task (shared/usbhost_pcm_sink.h). Outside // the slots because a slot is cleared for each stream, and a gate's // generations must carry on from one stream to the next, so a sink issued -// for a stream that closed stays closed. +// for a stream that closed stays closed. Static, in internal RAM, because +// the gates take read-modify-write atomics, which the ESP32-S3 can't do on +// PSRAM; they are 32 bytes each. static usbhost_pcm_sink_t usbhost_stream_gates[USBHOST_HOST_STREAMS]; -static bool usbhost_stream_gates_ready; static uint32_t usbhost_stream_gate_room(void *slot) { const usbhost_stream_t *s = slot; @@ -111,14 +119,22 @@ static void usbhost_stream_gate_wait(void) { vTaskDelay(1); // one real tick: lets a writer on this core finish } -static void usbhost_stream_gates_setup(void) { - if (!usbhost_stream_gates_ready) { +// The slots, and the gates pointed at them, on first use. False if the heap +// can't give them. +static bool usbhost_stream_gates_setup(void) { + if (usbhost_streams == NULL) { + usbhost_stream_t *slots = heap_caps_calloc_prefer(USBHOST_HOST_STREAMS, sizeof(*slots), 2, + MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_DEFAULT); + if (slots == NULL) { + return false; + } for (int i = 0; i < USBHOST_HOST_STREAMS; i++) { - usbhost_pcm_sink_init(&usbhost_stream_gates[i], &usbhost_stream_gate_ops, &usbhost_streams[i], + usbhost_pcm_sink_init(&usbhost_stream_gates[i], &usbhost_stream_gate_ops, &slots[i], usbhost_stream_gate_wait); } - usbhost_stream_gates_ready = true; + usbhost_streams = slots; // last: every reader tests this one } + return true; } static inline int usbhost_stream_index(const usbhost_stream_t *s) { @@ -126,6 +142,9 @@ static inline int usbhost_stream_index(const usbhost_stream_t *s) { } static usbhost_stream_t *usbhost_stream_find(uint32_t dev_id, uint8_t ep) { + if (usbhost_streams == NULL) { + return NULL; + } for (int i = 0; i < USBHOST_HOST_STREAMS; i++) { usbhost_stream_t *s = &usbhost_streams[i]; if (s->used && s->dev_id == dev_id && s->ep == ep) { @@ -285,6 +304,9 @@ static void usbhost_stream_stop_locked(usbhost_stream_t *s) { // The streams on one interface of a device (itf < 0: all of it), as the // transfer layer gives the interface up; device_gone when it's unplugged. void usbhost_stream_stop_itf(void *dev, int itf, bool device_gone) { + if (usbhost_streams == NULL) { + return; + } for (int i = 0; i < USBHOST_HOST_STREAMS; i++) { usbhost_stream_t *s = &usbhost_streams[i]; if (s->used && s->dev == (usb_device_handle_t)dev && (itf < 0 || s->itf == itf)) { @@ -297,6 +319,9 @@ void usbhost_stream_stop_itf(void *dev, int itf, bool device_gone) { } void usbhost_stream_close_all(void) { + if (usbhost_streams == NULL) { + return; + } usbhost_host_lock(); for (int i = 0; i < USBHOST_HOST_STREAMS; i++) { usbhost_stream_stop_locked(&usbhost_streams[i]); @@ -305,6 +330,9 @@ void usbhost_stream_close_all(void) { } uint32_t usbhost_stream_free_slots(void) { + if (usbhost_streams == NULL) { + return (1u << USBHOST_HOST_STREAMS) - 1; + } uint32_t bits = 0; for (int i = 0; i < USBHOST_HOST_STREAMS; i++) { if (!usbhost_streams[i].used) { @@ -317,7 +345,9 @@ uint32_t usbhost_stream_free_slots(void) { // --- the surface ---------------------------------------------------------- static int usbhost_stream_open_locked(uint32_t dev_id, uint8_t ep, const usbhost_stream_cfg_t *cfg) { - usbhost_stream_gates_setup(); + if (!usbhost_stream_gates_setup()) { + return -MP_ENOMEM; + } usb_device_handle_t dev; if (usbhost_host_dev_lookup(dev_id, &dev) != 0) { return -MP_ENODEV; diff --git a/modules/usbhost/src/usbhost_isr_iram.lf b/modules/usbhost/src/usbhost_isr_iram.lf index 8434db8c..a4258a73 100644 --- a/modules/usbhost/src/usbhost_isr_iram.lf +++ b/modules/usbhost/src/usbhost_isr_iram.lf @@ -1,12 +1,45 @@ -# The sound card's receive path, from the USB interrupt to the pump's wake-up, -# in IRAM instead of flash (audio meter spike). Out of flash, every cache miss -# in this path delays the endpoint re-arm, and anything that keeps the cache -# busy -- a display pushing frames through PSRAM -- turns into lost +# TinyUSB's device-side interrupt path in IRAM instead of flash: the DWC2 +# interrupt handler, the event handler it calls, the endpoint re-arm a +# streaming endpoint makes from that interrupt, and the FIFO copies. Out of +# flash, every cache miss here delays the re-arm, and anything that keeps the +# cache busy -- a display pushing frames through PSRAM -- turns into lost # isochronous packets. +# +# Only that path. TinyUSB registers its interrupt without ESP_INTR_FLAG_IRAM, +# so nothing here has to run with the cache off, and everything else -- set-up, +# tear-down, enumeration, control transfers, tud_task() -- stays in flash, +# where it costs no internal RAM. [mapping:usbhost_tinyusb_isr] archive: libespressif__tinyusb.a entries: + # The controller's interrupt handler and the static helpers it calls, + # whose names the compiler may change; the set-up calls below go back. dcd_dwc2 (noflash) - dwc2_common (noflash) - usbd (noflash) - tusb_fifo (noflash) + dcd_dwc2:dcd_init (default) + dcd_dwc2:dcd_deinit (default) + dcd_dwc2:dcd_connect (default) + dcd_dwc2:dcd_disconnect (default) + dcd_dwc2:dcd_remote_wakeup (default) + dcd_dwc2:dcd_set_address (default) + dcd_dwc2:dcd_edpt_open (default) + dcd_dwc2:dcd_edpt_close_all (default) + dcd_dwc2:dcd_edpt_clear_stall (default) + dcd_dwc2:dcd_int_enable (default) + dcd_dwc2:dcd_int_disable (default) + # The device core: events from the interrupt, and an endpoint re-armed + # from it. + usbd:dcd_event_handler (noflash) + usbd:usbd_edpt_xfer (noflash) + usbd:usbd_edpt_xfer_fifo (noflash) + usbd:usbd_edpt_busy (noflash) + usbd:usbd_edpt_claim (noflash) + usbd:usbd_edpt_release (noflash) + usbd:usbd_spin_lock (noflash) + usbd:usbd_spin_unlock (noflash) + usbd:usbd_int_set (noflash) + # Copies between the controller's FIFO and a buffer or a tu_fifo. + tusb_fifo:tu_hwfifo_write (noflash) + tusb_fifo:tu_hwfifo_read (noflash) + tusb_fifo:tu_fifo_write_n_access_mode (noflash) + tusb_fifo:tu_fifo_read_n_access_mode (noflash) + tusb_fifo:tu_fifo_peek_n_access_mode (noflash)