Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions modules/usbhost/micropython.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 34 additions & 4 deletions modules/usbhost/src/mod_usbhost.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
46 changes: 38 additions & 8 deletions modules/usbhost/src/usbhost_host_stream.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;
Expand All @@ -111,21 +119,32 @@ 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) {
return (int)(s - usbhost_streams);
}

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) {
Expand Down Expand Up @@ -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)) {
Expand All @@ -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]);
Expand All @@ -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) {
Expand All @@ -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;
Expand Down
47 changes: 40 additions & 7 deletions modules/usbhost/src/usbhost_isr_iram.lf
Original file line number Diff line number Diff line change
@@ -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)
Loading