From 9d68ed65f7200f155f01416b332c5e9f2340a8b2 Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 4 May 2026 11:19:26 +0700 Subject: refactor: replace `tu_edpt_state_t` struct with `uint8_t` and update all endpoint state handling methods and accesses --- src/host/usbh.c | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) (limited to 'src/host') diff --git a/src/host/usbh.c b/src/host/usbh.c index 8f80800e9..490724b02 100644 --- a/src/host/usbh.c +++ b/src/host/usbh.c @@ -140,7 +140,7 @@ typedef struct { uint8_t itf2drv[CFG_TUH_INTERFACE_MAX]; // map interface number to driver (0xff is invalid) uint8_t ep2drv[CFG_TUH_ENDPOINT_MAX][2]; // map endpoint to driver ( 0xff is invalid ), can use only 4-bit each - tu_edpt_state_t ep_status[CFG_TUH_ENDPOINT_MAX][2]; + volatile uint8_t ep_status[CFG_TUH_ENDPOINT_MAX][2]; #if CFG_TUH_API_EDPT_XFER // TODO array can be CFG_TUH_ENDPOINT_MAX-1 @@ -744,8 +744,8 @@ void tuh_task_ext(uint32_t timeout_ms, bool in_isr) { usbh_device_t* dev = get_device(event.dev_addr); TU_VERIFY(dev && dev->connected,); - dev->ep_status[epnum][ep_dir].busy = 0; - dev->ep_status[epnum][ep_dir].claimed = 0; + // clear busy and claimed + dev->ep_status[epnum][ep_dir] &= (uint8_t) ~(TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED); if (0 == epnum) { usbh_control_xfer_cb(event.dev_addr, ep_addr, (xfer_result_t) event.xfer_complete.result, event.xfer_complete.len); @@ -1016,10 +1016,10 @@ bool tuh_edpt_abort_xfer(uint8_t daddr, uint8_t ep_addr) { usbh_device_t* dev = get_device(daddr); TU_VERIFY(dev); - TU_VERIFY(dev->ep_status[epnum][dir].busy); // non-control skip if not busy + TU_VERIFY(dev->ep_status[epnum][dir] & TU_EDPT_STATE_BUSY); // non-control skip if not busy // abort then mark as ready and release endpoint hcd_edpt_abort_xfer(dev->bus_info.rhport, daddr, ep_addr); - dev->ep_status[epnum][dir].busy = false; + dev->ep_status[epnum][dir] &= (uint8_t) ~TU_EDPT_STATE_BUSY; // clear busy tu_edpt_release(&dev->ep_status[epnum][dir], _usbh_mutex); } @@ -1110,16 +1110,16 @@ bool usbh_edpt_xfer_with_callback(uint8_t dev_addr, uint8_t ep_addr, uint8_t* bu uint8_t const epnum = tu_edpt_number(ep_addr); uint8_t const dir = tu_edpt_dir(ep_addr); - tu_edpt_state_t* ep_state = &dev->ep_status[epnum][dir]; + volatile uint8_t* ep_state = &dev->ep_status[epnum][dir]; TU_LOG_USBH(" Queue EP %02X with %u bytes ... \r\n", ep_addr, total_bytes); // Attempt to transfer on a busy endpoint, sound like an race condition ! - TU_ASSERT(ep_state->busy == 0); + TU_ASSERT((*ep_state & TU_EDPT_STATE_BUSY) == 0); // Set busy first since the actual transfer can be complete before hcd_edpt_xfer() // could return and USBH task can preempt and clear the busy - ep_state->busy = 1; + *ep_state |= TU_EDPT_STATE_BUSY; #if CFG_TUH_API_EDPT_XFER dev->ep_callback[epnum][dir].complete_cb = complete_cb; @@ -1130,9 +1130,8 @@ bool usbh_edpt_xfer_with_callback(uint8_t dev_addr, uint8_t ep_addr, uint8_t* bu TU_LOG_USBH("OK\r\n"); return true; } else { - // HCD error, mark endpoint as ready to allow next transfer - ep_state->busy = 0; - ep_state->claimed = 0; + // HCD error, clear busy and claimed to allow next transfer + *ep_state &= (uint8_t) ~(TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED); TU_LOG1("Failed\r\n"); // TU_BREAKPOINT(); return false; @@ -1178,7 +1177,7 @@ bool usbh_edpt_busy(uint8_t dev_addr, uint8_t ep_addr) { uint8_t const epnum = tu_edpt_number(ep_addr); uint8_t const dir = tu_edpt_dir(ep_addr); - return dev->ep_status[epnum][dir].busy; + return (dev->ep_status[epnum][dir] & TU_EDPT_STATE_BUSY) != 0; } //--------------------------------------------------------------------+ -- cgit v1.3.1 From 7750e51ce1b64bd4d2f0ba6d9671303b96ba80ce Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Tue, 24 Mar 2026 23:38:02 -0300 Subject: feat: add MIDI 2.0 Host class driver (USB-MIDI 2.0) Add native USB-MIDI 2.0 Host class driver to TinyUSB. Implements reactive architecture: enumerate, detect MIDI 2.0 capability, inform application via callbacks. Driver features: - Parse both Alt Setting 0 (MIDI 1.0) and Alt Setting 1 (UMP) - Detect bcdMSC version from descriptor - Auto-select highest protocol (Alt 1 preferred if available) - UMP read/write via endpoint streams - Proper Audio Control interface skip (loop-based, following midi_host.c pattern) - Endpoint open with tuh_edpt_open/tu_edpt_stream_open/clear - usbh_driver_set_config_complete for USBH state machine - Handle Audio Control itf_num in set_config gracefully - 5 weak callback stubs (descriptor, mount, unmount, rx, tx) Build system: - Register midih2_* in usbh.c driver table - Add midi2_host.h include to tusb.h - Add CFG_TUH_MIDI2_LOG_LEVEL to tusb_option.h All changes guarded by #if CFG_TUH_MIDI2 (default 0). Zero impact on existing drivers and examples. Tested: Waveshare RP2350-USB-A (Host) receiving UMP from Raspberry Pi Pico (Device) via PIO-USB, board-to-board --- src/class/midi/midi2_host.c | 502 ++++++++++++++++++++++++++++++++++++++++++++ src/class/midi/midi2_host.h | 99 +++++++++ src/host/usbh.c | 12 ++ 3 files changed, 613 insertions(+) create mode 100644 src/class/midi/midi2_host.c create mode 100644 src/class/midi/midi2_host.h (limited to 'src/host') diff --git a/src/class/midi/midi2_host.c b/src/class/midi/midi2_host.c new file mode 100644 index 000000000..beb441b56 --- /dev/null +++ b/src/class/midi/midi2_host.c @@ -0,0 +1,502 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Saulo Verissimo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * This file is part of the TinyUSB stack. + */ + +#include "tusb_option.h" + +#if (CFG_TUH_ENABLED && CFG_TUH_MIDI2) + +#include "host/usbh.h" +#include "host/usbh_pvt.h" +#include "midi2_host.h" + +#define TU_LOG_DRV(...) TU_LOG(CFG_TUH_MIDI2_LOG_LEVEL, __VA_ARGS__) + +//--------------------------------------------------------------------+ +// Weak stubs for application callbacks +//--------------------------------------------------------------------+ + +TU_ATTR_WEAK void tuh_midi2_descriptor_cb(uint8_t idx, const tuh_midi2_descriptor_cb_t *desc_cb_data) { + (void) idx; (void) desc_cb_data; +} + +TU_ATTR_WEAK void tuh_midi2_mount_cb(uint8_t idx, const tuh_midi2_mount_cb_t *mount_cb_data) { + (void) idx; (void) mount_cb_data; +} + +TU_ATTR_WEAK void tuh_midi2_rx_cb(uint8_t idx, uint32_t xferred_bytes) { + (void) idx; (void) xferred_bytes; +} + +TU_ATTR_WEAK void tuh_midi2_tx_cb(uint8_t idx, uint32_t xferred_bytes) { + (void) idx; (void) xferred_bytes; +} + +TU_ATTR_WEAK void tuh_midi2_umount_cb(uint8_t idx) { + (void) idx; +} + +//--------------------------------------------------------------------+ +// Internal structure and state +//--------------------------------------------------------------------+ + +typedef struct { + uint8_t daddr; + uint8_t bInterfaceNumber; + + uint8_t alt_setting_current; + + uint8_t protocol_version; + uint8_t bcdMSC_hi, bcdMSC_lo; + uint8_t rx_cable_count_alt0; + uint8_t tx_cable_count_alt0; + uint8_t rx_cable_count_alt1; + uint8_t tx_cable_count_alt1; + + struct { + tu_edpt_stream_t tx; + tu_edpt_stream_t rx; + + uint8_t rx_ff_buf[CFG_TUH_MIDI2_RX_BUFSIZE]; + uint8_t tx_ff_buf[CFG_TUH_MIDI2_TX_BUFSIZE]; + } ep_stream; + + bool mounted; +} midih2_interface_t; + +static midih2_interface_t _midi2_host[CFG_TUH_MIDI2]; + +#if CFG_TUH_EDPT_DEDICATED_HWFIFO == 0 +typedef struct { + TUH_EPBUF_DEF(tx, TUH_EPSIZE_BULK_MAX); + TUH_EPBUF_DEF(rx, TUH_EPSIZE_BULK_MAX); +} midih2_epbuf_t; + +CFG_TUH_MEM_SECTION static midih2_epbuf_t _midi2_epbuf[CFG_TUH_MIDI2]; +#endif + +//--------------------------------------------------------------------+ +// Helper functions +//--------------------------------------------------------------------+ + +static inline uint8_t find_new_midi2_index(void) { + for (uint8_t idx = 0; idx < CFG_TUH_MIDI2; idx++) { + if (_midi2_host[idx].daddr == 0) { + return idx; + } + } + return TUSB_INDEX_INVALID_8; +} + +static inline uint8_t get_idx_by_ep_addr(uint8_t daddr, uint8_t ep_addr) { + for (uint8_t idx = 0; idx < CFG_TUH_MIDI2; idx++) { + const midih2_interface_t *p_midi = &_midi2_host[idx]; + if ((p_midi->daddr == daddr) && + (ep_addr == p_midi->ep_stream.rx.ep_addr || ep_addr == p_midi->ep_stream.tx.ep_addr)) { + return idx; + } + } + return TUSB_INDEX_INVALID_8; +} + +//--------------------------------------------------------------------+ +// Descriptor parsing +//--------------------------------------------------------------------+ + +static void midih2_parse_descriptors_alt0(midih2_interface_t *p_midi, + const tusb_desc_interface_t *desc_itf, const uint8_t *desc_end) { + TU_VERIFY(AUDIO_SUBCLASS_MIDI_STREAMING == desc_itf->bInterfaceSubClass,); + + p_midi->bInterfaceNumber = desc_itf->bInterfaceNumber; + + const uint8_t *p_desc = (const uint8_t *) desc_itf; + p_desc = tu_desc_next(p_desc); + + uint8_t rx_cable_count = 0; + uint8_t tx_cable_count = 0; + + while (tu_desc_in_bounds(p_desc, desc_end)) { + if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) { + break; + } + + if (tu_desc_type(p_desc) == TUSB_DESC_ENDPOINT) { + const tusb_desc_endpoint_t *p_ep = (const tusb_desc_endpoint_t *) p_desc; + + // Open endpoint and stream + TU_ASSERT(tuh_edpt_open(p_midi->daddr, p_ep),); + if (tu_edpt_dir(p_ep->bEndpointAddress) == TUSB_DIR_IN) { + tu_edpt_stream_open(&p_midi->ep_stream.rx, p_midi->daddr, p_ep, tu_edpt_packet_size(p_ep)); + tu_edpt_stream_clear(&p_midi->ep_stream.rx); + } else { + tu_edpt_stream_open(&p_midi->ep_stream.tx, p_midi->daddr, p_ep, tu_edpt_packet_size(p_ep)); + tu_edpt_stream_clear(&p_midi->ep_stream.tx); + } + + p_desc = tu_desc_next(p_desc); + + if (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) == TUSB_DESC_CS_ENDPOINT) { + const midi_desc_cs_endpoint_t *p_csep = (const midi_desc_cs_endpoint_t *) p_desc; + + if (tu_edpt_dir(p_ep->bEndpointAddress) == TUSB_DIR_OUT) { + tx_cable_count = p_csep->bNumEmbMIDIJack; + } else { + rx_cable_count = p_csep->bNumEmbMIDIJack; + } + } + } + + p_desc = tu_desc_next(p_desc); + } + + p_midi->rx_cable_count_alt0 = rx_cable_count; + p_midi->tx_cable_count_alt0 = tx_cable_count; +} + +static void midih2_parse_descriptors_alt1(midih2_interface_t *p_midi, + const tusb_desc_interface_t *desc_itf, const uint8_t *desc_end) { + TU_VERIFY(AUDIO_SUBCLASS_MIDI_STREAMING == desc_itf->bInterfaceSubClass,); + TU_VERIFY(desc_itf->bAlternateSetting == 1,); + + const uint8_t *p_desc = (const uint8_t *) desc_itf; + p_desc = tu_desc_next(p_desc); + + uint8_t rx_cable_count = 0; + uint8_t tx_cable_count = 0; + + while (tu_desc_in_bounds(p_desc, desc_end)) { + if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) { + break; + } + + if (tu_desc_type(p_desc) == TUSB_DESC_CS_INTERFACE) { + if (tu_desc_subtype(p_desc) == MIDI_CS_INTERFACE_HEADER) { + const uint8_t *bcd_ptr = p_desc + 3; + p_midi->bcdMSC_lo = bcd_ptr[0]; + p_midi->bcdMSC_hi = bcd_ptr[1]; + + if (p_midi->bcdMSC_hi == 0x02) { + p_midi->protocol_version = 1; + } + } + } + + if (tu_desc_type(p_desc) == TUSB_DESC_ENDPOINT) { + const tusb_desc_endpoint_t *p_ep = (const tusb_desc_endpoint_t *) p_desc; + p_desc = tu_desc_next(p_desc); + + if (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) == TUSB_DESC_CS_ENDPOINT) { + const midi_desc_cs_endpoint_t *p_csep = (const midi_desc_cs_endpoint_t *) p_desc; + + if (tu_edpt_dir(p_ep->bEndpointAddress) == TUSB_DIR_OUT) { + tx_cable_count = p_csep->bNumEmbMIDIJack; + } else { + rx_cable_count = p_csep->bNumEmbMIDIJack; + } + } + } + + p_desc = tu_desc_next(p_desc); + } + + p_midi->rx_cable_count_alt1 = rx_cable_count; + p_midi->tx_cable_count_alt1 = tx_cable_count; +} + +//--------------------------------------------------------------------+ +// Auto-selection logic +//--------------------------------------------------------------------+ + +static void midih2_auto_select_alt_setting(midih2_interface_t *p_midi) { + p_midi->alt_setting_current = 0; + if (p_midi->protocol_version == 1) { + p_midi->alt_setting_current = 1; + } +} + +//--------------------------------------------------------------------+ +// Init/Deinit +//--------------------------------------------------------------------+ + +bool midih2_init(void) { + tu_memclr(&_midi2_host, sizeof(_midi2_host)); + for (int inst = 0; inst < CFG_TUH_MIDI2; inst++) { + midih2_interface_t *p_midi = &_midi2_host[inst]; + + #if CFG_TUH_EDPT_DEDICATED_HWFIFO + uint8_t* rx_buf = NULL; + uint8_t* tx_buf = NULL; + #else + uint8_t* rx_buf = _midi2_epbuf[inst].rx; + uint8_t* tx_buf = _midi2_epbuf[inst].tx; + #endif + + tu_edpt_stream_init(&p_midi->ep_stream.rx, true, false, false, + p_midi->ep_stream.rx_ff_buf, CFG_TUH_MIDI2_RX_BUFSIZE, rx_buf); + tu_edpt_stream_init(&p_midi->ep_stream.tx, true, true, false, + p_midi->ep_stream.tx_ff_buf, CFG_TUH_MIDI2_TX_BUFSIZE, tx_buf); + } + return true; +} + +bool midih2_deinit(void) { + for (size_t i = 0; i < CFG_TUH_MIDI2; i++) { + midih2_interface_t* p_midi = &_midi2_host[i]; + tu_edpt_stream_deinit(&p_midi->ep_stream.rx); + tu_edpt_stream_deinit(&p_midi->ep_stream.tx); + } + return true; +} + +//--------------------------------------------------------------------+ +// Class driver callbacks +//--------------------------------------------------------------------+ + +uint16_t midih2_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface_t *desc_itf, uint16_t max_len) { + (void) rhport; + + TU_VERIFY(TUSB_CLASS_AUDIO == desc_itf->bInterfaceClass, 0); + + // For Alt Setting 1, reuse existing slot for same device+interface + uint8_t idx = TUSB_INDEX_INVALID_8; + if (desc_itf->bAlternateSetting > 0) { + for (uint8_t i = 0; i < CFG_TUH_MIDI2; i++) { + if (_midi2_host[i].daddr == dev_addr && + _midi2_host[i].bInterfaceNumber == desc_itf->bInterfaceNumber) { + idx = i; + break; + } + } + } + if (idx == TUSB_INDEX_INVALID_8) { + idx = find_new_midi2_index(); + } + TU_VERIFY(idx < CFG_TUH_MIDI2, 0); + + midih2_interface_t *p_midi = &_midi2_host[idx]; + p_midi->daddr = dev_addr; + + const uint8_t *desc_start = (const uint8_t *) desc_itf; + const uint8_t *desc_end = desc_start + max_len; + + // Skip Audio Control interface and any non-MIDI-Streaming descriptors + // (following midi_host.c pattern from Ha Thach) + if (AUDIO_SUBCLASS_CONTROL == desc_itf->bInterfaceSubClass) { + const uint8_t *p_desc = tu_desc_next((const uint8_t *)desc_itf); + // Skip CS_INTERFACE header + TU_VERIFY(tu_desc_type(p_desc) == TUSB_DESC_CS_INTERFACE, 0); + p_desc = tu_desc_next(p_desc); + desc_itf = (const tusb_desc_interface_t *) p_desc; + // Skip until we find MIDI Streaming interface + while (tu_desc_in_bounds(p_desc, desc_end) && + (desc_itf->bDescriptorType != TUSB_DESC_INTERFACE || + (desc_itf->bInterfaceClass == TUSB_CLASS_AUDIO && + desc_itf->bInterfaceSubClass != AUDIO_SUBCLASS_MIDI_STREAMING))) { + p_desc = tu_desc_next(p_desc); + desc_itf = (const tusb_desc_interface_t *) p_desc; + } + TU_VERIFY(tu_desc_in_bounds(p_desc, desc_end), 0); + TU_VERIFY(TUSB_CLASS_AUDIO == desc_itf->bInterfaceClass, 0); + } + + TU_VERIFY(AUDIO_SUBCLASS_MIDI_STREAMING == desc_itf->bInterfaceSubClass, 0); + + TU_LOG_DRV("MIDI2 opening Interface %u Alt %u (addr = %u)\r\n", + desc_itf->bInterfaceNumber, desc_itf->bAlternateSetting, dev_addr); + + // Dispatch to appropriate parser based on Alt Setting + if (desc_itf->bAlternateSetting == 0) { + midih2_parse_descriptors_alt0(p_midi, desc_itf, desc_end); + } else if (desc_itf->bAlternateSetting == 1) { + midih2_parse_descriptors_alt1(p_midi, desc_itf, desc_end); + } + + return max_len; +} + +bool midih2_set_config(uint8_t dev_addr, uint8_t itf_num) { + uint8_t idx = 0; + for (idx = 0; idx < CFG_TUH_MIDI2; idx++) { + if (_midi2_host[idx].daddr == dev_addr && _midi2_host[idx].bInterfaceNumber == itf_num) { + break; + } + } + + if (idx >= CFG_TUH_MIDI2) { + // Not our interface (e.g. Audio Control) - pass through to next + usbh_driver_set_config_complete(dev_addr, itf_num); + return true; + } + + midih2_interface_t *p_midi = &_midi2_host[idx]; + + // Auto-select alt setting + midih2_auto_select_alt_setting(p_midi); + + // Invoke descriptor_cb + tuh_midi2_descriptor_cb_t desc_cb = { + .protocol_version = p_midi->protocol_version, + .bcdMSC_hi = p_midi->bcdMSC_hi, + .bcdMSC_lo = p_midi->bcdMSC_lo, + .rx_cable_count = (p_midi->alt_setting_current == 0) ? + p_midi->rx_cable_count_alt0 : p_midi->rx_cable_count_alt1, + .tx_cable_count = (p_midi->alt_setting_current == 0) ? + p_midi->tx_cable_count_alt0 : p_midi->tx_cable_count_alt1, + }; + tuh_midi2_descriptor_cb(idx, &desc_cb); + + // Mark as mounted + TU_LOG_DRV("MIDI2 mounted addr = %u, alt = %u, protocol = %u\r\n", + dev_addr, p_midi->alt_setting_current, p_midi->protocol_version); + p_midi->mounted = true; + + // Invoke mount_cb + tuh_midi2_mount_cb_t mount_cb = { + .daddr = p_midi->daddr, + .bInterfaceNumber = p_midi->bInterfaceNumber, + .protocol_version = p_midi->protocol_version, + .alt_setting_active = p_midi->alt_setting_current, + .rx_cable_count = desc_cb.rx_cable_count, + .tx_cable_count = desc_cb.tx_cable_count, + }; + tuh_midi2_mount_cb(idx, &mount_cb); + + // Prepare RX transfer + tu_edpt_stream_read_xfer(&p_midi->ep_stream.rx); + + // Signal USBH that configuration is complete + usbh_driver_set_config_complete(dev_addr, itf_num); + + return true; +} + +void midih2_close(uint8_t dev_addr) { + for (uint8_t idx = 0; idx < CFG_TUH_MIDI2; idx++) { + midih2_interface_t *p_midi = &_midi2_host[idx]; + if (p_midi->daddr == dev_addr) { + TU_LOG_DRV(" MIDI2 close addr = %u index = %u\r\n", dev_addr, idx); + tu_edpt_stream_close(&p_midi->ep_stream.rx); + tu_edpt_stream_close(&p_midi->ep_stream.tx); + tuh_midi2_umount_cb(idx); + tu_memclr(p_midi, sizeof(midih2_interface_t)); + } + } +} + +bool midih2_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { + uint8_t idx = get_idx_by_ep_addr(dev_addr, ep_addr); + TU_VERIFY(idx < CFG_TUH_MIDI2); + + midih2_interface_t *p_midi = &_midi2_host[idx]; + + if (ep_addr == p_midi->ep_stream.rx.ep_addr) { + if (result == XFER_RESULT_SUCCESS && xferred_bytes > 0) { + tu_edpt_stream_read_xfer_complete(&p_midi->ep_stream.rx, xferred_bytes); + tuh_midi2_rx_cb(idx, xferred_bytes); + } + tu_edpt_stream_read_xfer(&p_midi->ep_stream.rx); + } else if (ep_addr == p_midi->ep_stream.tx.ep_addr) { + tuh_midi2_tx_cb(idx, xferred_bytes); + if (0 == tu_edpt_stream_write_xfer(&p_midi->ep_stream.tx)) { + tu_edpt_stream_write_zlp_if_needed(&p_midi->ep_stream.tx, xferred_bytes); + } + } + + return true; +} + +//--------------------------------------------------------------------+ +// Public API +//--------------------------------------------------------------------+ + +bool tuh_midi2_mounted(uint8_t idx) { + TU_VERIFY(idx < CFG_TUH_MIDI2); + return _midi2_host[idx].mounted; +} + +uint8_t tuh_midi2_get_protocol_version(uint8_t idx) { + TU_VERIFY(idx < CFG_TUH_MIDI2); + return _midi2_host[idx].protocol_version; +} + +uint8_t tuh_midi2_get_alt_setting_active(uint8_t idx) { + TU_VERIFY(idx < CFG_TUH_MIDI2); + return _midi2_host[idx].alt_setting_current; +} + +uint8_t tuh_midi2_get_cable_count(uint8_t idx) { + TU_VERIFY(idx < CFG_TUH_MIDI2); + return (_midi2_host[idx].alt_setting_current == 0) ? + _midi2_host[idx].rx_cable_count_alt0 : _midi2_host[idx].rx_cable_count_alt1; +} + +uint32_t tuh_midi2_ump_read(uint8_t idx, uint32_t* words, uint32_t max_words) { + TU_VERIFY(idx < CFG_TUH_MIDI2 && words && max_words); + + midih2_interface_t *p_midi = &_midi2_host[idx]; + tu_edpt_stream_t *ep_rx = &p_midi->ep_stream.rx; + + uint32_t n_words = 0; + for (uint32_t i = 0; i < max_words; i++) { + if (tu_edpt_stream_read_available(ep_rx) >= 4) { + tu_edpt_stream_read(ep_rx, (uint8_t *) &words[i], 4); + n_words++; + } else { + break; + } + } + + return n_words; +} + +uint32_t tuh_midi2_ump_write(uint8_t idx, const uint32_t* words, uint32_t count) { + TU_VERIFY(idx < CFG_TUH_MIDI2 && words && count); + + midih2_interface_t *p_midi = &_midi2_host[idx]; + tu_edpt_stream_t *ep_tx = &p_midi->ep_stream.tx; + + uint32_t n_words = 0; + for (uint32_t i = 0; i < count; i++) { + if (tu_edpt_stream_write_available(ep_tx) >= 4) { + tu_edpt_stream_write(ep_tx, (const uint8_t *) &words[i], 4); + n_words++; + } else { + break; + } + } + + return n_words; +} + +uint32_t tuh_midi2_write_flush(uint8_t idx) { + TU_VERIFY(idx < CFG_TUH_MIDI2); + + midih2_interface_t *p_midi = &_midi2_host[idx]; + tu_edpt_stream_t *ep_tx = &p_midi->ep_stream.tx; + + return tu_edpt_stream_write_xfer(ep_tx); +} + +#endif // CFG_TUH_ENABLED && CFG_TUH_MIDI2 diff --git a/src/class/midi/midi2_host.h b/src/class/midi/midi2_host.h new file mode 100644 index 000000000..d2de55270 --- /dev/null +++ b/src/class/midi/midi2_host.h @@ -0,0 +1,99 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Saulo Verissimo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * This file is part of the TinyUSB stack. + */ + +#ifndef TUSB_MIDI2_HOST_H_ +#define TUSB_MIDI2_HOST_H_ + +#include "class/audio/audio.h" +#include "midi.h" + +#ifdef __cplusplus +extern "C" { +#endif + +//--------------------------------------------------------------------+ +// Callback Type Definitions +//--------------------------------------------------------------------+ + +typedef struct { + uint8_t protocol_version; // 0 = MIDI 1.0 only, 1 = MIDI 2.0 + uint8_t bcdMSC_hi, bcdMSC_lo; // MIDI version from descriptor + uint8_t rx_cable_count; // For both alt settings (same for Alt 0 and Alt 1) + uint8_t tx_cable_count; +} tuh_midi2_descriptor_cb_t; + +typedef struct { + uint8_t daddr; + uint8_t bInterfaceNumber; + uint8_t protocol_version; // 0 = MIDI 1.0, 1 = MIDI 2.0 + uint8_t alt_setting_active; // 0 or 1 + uint8_t rx_cable_count; + uint8_t tx_cable_count; +} tuh_midi2_mount_cb_t; + +//--------------------------------------------------------------------+ +// Application Callback API (weak, optional) +//--------------------------------------------------------------------+ + +void tuh_midi2_descriptor_cb(uint8_t idx, const tuh_midi2_descriptor_cb_t *desc_cb_data); +void tuh_midi2_mount_cb(uint8_t idx, const tuh_midi2_mount_cb_t *mount_cb_data); +void tuh_midi2_rx_cb(uint8_t idx, uint32_t xferred_bytes); +void tuh_midi2_tx_cb(uint8_t idx, uint32_t xferred_bytes); +void tuh_midi2_umount_cb(uint8_t idx); + +//--------------------------------------------------------------------+ +// Application API - Query +//--------------------------------------------------------------------+ + +bool tuh_midi2_mounted(uint8_t idx); +uint8_t tuh_midi2_get_protocol_version(uint8_t idx); +uint8_t tuh_midi2_get_alt_setting_active(uint8_t idx); +uint8_t tuh_midi2_get_cable_count(uint8_t idx); + +//--------------------------------------------------------------------+ +// Application API - I/O +//--------------------------------------------------------------------+ + +uint32_t tuh_midi2_ump_read(uint8_t idx, uint32_t* words, uint32_t max_words); +uint32_t tuh_midi2_ump_write(uint8_t idx, const uint32_t* words, uint32_t count); +uint32_t tuh_midi2_write_flush(uint8_t idx); + +//--------------------------------------------------------------------+ +// Internal Class Driver API +//--------------------------------------------------------------------+ + +bool midih2_init(void); +bool midih2_deinit(void); +bool midih2_set_config(uint8_t dev_addr, uint8_t itf_num); +void midih2_close(uint8_t dev_addr); +uint16_t midih2_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface_t *desc_itf, uint16_t max_len); +bool midih2_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/host/usbh.c b/src/host/usbh.c index 490724b02..2e3c93c5e 100644 --- a/src/host/usbh.c +++ b/src/host/usbh.c @@ -278,6 +278,18 @@ static usbh_class_driver_t const usbh_class_drivers[] = { }, #endif + #if CFG_TUH_MIDI2 + { + .name = DRIVER_NAME("MIDI2"), + .init = midih2_init, + .deinit = midih2_deinit, + .open = midih2_open, + .set_config = midih2_set_config, + .xfer_cb = midih2_xfer_cb, + .close = midih2_close + }, + #endif + #if CFG_TUH_HUB { .name = DRIVER_NAME("HUB"), -- cgit v1.3.1 From d754c0697cbd29874c56254bd97561349fb9a229 Mon Sep 17 00:00:00 2001 From: hathach Date: Fri, 29 May 2026 16:16:21 +0700 Subject: Implement asynchronous control transfer queuing for USB host stack - Added a pending FIFO queue for asynchronous control transfers when the active slot is busy. - Introduced `control_xfer_dispatch_pending` to handle queued transfers on slot availability. - Improved synchronization for blocking and non-blocking transfer modes, preventing deadlocks in RTOS. - Refactored and renamed related functions for clarity and consistency. - Enhanced error handling and callback invocation for failed or stale transfers. --- src/common/tusb_types.h | 1 + src/host/usbh.c | 332 ++++++++++++++++++++++++++++++++++++----------- src/osal/osal_freertos.h | 8 +- 3 files changed, 263 insertions(+), 78 deletions(-) (limited to 'src/host') diff --git a/src/common/tusb_types.h b/src/common/tusb_types.h index 959fc129a..cb06b89bb 100644 --- a/src/common/tusb_types.h +++ b/src/common/tusb_types.h @@ -282,6 +282,7 @@ typedef enum { XFER_RESULT_FAILED, XFER_RESULT_STALLED, XFER_RESULT_TIMEOUT, + XFER_RESULT_ABORTED, XFER_RESULT_INVALID } xfer_result_t; diff --git a/src/host/usbh.c b/src/host/usbh.c index 2e3c93c5e..05e03245f 100644 --- a/src/host/usbh.c +++ b/src/host/usbh.c @@ -40,6 +40,10 @@ #define CFG_TUH_TASK_QUEUE_SZ 16 #endif +#ifndef CFG_TUH_CONTROL_PENDING_QUEUE_SZ + #define CFG_TUH_CONTROL_PENDING_QUEUE_SZ 4 +#endif + #ifndef CFG_TUH_INTERFACE_MAX #define CFG_TUH_INTERFACE_MAX 8 #endif @@ -175,11 +179,11 @@ static OSAL_SPINLOCK_DEF(_usbh_spin, usbh_int_set); OSAL_QUEUE_DEF(usbh_int_set, _usbh_qdef, CFG_TUH_TASK_QUEUE_SZ, hcd_event_t); static osal_queue_t _usbh_q; - #if CFG_TUH_HUB +#if CFG_TUH_HUB // Deferred attachment queue, only needed when using hub OSAL_QUEUE_DEF(usbh_int_set, _usbh_daqdef, CFG_TUH_HUB, hcd_event_t); static osal_queue_t _usbh_daq; - #endif +#endif // Control transfers: since most controllers do not support multiple control transfers // on multiple devices concurrently and control transfers are not used much except for @@ -189,9 +193,9 @@ typedef struct { tuh_xfer_cb_t complete_cb; uintptr_t user_data; + volatile uint16_t actual_len; volatile uint8_t stage; uint8_t daddr; - volatile uint16_t actual_len; uint8_t failed_count; } usbh_ctrl_xfer_info_t; @@ -202,17 +206,32 @@ typedef struct { } usbh_call_after_t; typedef struct { - uint8_t controller_id; // controller ID + tusb_control_request_t setup; + uint8_t* buffer; + tuh_xfer_cb_t complete_cb; + uintptr_t user_data; + uint8_t daddr; + uint8_t daddr_gen; +} usbh_pending_ctrl_t; + +// FIFO for pending async control transfers since we only execute 1 control transfer at a time +TU_FIFO_DEF(_usbh_pending_ctrl_q, CFG_TUH_CONTROL_PENDING_QUEUE_SZ * sizeof(usbh_pending_ctrl_t), false); + +typedef struct { uint8_t enumerating_daddr; // device address of the device being enumerated uint8_t attach_debouncing_bm; // bitmask for roothub port attach debouncing tuh_bus_info_t dev0_bus; // bus info for dev0 in enumeration usbh_ctrl_xfer_info_t ctrl_xfer_info; // control transfer usbh_call_after_t call_after; + // Per-daddr generation counter — bumped on usbh_device_close() to identify stale pending control transfer + uint8_t daddr_gen[TOTAL_DEVICES + 1]; +#if CFG_TUSB_OS_HAS_SCHEDULER + osal_task_handle_t task_hdl; // host task handle, lazy-captured on first tuh_task_ext() +#endif } usbh_data_t; -static usbh_data_t _usbh_data = { - .controller_id = TUSB_INDEX_INVALID_8, -}; +static uint8_t _usbh_controller_id = TUSB_INDEX_INVALID_8; +static usbh_data_t _usbh_data; typedef struct { TUH_EPBUF_TYPE_DEF(tusb_control_request_t, request); @@ -346,8 +365,11 @@ static void enum_new_device(hcd_event_t* event); static void enum_delay_async(uintptr_t state); static void process_remove_event(hcd_event_t *event); static void remove_device_tree(uint8_t rhport, uint8_t hub_addr, uint8_t hub_port); + static bool usbh_edpt_control_open(uint8_t dev_addr, uint8_t max_packet_size); static bool usbh_control_xfer_cb (uint8_t daddr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes); +static void control_xfer_dispatch_pending(void); +static void control_xfer_complete(uint8_t daddr, xfer_result_t result); TU_ATTR_ALWAYS_INLINE static inline usbh_device_t* get_device(uint8_t dev_addr) { TU_VERIFY(dev_addr > 0 && dev_addr <= TOTAL_DEVICES, NULL); @@ -364,7 +386,7 @@ TU_ATTR_ALWAYS_INLINE static inline bool queue_event(hcd_event_t const * event, return true; } -TU_ATTR_ALWAYS_INLINE static inline void _control_set_xfer_stage(uint8_t stage) { +TU_ATTR_ALWAYS_INLINE static inline void control_xfer_set_stage(uint8_t stage) { if (_usbh_data.ctrl_xfer_info.stage != stage) { (void) osal_mutex_lock(_usbh_mutex, OSAL_TIMEOUT_WAIT_FOREVER); _usbh_data.ctrl_xfer_info.stage = stage; @@ -372,15 +394,6 @@ TU_ATTR_ALWAYS_INLINE static inline void _control_set_xfer_stage(uint8_t stage) } } -TU_ATTR_ALWAYS_INLINE static inline bool usbh_setup_send(uint8_t daddr, const uint8_t setup_packet[8]) { - const uint8_t rhport = usbh_get_rhport(daddr); - const bool ret = hcd_setup_send(rhport, daddr, setup_packet); - if (!ret) { - _control_set_xfer_stage(CONTROL_STAGE_IDLE); - } - return ret; -} - bool usbh_defer_func_ms_async(uint32_t ms, tusb_defer_func_t func, uintptr_t param) { TU_ASSERT(_usbh_data.call_after.func == NULL); TU_LOG_USBH("USBH schedule function after %u ms\r\n", (unsigned int)ms); @@ -394,9 +407,16 @@ bool usbh_defer_func_ms_async(uint32_t ms, tusb_defer_func_t func, uintptr_t par TU_ATTR_ALWAYS_INLINE static inline void usbh_device_close(uint8_t rhport, uint8_t daddr) { hcd_device_close(rhport, daddr); - // abort any ongoing control transfer - if (daddr == _usbh_data.ctrl_xfer_info.daddr) { - _control_set_xfer_stage(CONTROL_STAGE_IDLE); + // Bump the generation under the mutex so a concurrent producer in + // tuh_control_xfer stamps a value that is strictly monotonic w.r.t. close. + (void) osal_mutex_lock(_usbh_mutex, OSAL_TIMEOUT_WAIT_FOREVER); + _usbh_data.daddr_gen[daddr]++; + (void) osal_mutex_unlock(_usbh_mutex); + + // If this device has in-flight control xfer, complete as FAILED + usbh_ctrl_xfer_info_t* ctrl_info = &_usbh_data.ctrl_xfer_info; + if (daddr == ctrl_info->daddr && ctrl_info->stage != CONTROL_STAGE_IDLE) { + control_xfer_complete(daddr, XFER_RESULT_FAILED); } // invalidate if enumerating @@ -458,7 +478,7 @@ tusb_speed_t tuh_speed_get(uint8_t daddr) { } bool tuh_rhport_is_active(uint8_t rhport) { - return _usbh_data.controller_id == rhport; + return _usbh_controller_id == rhport; } bool tuh_rhport_reset_bus(uint8_t rhport, bool active) { @@ -485,7 +505,7 @@ static void clear_device(usbh_device_t* dev) { } bool tuh_inited(void) { - return _usbh_data.controller_id != TUSB_INDEX_INVALID_8; + return _usbh_controller_id != TUSB_INDEX_INVALID_8; } bool tuh_rhport_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) { @@ -547,7 +567,7 @@ bool tuh_rhport_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) { tu_memclr(_usbh_devices, sizeof(_usbh_devices)); tu_memclr(&_usbh_data, sizeof(_usbh_data)); - _usbh_data.controller_id = TUSB_INDEX_INVALID_8; + _usbh_controller_id = TUSB_INDEX_INVALID_8; _usbh_data.enumerating_daddr = TUSB_INDEX_INVALID_8; for (uint8_t i = 0; i < TOTAL_DEVICES; i++) { @@ -565,7 +585,7 @@ bool tuh_rhport_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) { } // Init host controller - _usbh_data.controller_id = rhport; + _usbh_controller_id = rhport; TU_ASSERT(hcd_init(rhport, rh_init)); hcd_int_enable(rhport); @@ -580,7 +600,7 @@ bool tuh_deinit(uint8_t rhport) { // deinit host controller hcd_int_disable(rhport); TU_ASSERT(hcd_deinit(rhport)); - _usbh_data.controller_id = TUSB_INDEX_INVALID_8; + _usbh_controller_id = TUSB_INDEX_INVALID_8; // remove all devices on this rhport (hub_addr = 0, hub_port = 0) remove_device_tree(rhport, 0, 0); @@ -604,6 +624,25 @@ bool tuh_deinit(uint8_t rhport) { _usbh_daq = NULL; #endif + // Fire FAILED cb for any queued async control xfer so callers aren't stranded. + usbh_pending_ctrl_t pending; + while (tu_fifo_read_n(&_usbh_pending_ctrl_q, &pending, sizeof(pending)) == sizeof(pending)) { + if (pending.complete_cb) { + tuh_xfer_t x = { + .daddr = pending.daddr, + .ep_addr = 0, + .result = XFER_RESULT_FAILED, + .actual_len = 0, + .setup = &pending.setup, + .buffer = pending.buffer, + .complete_cb = pending.complete_cb, + .user_data = pending.user_data, + }; + pending.complete_cb(&x); + } + } + tu_fifo_clear(&_usbh_pending_ctrl_q); + #if OSAL_MUTEX_REQUIRED // TODO make sure there is no task waiting on this mutex osal_mutex_delete(_usbh_mutex); @@ -629,6 +668,12 @@ bool tuh_task_event_ready(void) { } #endif + // Pending control xfer waiting for an idle slot + if (_usbh_data.ctrl_xfer_info.stage == CONTROL_STAGE_IDLE && + !tu_fifo_empty(&_usbh_pending_ctrl_q)) { + return true; + } + if (_usbh_data.call_after.func) { int32_t remain_ms = (int32_t)(_usbh_data.call_after.at_ms - tusb_time_millis_api()); if (remain_ms <= 0) { @@ -663,6 +708,13 @@ void tuh_task_ext(uint32_t timeout_ms, bool in_isr) { (void) in_isr; // not implemented yet +#if CFG_TUSB_OS_HAS_SCHEDULER + // Save task handle on 1st run + if (_usbh_data.task_hdl == NULL) { + _usbh_data.task_hdl = osal_task_get_current_handle(); + } +#endif + // Loop until there are no more events in the queue or CFG_TUH_TASK_EVENTS_PER_RUN is reached for (unsigned epr = 0;; epr++) { #if CFG_TUH_TASK_EVENTS_PER_RUN > 0 @@ -695,6 +747,16 @@ void tuh_task_ext(uint32_t timeout_ms, bool in_isr) { } } + // Drain pending async control xfers. Slot transitions and dispatch are + // decoupled: completion / abort / device_close set stage = IDLE via + // control_xfer_set_stage() and the actual FIFO drain happens here in the + // event loop. The check is a fast non-mutex sanity gate; the dispatcher + // itself re-checks under the mutex. + if (_usbh_data.ctrl_xfer_info.stage == CONTROL_STAGE_IDLE && + !tu_fifo_empty(&_usbh_pending_ctrl_q)) { + control_xfer_dispatch_pending(); + } + hcd_event_t event; #if CFG_TUH_HUB @@ -818,73 +880,179 @@ void tuh_task_ext(uint32_t timeout_ms, bool in_isr) { // Control transfer //--------------------------------------------------------------------+ -static void _control_blocking_complete_cb(tuh_xfer_t* xfer) { - // update result - *((xfer_result_t*) xfer->user_data) = xfer->result; +// Carries both fields the sync waiter cares about — capturing from xfer_temp +// (snapshot taken before release_slot resets ctrl_info for the next pending +// entry) so the waiter sees this xfer's data, not the next dispatched one's. +typedef struct { + volatile xfer_result_t result; + volatile uint32_t actual_len; +} control_xfer_sync_param_t; + +static void control_xfer_sync_complete(tuh_xfer_t* xfer) { + control_xfer_sync_param_t* s = (control_xfer_sync_param_t*) xfer->user_data; + s->actual_len = xfer->actual_len; + s->result = xfer->result; } // TODO timeout_ms is not supported yet bool tuh_control_xfer (tuh_xfer_t* xfer) { - TU_VERIFY(xfer->ep_addr == 0 && xfer->setup); // EP0 with setup packet const uint8_t daddr = xfer->daddr; - TU_VERIFY(tuh_connected(daddr)); - + TU_VERIFY(daddr <= TOTAL_DEVICES && xfer->ep_addr == 0 && xfer->setup); // EP0 with setup packet usbh_ctrl_xfer_info_t* ctrl_info = &_usbh_data.ctrl_xfer_info; - TU_VERIFY(ctrl_info->stage == CONTROL_STAGE_IDLE); // pre-check to help reducing mutex lock - (void) osal_mutex_lock(_usbh_mutex, OSAL_TIMEOUT_WAIT_FOREVER); - bool const is_idle = (ctrl_info->stage == CONTROL_STAGE_IDLE); - if (is_idle) { - ctrl_info->stage = CONTROL_STAGE_SETUP; - ctrl_info->daddr = daddr; - ctrl_info->actual_len = 0; - ctrl_info->failed_count = 0; - - ctrl_info->buffer = xfer->buffer; - ctrl_info->complete_cb = xfer->complete_cb; - ctrl_info->user_data = xfer->user_data; - _usbh_epbuf.request = (*xfer->setup); - } - (void) osal_mutex_unlock(_usbh_mutex); +#if CFG_TUSB_OS_HAS_SCHEDULER + // Sync (complete_cb == NULL) from a host-stack callback is forbidden on + // RTOS targets — the event-loop driver can't block on its own pending xfer + // (deadlock if other control xfers are queued behind). Use async with a + // chained cb instead. OS_NONE / OS_PICO are exempt: they have a single + // execution context and the recursive-drive path is the only way to wait. + TU_ASSERT(!(xfer->complete_cb == NULL && + osal_task_get_current_handle() == _usbh_data.task_hdl)); +#endif - TU_VERIFY(is_idle); + // Slot is single-threaded — when busy, sync callers block until it frees + // (blocking semantics require the result); async callers get queued in the + // pending FIFO and submitted by control_xfer_complete() when the slot + // drains. The test-and-{claim|enqueue} is one critical section so a slot + // that becomes IDLE between the check and the enqueue can't strand an async + // request in a queue nothing else drains. + const bool is_nonblocking = (xfer->complete_cb != NULL); + while (true) { + TU_VERIFY(tuh_connected(daddr)); + bool claimed = false; + bool is_queued = false; + (void) osal_mutex_lock(_usbh_mutex, OSAL_TIMEOUT_WAIT_FOREVER); + if (ctrl_info->stage == CONTROL_STAGE_IDLE) { + ctrl_info->stage = CONTROL_STAGE_SETUP; + ctrl_info->daddr = daddr; + ctrl_info->actual_len = 0; + ctrl_info->failed_count = 0; + + ctrl_info->buffer = xfer->buffer; + ctrl_info->complete_cb = xfer->complete_cb; + ctrl_info->user_data = xfer->user_data; + _usbh_epbuf.request = (*xfer->setup); + claimed = true; + } else if (is_nonblocking) { + // Async + busy: queue the transfer. + const usbh_pending_ctrl_t entry = { + .setup = *xfer->setup, + .buffer = xfer->buffer, + .complete_cb = xfer->complete_cb, + .user_data = xfer->user_data, + .daddr = daddr, + .daddr_gen = _usbh_data.daddr_gen[daddr] + }; + is_queued = tu_fifo_write_n(&_usbh_pending_ctrl_q, &entry, sizeof(entry)) == sizeof(entry); + } + + (void) osal_mutex_unlock(_usbh_mutex); + + if (claimed) { + break; + } + + if (is_nonblocking) { + return is_queued; + } + + // - OS_HAS_SCHEDULER: delay 1 ms + // - Otherwise: single execution context; drive the loop ourselves to progress the in-flight transfer. +#if CFG_TUSB_OS_HAS_SCHEDULER + osal_task_delay(1); +#else + tuh_task_ext(0, false); +#endif + } TU_LOG_USBH("[%u:%u] %s: ", usbh_get_rhport(daddr), daddr, (xfer->setup->bmRequestType_bit.type == TUSB_REQ_TYPE_STANDARD && xfer->setup->bRequest <= TUSB_REQ_SYNCH_FRAME) ? tu_str_std_request[xfer->setup->bRequest] : "Class Request"); TU_LOG_BUF_USBH(xfer->setup, 8); - if (xfer->complete_cb != NULL) { - TU_ASSERT(usbh_setup_send(daddr, (uint8_t const *) &_usbh_epbuf.request)); - }else { - // blocking if complete callback is not provided - // change callback to internal blocking, and result as user argument - volatile xfer_result_t result = XFER_RESULT_INVALID; - - // use user_data to point to xfer_result_t - ctrl_info->user_data = (uintptr_t) &result; - ctrl_info->complete_cb = _control_blocking_complete_cb; + // Sync: wire control_xfer_sync_complete BEFORE submit so a fast completion + // event has the cb in place. control_xfer_complete() captures both result + // and actual_len through this cb before release_slot overwrites ctrl_info. + volatile control_xfer_sync_param_t sync_state; + if (!is_nonblocking) { + sync_state.result = XFER_RESULT_INVALID; + sync_state.actual_len = 0; + ctrl_info->user_data = (uintptr_t) &sync_state; + ctrl_info->complete_cb = control_xfer_sync_complete; + } - TU_ASSERT(usbh_setup_send(daddr, (uint8_t const *) &_usbh_epbuf.request)); + if (!hcd_setup_send(usbh_get_rhport(daddr), daddr, (uint8_t const *) &_usbh_epbuf.request)) { + control_xfer_set_stage(CONTROL_STAGE_IDLE); + return false; + } - while (result == XFER_RESULT_INVALID) { - // Note: this can be called within an callback ie. part of tuh_task() - // therefore even with RTOS tuh_task_ext() still need to be invoked + if (!is_nonblocking) { + // No tuh_connected() escape needed: usbh_device_close() routes through + // control_xfer_complete(daddr, FAILED) on disconnect, which fires + // sync_complete and unblocks this poll. + while (sync_state.result == XFER_RESULT_INVALID) { +#if CFG_TUSB_OS_HAS_SCHEDULER + osal_task_delay(1); +#else tuh_task_ext(0, false); - // TODO probably some timeout to prevent hanged +#endif } - // update transfer result, user_data is expected to point to xfer_result_t + // Forward to caller (xfer->user_data, if set, is a xfer_result_t pointer). if (xfer->user_data != 0) { - *((xfer_result_t*) xfer->user_data) = result; + *((xfer_result_t*) xfer->user_data) = sync_state.result; } - xfer->result = result; - xfer->actual_len = ctrl_info->actual_len; + xfer->result = sync_state.result; + xfer->actual_len = sync_state.actual_len; } return true; } -static void _control_xfer_complete(uint8_t daddr, xfer_result_t result) { +// Start control transfer from pending fifo +static void control_xfer_dispatch_pending(void) { + usbh_ctrl_xfer_info_t* ctrl_info = &_usbh_data.ctrl_xfer_info; + + while (true) { + usbh_pending_ctrl_t xfer; + bool has_xfer = false; + + (void) osal_mutex_lock(_usbh_mutex, OSAL_TIMEOUT_WAIT_FOREVER); + if (ctrl_info->stage == CONTROL_STAGE_IDLE && + tu_fifo_read_n(&_usbh_pending_ctrl_q, &xfer, sizeof(xfer)) == sizeof(xfer)) { + ctrl_info->stage = CONTROL_STAGE_SETUP; + ctrl_info->daddr = xfer.daddr; + ctrl_info->actual_len = 0; + ctrl_info->failed_count = 0; + ctrl_info->buffer = xfer.buffer; + ctrl_info->complete_cb = xfer.complete_cb; + ctrl_info->user_data = xfer.user_data; + _usbh_epbuf.request = xfer.setup; + has_xfer = true; + } + (void) osal_mutex_unlock(_usbh_mutex); + + if (!has_xfer) { + return; // nothing to do + } + + // mismatched daddr_gen means pending transfer is stale due to the device got disconnected while in the FIFO + // Note: the address can be re-allocated to another device at this point. + if (xfer.daddr_gen == _usbh_data.daddr_gen[xfer.daddr]) { + TU_LOG_USBH("[%u:%u] %s: ", usbh_get_rhport(xfer.daddr), xfer.daddr, + (xfer.setup.bmRequestType_bit.type == TUSB_REQ_TYPE_STANDARD && xfer.setup.bRequest <= TUSB_REQ_SYNCH_FRAME) ? + tu_str_std_request[xfer.setup.bRequest] : "Class Request"); + TU_LOG_BUF_USBH(&xfer.setup, 8); + if (hcd_setup_send(usbh_get_rhport(xfer.daddr), xfer.daddr, (uint8_t const *) &_usbh_epbuf.request)) { + return; // transfer kicked-off, we are done + } + } + + // complete callback as FAILED and continue with next pending xfer + control_xfer_complete(xfer.daddr, XFER_RESULT_FAILED); + } +} + +static void control_xfer_complete(uint8_t daddr, xfer_result_t result) { TU_LOG_USBH("\r\n"); usbh_ctrl_xfer_info_t* ctrl_info = &_usbh_data.ctrl_xfer_info; @@ -901,7 +1069,8 @@ static void _control_xfer_complete(uint8_t daddr, xfer_result_t result) { .user_data = ctrl_info->user_data }; - _control_set_xfer_stage(CONTROL_STAGE_IDLE); + // set to IDLE before callback since cb can invoke another transfer + control_xfer_set_stage(CONTROL_STAGE_IDLE); if (xfer_temp.complete_cb != NULL) { xfer_temp.complete_cb(&xfer_temp); @@ -915,11 +1084,17 @@ static bool usbh_control_xfer_cb (uint8_t daddr, uint8_t ep_addr, xfer_result_t tusb_control_request_t const * request = &_usbh_epbuf.request; usbh_ctrl_xfer_info_t* ctrl_info = &_usbh_data.ctrl_xfer_info; + // Drop stale completions: slot already released (abort/close fired its cb) + // or now owns a different device's xfer (a pending entry was dispatched). + if (ctrl_info->stage == CONTROL_STAGE_IDLE || ctrl_info->daddr != daddr) { + return true; + } + switch (result) { case XFER_RESULT_STALLED: TU_LOG_USBH("[%u:%u] Control STALLED, xferred_bytes = %" PRIu32 "\r\n", rhport, daddr, xferred_bytes); TU_LOG_BUF_USBH(request, 8); - _control_xfer_complete(daddr, result); + control_xfer_complete(daddr, result); break; case XFER_RESULT_FAILED: @@ -931,11 +1106,14 @@ static bool usbh_control_xfer_cb (uint8_t daddr, uint8_t ep_addr, xfer_result_t ctrl_info->actual_len = 0; // reset actual_len (void) osal_mutex_unlock(_usbh_mutex); - TU_ASSERT(usbh_setup_send(daddr, (uint8_t const *) request)); + if (!hcd_setup_send(rhport, daddr, (uint8_t const *) request)) { + control_xfer_complete(daddr, XFER_RESULT_FAILED); + return false; + } } else { TU_LOG_USBH("[%u:%u] Control FAILED, xferred_bytes = %" PRIu32 "\r\n", rhport, daddr, xferred_bytes); TU_LOG_BUF_USBH(request, 8); - _control_xfer_complete(daddr, result); + control_xfer_complete(daddr, result); } break; @@ -944,7 +1122,7 @@ static bool usbh_control_xfer_cb (uint8_t daddr, uint8_t ep_addr, xfer_result_t case CONTROL_STAGE_SETUP: if (request->wLength > 0) { // DATA stage: initial data toggle is always 1 - _control_set_xfer_stage(CONTROL_STAGE_DATA); + control_xfer_set_stage(CONTROL_STAGE_DATA); const uint8_t ep_data = tu_edpt_addr(0, request->bmRequestType_bit.direction); TU_ASSERT(hcd_edpt_xfer(rhport, daddr, ep_data, ctrl_info->buffer, request->wLength)); return true; @@ -959,7 +1137,7 @@ static bool usbh_control_xfer_cb (uint8_t daddr, uint8_t ep_addr, xfer_result_t ctrl_info->actual_len = (uint16_t) xferred_bytes; // ACK stage: toggle is always 1 - _control_set_xfer_stage(CONTROL_STAGE_ACK); + control_xfer_set_stage(CONTROL_STAGE_ACK); const uint8_t ep_status = tu_edpt_addr(0, 1 - request->bmRequestType_bit.direction); TU_ASSERT(hcd_edpt_xfer(rhport, daddr, ep_status, NULL, 0)); break; @@ -976,7 +1154,7 @@ static bool usbh_control_xfer_cb (uint8_t daddr, uint8_t ep_addr, xfer_result_t } } - _control_xfer_complete(daddr, result); + control_xfer_complete(daddr, result); break; } @@ -1023,7 +1201,7 @@ bool tuh_edpt_abort_xfer(uint8_t daddr, uint8_t ep_addr) { const usbh_ctrl_xfer_info_t* ctrl_info = &_usbh_data.ctrl_xfer_info; TU_VERIFY(daddr == ctrl_info->daddr && ctrl_info->stage != CONTROL_STAGE_IDLE); hcd_edpt_abort_xfer(rhport, daddr, ep_addr); - _control_set_xfer_stage(CONTROL_STAGE_IDLE); // reset control transfer state to idle + control_xfer_complete(daddr, XFER_RESULT_ABORTED); } else { usbh_device_t* dev = get_device(daddr); TU_VERIFY(dev); @@ -1055,9 +1233,9 @@ uint8_t *usbh_get_enum_buf(void) { void usbh_int_set(bool enabled) { // TODO all host controller if multiple are used since they shared the same event queue if (enabled) { - hcd_int_enable(_usbh_data.controller_id); + hcd_int_enable(_usbh_controller_id); } else { - hcd_int_disable(_usbh_data.controller_id); + hcd_int_disable(_usbh_controller_id); } } diff --git a/src/osal/osal_freertos.h b/src/osal/osal_freertos.h index 9b12b5c0e..2f36aa9e8 100644 --- a/src/osal/osal_freertos.h +++ b/src/osal/osal_freertos.h @@ -85,7 +85,13 @@ typedef struct { //--------------------------------------------------------------------+ typedef TaskHandle_t osal_task_handle_t; -// Requires INCLUDE_xTaskGetCurrentTaskHandle == 1 in FreeRTOSConfig.h. +// Requires INCLUDE_xTaskGetCurrentTaskHandle == 1 in FreeRTOSConfig.h. FreeRTOS +// also exposes the symbol when configUSE_MUTEXES == 1, so accept either. +#if !defined(INCLUDE_xTaskGetCurrentTaskHandle) || (INCLUDE_xTaskGetCurrentTaskHandle == 0) + #if !defined(configUSE_MUTEXES) || (configUSE_MUTEXES == 0) + #error "TinyUSB host stack requires INCLUDE_xTaskGetCurrentTaskHandle or configUSE_MUTEXES to be enabled in FreeRTOSConfig.h" + #endif +#endif TU_ATTR_ALWAYS_INLINE static inline osal_task_handle_t osal_task_get_current_handle(void) { return xTaskGetCurrentTaskHandle(); } -- cgit v1.3.1 From 17185428df755d7229407e6ac87c124e522877dc Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 1 Jun 2026 10:58:36 +0700 Subject: CFG_TUH_CONTROL_PENDING_QUEUE_SZ defefault to 4 if hub is eanbled, 2 if not --- src/host/usbh.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src/host') diff --git a/src/host/usbh.c b/src/host/usbh.c index 05e03245f..9d159985e 100644 --- a/src/host/usbh.c +++ b/src/host/usbh.c @@ -41,7 +41,11 @@ #endif #ifndef CFG_TUH_CONTROL_PENDING_QUEUE_SZ - #define CFG_TUH_CONTROL_PENDING_QUEUE_SZ 4 + #if CFG_TUH_HUB + #define CFG_TUH_CONTROL_PENDING_QUEUE_SZ 4 + #else + #define CFG_TUH_CONTROL_PENDING_QUEUE_SZ 2 + #endif #endif #ifndef CFG_TUH_INTERFACE_MAX -- cgit v1.3.1