From 439015377976bb51f6d9ae6c3101b72385ecfb1a Mon Sep 17 00:00:00 2001 From: "Zhang, Zhenjiang" Date: Thu, 16 Jul 2026 15:03:31 +0800 Subject: feat(audio): add USB Audio Host (UAC 1.0) support Add TinyUSB Host Audio class driver supporting UAC 1.0 devices. Features: - Support multiple Audio Streaming (AS) interfaces with independent format storage - Support both IN (Microphone) and OUT (Speaker) endpoints - Per-AS interface format info: channels, sample rate, bit resolution - Support Feature Unit volume control - Support sampling frequency get/set - Add host/audio_host example for STM32F407 discovery board - Support mono-to-stereo conversion for loopback Changes: - Add src/class/audio/audio_host.c and audio_host.h - Register AUDIO driver in usbh.c - Add CFG_TUH_AUDIO macro in tusb_option.h - Add host/audio_host example with CMake and Makefile build support Tested with Jabra USB headset (stereo speaker + mono microphone) on STM32F407 disco. --- src/CMakeLists.txt | 1 + src/class/audio/audio_host.c | 809 +++++++++++++++++++++++++++++++++++++++++++ src/class/audio/audio_host.h | 235 +++++++++++++ src/host/usbh.c | 12 + src/tinyusb.mk | 1 + src/tusb.h | 4 + src/tusb_option.h | 4 + 7 files changed, 1066 insertions(+) create mode 100644 src/class/audio/audio_host.c create mode 100644 src/class/audio/audio_host.h (limited to 'src') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b3e05f60f..b5a5a0b3b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -27,6 +27,7 @@ function(tinyusb_sources_get OUTPUT_VAR) ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/host/usbh.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/host/hub.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/cdc/cdc_host.c + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/audio/audio_host.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/hid/hid_host.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/midi/midi_host.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/midi/midi2_host.c diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c new file mode 100644 index 000000000..9414c1ab4 --- /dev/null +++ b/src/class/audio/audio_host.c @@ -0,0 +1,809 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2025 TinyUSB contributors + * + * 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_AUDIO) + +#include "host/usbh.h" +#include "host/usbh_pvt.h" +#include "audio_host.h" + +// Level where CFG_TUSB_DEBUG must be at least for this driver is logged +#ifndef CFG_TUH_AUDIO_LOG_LEVEL + #define CFG_TUH_AUDIO_LOG_LEVEL CFG_TUH_LOG_LEVEL +#endif + +#define TU_LOG_DRV(...) TU_LOG(CFG_TUH_AUDIO_LOG_LEVEL, __VA_ARGS__) + +//--------------------------------------------------------------------+ +// Weak stubs: invoked if no strong implementation is available +//--------------------------------------------------------------------+ +TU_ATTR_WEAK void tuh_audio_descriptor_cb(uint8_t idx, const tuh_audio_descriptor_cb_t *desc_cb_data) { + (void) idx; + (void) desc_cb_data; +} + +TU_ATTR_WEAK void tuh_audio_mount_cb(uint8_t idx, const tuh_audio_mount_cb_t *mount_cb_data) { + (void) idx; + (void) mount_cb_data; +} + +TU_ATTR_WEAK void tuh_audio_umount_cb(uint8_t idx) { + (void) idx; +} + +TU_ATTR_WEAK void tuh_audio_rx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes) { + (void) idx; + (void) ep_addr; + (void) xferred_bytes; +} + +TU_ATTR_WEAK void tuh_audio_tx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes) { + (void) idx; + (void) ep_addr; + (void) xferred_bytes; +} + +//--------------------------------------------------------------------+ +// MACRO CONSTANT TYPEDEF +//--------------------------------------------------------------------+ + +// Per-AS interface internal storage +typedef struct { + uint8_t interface_num; + uint8_t alt_setting; + uint8_t ep_addr; + uint16_t ep_size; + uint8_t ep_dir; + + uint8_t format_type; + uint8_t num_channels; + uint8_t sub_frame_size; + uint8_t bit_resolution; + uint8_t sam_freq_type; + uint32_t sam_freq[CFG_TUH_AUDIO_MAX_SAM_FREQ]; + uint32_t sam_freq_lower; + uint32_t sam_freq_upper; +} audioh_as_t; + +typedef struct { + uint8_t daddr; + uint8_t bInterfaceNumber; // Audio Control interface number + uint8_t iInterface; + uint8_t itf_count; // number of interfaces (AC + AS) + + // Audio Streaming Interface + uint8_t as_interface_num; // Audio Streaming interface number + uint8_t alt_setting; // current alt setting + + // Terminal info (from Audio Control Interface) + uint16_t input_terminal_type; // wTerminalType of Input Terminal + uint8_t input_terminal_id; // bTerminalID of Input Terminal + uint8_t input_terminal_channels; // bNrChannels of Input Terminal + uint16_t output_terminal_type; // wTerminalType of Output Terminal + uint8_t output_terminal_id; // bTerminalID of Output Terminal + + // Feature Unit info + uint8_t feature_unit_id; // bUnitID of Feature Unit (0 = none) + uint8_t feature_unit_source_id; // bSourceID of Feature Unit + + // Isochronous IN endpoint + uint8_t ep_in; + uint16_t ep_in_size; + uint16_t ep_in_interval; + + // Isochronous OUT endpoint + uint8_t ep_out; + uint16_t ep_out_size; + uint16_t ep_out_interval; + + // Multiple AS interfaces support + uint8_t as_interfaces[CFG_TUH_AUDIO_MAX_AS]; + uint8_t as_alt_settings[CFG_TUH_AUDIO_MAX_AS]; + uint8_t as_count; + uint8_t as_set_idx; + + // Per-AS interface independent storage (new) + audioh_as_t as[CFG_TUH_AUDIO_MAX_AS]; + + bool mounted; +} audioh_interface_t; + +typedef struct { + TUH_EPBUF_DEF(epin, CFG_TUH_AUDIO_EPIN_BUFSIZE); + TUH_EPBUF_DEF(epout, CFG_TUH_AUDIO_EPOUT_BUFSIZE); +} audioh_epbuf_t; + +static audioh_interface_t _audioh_itf[CFG_TUH_AUDIO_MAX]; + +//--------------------------------------------------------------------+ +// Helper +//--------------------------------------------------------------------+ +TU_ATTR_ALWAYS_INLINE static inline uint8_t find_new_audio_index(void) { + for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { + if (_audioh_itf[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_AUDIO_MAX; idx++) { + const audioh_interface_t *p_audio = &_audioh_itf[idx]; + if ((p_audio->daddr == daddr) && + (ep_addr == p_audio->ep_in || ep_addr == p_audio->ep_out)) { + return idx; + } + } + return TUSB_INDEX_INVALID_8; +} + +//--------------------------------------------------------------------+ +// USBH API +//--------------------------------------------------------------------+ +bool audioh_init(void) { + tu_memclr(&_audioh_itf, sizeof(_audioh_itf)); + return true; +} + +bool audioh_deinit(void) { + return true; +} + +void audioh_close(uint8_t daddr) { + for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { + audioh_interface_t *p_audio = &_audioh_itf[idx]; + if (p_audio->daddr == daddr) { + TU_LOG_DRV(" AUDIO close addr = %u index = %u\r\n", daddr, idx); + tuh_audio_umount_cb(idx); + + p_audio->bInterfaceNumber = 0; + p_audio->as_interface_num = 0; + p_audio->alt_setting = 0; + p_audio->daddr = 0; + p_audio->mounted = false; + p_audio->ep_in = 0; + p_audio->ep_out = 0; + p_audio->as_count = 0; + p_audio->as_set_idx = 0; + tu_memclr(p_audio->as_interfaces, sizeof(p_audio->as_interfaces)); + tu_memclr(p_audio->as_alt_settings, sizeof(p_audio->as_alt_settings)); + tu_memclr(p_audio->as, sizeof(p_audio->as)); + } + } +} + +bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { + (void) result; + const uint8_t idx = get_idx_by_ep_addr(dev_addr, ep_addr); + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX); + audioh_interface_t *p_audio = &_audioh_itf[idx]; + + if (ep_addr == p_audio->ep_in) { + tuh_audio_rx_cb(idx, ep_addr, (uint16_t) xferred_bytes); + } else if (ep_addr == p_audio->ep_out) { + tuh_audio_tx_cb(idx, ep_addr, (uint16_t) xferred_bytes); + } + + return true; +} + +//--------------------------------------------------------------------+ +// Enumeration +//--------------------------------------------------------------------+ +uint16_t audioh_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); + TU_VERIFY(AUDIO_SUBCLASS_CONTROL == desc_itf->bInterfaceSubClass, 0); + + const uint8_t *desc_start = (const uint8_t *)desc_itf; + const uint8_t *p_desc = desc_start; + const uint8_t *desc_end = desc_start + max_len; + + const uint8_t idx = find_new_audio_index(); + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, 0); + audioh_interface_t *p_audio = &_audioh_itf[idx]; + p_audio->itf_count = 0; + + tuh_audio_descriptor_cb_t desc_cb = { 0 }; + + // Parse Audio Control Interface + TU_LOG_DRV("AUDIO opening AC Interface %u (addr = %u)\r\n", desc_itf->bInterfaceNumber, dev_addr); + p_audio->bInterfaceNumber = desc_itf->bInterfaceNumber; + p_audio->iInterface = desc_itf->iInterface; + p_audio->itf_count = 1; + desc_cb.desc_ac_interface = desc_itf; + desc_cb.ac_interface_num = desc_itf->bInterfaceNumber; + + // Parse Audio Control interface descriptors (Input Terminal, Output Terminal, Feature Unit, etc.) + p_desc = tu_desc_next(p_desc); + while (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) != TUSB_DESC_INTERFACE) { + if (tu_desc_type(p_desc) == TUSB_DESC_CS_INTERFACE) { + switch (tu_desc_subtype(p_desc)) { + case AUDIO10_CS_AC_INTERFACE_INPUT_TERMINAL: { + const audio10_desc_input_terminal_t *it = (const audio10_desc_input_terminal_t *)p_desc; + p_audio->input_terminal_type = tu_le16toh(it->wTerminalType); + p_audio->input_terminal_id = it->bTerminalID; + p_audio->input_terminal_channels = it->bNrChannels; + TU_LOG_DRV(" Input Terminal: ID=%u, Type=0x%04x, Channels=%u\r\n", + it->bTerminalID, tu_le16toh(it->wTerminalType), it->bNrChannels); + break; + } + case AUDIO10_CS_AC_INTERFACE_OUTPUT_TERMINAL: { + const audio10_desc_output_terminal_t *ot = (const audio10_desc_output_terminal_t *)p_desc; + p_audio->output_terminal_type = tu_le16toh(ot->wTerminalType); + p_audio->output_terminal_id = ot->bTerminalID; + TU_LOG_DRV(" Output Terminal: ID=%u, Type=0x%04x\r\n", + ot->bTerminalID, tu_le16toh(ot->wTerminalType)); + break; + } + case AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT: { + const uint8_t *fu = p_desc; + p_audio->feature_unit_id = fu[3]; // bUnitID + p_audio->feature_unit_source_id = fu[4]; // bSourceID + TU_LOG_DRV(" Feature Unit: ID=%u, SourceID=%u\r\n", fu[3], fu[4]); + break; + } + default: + break; + } + } + p_desc = tu_desc_next(p_desc); + } + + // Parse all remaining descriptors in this configuration looking for Audio Streaming interfaces + while (tu_desc_in_bounds(p_desc, desc_end)) { + if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) { + const tusb_desc_interface_t *itf = (const tusb_desc_interface_t *)p_desc; + if (itf->bInterfaceClass == TUSB_CLASS_AUDIO && itf->bInterfaceSubClass == AUDIO_SUBCLASS_STREAMING) { + // Found Audio Streaming Interface + TU_LOG_DRV(" Found AS Interface %u (alt = %u)\r\n", itf->bInterfaceNumber, itf->bAlternateSetting); + + if (itf->bAlternateSetting == 0) { + // Interface descriptor with alt setting 0 (no endpoints) + // Add to AS interfaces array + if (p_audio->as_count < CFG_TUH_AUDIO_MAX_AS) { + p_audio->as_interface_num = itf->bInterfaceNumber; + p_audio->as_interfaces[p_audio->as_count] = itf->bInterfaceNumber; + desc_cb.desc_as_interface = itf; + desc_cb.as_interface_num = itf->bInterfaceNumber; + // Create new AS entry for per-interface storage + p_audio->as[p_audio->as_count].interface_num = itf->bInterfaceNumber; + p_audio->as[p_audio->as_count].alt_setting = 0; + p_audio->as_count++; + } + } else if (itf->bNumEndpoints > 0) { + // Interface descriptor with alt setting > 0 (has endpoints) + // Find matching AS interface and set alt_setting + uint8_t as_entry_idx = CFG_TUH_AUDIO_MAX_AS; + for (uint8_t as_idx = 0; as_idx < p_audio->as_count; as_idx++) { + if (p_audio->as_interfaces[as_idx] == itf->bInterfaceNumber) { + p_audio->alt_setting = itf->bAlternateSetting; + p_audio->as_alt_settings[as_idx] = itf->bAlternateSetting; + desc_cb.alt_setting = itf->bAlternateSetting; + desc_cb.desc_as_interface_alt = itf; + break; + } + } + // Find or create AS entry for per-interface storage + for (uint8_t i = 0; i < p_audio->as_count; i++) { + if (p_audio->as[i].interface_num == itf->bInterfaceNumber) { + as_entry_idx = i; + break; + } + } + if (as_entry_idx >= CFG_TUH_AUDIO_MAX_AS && p_audio->as_count < CFG_TUH_AUDIO_MAX_AS) { + as_entry_idx = p_audio->as_count; + p_audio->as[as_entry_idx].interface_num = itf->bInterfaceNumber; + p_audio->as_count++; + } + if (as_entry_idx < CFG_TUH_AUDIO_MAX_AS) { + p_audio->as[as_entry_idx].alt_setting = itf->bAlternateSetting; + } + + // Parse the interface's descriptors + p_desc = tu_desc_next(p_desc); + // Temporary variables to hold format info until endpoint direction is known + uint8_t tmp_format_type = 0; + uint8_t tmp_num_channels = 0; + uint8_t tmp_sub_frame_size = 0; + uint8_t tmp_bit_resolution = 0; + uint8_t tmp_sam_freq_type = 0; + uint32_t tmp_sam_freq[CFG_TUH_AUDIO_MAX_SAM_FREQ] = {0}; + uint32_t tmp_sam_freq_lower = 0; + uint32_t tmp_sam_freq_upper = 0; + while (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) != TUSB_DESC_INTERFACE) { + switch (tu_desc_type(p_desc)) { + case TUSB_DESC_CS_INTERFACE: { + switch (tu_desc_subtype(p_desc)) { + case AUDIO10_CS_AS_INTERFACE_AS_GENERAL: { + TU_LOG_DRV(" AS General descriptor\r\n"); + desc_cb.desc_cs_as_general = p_desc; + break; + } + case AUDIO10_CS_AS_INTERFACE_FORMAT_TYPE: { + TU_LOG_DRV(" Format Type descriptor\r\n"); + desc_cb.desc_format_type = p_desc; + // Parse UAC 1.0 Format Type I descriptor fields into temporary variables + tmp_format_type = p_desc[3]; // bFormatType + tmp_num_channels = p_desc[4]; // bNrChannels + tmp_sub_frame_size = p_desc[5]; // bSubFrameSize + tmp_bit_resolution = p_desc[6]; // bBitResolution + + // Parse sampling frequencies + uint8_t bLength = p_desc[0]; + if (bLength >= 8) { + tmp_sam_freq_type = p_desc[7]; // bSamFreqType + if (tmp_sam_freq_type == 0) { + // Continuous range: tLowerSamFreq, tUpperSamFreq (3 bytes each) + if (bLength >= 14) { + tmp_sam_freq_lower = ((uint32_t)p_desc[8] | ((uint32_t)p_desc[9] << 8) | ((uint32_t)p_desc[10] << 16)); + tmp_sam_freq_upper = ((uint32_t)p_desc[11] | ((uint32_t)p_desc[12] << 8) | ((uint32_t)p_desc[13] << 16)); + } + } else { + // Discrete sampling frequencies + uint8_t max_freqs = tmp_sam_freq_type < CFG_TUH_AUDIO_MAX_SAM_FREQ ? tmp_sam_freq_type : CFG_TUH_AUDIO_MAX_SAM_FREQ; + for (uint8_t i = 0; i < max_freqs && (8 + i * 3 + 2) < bLength; i++) { + tmp_sam_freq[i] = ((uint32_t)p_desc[8 + i * 3] | + ((uint32_t)p_desc[9 + i * 3] << 8) | + ((uint32_t)p_desc[10 + i * 3] << 16)); + } + } + } + break; + } + default: + break; + } + break; + } + case TUSB_DESC_ENDPOINT: { + const tusb_desc_endpoint_t *p_ep = (const tusb_desc_endpoint_t *)p_desc; + if (p_ep->bmAttributes.xfer == TUSB_XFER_ISOCHRONOUS) { + TU_LOG_DRV(" Isochronous EP %02x\r\n", p_ep->bEndpointAddress); + if (tu_edpt_dir(p_ep->bEndpointAddress) == TUSB_DIR_IN) { + p_audio->ep_in = p_ep->bEndpointAddress; + p_audio->ep_in_size = tu_edpt_packet_size(p_ep); + p_audio->ep_in_interval = p_ep->bInterval; + desc_cb.desc_ep_in = p_ep; + // Save to per-AS storage + if (as_entry_idx < CFG_TUH_AUDIO_MAX_AS) { + audioh_as_t *as = &p_audio->as[as_entry_idx]; + as->ep_addr = p_ep->bEndpointAddress; + as->ep_size = tu_edpt_packet_size(p_ep); + as->ep_dir = TUSB_DIR_IN; + as->format_type = tmp_format_type; + as->num_channels = tmp_num_channels; + as->sub_frame_size = tmp_sub_frame_size; + as->bit_resolution = tmp_bit_resolution; + as->sam_freq_type = tmp_sam_freq_type; + as->sam_freq_lower = tmp_sam_freq_lower; + as->sam_freq_upper = tmp_sam_freq_upper; + for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX_SAM_FREQ; i++) { + as->sam_freq[i] = tmp_sam_freq[i]; + } + } + } else { + p_audio->ep_out = p_ep->bEndpointAddress; + p_audio->ep_out_size = tu_edpt_packet_size(p_ep); + p_audio->ep_out_interval = p_ep->bInterval; + desc_cb.desc_ep_out = p_ep; + // Save to per-AS storage + if (as_entry_idx < CFG_TUH_AUDIO_MAX_AS) { + audioh_as_t *as = &p_audio->as[as_entry_idx]; + as->ep_addr = p_ep->bEndpointAddress; + as->ep_size = tu_edpt_packet_size(p_ep); + as->ep_dir = TUSB_DIR_OUT; + as->format_type = tmp_format_type; + as->num_channels = tmp_num_channels; + as->sub_frame_size = tmp_sub_frame_size; + as->bit_resolution = tmp_bit_resolution; + as->sam_freq_type = tmp_sam_freq_type; + as->sam_freq_lower = tmp_sam_freq_lower; + as->sam_freq_upper = tmp_sam_freq_upper; + for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX_SAM_FREQ; i++) { + as->sam_freq[i] = tmp_sam_freq[i]; + } + } + } + TU_ASSERT(tuh_edpt_open(dev_addr, p_ep), 0); + } + break; + } + default: + break; + } + p_desc = tu_desc_next(p_desc); + } + // Continue to parse other AS interfaces (don't break, device may have both IN and OUT) + // break; // Removed: allow parsing multiple AS interfaces (e.g. mic + speaker) + continue; + } + p_audio->itf_count++; + } else if (itf->bInterfaceClass == TUSB_CLASS_AUDIO && itf->bInterfaceSubClass == AUDIO_SUBCLASS_CONTROL) { + // Another Audio Control interface (shouldn't happen in normal UAC 1.0) + p_audio->itf_count++; + } + } + p_desc = tu_desc_next(p_desc); + } + + p_audio->daddr = dev_addr; + tuh_audio_descriptor_cb(idx, &desc_cb); + + return (uint16_t)((uintptr_t)p_desc - (uintptr_t)desc_start); +} + +static void _audioh_mount(uint8_t dev_addr, uint8_t idx); + +static void audioh_set_interface_complete(tuh_xfer_t* xfer) { + uint8_t idx = (uint8_t) xfer->user_data; + audioh_interface_t *p_audio = &_audioh_itf[idx]; + + // Send SET_INTERFACE for next AS interface if any + p_audio->as_set_idx++; + if (p_audio->as_set_idx < p_audio->as_count) { + uint8_t as_idx = p_audio->as_set_idx; + uint8_t itf = p_audio->as_interfaces[as_idx]; + uint8_t alt = p_audio->as_alt_settings[as_idx]; + if (alt > 0) { + TU_LOG_DRV("AUDIO Set Interface %u Alt %u (addr = %u)\r\n", itf, alt, xfer->daddr); + tuh_interface_set(xfer->daddr, itf, alt, audioh_set_interface_complete, idx); + return; + } + } + + // All SET_INTERFACE done, mount the device + _audioh_mount(xfer->daddr, idx); +} + +static void _audioh_mount(uint8_t dev_addr, uint8_t idx) { + audioh_interface_t *p_audio = &_audioh_itf[idx]; + p_audio->mounted = true; + + tuh_audio_mount_cb_t mount_cb_data = { + .daddr = dev_addr, + .bInterfaceNumber = p_audio->bInterfaceNumber, + .bAltSetting = p_audio->alt_setting, + .input_terminal_type = p_audio->input_terminal_type, + .input_terminal_id = p_audio->input_terminal_id, + .input_terminal_channels = p_audio->input_terminal_channels, + .output_terminal_type = p_audio->output_terminal_type, + .output_terminal_id = p_audio->output_terminal_id, + .feature_unit_id = p_audio->feature_unit_id, + .feature_unit_source_id = p_audio->feature_unit_source_id, + .ep_in = p_audio->ep_in, + .ep_out = p_audio->ep_out, + .ep_in_size = p_audio->ep_in_size, + .ep_out_size = p_audio->ep_out_size, + }; + + // Fill per-AS interface info + mount_cb_data.as_count = p_audio->as_count; + for (uint8_t i = 0; i < p_audio->as_count && i < CFG_TUH_AUDIO_MAX_AS; i++) { + audioh_as_t *as = &p_audio->as[i]; + mount_cb_data.as_info[i].interface_num = as->interface_num; + mount_cb_data.as_info[i].alt_setting = as->alt_setting; + mount_cb_data.as_info[i].ep_addr = as->ep_addr; + mount_cb_data.as_info[i].ep_size = as->ep_size; + mount_cb_data.as_info[i].ep_dir = as->ep_dir; + mount_cb_data.as_info[i].format_type = as->format_type; + mount_cb_data.as_info[i].num_channels = as->num_channels; + mount_cb_data.as_info[i].sub_frame_size = as->sub_frame_size; + mount_cb_data.as_info[i].bit_resolution = as->bit_resolution; + mount_cb_data.as_info[i].sam_freq_type = as->sam_freq_type; + mount_cb_data.as_info[i].sam_freq_lower = as->sam_freq_lower; + mount_cb_data.as_info[i].sam_freq_upper = as->sam_freq_upper; + for (uint8_t j = 0; j < CFG_TUH_AUDIO_MAX_SAM_FREQ; j++) { + mount_cb_data.as_info[i].sam_freq[j] = as->sam_freq[j]; + } + } + + tuh_audio_mount_cb(idx, &mount_cb_data); + + usbh_driver_set_config_complete(dev_addr, p_audio->bInterfaceNumber); +} + +bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num) { + uint8_t idx = tuh_audio_itf_get_index(dev_addr, itf_num); + + // If not found, check if this is an AS interface that belongs to a known AC interface + if (idx >= CFG_TUH_AUDIO_MAX) { + for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX; i++) { + if (_audioh_itf[i].daddr == dev_addr && _audioh_itf[i].as_interface_num == itf_num) { + // AS interface, already handled by AC interface's set_config + return true; + } + } + return false; + } + + audioh_interface_t *p_audio = &_audioh_itf[idx]; + + // Send SET_INTERFACE for all AS interfaces with alt_setting > 0 + if (p_audio->as_count > 0) { + p_audio->as_set_idx = 0; + uint8_t itf = p_audio->as_interfaces[0]; + uint8_t alt = p_audio->as_alt_settings[0]; + if (alt > 0) { + TU_LOG_DRV("AUDIO Set Interface %u Alt %u (addr = %u)\r\n", itf, alt, dev_addr); + tuh_interface_set(dev_addr, itf, alt, audioh_set_interface_complete, idx); + return true; + } + } + + _audioh_mount(dev_addr, idx); + return true; +} + +//--------------------------------------------------------------------+ +// Application API +//--------------------------------------------------------------------+ +bool tuh_audio_mounted(uint8_t idx) { + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX); + audioh_interface_t *p_audio = &_audioh_itf[idx]; + return p_audio->mounted; +} + +uint8_t tuh_audio_itf_get_index(uint8_t daddr, uint8_t itf_num) { + for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { + const audioh_interface_t *p_audio = &_audioh_itf[idx]; + if (p_audio->daddr == daddr && p_audio->bInterfaceNumber == itf_num) { + return idx; + } + } + return TUSB_INDEX_INVALID_8; +} + +bool tuh_audio_itf_get_info(uint8_t idx, tuh_itf_info_t *info) { + audioh_interface_t *p_audio = &_audioh_itf[idx]; + TU_VERIFY(p_audio && info); + + info->daddr = p_audio->daddr; + + // re-construct descriptor + tusb_desc_interface_t *desc = &info->desc; + desc->bLength = sizeof(tusb_desc_interface_t); + desc->bDescriptorType = TUSB_DESC_INTERFACE; + + desc->bInterfaceNumber = p_audio->bInterfaceNumber; + desc->bAlternateSetting = 0; + desc->bNumEndpoints = (uint8_t)((p_audio->ep_in ? 1u : 0u) + (p_audio->ep_out ? 1u : 0u)); + desc->bInterfaceClass = TUSB_CLASS_AUDIO; + desc->bInterfaceSubClass = AUDIO_SUBCLASS_CONTROL; + desc->bInterfaceProtocol = 0; + desc->iInterface = p_audio->iInterface; + + return true; +} + +//--------------------------------------------------------------------+ +// Control Endpoint API +//--------------------------------------------------------------------+ +bool tuh_audio_set_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t sampling_freq, + tuh_xfer_cb_t complete_cb, uintptr_t user_data) { + static uint8_t freq_buf[3] = {0}; + tusb_control_request_t const request = { + .bmRequestType_bit = { + .recipient = TUSB_REQ_RCPT_ENDPOINT, + .type = TUSB_REQ_TYPE_CLASS, + .direction = TUSB_DIR_OUT + }, + .bRequest = AUDIO10_CS_REQ_SET_CUR, + .wValue = tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0), // Control Selector = Sampling Freq, Channel = 0 + .wIndex = tu_u16_low(ep_addr), + .wLength = 3 + }; + + // UAC 1.0 sampling frequency is 3 bytes little-endian + // uint8_t freq_buf[3] = { + // (uint8_t)(sampling_freq & 0xFF), + // (uint8_t)((sampling_freq >> 8) & 0xFF), + // (uint8_t)((sampling_freq >> 16) & 0xFF) + // }; + freq_buf[0] = (uint8_t)(sampling_freq & 0xFF); + freq_buf[1] = (uint8_t)((sampling_freq >> 8) & 0xFF); + freq_buf[2] = (uint8_t)((sampling_freq >> 16) & 0xFF); + tuh_xfer_t xfer = { + .daddr = daddr, + .ep_addr = 0, + .setup = &request, + .buffer = freq_buf, + .complete_cb = complete_cb, + .user_data = user_data + }; + + return tuh_control_xfer(&xfer); +} + +bool tuh_audio_get_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t *sampling_freq, + tuh_xfer_cb_t complete_cb, uintptr_t user_data) { + tusb_control_request_t const request = { + .bmRequestType_bit = { + .recipient = TUSB_REQ_RCPT_ENDPOINT, + .type = TUSB_REQ_TYPE_CLASS, + .direction = TUSB_DIR_IN + }, + .bRequest = AUDIO10_CS_REQ_GET_CUR, + .wValue = tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0), // Control Selector = Sampling Freq, Channel = 0 + .wIndex = tu_u16_low(ep_addr), + .wLength = 3 + }; + + // Application needs to parse 3-byte little-endian sampling frequency from buffer + tuh_xfer_t xfer = { + .daddr = daddr, + .ep_addr = 0, + .setup = &request, + .buffer = (uint8_t *)sampling_freq, + .complete_cb = complete_cb, + .user_data = user_data + }; + + return tuh_control_xfer(&xfer); +} + +bool tuh_audio_feature_unit_set(uint8_t daddr, uint8_t itf_num, uint8_t unit_id, + uint8_t control_selector, uint8_t channel, + uint16_t value, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { + tusb_control_request_t const request = { + .bmRequestType_bit = { + .recipient = TUSB_REQ_RCPT_INTERFACE, + .type = TUSB_REQ_TYPE_CLASS, + .direction = TUSB_DIR_OUT + }, + .bRequest = AUDIO10_CS_REQ_SET_CUR, + .wValue = tu_u16(control_selector, channel), + .wIndex = tu_u16(itf_num, unit_id), + .wLength = 2 + }; + + uint8_t val_buf[2] = { (uint8_t)(value & 0xFF), (uint8_t)((value >> 8) & 0xFF) }; + + tuh_xfer_t xfer = { + .daddr = daddr, + .ep_addr = 0, + .setup = &request, + .buffer = val_buf, + .complete_cb = complete_cb, + .user_data = user_data + }; + + return tuh_control_xfer(&xfer); +} + +bool tuh_audio_feature_unit_get(uint8_t daddr, uint8_t itf_num, uint8_t unit_id, + uint8_t control_selector, uint8_t channel, + void *buffer, uint8_t len, + tuh_xfer_cb_t complete_cb, uintptr_t user_data) { + tusb_control_request_t const request = { + .bmRequestType_bit = { + .recipient = TUSB_REQ_RCPT_INTERFACE, + .type = TUSB_REQ_TYPE_CLASS, + .direction = TUSB_DIR_IN + }, + .bRequest = AUDIO10_CS_REQ_GET_CUR, + .wValue = tu_u16(control_selector, channel), + .wIndex = tu_u16(itf_num, unit_id), + .wLength = len + }; + + tuh_xfer_t xfer = { + .daddr = daddr, + .ep_addr = 0, + .setup = &request, + .buffer = buffer, + .complete_cb = complete_cb, + .user_data = user_data + }; + + return tuh_control_xfer(&xfer); +} + +//--------------------------------------------------------------------+ +// Multi-AS interface API +//--------------------------------------------------------------------+ +uint8_t tuh_audio_as_get_count(uint8_t idx) { + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, 0); + return _audioh_itf[idx].as_count; +} + +bool tuh_audio_as_get_info(uint8_t idx, uint8_t as_idx, tuh_audio_as_info_t *info) { + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); + TU_VERIFY(as_idx < _audioh_itf[idx].as_count, false); + TU_VERIFY(info, false); + + audioh_as_t *as = &_audioh_itf[idx].as[as_idx]; + info->interface_num = as->interface_num; + info->alt_setting = as->alt_setting; + info->ep_addr = as->ep_addr; + info->ep_size = as->ep_size; + info->ep_dir = as->ep_dir; + info->format_type = as->format_type; + info->num_channels = as->num_channels; + info->sub_frame_size = as->sub_frame_size; + info->bit_resolution = as->bit_resolution; + info->sam_freq_type = as->sam_freq_type; + info->sam_freq_lower = as->sam_freq_lower; + info->sam_freq_upper = as->sam_freq_upper; + memcpy(info->sam_freq, as->sam_freq, sizeof(info->sam_freq)); + return true; +} + +//--------------------------------------------------------------------+ +// Isochronous Endpoint API +//--------------------------------------------------------------------+ +bool tuh_audio_receive(uint8_t daddr, uint8_t idx, uint8_t *buffer, uint16_t len) { + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX); + audioh_interface_t *p_audio = &_audioh_itf[idx]; + TU_VERIFY(p_audio->daddr == daddr); + TU_VERIFY(p_audio->ep_in != 0); + + return usbh_edpt_xfer(daddr, p_audio->ep_in, buffer, len); +} + +bool tuh_audio_send(uint8_t daddr, uint8_t idx, uint8_t *buffer, uint16_t len) { + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX); + audioh_interface_t *p_audio = &_audioh_itf[idx]; + TU_VERIFY(p_audio->daddr == daddr); + TU_VERIFY(p_audio->ep_out != 0); + + return usbh_edpt_xfer(daddr, p_audio->ep_out, (uint8_t *)buffer, len); +} + +//--------------------------------------------------------------------+ +// Set Interface +//--------------------------------------------------------------------+ +bool tuh_audio_set_interface(uint8_t daddr, uint8_t itf_num, uint8_t alt_setting, + tuh_xfer_cb_t complete_cb, uintptr_t user_data) { + tusb_control_request_t const request = { + .bmRequestType_bit = { + .recipient = TUSB_REQ_RCPT_INTERFACE, + .type = TUSB_REQ_TYPE_STANDARD, + .direction = TUSB_DIR_OUT + }, + .bRequest = TUSB_REQ_SET_INTERFACE, + .wValue = alt_setting, + .wIndex = itf_num, + .wLength = 0 + }; + + tuh_xfer_t xfer = { + .daddr = daddr, + .ep_addr = 0, + .setup = &request, + .buffer = NULL, + .complete_cb = complete_cb, + .user_data = user_data + }; + + return tuh_control_xfer(&xfer); +} + +#endif diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h new file mode 100644 index 000000000..6dd44f830 --- /dev/null +++ b/src/class/audio/audio_host.h @@ -0,0 +1,235 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2025 TinyUSB contributors + * + * 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_AUDIO_HOST_H_ +#define TUSB_AUDIO_HOST_H_ + +#include "audio.h" + +#ifdef __cplusplus +extern "C" { +#endif + +//--------------------------------------------------------------------+ +// Class Driver Configuration +//--------------------------------------------------------------------+ +#ifndef CFG_TUH_AUDIO_MAX + #define CFG_TUH_AUDIO_MAX 1 +#endif + +#ifndef CFG_TUH_AUDIO_MAX_SAM_FREQ + #define CFG_TUH_AUDIO_MAX_SAM_FREQ 5 +#endif + +#ifndef CFG_TUH_AUDIO_MAX_AS + #define CFG_TUH_AUDIO_MAX_AS 4 +#endif + +//--------------------------------------------------------------------+ +// AS Interface Info (per-interface independent storage) +//--------------------------------------------------------------------+ +typedef struct { + uint8_t interface_num; // AS interface number + uint8_t alt_setting; // Current alt setting + uint8_t ep_addr; // Endpoint address + uint16_t ep_size; // Max packet size + uint8_t ep_dir; // TUSB_DIR_IN or TUSB_DIR_OUT + + // Format info + uint8_t format_type; + uint8_t num_channels; + uint8_t sub_frame_size; + uint8_t bit_resolution; + uint8_t sam_freq_type; + uint32_t sam_freq[CFG_TUH_AUDIO_MAX_SAM_FREQ]; + uint32_t sam_freq_lower; + uint32_t sam_freq_upper; +} tuh_audio_as_info_t; + +#ifndef CFG_TUH_AUDIO_EPIN_BUFSIZE + #define CFG_TUH_AUDIO_EPIN_BUFSIZE 192 +#endif + +#ifndef CFG_TUH_AUDIO_EPOUT_BUFSIZE + #define CFG_TUH_AUDIO_EPOUT_BUFSIZE 192 +#endif + +//--------------------------------------------------------------------+ +// Descriptor Information +//--------------------------------------------------------------------+ +// Information about parsed UAC 1.0 descriptors passed to the application +// during enumeration (via tuh_audio_descriptor_cb) +typedef struct { + // Audio Control Interface descriptor + const tusb_desc_interface_t *desc_ac_interface; + + // Audio Streaming Interface descriptor (alt setting 0) + const tusb_desc_interface_t *desc_as_interface; + + // Audio Streaming Interface alt setting (with endpoints) + const tusb_desc_interface_t *desc_as_interface_alt; + + // Format Type descriptor + const uint8_t *desc_format_type; + + // Class-Specific AS Interface (AS General) descriptor + const uint8_t *desc_cs_as_general; + + // Standard Isochronous Endpoint descriptor (IN) + const tusb_desc_endpoint_t *desc_ep_in; + + // Standard Isochronous Endpoint descriptor (OUT) + const tusb_desc_endpoint_t *desc_ep_out; + + // Audio function information + uint8_t ac_interface_num; // Audio Control interface number + uint8_t as_interface_num; // Audio Streaming interface number + uint8_t alt_setting; // Current alt setting with endpoints +} tuh_audio_descriptor_cb_t; + +typedef struct { + uint8_t daddr; + uint8_t bInterfaceNumber; + uint8_t bAltSetting; + + // Terminal info (from Audio Control Interface) + uint16_t input_terminal_type; // wTerminalType of Input Terminal (0x0201 = Mic, etc.) + uint8_t input_terminal_id; // bTerminalID of Input Terminal + uint8_t input_terminal_channels; // bNrChannels of Input Terminal + uint16_t output_terminal_type; // wTerminalType of Output Terminal (0x0301 = Speaker, etc.) + uint8_t output_terminal_id; // bTerminalID of Output Terminal + + // Feature Unit info + uint8_t feature_unit_id; // bUnitID of Feature Unit (0 = none) + uint8_t feature_unit_source_id; // bSourceID of Feature Unit + + // Endpoint info + uint8_t ep_in; + uint8_t ep_out; + uint16_t ep_in_size; + uint16_t ep_out_size; + + // Multi-AS support (per AS interface independent storage) + uint8_t as_count; + tuh_audio_as_info_t as_info[CFG_TUH_AUDIO_MAX_AS]; +} tuh_audio_mount_cb_t; + +//--------------------------------------------------------------------+ +// Application API +//--------------------------------------------------------------------+ + +// Check if Audio interface is mounted +bool tuh_audio_mounted(uint8_t idx); + +// Get Interface index from device address + interface number +// return TUSB_INDEX_INVALID_8 (0xFF) if not found +uint8_t tuh_audio_itf_get_index(uint8_t daddr, uint8_t itf_num); + +// Get Interface information +// return true if index is correct and interface is currently mounted +bool tuh_audio_itf_get_info(uint8_t idx, tuh_itf_info_t *info); + +// Get number of AS interfaces for an audio device +uint8_t tuh_audio_as_get_count(uint8_t idx); + +// Get AS interface info by index +// as_idx: 0 to (as_count - 1) +bool tuh_audio_as_get_info(uint8_t idx, uint8_t as_idx, tuh_audio_as_info_t *info); + +// Set Audio Streaming interface alternate setting (to enable/disable endpoints) +bool tuh_audio_set_interface(uint8_t daddr, uint8_t itf_num, uint8_t alt_setting, + tuh_xfer_cb_t complete_cb, uintptr_t user_data); + +//--------------------------------------------------------------------+ +// Control Endpoint API +//--------------------------------------------------------------------+ + +// Set current sampling frequency on an isochronous endpoint (UAC 1.0) +// Sampling frequency is 3 bytes little-endian +bool tuh_audio_set_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t sampling_freq, + tuh_xfer_cb_t complete_cb, uintptr_t user_data); + +// Get current sampling frequency from an isochronous endpoint (UAC 1.0) +bool tuh_audio_get_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t *sampling_freq, + tuh_xfer_cb_t complete_cb, uintptr_t user_data); + +// Set current/mute/volume etc. for a feature unit (UAC 1.0) +bool tuh_audio_feature_unit_set(uint8_t daddr, uint8_t itf_num, uint8_t unit_id, + uint8_t control_selector, uint8_t channel, + uint16_t value, tuh_xfer_cb_t complete_cb, uintptr_t user_data); + +// Get current/mute/volume etc. from a feature unit (UAC 1.0) +bool tuh_audio_feature_unit_get(uint8_t daddr, uint8_t itf_num, uint8_t unit_id, + uint8_t control_selector, uint8_t channel, + void *buffer, uint8_t len, + tuh_xfer_cb_t complete_cb, uintptr_t user_data); + +//--------------------------------------------------------------------+ +// Interrupt/Isochronous Endpoint API +//--------------------------------------------------------------------+ + +// Submit an isochronous transfer to receive audio data from IN endpoint +bool tuh_audio_receive(uint8_t daddr, uint8_t idx, uint8_t *buffer, uint16_t len); + +// Submit an isochronous transfer to send audio data to OUT endpoint +bool tuh_audio_send(uint8_t daddr, uint8_t idx, uint8_t *buffer, uint16_t len); + +//--------------------------------------------------------------------+ +// Callbacks (Weak is optional) +//--------------------------------------------------------------------+ + +// Invoked when Audio interface descriptor is detected during enumeration. +// Application can copy/parse descriptor if needed. +// Note: may be fired before tuh_audio_mount_cb(), therefore audio interface is not mounted/ready. +void tuh_audio_descriptor_cb(uint8_t idx, const tuh_audio_descriptor_cb_t *desc_cb_data); + +// Invoked when device with Audio interface is mounted +void tuh_audio_mount_cb(uint8_t idx, const tuh_audio_mount_cb_t *mount_cb_data); + +// Invoked when device with Audio interface is un-mounted +void tuh_audio_umount_cb(uint8_t idx); + +// Invoked when an isochronous IN transfer is complete +void tuh_audio_rx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes); + +// Invoked when an isochronous OUT transfer is complete +void tuh_audio_tx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes); + +//--------------------------------------------------------------------+ +// Internal Class Driver API +//--------------------------------------------------------------------+ +bool audioh_init(void); +bool audioh_deinit(void); +uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface_t *desc_itf, uint16_t max_len); +bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num); +bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes); +void audioh_close(uint8_t daddr); + +#ifdef __cplusplus +} +#endif + +#endif /* TUSB_AUDIO_HOST_H_ */ diff --git a/src/host/usbh.c b/src/host/usbh.c index e307bb5e5..6e16c0b72 100644 --- a/src/host/usbh.c +++ b/src/host/usbh.c @@ -258,6 +258,18 @@ static usbh_class_driver_t const usbh_class_drivers[] = { }, #endif + #if CFG_TUH_AUDIO + { + .name = DRIVER_NAME("AUDIO"), + .init = audioh_init, + .deinit = audioh_deinit, + .open = audioh_open, + .set_config = audioh_set_config, + .xfer_cb = audioh_xfer_cb, + .close = audioh_close + }, + #endif + #if CFG_TUH_HID { .name = DRIVER_NAME("HID"), diff --git a/src/tinyusb.mk b/src/tinyusb.mk index 365043927..ab8549bc8 100644 --- a/src/tinyusb.mk +++ b/src/tinyusb.mk @@ -19,6 +19,7 @@ TINYUSB_SRC_C += \ src/class/usbtmc/usbtmc_device.c \ src/class/video/video_device.c \ src/class/vendor/vendor_device.c \ + src/class/audio/audio_host.c \ src/host/usbh.c \ src/host/hub.c \ src/class/cdc/cdc_host.c \ diff --git a/src/tusb.h b/src/tusb.h index 6a30f7c13..219951692 100644 --- a/src/tusb.h +++ b/src/tusb.h @@ -28,6 +28,10 @@ #if CFG_TUH_ENABLED #include "host/usbh.h" + #if CFG_TUH_AUDIO + #include "class/audio/audio_host.h" + #endif + #if CFG_TUH_HID #include "class/hid/hid_host.h" #endif diff --git a/src/tusb_option.h b/src/tusb_option.h index e19ee1629..7a636657f 100644 --- a/src/tusb_option.h +++ b/src/tusb_option.h @@ -807,6 +807,10 @@ { 0x067b, 0x23f3 } /* GS */ #endif +#ifndef CFG_TUH_AUDIO + #define CFG_TUH_AUDIO 0 +#endif + #ifndef CFG_TUH_HID #define CFG_TUH_HID 0 #endif -- cgit v1.3.1 From 817807dc64ae3124a315f42d67eb00271c3846e0 Mon Sep 17 00:00:00 2001 From: "Zhang, Zhenjiang" Date: Fri, 17 Jul 2026 10:40:44 +0800 Subject: fix(class/audio): handle non-audio interface in enumeration and fix async control transfer buffer - Stop parsing at first non-Audio interface in audioh_open to avoid claiming unrelated interfaces - Call usbh_driver_set_config_complete for AS and unknown interfaces to allow enumeration to continue - Add global ctrl endpoint buffer to audioh_epbuf_t to fix use-after-return in feature_unit_set - Add sampling_freq NULL check and initialize to 0 in tuh_audio_get_sampling_freq - Change BOARD_TUH_RHPORT from 1 to 0 in audio_host example - Add only.txt with supported MCU/family list for audio_host example --- examples/host/audio_host/only.txt | 32 ++++++++++++++++++++++++++++++ examples/host/audio_host/src/tusb_config.h | 2 +- src/class/audio/audio_host.c | 24 +++++++++++++++++----- 3 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 examples/host/audio_host/only.txt (limited to 'src') diff --git a/examples/host/audio_host/only.txt b/examples/host/audio_host/only.txt new file mode 100644 index 000000000..a2ff93be5 --- /dev/null +++ b/examples/host/audio_host/only.txt @@ -0,0 +1,32 @@ +family:hpmicro +family:samd21 +family:samd5x_e5x +mcu:CH32V20X +mcu:KINETIS_KL +mcu:LPC175X_6X +mcu:LPC177X_8X +mcu:LPC18XX +mcu:LPC40XX +mcu:LPC43XX +mcu:LPC54 +mcu:LPC55 +mcu:MAX3421 +mcu:MIMXRT10XX +mcu:MIMXRT11XX +mcu:MIMXRT1XXX +mcu:MSP432E4 +mcu:RAXXX +mcu:RP2040 +mcu:RW61X +mcu:RX65X +mcu:STM32C0 +mcu:STM32C5 +mcu:STM32F4 +mcu:STM32F7 +mcu:STM32G0 +mcu:STM32H5 +mcu:STM32H7 +mcu:STM32H7RS +mcu:STM32N6 +mcu:STM32U3 +mcu:STM32U5 diff --git a/examples/host/audio_host/src/tusb_config.h b/examples/host/audio_host/src/tusb_config.h index a4fb17fa5..4a7a6ad56 100644 --- a/examples/host/audio_host/src/tusb_config.h +++ b/examples/host/audio_host/src/tusb_config.h @@ -68,7 +68,7 @@ extern "C" { #define CFG_TUH_MAX_SPEED BOARD_TUH_MAX_SPEED #ifndef BOARD_TUH_RHPORT - #define BOARD_TUH_RHPORT 1 + #define BOARD_TUH_RHPORT 0 #endif #ifndef BOARD_TUH_MAX_SPEED diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 9414c1ab4..85c2374be 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -136,10 +136,11 @@ typedef struct { typedef struct { TUH_EPBUF_DEF(epin, CFG_TUH_AUDIO_EPIN_BUFSIZE); TUH_EPBUF_DEF(epout, CFG_TUH_AUDIO_EPOUT_BUFSIZE); + TUH_EPBUF_DEF(ctrl, 8); } audioh_epbuf_t; static audioh_interface_t _audioh_itf[CFG_TUH_AUDIO_MAX]; - + static audioh_epbuf_t _audioh_epbuf[CFG_TUH_AUDIO_MAX]; //--------------------------------------------------------------------+ // Helper //--------------------------------------------------------------------+ @@ -281,7 +282,9 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface while (tu_desc_in_bounds(p_desc, desc_end)) { if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) { const tusb_desc_interface_t *itf = (const tusb_desc_interface_t *)p_desc; - if (itf->bInterfaceClass == TUSB_CLASS_AUDIO && itf->bInterfaceSubClass == AUDIO_SUBCLASS_STREAMING) { + // Stop at the first non-Audio interface so we don't claim the rest of the configuration + if (itf->bInterfaceClass != TUSB_CLASS_AUDIO) break; + if (itf->bInterfaceSubClass == AUDIO_SUBCLASS_STREAMING) { // Found Audio Streaming Interface TU_LOG_DRV(" Found AS Interface %u (alt = %u)\r\n", itf->bInterfaceNumber, itf->bAlternateSetting); @@ -537,11 +540,14 @@ bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num) { if (idx >= CFG_TUH_AUDIO_MAX) { for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX; i++) { if (_audioh_itf[i].daddr == dev_addr && _audioh_itf[i].as_interface_num == itf_num) { - // AS interface, already handled by AC interface's set_config + // AS interface: configuration is driven by the AC interface, so just pass through + usbh_driver_set_config_complete(dev_addr, itf_num); return true; } } - return false; + // Not an Audio interface we own; pass through so enumeration can continue + usbh_driver_set_config_complete(dev_addr, itf_num); + return true; } audioh_interface_t *p_audio = &_audioh_itf[idx]; @@ -644,6 +650,9 @@ bool tuh_audio_set_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t sampli bool tuh_audio_get_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t *sampling_freq, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { + TU_VERIFY(sampling_freq, false); + *sampling_freq = 0; + tusb_control_request_t const request = { .bmRequestType_bit = { .recipient = TUSB_REQ_RCPT_ENDPOINT, @@ -684,7 +693,12 @@ bool tuh_audio_feature_unit_set(uint8_t daddr, uint8_t itf_num, uint8_t unit_id, .wLength = 2 }; - uint8_t val_buf[2] = { (uint8_t)(value & 0xFF), (uint8_t)((value >> 8) & 0xFF) }; + uint8_t const idx = tuh_audio_itf_get_index(daddr, itf_num); + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); + + uint8_t* val_buf = _audioh_epbuf[idx].ctrl; + val_buf[0] = (uint8_t)(value & 0xFF); + val_buf[1] = (uint8_t)((value >> 8) & 0xFF); tuh_xfer_t xfer = { .daddr = daddr, -- cgit v1.3.1 From 70113a39a3ffac7b310ff7d661fe744c35364c50 Mon Sep 17 00:00:00 2001 From: "Zhang, Zhenjiang" Date: Fri, 17 Jul 2026 13:47:18 +0800 Subject: fix(class/audio): fix control request byte order and buffer usage in audio host - Fix missing tu_htole16() conversions for wValue and wIndex in tuh_audio_set_sampling_freq, tuh_audio_get_sampling_freq, tuh_audio_feature_unit_set, and tuh_audio_feature_unit_get - Fix incorrect wIndex parameter order in feature unit requests (unit_id and itf_num were swapped) - Replace static freq_buf with per-endpoint ctrl buffer in tuh_audio_set_sampling_freq to avoid concurrency issues - Update audio_host README to match actual example behavior --- examples/host/audio_host/README.md | 4 ++-- src/class/audio/audio_host.c | 20 +++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md index 4d8f66b71..072adbbf9 100644 --- a/examples/host/audio_host/README.md +++ b/examples/host/audio_host/README.md @@ -54,9 +54,9 @@ make BOARD= flash 3. Open a serial terminal to view output 4. The example will: - Print device information when mounted - - Set sampling frequency to 48kHz + - Set sampling frequency based on the device's advertised capabilities - Receive audio samples from the device (IN endpoint) - - Send test sine wave audio to the device (OUT endpoint) + - Loop back received audio to the device (OUT endpoint) for testing ## Serial Output Example diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 85c2374be..4b25835f4 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -614,7 +614,9 @@ bool tuh_audio_itf_get_info(uint8_t idx, tuh_itf_info_t *info) { //--------------------------------------------------------------------+ bool tuh_audio_set_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t sampling_freq, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { - static uint8_t freq_buf[3] = {0}; + uint8_t const idx = get_idx_by_ep_addr(daddr, ep_addr); + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); + uint8_t* freq_buf = _audioh_epbuf[idx].ctrl; tusb_control_request_t const request = { .bmRequestType_bit = { .recipient = TUSB_REQ_RCPT_ENDPOINT, @@ -622,8 +624,8 @@ bool tuh_audio_set_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t sampli .direction = TUSB_DIR_OUT }, .bRequest = AUDIO10_CS_REQ_SET_CUR, - .wValue = tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0), // Control Selector = Sampling Freq, Channel = 0 - .wIndex = tu_u16_low(ep_addr), + .wValue = tu_htole16(tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0)), // Control Selector = Sampling Freq, Channel = 0 + .wIndex = tu_htole16((uint16_t) ep_addr), .wLength = 3 }; @@ -660,8 +662,8 @@ bool tuh_audio_get_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t *sampl .direction = TUSB_DIR_IN }, .bRequest = AUDIO10_CS_REQ_GET_CUR, - .wValue = tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0), // Control Selector = Sampling Freq, Channel = 0 - .wIndex = tu_u16_low(ep_addr), + .wValue = tu_htole16(tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0)), // Control Selector = Sampling Freq, Channel = 0 + .wIndex = tu_htole16((uint16_t) ep_addr), .wLength = 3 }; @@ -688,8 +690,8 @@ bool tuh_audio_feature_unit_set(uint8_t daddr, uint8_t itf_num, uint8_t unit_id, .direction = TUSB_DIR_OUT }, .bRequest = AUDIO10_CS_REQ_SET_CUR, - .wValue = tu_u16(control_selector, channel), - .wIndex = tu_u16(itf_num, unit_id), + .wValue = tu_htole16(tu_u16(control_selector, channel)), + .wIndex = tu_htole16(tu_u16(unit_id, itf_num)), .wLength = 2 }; @@ -723,8 +725,8 @@ bool tuh_audio_feature_unit_get(uint8_t daddr, uint8_t itf_num, uint8_t unit_id, .direction = TUSB_DIR_IN }, .bRequest = AUDIO10_CS_REQ_GET_CUR, - .wValue = tu_u16(control_selector, channel), - .wIndex = tu_u16(itf_num, unit_id), + .wValue = tu_htole16(tu_u16(control_selector, channel)), + .wIndex = tu_htole16(tu_u16(unit_id, itf_num)), .wLength = len }; -- cgit v1.3.1 From d4dfed4106fdfa5af21935f0ad84ed814b23a9f7 Mon Sep 17 00:00:00 2001 From: "Zhang, Zhenjiang" Date: Fri, 17 Jul 2026 14:41:36 +0800 Subject: fix(class/audio): fix indentation in audioh_set_config after UAC 1.0 changes --- src/class/audio/audio_host.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 4b25835f4..99d0f428e 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -136,7 +136,7 @@ typedef struct { typedef struct { TUH_EPBUF_DEF(epin, CFG_TUH_AUDIO_EPIN_BUFSIZE); TUH_EPBUF_DEF(epout, CFG_TUH_AUDIO_EPOUT_BUFSIZE); - TUH_EPBUF_DEF(ctrl, 8); + TUH_EPBUF_DEF(ctrl, 8); } audioh_epbuf_t; static audioh_interface_t _audioh_itf[CFG_TUH_AUDIO_MAX]; @@ -541,7 +541,7 @@ bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num) { for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX; i++) { if (_audioh_itf[i].daddr == dev_addr && _audioh_itf[i].as_interface_num == itf_num) { // AS interface: configuration is driven by the AC interface, so just pass through - usbh_driver_set_config_complete(dev_addr, itf_num); + usbh_driver_set_config_complete(dev_addr, itf_num); return true; } } -- cgit v1.3.1 From 786eef5fd29d0a1ba46eda1e907399aee5af16d0 Mon Sep 17 00:00:00 2001 From: "Zhang, Zhenjiang" Date: Fri, 17 Jul 2026 15:17:16 +0800 Subject: style(class/audio): rename descriptor variables to desc_ prefix and unify const style Rename descriptor pointer variables to use desc_ prefix for consistency with audio_device.c: - it -> desc_input_terminal - ot -> desc_output_terminal - fu -> desc_feature_unit - itf -> desc_interface - p_ep -> desc_endpoint Unify const qualifier placement to type_t const * style. Add file header comment describing UAC 1.0 host driver capabilities. Switch license header to SPDX identifier. --- src/class/audio/audio_host.c | 162 +++++++++++++++++++++---------------------- src/class/audio/audio_host.h | 23 +----- 2 files changed, 83 insertions(+), 102 deletions(-) (limited to 'src') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 99d0f428e..396f75987 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -1,28 +1,28 @@ /* - * The MIT License (MIT) + * SPDX-FileCopyrightText: Copyright (c) 2026 Zhenjiang Zhang + * SPDX-License-Identifier: MIT * - * Copyright (c) 2025 TinyUSB contributors - * - * 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: + * This file is part of the TinyUSB stack. + */ + +/* + * This driver implements a USB Audio Host (UAC 1.0) class driver. + * It supports multiple Audio Streaming (AS) interfaces with independent format storage. + * Each AS interface can have its own sample rate, channel count, bit resolution, + * and endpoint configuration. * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. + * The driver handles: + * 1. Audio Control (AC) interface parsing — Input Terminal, Output Terminal, + * and Feature Unit descriptors. + * 2. Audio Streaming (AS) interface enumeration — multiple AS interfaces with + * alternate settings, each storing its own format information. + * 3. Isochronous IN/OUT endpoint management for audio data transfer. + * 4. Asynchronous control transfers for sample frequency get/set. * - * 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. + * In case you need to adjust the number of supported AS interfaces, change + * CFG_TUH_AUDIO_MAX_AS in your tusb_config.h. * - * This file is part of the TinyUSB stack. - */ + * */ #include "tusb_option.h" @@ -248,27 +248,27 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface if (tu_desc_type(p_desc) == TUSB_DESC_CS_INTERFACE) { switch (tu_desc_subtype(p_desc)) { case AUDIO10_CS_AC_INTERFACE_INPUT_TERMINAL: { - const audio10_desc_input_terminal_t *it = (const audio10_desc_input_terminal_t *)p_desc; - p_audio->input_terminal_type = tu_le16toh(it->wTerminalType); - p_audio->input_terminal_id = it->bTerminalID; - p_audio->input_terminal_channels = it->bNrChannels; + audio10_desc_input_terminal_t const *desc_input_terminal = (audio10_desc_input_terminal_t const *)p_desc; + p_audio->input_terminal_type = tu_le16toh(desc_input_terminal->wTerminalType); + p_audio->input_terminal_id = desc_input_terminal->bTerminalID; + p_audio->input_terminal_channels = desc_input_terminal->bNrChannels; TU_LOG_DRV(" Input Terminal: ID=%u, Type=0x%04x, Channels=%u\r\n", - it->bTerminalID, tu_le16toh(it->wTerminalType), it->bNrChannels); + desc_input_terminal->bTerminalID, tu_le16toh(desc_input_terminal->wTerminalType), desc_input_terminal->bNrChannels); break; } case AUDIO10_CS_AC_INTERFACE_OUTPUT_TERMINAL: { - const audio10_desc_output_terminal_t *ot = (const audio10_desc_output_terminal_t *)p_desc; - p_audio->output_terminal_type = tu_le16toh(ot->wTerminalType); - p_audio->output_terminal_id = ot->bTerminalID; + audio10_desc_output_terminal_t const *desc_output_terminal = (audio10_desc_output_terminal_t const *)p_desc; + p_audio->output_terminal_type = tu_le16toh(desc_output_terminal->wTerminalType); + p_audio->output_terminal_id = desc_output_terminal->bTerminalID; TU_LOG_DRV(" Output Terminal: ID=%u, Type=0x%04x\r\n", - ot->bTerminalID, tu_le16toh(ot->wTerminalType)); + desc_output_terminal->bTerminalID, tu_le16toh(desc_output_terminal->wTerminalType)); break; } case AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT: { - const uint8_t *fu = p_desc; - p_audio->feature_unit_id = fu[3]; // bUnitID - p_audio->feature_unit_source_id = fu[4]; // bSourceID - TU_LOG_DRV(" Feature Unit: ID=%u, SourceID=%u\r\n", fu[3], fu[4]); + uint8_t const *desc_feature_unit = p_desc; + p_audio->feature_unit_id = desc_feature_unit[3]; // bUnitID + p_audio->feature_unit_source_id = desc_feature_unit[4]; // bSourceID + TU_LOG_DRV(" Feature Unit: ID=%u, SourceID=%u\r\n", desc_feature_unit[3], desc_feature_unit[4]); break; } default: @@ -281,53 +281,53 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface // Parse all remaining descriptors in this configuration looking for Audio Streaming interfaces while (tu_desc_in_bounds(p_desc, desc_end)) { if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) { - const tusb_desc_interface_t *itf = (const tusb_desc_interface_t *)p_desc; + tusb_desc_interface_t const *desc_interface = (tusb_desc_interface_t const *)p_desc; // Stop at the first non-Audio interface so we don't claim the rest of the configuration - if (itf->bInterfaceClass != TUSB_CLASS_AUDIO) break; - if (itf->bInterfaceSubClass == AUDIO_SUBCLASS_STREAMING) { + if (desc_interface->bInterfaceClass != TUSB_CLASS_AUDIO) break; + if (desc_interface->bInterfaceSubClass == AUDIO_SUBCLASS_STREAMING) { // Found Audio Streaming Interface - TU_LOG_DRV(" Found AS Interface %u (alt = %u)\r\n", itf->bInterfaceNumber, itf->bAlternateSetting); + TU_LOG_DRV(" Found AS Interface %u (alt = %u)\r\n", desc_interface->bInterfaceNumber, desc_interface->bAlternateSetting); - if (itf->bAlternateSetting == 0) { + if (desc_interface->bAlternateSetting == 0) { // Interface descriptor with alt setting 0 (no endpoints) // Add to AS interfaces array if (p_audio->as_count < CFG_TUH_AUDIO_MAX_AS) { - p_audio->as_interface_num = itf->bInterfaceNumber; - p_audio->as_interfaces[p_audio->as_count] = itf->bInterfaceNumber; - desc_cb.desc_as_interface = itf; - desc_cb.as_interface_num = itf->bInterfaceNumber; + p_audio->as_interface_num = desc_interface->bInterfaceNumber; + p_audio->as_interfaces[p_audio->as_count] = desc_interface->bInterfaceNumber; + desc_cb.desc_as_interface = desc_interface; + desc_cb.as_interface_num = desc_interface->bInterfaceNumber; // Create new AS entry for per-interface storage - p_audio->as[p_audio->as_count].interface_num = itf->bInterfaceNumber; + p_audio->as[p_audio->as_count].interface_num = desc_interface->bInterfaceNumber; p_audio->as[p_audio->as_count].alt_setting = 0; p_audio->as_count++; } - } else if (itf->bNumEndpoints > 0) { + } else if (desc_interface->bNumEndpoints > 0) { // Interface descriptor with alt setting > 0 (has endpoints) // Find matching AS interface and set alt_setting uint8_t as_entry_idx = CFG_TUH_AUDIO_MAX_AS; for (uint8_t as_idx = 0; as_idx < p_audio->as_count; as_idx++) { - if (p_audio->as_interfaces[as_idx] == itf->bInterfaceNumber) { - p_audio->alt_setting = itf->bAlternateSetting; - p_audio->as_alt_settings[as_idx] = itf->bAlternateSetting; - desc_cb.alt_setting = itf->bAlternateSetting; - desc_cb.desc_as_interface_alt = itf; + if (p_audio->as_interfaces[as_idx] == desc_interface->bInterfaceNumber) { + p_audio->alt_setting = desc_interface->bAlternateSetting; + p_audio->as_alt_settings[as_idx] = desc_interface->bAlternateSetting; + desc_cb.alt_setting = desc_interface->bAlternateSetting; + desc_cb.desc_as_interface_alt = desc_interface; break; } } // Find or create AS entry for per-interface storage for (uint8_t i = 0; i < p_audio->as_count; i++) { - if (p_audio->as[i].interface_num == itf->bInterfaceNumber) { + if (p_audio->as[i].interface_num == desc_interface->bInterfaceNumber) { as_entry_idx = i; break; } } if (as_entry_idx >= CFG_TUH_AUDIO_MAX_AS && p_audio->as_count < CFG_TUH_AUDIO_MAX_AS) { as_entry_idx = p_audio->as_count; - p_audio->as[as_entry_idx].interface_num = itf->bInterfaceNumber; + p_audio->as[as_entry_idx].interface_num = desc_interface->bInterfaceNumber; p_audio->as_count++; } if (as_entry_idx < CFG_TUH_AUDIO_MAX_AS) { - p_audio->as[as_entry_idx].alt_setting = itf->bAlternateSetting; + p_audio->as[as_entry_idx].alt_setting = desc_interface->bAlternateSetting; } // Parse the interface's descriptors @@ -387,19 +387,19 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface break; } case TUSB_DESC_ENDPOINT: { - const tusb_desc_endpoint_t *p_ep = (const tusb_desc_endpoint_t *)p_desc; - if (p_ep->bmAttributes.xfer == TUSB_XFER_ISOCHRONOUS) { - TU_LOG_DRV(" Isochronous EP %02x\r\n", p_ep->bEndpointAddress); - if (tu_edpt_dir(p_ep->bEndpointAddress) == TUSB_DIR_IN) { - p_audio->ep_in = p_ep->bEndpointAddress; - p_audio->ep_in_size = tu_edpt_packet_size(p_ep); - p_audio->ep_in_interval = p_ep->bInterval; - desc_cb.desc_ep_in = p_ep; + const tusb_desc_endpoint_t *desc_endpoint = (const tusb_desc_endpoint_t *)p_desc; + if (desc_endpoint->bmAttributes.xfer == TUSB_XFER_ISOCHRONOUS) { + TU_LOG_DRV(" Isochronous EP %02x\r\n", desc_endpoint->bEndpointAddress); + if (tu_edpt_dir(desc_endpoint->bEndpointAddress) == TUSB_DIR_IN) { + p_audio->ep_in = desc_endpoint->bEndpointAddress; + p_audio->ep_in_size = tu_edpt_packet_size(desc_endpoint); + p_audio->ep_in_interval = desc_endpoint->bInterval; + desc_cb.desc_ep_in = desc_endpoint; // Save to per-AS storage if (as_entry_idx < CFG_TUH_AUDIO_MAX_AS) { audioh_as_t *as = &p_audio->as[as_entry_idx]; - as->ep_addr = p_ep->bEndpointAddress; - as->ep_size = tu_edpt_packet_size(p_ep); + as->ep_addr = desc_endpoint->bEndpointAddress; + as->ep_size = tu_edpt_packet_size(desc_endpoint); as->ep_dir = TUSB_DIR_IN; as->format_type = tmp_format_type; as->num_channels = tmp_num_channels; @@ -413,15 +413,15 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface } } } else { - p_audio->ep_out = p_ep->bEndpointAddress; - p_audio->ep_out_size = tu_edpt_packet_size(p_ep); - p_audio->ep_out_interval = p_ep->bInterval; - desc_cb.desc_ep_out = p_ep; + p_audio->ep_out = desc_endpoint->bEndpointAddress; + p_audio->ep_out_size = tu_edpt_packet_size(desc_endpoint); + p_audio->ep_out_interval = desc_endpoint->bInterval; + desc_cb.desc_ep_out = desc_endpoint; // Save to per-AS storage if (as_entry_idx < CFG_TUH_AUDIO_MAX_AS) { audioh_as_t *as = &p_audio->as[as_entry_idx]; - as->ep_addr = p_ep->bEndpointAddress; - as->ep_size = tu_edpt_packet_size(p_ep); + as->ep_addr = desc_endpoint->bEndpointAddress; + as->ep_size = tu_edpt_packet_size(desc_endpoint); as->ep_dir = TUSB_DIR_OUT; as->format_type = tmp_format_type; as->num_channels = tmp_num_channels; @@ -435,7 +435,7 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface } } } - TU_ASSERT(tuh_edpt_open(dev_addr, p_ep), 0); + TU_ASSERT(tuh_edpt_open(dev_addr, desc_endpoint), 0); } break; } @@ -449,7 +449,7 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface continue; } p_audio->itf_count++; - } else if (itf->bInterfaceClass == TUSB_CLASS_AUDIO && itf->bInterfaceSubClass == AUDIO_SUBCLASS_CONTROL) { + } else if (desc_interface->bInterfaceClass == TUSB_CLASS_AUDIO && desc_interface->bInterfaceSubClass == AUDIO_SUBCLASS_CONTROL) { // Another Audio Control interface (shouldn't happen in normal UAC 1.0) p_audio->itf_count++; } @@ -594,17 +594,17 @@ bool tuh_audio_itf_get_info(uint8_t idx, tuh_itf_info_t *info) { info->daddr = p_audio->daddr; // re-construct descriptor - tusb_desc_interface_t *desc = &info->desc; - desc->bLength = sizeof(tusb_desc_interface_t); - desc->bDescriptorType = TUSB_DESC_INTERFACE; - - desc->bInterfaceNumber = p_audio->bInterfaceNumber; - desc->bAlternateSetting = 0; - desc->bNumEndpoints = (uint8_t)((p_audio->ep_in ? 1u : 0u) + (p_audio->ep_out ? 1u : 0u)); - desc->bInterfaceClass = TUSB_CLASS_AUDIO; - desc->bInterfaceSubClass = AUDIO_SUBCLASS_CONTROL; - desc->bInterfaceProtocol = 0; - desc->iInterface = p_audio->iInterface; + tusb_desc_interface_t *desc_interface = &info->desc; + desc_interface->bLength = sizeof(tusb_desc_interface_t); + desc_interface->bDescriptorType = TUSB_DESC_INTERFACE; + + desc_interface->bInterfaceNumber = p_audio->bInterfaceNumber; + desc_interface->bAlternateSetting = 0; + desc_interface->bNumEndpoints = (uint8_t)((p_audio->ep_in ? 1u : 0u) + (p_audio->ep_out ? 1u : 0u)); + desc_interface->bInterfaceClass = TUSB_CLASS_AUDIO; + desc_interface->bInterfaceSubClass = AUDIO_SUBCLASS_CONTROL; + desc_interface->bInterfaceProtocol = 0; + desc_interface->iInterface = p_audio->iInterface; return true; } diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h index 6dd44f830..5c5a8b3df 100644 --- a/src/class/audio/audio_host.h +++ b/src/class/audio/audio_host.h @@ -1,25 +1,6 @@ /* - * The MIT License (MIT) - * - * Copyright (c) 2025 TinyUSB contributors - * - * 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. + * SPDX-FileCopyrightText: Copyright (c) 2026 Zhenjiang Zhang + * SPDX-License-Identifier: MIT * * This file is part of the TinyUSB stack. */ -- cgit v1.3.1 From e9578eb103a90d5ab059a067b2e17ca1ddcedee5 Mon Sep 17 00:00:00 2001 From: "Zhang, Zhenjiang" Date: Tue, 21 Jul 2026 11:23:27 +0800 Subject: Refactor TUH_AUDIO API and simplify multi-AS interface support This commit refactors the TUH_AUDIO (USB Audio Host) class driver to simplify its public API and improve multi-AS (Audio Streaming) interface support. The changes are focused on three files: the core driver (audio_host.c/h) and the example application (audio_app.c). Key changes in src/class/audio/audio_host.h: - Remove tuh_audio_descriptor_cb_t and tuh_audio_mount_cb_t structures. The mount callback no longer passes a large descriptor-info struct; applications query per-AS info via tuh_audio_as_get_info(). - Add tuh_audio_get_dev_addr() and tuh_audio_get_feature_unit_id() accessors to retrieve device address and feature-unit ID from an interface index. - Simplify control-transfer APIs by replacing (daddr, itf_num, unit_id) parameters with a single idx parameter: tuh_audio_set_sampling_freq(idx, as_idx, ...) tuh_audio_get_sampling_freq(idx, as_idx, ...) tuh_audio_feature_unit_set(idx, control_selector, channel, ...) tuh_audio_feature_unit_get(idx, control_selector, channel, ...) - Add synchronous wrapper APIs using TU_API_SYNC macro: tuh_audio_get_sampling_freq_sync() tuh_audio_set_sampling_freq_sync() tuh_audio_feature_unit_set_sync() tuh_audio_feature_unit_get_sync() - Update isochronous endpoint APIs to use (idx, as_idx) instead of (daddr, idx): tuh_audio_receive(idx, as_idx, buffer, len) tuh_audio_send(idx, as_idx, buffer, len) - Remove tuh_audio_descriptor_cb() weak callback. - Update tuh_audio_mount_cb() signature from mount_cb(param) to no param. - Update tuh_audio_rx_cb()/tuh_audio_tx_cb() first parameter from idx to dev_addr for consistency with other class drivers. Key changes in src/class/audio/audio_host.c: - Delete tuh_audio_descriptor_cb weak stub. - Refactor get_idx_by_ep_addr() to iterate all AS interfaces per device instead of relying on single ep_in/ep_out fields. - Add audioh_get_ep_addr_by_dir() helper to find an endpoint address by direction across multiple AS interfaces. - Simplify audioh_close() cleanup: remove now-removed single-endpoint fields (ep_in, ep_out) and rely on tu_memclr(p_audio->as, ...). - Update audioh_xfer_cb() to pass dev_addr (not idx) to rx/tx callbacks, matching the new callback signature. - Simplify audioh_open(): remove descriptor-callback emission and the temporary desc_cb structure; store only ac_itf_num instead of bInterfaceNumber + iInterface + as_interface_num. - Rename local descriptor pointers for clarity: desc_input_terminal (was desc_it) desc_output_terminal (was desc_ot) Key changes in examples/host/audio_host/src/audio_app.c: - Remove now-unnecessary globals: audio_ep_in, audio_ep_out, audio_ac_itf, audio_feature_unit_id. - Initialize audio_dev_addr, audio_idx, audiostream_in_idx, audiostream_out_idx to 0xFF (TUSB_INDEX_INVALID_8) instead of 0. - Update print_as_interfaces() to use tuh_audio_as_get_count() and tuh_audio_as_get_info() instead of accessing mount_cb_data. - Update all callback signatures and API calls to match the new driver API. --- examples/host/audio_host/src/audio_app.c | 192 +++++---- src/class/audio/audio_host.c | 681 ++++++++++++++----------------- src/class/audio/audio_host.h | 163 +++----- 3 files changed, 452 insertions(+), 584 deletions(-) (limited to 'src') diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c index d264426fd..d9134f089 100644 --- a/examples/host/audio_host/src/audio_app.c +++ b/examples/host/audio_host/src/audio_app.c @@ -23,18 +23,16 @@ // MACRO TYPEDEF CONSTANT ENUM DECLARATION //--------------------------------------------------------------------+ -static bool audio_mounted = false; -static uint8_t audio_dev_addr = 0; -static uint8_t audio_idx = 0; -static uint8_t audio_ep_in = 0; -static uint8_t audio_ep_out = 0; -static uint32_t sampling_freq = 48000; // Default sampling frequency (Hz) -static uint8_t audio_mic_channels = 1; -static volatile bool audio_rx_busy = false; // Track IN endpoint transfer state -static volatile bool audio_tx_busy = false; // Track OUT endpoint transfer state -static volatile bool audio_ready = false; // Wait for sampling freq set before starting isochronous transfer -static uint8_t audio_ac_itf = 0; // Audio Control interface number -static uint8_t audio_feature_unit_id = 0; // Feature Unit ID +static bool audio_mounted = false; +static uint8_t audio_dev_addr = 0xFF; +static volatile bool audio_ready = false; // Wait for sampling freq set before starting isochronous transfer +static volatile bool audio_rx_busy = false; // Track IN endpoint transfer state +static volatile bool audio_tx_busy = false; // Track OUT endpoint transfer state +static uint8_t audio_idx = 0xFF; +static uint8_t audiostream_in_idx = 0xFF; +static uint8_t audiostream_out_idx = 0xFF; +static uint32_t sampling_freq = 48000; // Default sampling frequency (Hz) +static uint8_t audio_mic_channels = 1; static uint8_t audio_rx_buffer[CFG_TUH_AUDIO_EPIN_BUFSIZE] __attribute__((aligned(4))); static uint8_t audio_tx_buffer[CFG_TUH_AUDIO_EPOUT_BUFSIZE] __attribute__((aligned(4))); @@ -69,55 +67,27 @@ static void print_sampling_freq(const tuh_audio_as_info_t *as) { } // Print all AS interface info -static void print_as_interfaces(const tuh_audio_mount_cb_t *mount_cb_data) { - for (uint8_t i = 0; i < mount_cb_data->as_count; i++) { - const tuh_audio_as_info_t *as = &mount_cb_data->as_info[i]; - if (as->ep_dir == TUSB_DIR_IN) { +static void print_as_interfaces(uint8_t idx) { + tuh_audio_as_info_t as = {}; + uint8_t as_count = tuh_audio_as_get_count(idx); + for (uint8_t i = 0; i < as_count; i++) { + tuh_audio_as_get_info(idx, i, &as); + if (as.ep_dir == TUSB_DIR_IN) { // Save microphone channel count for mono-to-stereo conversion - audio_mic_channels = as->num_channels; + audio_mic_channels = as.num_channels; printf(" --- Microphone (AS %u) ---\r\n", i); - printf(" IN EP: 0x%02x (max size: %u)\r\n", as->ep_addr, as->ep_size); + printf(" IN EP: 0x%02x (max size: %u)\r\n", as.ep_addr, as.ep_size); } else { printf(" --- Speaker (AS %u) ---\r\n", i); - printf(" OUT EP: 0x%02x (max size: %u)\r\n", as->ep_addr, as->ep_size); + printf(" OUT EP: 0x%02x (max size: %u)\r\n", as.ep_addr, as.ep_size); } - printf(" Interface: %u, Alt: %u\r\n", as->interface_num, as->alt_setting); - printf(" Format Type: %u, Channels: %u, SubFrameSize: %u, BitResolution: %u\r\n", as->format_type, - as->num_channels, as->sub_frame_size, as->bit_resolution); - print_sampling_freq(as); + printf(" Interface: %u, Alt: %u\r\n", as.interface_num, as.alt_setting); + printf(" Format Type: %u, Channels: %u, SubFrameSize: %u, BitResolution: %u\r\n", as.format_type, + as.num_channels, as.sub_frame_size, as.bit_resolution); + print_sampling_freq(&as); } } -// Find IN and OUT endpoints from AS interfaces, return IN sampling freq -static uint32_t find_audio_endpoints(const tuh_audio_mount_cb_t *mount_cb_data) { - uint32_t in_sam_freq = 0; - audio_ep_in = 0; - audio_ep_out = 0; - - for (uint8_t i = 0; i < mount_cb_data->as_count; i++) { - const tuh_audio_as_info_t *as = &mount_cb_data->as_info[i]; - if (as->ep_dir == TUSB_DIR_IN) { - audio_ep_in = as->ep_addr; - if (as->sam_freq_type > 0) { - in_sam_freq = as->sam_freq[0]; - } - } else { - audio_ep_out = as->ep_addr; - } - } - return in_sam_freq; -} - -// Set Feature Unit volume to un-mute -static void set_feature_unit_volume(void) { - if (audio_feature_unit_id == 0) { - return; - } - printf(" Setting Feature Unit %u volume to 0x0600\r\n", audio_feature_unit_id); - tuh_audio_feature_unit_set(audio_dev_addr, audio_ac_itf, audio_feature_unit_id, AUDIO10_FU_CTRL_VOLUME, 0, 0x0600, - NULL, 0); -} - //--------------------------------------------------------------------+ // Application Task //--------------------------------------------------------------------+ @@ -127,7 +97,7 @@ void audio_app_task(void) { } if (!audio_rx_busy) { - if (tuh_audio_receive(audio_dev_addr, audio_idx, audio_rx_buffer, CFG_TUH_AUDIO_EPIN_BUFSIZE)) { + if (tuh_audio_receive(audio_idx, audiostream_in_idx, audio_rx_buffer, CFG_TUH_AUDIO_EPIN_BUFSIZE)) { audio_rx_busy = true; } } @@ -138,82 +108,102 @@ void audio_app_task(void) { //--------------------------------------------------------------------+ -// Callback after IN sampling frequency is set -static void in_sampling_freq_set_cb(tuh_xfer_t *xfer) { - if (xfer->result != XFER_RESULT_SUCCESS) { - printf(" Sampling frequency set FAILED: result=%u\r\n", xfer->result); - return; - } - printf(" Sampling frequency set OK, ready for isochronous transfer\r\n"); - // Set Feature Unit volume to un-mute - set_feature_unit_volume(); - // Set OUT sampling frequency then send empty packet to kick-start device - tuh_audio_set_sampling_freq(audio_dev_addr, audio_ep_out, sampling_freq, NULL, 0); - audio_ready = true; -} - -void tuh_audio_mount_cb(uint8_t idx, const tuh_audio_mount_cb_t *mount_cb_data) { - if (!mount_cb_data) { +void tuh_audio_mount_cb(uint8_t idx) { + if (idx >= CFG_TUH_AUDIO_MAX) { + printf("Audio device mount failed: idx=%u exceeds max=%u\r\n", idx, CFG_TUH_AUDIO_MAX); return; } - printf("Audio device mounted: idx=%u, daddr=%u, AS count=%u\r\n", idx, mount_cb_data->daddr, mount_cb_data->as_count); - - print_as_interfaces(mount_cb_data); - - // Feature Unit - if (mount_cb_data->feature_unit_id != 0) { - printf(" Feature Unit: ID=%u, SourceID=%u\r\n", mount_cb_data->feature_unit_id, - mount_cb_data->feature_unit_source_id); - } + print_as_interfaces(idx); // Save device info - audio_dev_addr = mount_cb_data->daddr; - audio_idx = idx; - audio_mounted = true; - audio_ac_itf = mount_cb_data->bInterfaceNumber; - audio_feature_unit_id = mount_cb_data->feature_unit_id; + audio_dev_addr = tuh_audio_get_dev_addr(idx); + audio_idx = idx; + audio_mounted = true; // Find endpoints and IN sampling frequency - uint32_t in_sam_freq = find_audio_endpoints(mount_cb_data); + tuh_audio_as_info_t as; + for (uint8_t i = 0; i < tuh_audio_as_get_count(idx); i++) { + + tuh_audio_as_get_info(idx, i, &as); + if (as.ep_dir == TUSB_DIR_IN) { + audiostream_in_idx = i; + if (as.sam_freq_type > 0) { + sampling_freq = as.sam_freq[0]; + } + } else { + audiostream_out_idx = i; + } + } // Set IN sampling frequency before starting isochronous transfer - if (audio_ep_in != 0 && in_sam_freq != 0) { - sampling_freq = in_sam_freq; + if (audiostream_in_idx != 0xFF && sampling_freq != 0) { printf(" Setting IN sampling frequency to %lu Hz\r\n", (unsigned long)sampling_freq); - tuh_audio_set_sampling_freq(mount_cb_data->daddr, audio_ep_in, sampling_freq, in_sampling_freq_set_cb, 0); + // tuh_audio_set_sampling_freq(audio_idx, audiostream_in_idx, sampling_freq, in_sampling_freq_set_cb, 0); + + tusb_xfer_result_t result; + result = tuh_audio_set_sampling_freq_sync(audio_idx, audiostream_in_idx, sampling_freq); + if (result == XFER_RESULT_SUCCESS) { + tuh_audio_get_sampling_freq_sync(audio_idx, audiostream_in_idx, &sampling_freq); + printf(" IN sampling frequency set to %lu Hz\r\n", (unsigned long)sampling_freq); + if (audiostream_out_idx != 0xFF) { + printf(" Setting OUT sampling frequency to %lu Hz\r\n", (unsigned long)sampling_freq); + result = tuh_audio_set_sampling_freq_sync(audio_idx, audiostream_out_idx, sampling_freq); + if (result == XFER_RESULT_SUCCESS) { + tuh_audio_get_sampling_freq_sync(audio_idx, audiostream_out_idx, &sampling_freq); + printf(" OUT sampling frequency set to %lu Hz\r\n", (unsigned long)sampling_freq); + } else { + printf(" Setting OUT sampling frequency FAILED: result=%u\r\n", result); + } + } + } else { + printf(" Setting IN sampling frequency FAILED: result=%u\r\n", result); + } + uint16_t volume = 0x0600; + + result = tuh_audio_feature_unit_set_sync(audio_idx, AUDIO10_FU_CTRL_VOLUME, 0, volume); + if (result == XFER_RESULT_SUCCESS) { + printf(" Feature Unit volume set:volume 0x%04x\r\n", (unsigned int)volume); + tuh_audio_feature_unit_get_sync(audio_idx, AUDIO10_FU_CTRL_VOLUME, 0, &volume); + printf(" Feature Unit volume get: 0x%04x\r\n", (unsigned int)volume); + } else { + printf(" Setting Feature Unit volume FAILED: result=%u\r\n", result); + } } + audio_ready = true; } // Invoked when device with Audio interface is un-mounted void tuh_audio_umount_cb(uint8_t idx) { printf("Audio device unmounted: idx=%u\r\n", idx); if (audio_mounted && audio_idx == idx) { - audio_mounted = false; - audio_ready = false; - audio_rx_busy = false; - audio_tx_busy = false; - audio_dev_addr = 0; - audio_idx = 0; + audio_mounted = false; + audio_ready = false; + audio_rx_busy = false; + audio_tx_busy = false; + audio_dev_addr = 0; + audio_idx = 0; + audiostream_in_idx = 0xFF; + audiostream_out_idx = 0xFF; } } // Invoked when an isochronous IN transfer is complete -void tuh_audio_rx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes) { - (void)idx; +void tuh_audio_rx_cb(uint8_t dev_addr, uint8_t ep_addr, uint16_t xferred_bytes) { + (void)dev_addr; (void)ep_addr; audio_rx_busy = false; - if (xferred_bytes > 0 && audio_ep_out != 0 && !audio_tx_busy) { + if (xferred_bytes > 0 && audiostream_out_idx != 0xFF && !audio_tx_busy) { bool ok; if (audio_mic_channels == 1) { // Mono microphone, convert to stereo and send to OUT endpoint uint16_t samples = xferred_bytes / 2; mono_to_stereo(audio_rx_buffer, audio_tx_buffer, samples); - ok = tuh_audio_send(audio_dev_addr, audio_idx, audio_tx_buffer, xferred_bytes * 2); + ok = tuh_audio_send(audio_idx, audiostream_out_idx, audio_tx_buffer, xferred_bytes * 2); } else { // Stereo microphone, send directly to OUT endpoint - ok = tuh_audio_send(audio_dev_addr, audio_idx, audio_rx_buffer, xferred_bytes); + ok = tuh_audio_send(audio_idx, audiostream_out_idx, audio_rx_buffer, xferred_bytes); } if (ok) { @@ -223,8 +213,8 @@ void tuh_audio_rx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes) { } // Invoked when an isochronous OUT transfer is complete -void tuh_audio_tx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes) { - (void)idx; +void tuh_audio_tx_cb(uint8_t dev_addr, uint8_t ep_addr, uint16_t xferred_bytes) { + (void)dev_addr; (void)ep_addr; (void)xferred_bytes; audio_tx_busy = false; diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 396f75987..c74a95d48 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -4,7 +4,7 @@ * * This file is part of the TinyUSB stack. */ - + /* * This driver implements a USB Audio Host (UAC 1.0) class driver. * It supports multiple Audio Streaming (AS) interfaces with independent format storage. @@ -28,107 +28,70 @@ #if (CFG_TUH_ENABLED && CFG_TUH_AUDIO) -#include "host/usbh.h" -#include "host/usbh_pvt.h" -#include "audio_host.h" + #include "host/usbh.h" + #include "host/usbh_pvt.h" + #include "audio_host.h" -// Level where CFG_TUSB_DEBUG must be at least for this driver is logged -#ifndef CFG_TUH_AUDIO_LOG_LEVEL - #define CFG_TUH_AUDIO_LOG_LEVEL CFG_TUH_LOG_LEVEL -#endif + // Level where CFG_TUSB_DEBUG must be at least for this driver is logged + #ifndef CFG_TUH_AUDIO_LOG_LEVEL + #define CFG_TUH_AUDIO_LOG_LEVEL CFG_TUH_LOG_LEVEL + #endif -#define TU_LOG_DRV(...) TU_LOG(CFG_TUH_AUDIO_LOG_LEVEL, __VA_ARGS__) + #define TU_LOG_DRV(...) TU_LOG(CFG_TUH_AUDIO_LOG_LEVEL, __VA_ARGS__) //--------------------------------------------------------------------+ // Weak stubs: invoked if no strong implementation is available //--------------------------------------------------------------------+ -TU_ATTR_WEAK void tuh_audio_descriptor_cb(uint8_t idx, const tuh_audio_descriptor_cb_t *desc_cb_data) { - (void) idx; - (void) desc_cb_data; -} -TU_ATTR_WEAK void tuh_audio_mount_cb(uint8_t idx, const tuh_audio_mount_cb_t *mount_cb_data) { - (void) idx; - (void) mount_cb_data; + +TU_ATTR_WEAK void tuh_audio_mount_cb(uint8_t idx) { + (void)idx; } TU_ATTR_WEAK void tuh_audio_umount_cb(uint8_t idx) { - (void) idx; + (void)idx; } TU_ATTR_WEAK void tuh_audio_rx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes) { - (void) idx; - (void) ep_addr; - (void) xferred_bytes; + (void)idx; + (void)ep_addr; + (void)xferred_bytes; } TU_ATTR_WEAK void tuh_audio_tx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes) { - (void) idx; - (void) ep_addr; - (void) xferred_bytes; + (void)idx; + (void)ep_addr; + (void)xferred_bytes; } //--------------------------------------------------------------------+ // MACRO CONSTANT TYPEDEF //--------------------------------------------------------------------+ -// Per-AS interface internal storage -typedef struct { - uint8_t interface_num; - uint8_t alt_setting; - uint8_t ep_addr; - uint16_t ep_size; - uint8_t ep_dir; - - uint8_t format_type; - uint8_t num_channels; - uint8_t sub_frame_size; - uint8_t bit_resolution; - uint8_t sam_freq_type; - uint32_t sam_freq[CFG_TUH_AUDIO_MAX_SAM_FREQ]; - uint32_t sam_freq_lower; - uint32_t sam_freq_upper; -} audioh_as_t; +// Per-interface storage typedef struct { - uint8_t daddr; - uint8_t bInterfaceNumber; // Audio Control interface number - uint8_t iInterface; - uint8_t itf_count; // number of interfaces (AC + AS) - - // Audio Streaming Interface - uint8_t as_interface_num; // Audio Streaming interface number - uint8_t alt_setting; // current alt setting + uint8_t daddr; // device address + uint8_t ac_itf_num; // Audio Control interface number + uint8_t itf_count; // number of interfaces (AC + AS) // Terminal info (from Audio Control Interface) - uint16_t input_terminal_type; // wTerminalType of Input Terminal - uint8_t input_terminal_id; // bTerminalID of Input Terminal - uint8_t input_terminal_channels; // bNrChannels of Input Terminal - uint16_t output_terminal_type; // wTerminalType of Output Terminal - uint8_t output_terminal_id; // bTerminalID of Output Terminal + uint16_t input_terminal_type; // wTerminalType of Input Terminal + uint8_t input_terminal_id; // bTerminalID of Input Terminal + uint8_t input_terminal_channels; // bNrChannels of Input Terminal + uint16_t output_terminal_type; // wTerminalType of Output Terminal + uint8_t output_terminal_id; // bTerminalID of Output Terminal // Feature Unit info - uint8_t feature_unit_id; // bUnitID of Feature Unit (0 = none) - uint8_t feature_unit_source_id; // bSourceID of Feature Unit - - // Isochronous IN endpoint - uint8_t ep_in; - uint16_t ep_in_size; - uint16_t ep_in_interval; - - // Isochronous OUT endpoint - uint8_t ep_out; - uint16_t ep_out_size; - uint16_t ep_out_interval; + uint8_t feature_unit_id; // bUnitID of Feature Unit (0 = none) + uint8_t feature_unit_source_id; // bSourceID of Feature Unit // Multiple AS interfaces support - uint8_t as_interfaces[CFG_TUH_AUDIO_MAX_AS]; - uint8_t as_alt_settings[CFG_TUH_AUDIO_MAX_AS]; uint8_t as_count; uint8_t as_set_idx; // Per-AS interface independent storage (new) - audioh_as_t as[CFG_TUH_AUDIO_MAX_AS]; + tuh_audio_as_info_t as[CFG_TUH_AUDIO_MAX_AS]; // Array of Audio Streaming interface info structures bool mounted; } audioh_interface_t; @@ -140,7 +103,7 @@ typedef struct { } audioh_epbuf_t; static audioh_interface_t _audioh_itf[CFG_TUH_AUDIO_MAX]; - static audioh_epbuf_t _audioh_epbuf[CFG_TUH_AUDIO_MAX]; +static audioh_epbuf_t _audioh_epbuf[CFG_TUH_AUDIO_MAX]; //--------------------------------------------------------------------+ // Helper //--------------------------------------------------------------------+ @@ -156,14 +119,28 @@ TU_ATTR_ALWAYS_INLINE static inline uint8_t find_new_audio_index(void) { static inline uint8_t get_idx_by_ep_addr(uint8_t daddr, uint8_t ep_addr) { for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { const audioh_interface_t *p_audio = &_audioh_itf[idx]; - if ((p_audio->daddr == daddr) && - (ep_addr == p_audio->ep_in || ep_addr == p_audio->ep_out)) { - return idx; + if (p_audio->daddr == daddr) { + for (uint8_t as_idx = 0; as_idx < p_audio->as_count; as_idx++) { + if (p_audio->as[as_idx].ep_addr == ep_addr) { + return idx; + } + } } } return TUSB_INDEX_INVALID_8; } +static uint8_t audioh_get_ep_addr_by_dir(const audioh_interface_t *p_audio, uint8_t dir) { + for (uint8_t as_idx = 0; as_idx < p_audio->as_count; as_idx++) { + const tuh_audio_as_info_t *as = &p_audio->as[as_idx]; + if (as->ep_addr != 0 && as->ep_dir == dir) { + return as->ep_addr; + } + } + + return 0; +} + //--------------------------------------------------------------------+ // USBH API //--------------------------------------------------------------------+ @@ -183,32 +160,22 @@ void audioh_close(uint8_t daddr) { TU_LOG_DRV(" AUDIO close addr = %u index = %u\r\n", daddr, idx); tuh_audio_umount_cb(idx); - p_audio->bInterfaceNumber = 0; - p_audio->as_interface_num = 0; - p_audio->alt_setting = 0; - p_audio->daddr = 0; - p_audio->mounted = false; - p_audio->ep_in = 0; - p_audio->ep_out = 0; - p_audio->as_count = 0; + p_audio->ac_itf_num = 0; + p_audio->daddr = 0; + p_audio->mounted = false; + p_audio->as_count = 0; p_audio->as_set_idx = 0; - tu_memclr(p_audio->as_interfaces, sizeof(p_audio->as_interfaces)); - tu_memclr(p_audio->as_alt_settings, sizeof(p_audio->as_alt_settings)); tu_memclr(p_audio->as, sizeof(p_audio->as)); } } } bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { - (void) result; - const uint8_t idx = get_idx_by_ep_addr(dev_addr, ep_addr); - TU_VERIFY(idx < CFG_TUH_AUDIO_MAX); - audioh_interface_t *p_audio = &_audioh_itf[idx]; - - if (ep_addr == p_audio->ep_in) { - tuh_audio_rx_cb(idx, ep_addr, (uint16_t) xferred_bytes); - } else if (ep_addr == p_audio->ep_out) { - tuh_audio_tx_cb(idx, ep_addr, (uint16_t) xferred_bytes); + (void)result; + if (tu_edpt_dir(ep_addr) == TUSB_DIR_IN) { + tuh_audio_rx_cb(dev_addr, ep_addr, (uint16_t)xferred_bytes); + } else { + tuh_audio_tx_cb(dev_addr, ep_addr, (uint16_t)xferred_bytes); } return true; @@ -218,29 +185,24 @@ bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uin // Enumeration //--------------------------------------------------------------------+ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface_t *desc_itf, uint16_t max_len) { - (void) rhport; + (void)rhport; TU_VERIFY(TUSB_CLASS_AUDIO == desc_itf->bInterfaceClass, 0); TU_VERIFY(AUDIO_SUBCLASS_CONTROL == desc_itf->bInterfaceSubClass, 0); const uint8_t *desc_start = (const uint8_t *)desc_itf; - const uint8_t *p_desc = desc_start; - const uint8_t *desc_end = desc_start + max_len; + const uint8_t *p_desc = desc_start; + const uint8_t *desc_end = desc_start + max_len; const uint8_t idx = find_new_audio_index(); TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, 0); audioh_interface_t *p_audio = &_audioh_itf[idx]; - p_audio->itf_count = 0; - - tuh_audio_descriptor_cb_t desc_cb = { 0 }; + p_audio->itf_count = 0; // Parse Audio Control Interface TU_LOG_DRV("AUDIO opening AC Interface %u (addr = %u)\r\n", desc_itf->bInterfaceNumber, dev_addr); - p_audio->bInterfaceNumber = desc_itf->bInterfaceNumber; - p_audio->iInterface = desc_itf->iInterface; - p_audio->itf_count = 1; - desc_cb.desc_ac_interface = desc_itf; - desc_cb.ac_interface_num = desc_itf->bInterfaceNumber; + p_audio->ac_itf_num = desc_itf->bInterfaceNumber; + p_audio->itf_count = 1; // Parse Audio Control interface descriptors (Input Terminal, Output Terminal, Feature Unit, etc.) p_desc = tu_desc_next(p_desc); @@ -248,26 +210,26 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface if (tu_desc_type(p_desc) == TUSB_DESC_CS_INTERFACE) { switch (tu_desc_subtype(p_desc)) { case AUDIO10_CS_AC_INTERFACE_INPUT_TERMINAL: { - audio10_desc_input_terminal_t const *desc_input_terminal = (audio10_desc_input_terminal_t const *)p_desc; - p_audio->input_terminal_type = tu_le16toh(desc_input_terminal->wTerminalType); - p_audio->input_terminal_id = desc_input_terminal->bTerminalID; - p_audio->input_terminal_channels = desc_input_terminal->bNrChannels; - TU_LOG_DRV(" Input Terminal: ID=%u, Type=0x%04x, Channels=%u\r\n", - desc_input_terminal->bTerminalID, tu_le16toh(desc_input_terminal->wTerminalType), desc_input_terminal->bNrChannels); + const audio10_desc_input_terminal_t *desc_input_terminal = (const audio10_desc_input_terminal_t *)p_desc; + p_audio->input_terminal_type = tu_le16toh(desc_input_terminal->wTerminalType); + p_audio->input_terminal_id = desc_input_terminal->bTerminalID; + p_audio->input_terminal_channels = desc_input_terminal->bNrChannels; + TU_LOG_DRV(" Input Terminal: ID=%u, Type=0x%04x, Channels=%u\r\n", desc_input_terminal->bTerminalID, + tu_le16toh(desc_input_terminal->wTerminalType), desc_input_terminal->bNrChannels); break; } case AUDIO10_CS_AC_INTERFACE_OUTPUT_TERMINAL: { - audio10_desc_output_terminal_t const *desc_output_terminal = (audio10_desc_output_terminal_t const *)p_desc; - p_audio->output_terminal_type = tu_le16toh(desc_output_terminal->wTerminalType); - p_audio->output_terminal_id = desc_output_terminal->bTerminalID; - TU_LOG_DRV(" Output Terminal: ID=%u, Type=0x%04x\r\n", - desc_output_terminal->bTerminalID, tu_le16toh(desc_output_terminal->wTerminalType)); + const audio10_desc_output_terminal_t *desc_output_terminal = (const audio10_desc_output_terminal_t *)p_desc; + p_audio->output_terminal_type = tu_le16toh(desc_output_terminal->wTerminalType); + p_audio->output_terminal_id = desc_output_terminal->bTerminalID; + TU_LOG_DRV(" Output Terminal: ID=%u, Type=0x%04x\r\n", desc_output_terminal->bTerminalID, + tu_le16toh(desc_output_terminal->wTerminalType)); break; } case AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT: { - uint8_t const *desc_feature_unit = p_desc; - p_audio->feature_unit_id = desc_feature_unit[3]; // bUnitID - p_audio->feature_unit_source_id = desc_feature_unit[4]; // bSourceID + const uint8_t *desc_feature_unit = p_desc; + p_audio->feature_unit_id = desc_feature_unit[3]; // bUnitID + p_audio->feature_unit_source_id = desc_feature_unit[4]; // bSourceID TU_LOG_DRV(" Feature Unit: ID=%u, SourceID=%u\r\n", desc_feature_unit[3], desc_feature_unit[4]); break; } @@ -281,40 +243,31 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface // Parse all remaining descriptors in this configuration looking for Audio Streaming interfaces while (tu_desc_in_bounds(p_desc, desc_end)) { if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) { - tusb_desc_interface_t const *desc_interface = (tusb_desc_interface_t const *)p_desc; - // Stop at the first non-Audio interface so we don't claim the rest of the configuration - if (desc_interface->bInterfaceClass != TUSB_CLASS_AUDIO) break; - if (desc_interface->bInterfaceSubClass == AUDIO_SUBCLASS_STREAMING) { + const tusb_desc_interface_t *desc_interface = (const tusb_desc_interface_t *)p_desc; + // Stop at the first non-Audio interface so we don't claim the rest of the configuration + if (desc_interface->bInterfaceClass != TUSB_CLASS_AUDIO) { + break; + } + if (desc_interface->bInterfaceSubClass == AUDIO_SUBCLASS_STREAMING) { // Found Audio Streaming Interface - TU_LOG_DRV(" Found AS Interface %u (alt = %u)\r\n", desc_interface->bInterfaceNumber, desc_interface->bAlternateSetting); + TU_LOG_DRV(" Found AS Interface %u (alt = %u)\r\n", desc_interface->bInterfaceNumber, + desc_interface->bAlternateSetting); if (desc_interface->bAlternateSetting == 0) { // Interface descriptor with alt setting 0 (no endpoints) - // Add to AS interfaces array + // Add to AS entries if (p_audio->as_count < CFG_TUH_AUDIO_MAX_AS) { - p_audio->as_interface_num = desc_interface->bInterfaceNumber; - p_audio->as_interfaces[p_audio->as_count] = desc_interface->bInterfaceNumber; - desc_cb.desc_as_interface = desc_interface; - desc_cb.as_interface_num = desc_interface->bInterfaceNumber; - // Create new AS entry for per-interface storage p_audio->as[p_audio->as_count].interface_num = desc_interface->bInterfaceNumber; - p_audio->as[p_audio->as_count].alt_setting = 0; + p_audio->as[p_audio->as_count].alt_setting = 0; p_audio->as_count++; + } else { + TU_LOG_DRV(" Skip AS Interface %u: reach CFG_TUH_AUDIO_MAX_AS=%u\r\n", desc_interface->bInterfaceNumber, + CFG_TUH_AUDIO_MAX_AS); } } else if (desc_interface->bNumEndpoints > 0) { // Interface descriptor with alt setting > 0 (has endpoints) - // Find matching AS interface and set alt_setting + // Find matching AS entry and set alt_setting uint8_t as_entry_idx = CFG_TUH_AUDIO_MAX_AS; - for (uint8_t as_idx = 0; as_idx < p_audio->as_count; as_idx++) { - if (p_audio->as_interfaces[as_idx] == desc_interface->bInterfaceNumber) { - p_audio->alt_setting = desc_interface->bAlternateSetting; - p_audio->as_alt_settings[as_idx] = desc_interface->bAlternateSetting; - desc_cb.alt_setting = desc_interface->bAlternateSetting; - desc_cb.desc_as_interface_alt = desc_interface; - break; - } - } - // Find or create AS entry for per-interface storage for (uint8_t i = 0; i < p_audio->as_count; i++) { if (p_audio->as[i].interface_num == desc_interface->bInterfaceNumber) { as_entry_idx = i; @@ -322,7 +275,7 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface } } if (as_entry_idx >= CFG_TUH_AUDIO_MAX_AS && p_audio->as_count < CFG_TUH_AUDIO_MAX_AS) { - as_entry_idx = p_audio->as_count; + as_entry_idx = p_audio->as_count; p_audio->as[as_entry_idx].interface_num = desc_interface->bInterfaceNumber; p_audio->as_count++; } @@ -333,48 +286,49 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface // Parse the interface's descriptors p_desc = tu_desc_next(p_desc); // Temporary variables to hold format info until endpoint direction is known - uint8_t tmp_format_type = 0; - uint8_t tmp_num_channels = 0; - uint8_t tmp_sub_frame_size = 0; - uint8_t tmp_bit_resolution = 0; - uint8_t tmp_sam_freq_type = 0; + uint8_t tmp_format_type = 0; + uint8_t tmp_num_channels = 0; + uint8_t tmp_sub_frame_size = 0; + uint8_t tmp_bit_resolution = 0; + uint8_t tmp_sam_freq_type = 0; uint32_t tmp_sam_freq[CFG_TUH_AUDIO_MAX_SAM_FREQ] = {0}; - uint32_t tmp_sam_freq_lower = 0; - uint32_t tmp_sam_freq_upper = 0; + uint32_t tmp_sam_freq_lower = 0; + uint32_t tmp_sam_freq_upper = 0; while (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) != TUSB_DESC_INTERFACE) { switch (tu_desc_type(p_desc)) { case TUSB_DESC_CS_INTERFACE: { switch (tu_desc_subtype(p_desc)) { case AUDIO10_CS_AS_INTERFACE_AS_GENERAL: { TU_LOG_DRV(" AS General descriptor\r\n"); - desc_cb.desc_cs_as_general = p_desc; break; } case AUDIO10_CS_AS_INTERFACE_FORMAT_TYPE: { TU_LOG_DRV(" Format Type descriptor\r\n"); - desc_cb.desc_format_type = p_desc; + TU_ASSERT(p_desc[0] >= 8, 0); // Parse UAC 1.0 Format Type I descriptor fields into temporary variables - tmp_format_type = p_desc[3]; // bFormatType - tmp_num_channels = p_desc[4]; // bNrChannels - tmp_sub_frame_size = p_desc[5]; // bSubFrameSize - tmp_bit_resolution = p_desc[6]; // bBitResolution + tmp_format_type = p_desc[3]; // bFormatType + tmp_num_channels = p_desc[4]; // bNrChannels + tmp_sub_frame_size = p_desc[5]; // bSubFrameSize + tmp_bit_resolution = p_desc[6]; // bBitResolution // Parse sampling frequencies uint8_t bLength = p_desc[0]; if (bLength >= 8) { - tmp_sam_freq_type = p_desc[7]; // bSamFreqType + tmp_sam_freq_type = p_desc[7]; // bSamFreqType if (tmp_sam_freq_type == 0) { // Continuous range: tLowerSamFreq, tUpperSamFreq (3 bytes each) if (bLength >= 14) { - tmp_sam_freq_lower = ((uint32_t)p_desc[8] | ((uint32_t)p_desc[9] << 8) | ((uint32_t)p_desc[10] << 16)); - tmp_sam_freq_upper = ((uint32_t)p_desc[11] | ((uint32_t)p_desc[12] << 8) | ((uint32_t)p_desc[13] << 16)); + tmp_sam_freq_lower = + ((uint32_t)p_desc[8] | ((uint32_t)p_desc[9] << 8) | ((uint32_t)p_desc[10] << 16)); + tmp_sam_freq_upper = + ((uint32_t)p_desc[11] | ((uint32_t)p_desc[12] << 8) | ((uint32_t)p_desc[13] << 16)); } } else { // Discrete sampling frequencies - uint8_t max_freqs = tmp_sam_freq_type < CFG_TUH_AUDIO_MAX_SAM_FREQ ? tmp_sam_freq_type : CFG_TUH_AUDIO_MAX_SAM_FREQ; + uint8_t max_freqs = tmp_sam_freq_type < CFG_TUH_AUDIO_MAX_SAM_FREQ ? tmp_sam_freq_type + : CFG_TUH_AUDIO_MAX_SAM_FREQ; for (uint8_t i = 0; i < max_freqs && (8 + i * 3 + 2) < bLength; i++) { - tmp_sam_freq[i] = ((uint32_t)p_desc[8 + i * 3] | - ((uint32_t)p_desc[9 + i * 3] << 8) | + tmp_sam_freq[i] = ((uint32_t)p_desc[8 + i * 3] | ((uint32_t)p_desc[9 + i * 3] << 8) | ((uint32_t)p_desc[10 + i * 3] << 16)); } } @@ -391,21 +345,17 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface if (desc_endpoint->bmAttributes.xfer == TUSB_XFER_ISOCHRONOUS) { TU_LOG_DRV(" Isochronous EP %02x\r\n", desc_endpoint->bEndpointAddress); if (tu_edpt_dir(desc_endpoint->bEndpointAddress) == TUSB_DIR_IN) { - p_audio->ep_in = desc_endpoint->bEndpointAddress; - p_audio->ep_in_size = tu_edpt_packet_size(desc_endpoint); - p_audio->ep_in_interval = desc_endpoint->bInterval; - desc_cb.desc_ep_in = desc_endpoint; // Save to per-AS storage if (as_entry_idx < CFG_TUH_AUDIO_MAX_AS) { - audioh_as_t *as = &p_audio->as[as_entry_idx]; - as->ep_addr = desc_endpoint->bEndpointAddress; - as->ep_size = tu_edpt_packet_size(desc_endpoint); - as->ep_dir = TUSB_DIR_IN; - as->format_type = tmp_format_type; - as->num_channels = tmp_num_channels; + tuh_audio_as_info_t *as = &p_audio->as[as_entry_idx]; + as->ep_addr = desc_endpoint->bEndpointAddress; + as->ep_size = tu_edpt_packet_size(desc_endpoint); + as->ep_dir = TUSB_DIR_IN; + as->format_type = tmp_format_type; + as->num_channels = tmp_num_channels; as->sub_frame_size = tmp_sub_frame_size; as->bit_resolution = tmp_bit_resolution; - as->sam_freq_type = tmp_sam_freq_type; + as->sam_freq_type = tmp_sam_freq_type; as->sam_freq_lower = tmp_sam_freq_lower; as->sam_freq_upper = tmp_sam_freq_upper; for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX_SAM_FREQ; i++) { @@ -413,21 +363,17 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface } } } else { - p_audio->ep_out = desc_endpoint->bEndpointAddress; - p_audio->ep_out_size = tu_edpt_packet_size(desc_endpoint); - p_audio->ep_out_interval = desc_endpoint->bInterval; - desc_cb.desc_ep_out = desc_endpoint; // Save to per-AS storage if (as_entry_idx < CFG_TUH_AUDIO_MAX_AS) { - audioh_as_t *as = &p_audio->as[as_entry_idx]; - as->ep_addr = desc_endpoint->bEndpointAddress; - as->ep_size = tu_edpt_packet_size(desc_endpoint); - as->ep_dir = TUSB_DIR_OUT; - as->format_type = tmp_format_type; - as->num_channels = tmp_num_channels; + tuh_audio_as_info_t *as = &p_audio->as[as_entry_idx]; + as->ep_addr = desc_endpoint->bEndpointAddress; + as->ep_size = tu_edpt_packet_size(desc_endpoint); + as->ep_dir = TUSB_DIR_OUT; + as->format_type = tmp_format_type; + as->num_channels = tmp_num_channels; as->sub_frame_size = tmp_sub_frame_size; as->bit_resolution = tmp_bit_resolution; - as->sam_freq_type = tmp_sam_freq_type; + as->sam_freq_type = tmp_sam_freq_type; as->sam_freq_lower = tmp_sam_freq_lower; as->sam_freq_upper = tmp_sam_freq_upper; for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX_SAM_FREQ; i++) { @@ -449,7 +395,8 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface continue; } p_audio->itf_count++; - } else if (desc_interface->bInterfaceClass == TUSB_CLASS_AUDIO && desc_interface->bInterfaceSubClass == AUDIO_SUBCLASS_CONTROL) { + } else if (desc_interface->bInterfaceClass == TUSB_CLASS_AUDIO && + desc_interface->bInterfaceSubClass == AUDIO_SUBCLASS_CONTROL) { // Another Audio Control interface (shouldn't happen in normal UAC 1.0) p_audio->itf_count++; } @@ -458,23 +405,22 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface } p_audio->daddr = dev_addr; - tuh_audio_descriptor_cb(idx, &desc_cb); return (uint16_t)((uintptr_t)p_desc - (uintptr_t)desc_start); } static void _audioh_mount(uint8_t dev_addr, uint8_t idx); -static void audioh_set_interface_complete(tuh_xfer_t* xfer) { - uint8_t idx = (uint8_t) xfer->user_data; +static void audioh_set_interface_complete(tuh_xfer_t *xfer) { + uint8_t idx = (uint8_t)xfer->user_data; audioh_interface_t *p_audio = &_audioh_itf[idx]; // Send SET_INTERFACE for next AS interface if any p_audio->as_set_idx++; if (p_audio->as_set_idx < p_audio->as_count) { uint8_t as_idx = p_audio->as_set_idx; - uint8_t itf = p_audio->as_interfaces[as_idx]; - uint8_t alt = p_audio->as_alt_settings[as_idx]; + uint8_t itf = p_audio->as[as_idx].interface_num; + uint8_t alt = p_audio->as[as_idx].alt_setting; if (alt > 0) { TU_LOG_DRV("AUDIO Set Interface %u Alt %u (addr = %u)\r\n", itf, alt, xfer->daddr); tuh_interface_set(xfer->daddr, itf, alt, audioh_set_interface_complete, idx); @@ -488,49 +434,12 @@ static void audioh_set_interface_complete(tuh_xfer_t* xfer) { static void _audioh_mount(uint8_t dev_addr, uint8_t idx) { audioh_interface_t *p_audio = &_audioh_itf[idx]; - p_audio->mounted = true; - - tuh_audio_mount_cb_t mount_cb_data = { - .daddr = dev_addr, - .bInterfaceNumber = p_audio->bInterfaceNumber, - .bAltSetting = p_audio->alt_setting, - .input_terminal_type = p_audio->input_terminal_type, - .input_terminal_id = p_audio->input_terminal_id, - .input_terminal_channels = p_audio->input_terminal_channels, - .output_terminal_type = p_audio->output_terminal_type, - .output_terminal_id = p_audio->output_terminal_id, - .feature_unit_id = p_audio->feature_unit_id, - .feature_unit_source_id = p_audio->feature_unit_source_id, - .ep_in = p_audio->ep_in, - .ep_out = p_audio->ep_out, - .ep_in_size = p_audio->ep_in_size, - .ep_out_size = p_audio->ep_out_size, - }; - - // Fill per-AS interface info - mount_cb_data.as_count = p_audio->as_count; - for (uint8_t i = 0; i < p_audio->as_count && i < CFG_TUH_AUDIO_MAX_AS; i++) { - audioh_as_t *as = &p_audio->as[i]; - mount_cb_data.as_info[i].interface_num = as->interface_num; - mount_cb_data.as_info[i].alt_setting = as->alt_setting; - mount_cb_data.as_info[i].ep_addr = as->ep_addr; - mount_cb_data.as_info[i].ep_size = as->ep_size; - mount_cb_data.as_info[i].ep_dir = as->ep_dir; - mount_cb_data.as_info[i].format_type = as->format_type; - mount_cb_data.as_info[i].num_channels = as->num_channels; - mount_cb_data.as_info[i].sub_frame_size = as->sub_frame_size; - mount_cb_data.as_info[i].bit_resolution = as->bit_resolution; - mount_cb_data.as_info[i].sam_freq_type = as->sam_freq_type; - mount_cb_data.as_info[i].sam_freq_lower = as->sam_freq_lower; - mount_cb_data.as_info[i].sam_freq_upper = as->sam_freq_upper; - for (uint8_t j = 0; j < CFG_TUH_AUDIO_MAX_SAM_FREQ; j++) { - mount_cb_data.as_info[i].sam_freq[j] = as->sam_freq[j]; - } - } + p_audio->mounted = true; - tuh_audio_mount_cb(idx, &mount_cb_data); - usbh_driver_set_config_complete(dev_addr, p_audio->bInterfaceNumber); + tuh_audio_mount_cb(idx); + + usbh_driver_set_config_complete(dev_addr, p_audio->ac_itf_num); } bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num) { @@ -539,24 +448,30 @@ bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num) { // If not found, check if this is an AS interface that belongs to a known AC interface if (idx >= CFG_TUH_AUDIO_MAX) { for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX; i++) { - if (_audioh_itf[i].daddr == dev_addr && _audioh_itf[i].as_interface_num == itf_num) { - // AS interface: configuration is driven by the AC interface, so just pass through - usbh_driver_set_config_complete(dev_addr, itf_num); - return true; + const audioh_interface_t *p_audio = &_audioh_itf[i]; + if (p_audio->daddr == dev_addr) { + for (uint8_t as_idx = 0; as_idx < p_audio->as_count; as_idx++) { + if (p_audio->as[as_idx].interface_num == itf_num) { + // AS interface: configuration is driven by the AC interface, so just pass through + usbh_driver_set_config_complete(dev_addr, itf_num); + return true; + } + } } } - // Not an Audio interface we own; pass through so enumeration can continue - usbh_driver_set_config_complete(dev_addr, itf_num); - return true; + // Not an Audio interface we own; pass through so enumeration can continue + usbh_driver_set_config_complete(dev_addr, itf_num); + return true; } audioh_interface_t *p_audio = &_audioh_itf[idx]; + TU_VERIFY(p_audio->as_count <= CFG_TUH_AUDIO_MAX_AS, false); // Send SET_INTERFACE for all AS interfaces with alt_setting > 0 if (p_audio->as_count > 0) { p_audio->as_set_idx = 0; - uint8_t itf = p_audio->as_interfaces[0]; - uint8_t alt = p_audio->as_alt_settings[0]; + uint8_t itf = p_audio->as[0].interface_num; + uint8_t alt = p_audio->as[0].alt_setting; if (alt > 0) { TU_LOG_DRV("AUDIO Set Interface %u Alt %u (addr = %u)\r\n", itf, alt, dev_addr); tuh_interface_set(dev_addr, itf, alt, audioh_set_interface_complete, idx); @@ -577,10 +492,21 @@ bool tuh_audio_mounted(uint8_t idx) { return p_audio->mounted; } +uint8_t tuh_audio_get_dev_addr(uint8_t idx) { + audioh_interface_t *p_audio = &_audioh_itf[idx]; + return p_audio->daddr; +} + + +uint8_t tuh_audio_get_feature_unit_id(uint8_t idx) { + audioh_interface_t *p_audio = &_audioh_itf[idx]; + return p_audio->feature_unit_id; +} + uint8_t tuh_audio_itf_get_index(uint8_t daddr, uint8_t itf_num) { for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { const audioh_interface_t *p_audio = &_audioh_itf[idx]; - if (p_audio->daddr == daddr && p_audio->bInterfaceNumber == itf_num) { + if (p_audio->daddr == daddr && p_audio->ac_itf_num == itf_num) { return idx; } } @@ -595,16 +521,19 @@ bool tuh_audio_itf_get_info(uint8_t idx, tuh_itf_info_t *info) { // re-construct descriptor tusb_desc_interface_t *desc_interface = &info->desc; - desc_interface->bLength = sizeof(tusb_desc_interface_t); - desc_interface->bDescriptorType = TUSB_DESC_INTERFACE; + desc_interface->bLength = sizeof(tusb_desc_interface_t); + desc_interface->bDescriptorType = TUSB_DESC_INTERFACE; + + uint8_t ep_in = audioh_get_ep_addr_by_dir(p_audio, TUSB_DIR_IN); + uint8_t ep_out = audioh_get_ep_addr_by_dir(p_audio, TUSB_DIR_OUT); - desc_interface->bInterfaceNumber = p_audio->bInterfaceNumber; - desc_interface->bAlternateSetting = 0; - desc_interface->bNumEndpoints = (uint8_t)((p_audio->ep_in ? 1u : 0u) + (p_audio->ep_out ? 1u : 0u)); - desc_interface->bInterfaceClass = TUSB_CLASS_AUDIO; + desc_interface->bInterfaceNumber = p_audio->ac_itf_num; + desc_interface->bAlternateSetting = 0; + desc_interface->bNumEndpoints = (uint8_t)((ep_in ? 1u : 0u) + (ep_out ? 1u : 0u)); + desc_interface->bInterfaceClass = TUSB_CLASS_AUDIO; desc_interface->bInterfaceSubClass = AUDIO_SUBCLASS_CONTROL; desc_interface->bInterfaceProtocol = 0; - desc_interface->iInterface = p_audio->iInterface; + desc_interface->iInterface = 0; return true; } @@ -612,22 +541,22 @@ bool tuh_audio_itf_get_info(uint8_t idx, tuh_itf_info_t *info) { //--------------------------------------------------------------------+ // Control Endpoint API //--------------------------------------------------------------------+ -bool tuh_audio_set_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t sampling_freq, - tuh_xfer_cb_t complete_cb, uintptr_t user_data) { - uint8_t const idx = get_idx_by_ep_addr(daddr, ep_addr); +bool tuh_audio_set_sampling_freq(uint8_t idx, uint8_t as_idx, uint32_t sampling_freq, tuh_xfer_cb_t complete_cb, + uintptr_t user_data) { TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); - uint8_t* freq_buf = _audioh_epbuf[idx].ctrl; - tusb_control_request_t const request = { - .bmRequestType_bit = { - .recipient = TUSB_REQ_RCPT_ENDPOINT, - .type = TUSB_REQ_TYPE_CLASS, - .direction = TUSB_DIR_OUT - }, - .bRequest = AUDIO10_CS_REQ_SET_CUR, - .wValue = tu_htole16(tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0)), // Control Selector = Sampling Freq, Channel = 0 - .wIndex = tu_htole16((uint16_t) ep_addr), - .wLength = 3 - }; + audioh_interface_t *p_audio = &_audioh_itf[idx]; + TU_VERIFY(p_audio && as_idx < p_audio->as_count, false); + + uint8_t ep_addr = p_audio->as[as_idx].ep_addr; + uint8_t daddr = p_audio->daddr; + uint8_t *freq_buf = _audioh_epbuf[idx].ctrl; + + const tusb_control_request_t request = + {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_ENDPOINT, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_OUT}, + .bRequest = AUDIO10_CS_REQ_SET_CUR, + .wValue = tu_htole16(tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0)), // Control Selector = Sampling Freq, Channel = 0 + .wIndex = tu_htole16((uint16_t)ep_addr), + .wLength = 3}; // UAC 1.0 sampling frequency is 3 bytes little-endian // uint8_t freq_buf[3] = { @@ -635,109 +564,97 @@ bool tuh_audio_set_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t sampli // (uint8_t)((sampling_freq >> 8) & 0xFF), // (uint8_t)((sampling_freq >> 16) & 0xFF) // }; - freq_buf[0] = (uint8_t)(sampling_freq & 0xFF); - freq_buf[1] = (uint8_t)((sampling_freq >> 8) & 0xFF); - freq_buf[2] = (uint8_t)((sampling_freq >> 16) & 0xFF); - tuh_xfer_t xfer = { - .daddr = daddr, - .ep_addr = 0, - .setup = &request, - .buffer = freq_buf, - .complete_cb = complete_cb, - .user_data = user_data - }; + freq_buf[0] = (uint8_t)(sampling_freq & 0xFF); + freq_buf[1] = (uint8_t)((sampling_freq >> 8) & 0xFF); + freq_buf[2] = (uint8_t)((sampling_freq >> 16) & 0xFF); + tuh_xfer_t xfer = {.daddr = daddr, + .ep_addr = 0, + .setup = &request, + .buffer = freq_buf, + .complete_cb = complete_cb, + .user_data = user_data}; return tuh_control_xfer(&xfer); } -bool tuh_audio_get_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t *sampling_freq, - tuh_xfer_cb_t complete_cb, uintptr_t user_data) { - TU_VERIFY(sampling_freq, false); +bool tuh_audio_get_sampling_freq(uint8_t idx, uint8_t as_idx, uint32_t *sampling_freq, tuh_xfer_cb_t complete_cb, + uintptr_t user_data) { + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); + audioh_interface_t *p_audio = &_audioh_itf[idx]; + TU_VERIFY(p_audio && as_idx < p_audio->as_count && sampling_freq, false); + uint8_t ep_addr = p_audio->as[as_idx].ep_addr; + uint8_t daddr = p_audio->daddr; + *sampling_freq = 0; - tusb_control_request_t const request = { - .bmRequestType_bit = { - .recipient = TUSB_REQ_RCPT_ENDPOINT, - .type = TUSB_REQ_TYPE_CLASS, - .direction = TUSB_DIR_IN - }, - .bRequest = AUDIO10_CS_REQ_GET_CUR, - .wValue = tu_htole16(tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0)), // Control Selector = Sampling Freq, Channel = 0 - .wIndex = tu_htole16((uint16_t) ep_addr), - .wLength = 3 - }; + const tusb_control_request_t request = + {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_ENDPOINT, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_IN}, + .bRequest = AUDIO10_CS_REQ_GET_CUR, + .wValue = tu_htole16(tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0)), // Control Selector = Sampling Freq, Channel = 0 + .wIndex = tu_htole16((uint16_t)ep_addr), + .wLength = 3}; // Application needs to parse 3-byte little-endian sampling frequency from buffer - tuh_xfer_t xfer = { - .daddr = daddr, - .ep_addr = 0, - .setup = &request, - .buffer = (uint8_t *)sampling_freq, - .complete_cb = complete_cb, - .user_data = user_data - }; + tuh_xfer_t xfer = {.daddr = daddr, + .ep_addr = 0, + .setup = &request, + .buffer = (uint8_t *)sampling_freq, + .complete_cb = complete_cb, + .user_data = user_data}; return tuh_control_xfer(&xfer); } -bool tuh_audio_feature_unit_set(uint8_t daddr, uint8_t itf_num, uint8_t unit_id, - uint8_t control_selector, uint8_t channel, - uint16_t value, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { - tusb_control_request_t const request = { - .bmRequestType_bit = { - .recipient = TUSB_REQ_RCPT_INTERFACE, - .type = TUSB_REQ_TYPE_CLASS, - .direction = TUSB_DIR_OUT - }, - .bRequest = AUDIO10_CS_REQ_SET_CUR, - .wValue = tu_htole16(tu_u16(control_selector, channel)), - .wIndex = tu_htole16(tu_u16(unit_id, itf_num)), - .wLength = 2 - }; - - uint8_t const idx = tuh_audio_itf_get_index(daddr, itf_num); +bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t value, + tuh_xfer_cb_t complete_cb, uintptr_t user_data) { TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); - - uint8_t* val_buf = _audioh_epbuf[idx].ctrl; - val_buf[0] = (uint8_t)(value & 0xFF); - val_buf[1] = (uint8_t)((value >> 8) & 0xFF); - - tuh_xfer_t xfer = { - .daddr = daddr, - .ep_addr = 0, - .setup = &request, - .buffer = val_buf, - .complete_cb = complete_cb, - .user_data = user_data - }; + uint8_t daddr = _audioh_itf[idx].daddr; + uint8_t itf_num = _audioh_itf[idx].ac_itf_num; + uint8_t unit_id = _audioh_itf[idx].feature_unit_id; + + const tusb_control_request_t request = {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, + .type = TUSB_REQ_TYPE_CLASS, + .direction = TUSB_DIR_OUT}, + .bRequest = AUDIO10_CS_REQ_SET_CUR, + .wValue = tu_htole16(tu_u16(control_selector, channel)), + .wIndex = tu_htole16(tu_u16(unit_id, itf_num)), + .wLength = 2}; + + uint8_t *val_buf = _audioh_epbuf[idx].ctrl; + val_buf[0] = (uint8_t)(value & 0xFF); + val_buf[1] = (uint8_t)((value >> 8) & 0xFF); + + tuh_xfer_t xfer = {.daddr = daddr, + .ep_addr = 0, + .setup = &request, + .buffer = val_buf, + .complete_cb = complete_cb, + .user_data = user_data}; return tuh_control_xfer(&xfer); } -bool tuh_audio_feature_unit_get(uint8_t daddr, uint8_t itf_num, uint8_t unit_id, - uint8_t control_selector, uint8_t channel, - void *buffer, uint8_t len, - tuh_xfer_cb_t complete_cb, uintptr_t user_data) { - tusb_control_request_t const request = { - .bmRequestType_bit = { - .recipient = TUSB_REQ_RCPT_INTERFACE, - .type = TUSB_REQ_TYPE_CLASS, - .direction = TUSB_DIR_IN - }, - .bRequest = AUDIO10_CS_REQ_GET_CUR, - .wValue = tu_htole16(tu_u16(control_selector, channel)), - .wIndex = tu_htole16(tu_u16(unit_id, itf_num)), - .wLength = len - }; - - tuh_xfer_t xfer = { - .daddr = daddr, - .ep_addr = 0, - .setup = &request, - .buffer = buffer, - .complete_cb = complete_cb, - .user_data = user_data - }; +bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t *buffer, + tuh_xfer_cb_t complete_cb, uintptr_t user_data) { + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); + uint8_t daddr = _audioh_itf[idx].daddr; + uint8_t itf_num = _audioh_itf[idx].ac_itf_num; + uint8_t unit_id = _audioh_itf[idx].feature_unit_id; + + const tusb_control_request_t request = {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, + .type = TUSB_REQ_TYPE_CLASS, + .direction = TUSB_DIR_IN}, + .bRequest = AUDIO10_CS_REQ_GET_CUR, + .wValue = tu_htole16(tu_u16(control_selector, channel)), + .wIndex = tu_htole16(tu_u16(unit_id, itf_num)), + .wLength = 2}; + + tuh_xfer_t xfer = {.daddr = daddr, + .ep_addr = 0, + .setup = &request, + .buffer = (uint8_t *)buffer, + .complete_cb = complete_cb, + .user_data = user_data}; return tuh_control_xfer(&xfer); } @@ -755,17 +672,17 @@ bool tuh_audio_as_get_info(uint8_t idx, uint8_t as_idx, tuh_audio_as_info_t *inf TU_VERIFY(as_idx < _audioh_itf[idx].as_count, false); TU_VERIFY(info, false); - audioh_as_t *as = &_audioh_itf[idx].as[as_idx]; - info->interface_num = as->interface_num; - info->alt_setting = as->alt_setting; - info->ep_addr = as->ep_addr; - info->ep_size = as->ep_size; - info->ep_dir = as->ep_dir; - info->format_type = as->format_type; - info->num_channels = as->num_channels; + tuh_audio_as_info_t *as = &_audioh_itf[idx].as[as_idx]; + info->interface_num = as->interface_num; + info->alt_setting = as->alt_setting; + info->ep_addr = as->ep_addr; + info->ep_size = as->ep_size; + info->ep_dir = as->ep_dir; + info->format_type = as->format_type; + info->num_channels = as->num_channels; info->sub_frame_size = as->sub_frame_size; info->bit_resolution = as->bit_resolution; - info->sam_freq_type = as->sam_freq_type; + info->sam_freq_type = as->sam_freq_type; info->sam_freq_lower = as->sam_freq_lower; info->sam_freq_upper = as->sam_freq_upper; memcpy(info->sam_freq, as->sam_freq, sizeof(info->sam_freq)); @@ -775,49 +692,43 @@ bool tuh_audio_as_get_info(uint8_t idx, uint8_t as_idx, tuh_audio_as_info_t *inf //--------------------------------------------------------------------+ // Isochronous Endpoint API //--------------------------------------------------------------------+ -bool tuh_audio_receive(uint8_t daddr, uint8_t idx, uint8_t *buffer, uint16_t len) { +bool tuh_audio_receive(uint8_t idx, uint8_t as_idx, uint8_t *buffer, uint16_t len) { TU_VERIFY(idx < CFG_TUH_AUDIO_MAX); audioh_interface_t *p_audio = &_audioh_itf[idx]; - TU_VERIFY(p_audio->daddr == daddr); - TU_VERIFY(p_audio->ep_in != 0); + tuh_audio_as_info_t *as = &p_audio->as[as_idx]; + TU_VERIFY(as->ep_addr != 0); - return usbh_edpt_xfer(daddr, p_audio->ep_in, buffer, len); + return usbh_edpt_xfer(p_audio->daddr, as->ep_addr, buffer, len); } -bool tuh_audio_send(uint8_t daddr, uint8_t idx, uint8_t *buffer, uint16_t len) { +bool tuh_audio_send(uint8_t idx, uint8_t as_idx, uint8_t *buffer, uint16_t len) { TU_VERIFY(idx < CFG_TUH_AUDIO_MAX); audioh_interface_t *p_audio = &_audioh_itf[idx]; - TU_VERIFY(p_audio->daddr == daddr); - TU_VERIFY(p_audio->ep_out != 0); + tuh_audio_as_info_t *as = &p_audio->as[as_idx]; + TU_VERIFY(as->ep_addr != 0); - return usbh_edpt_xfer(daddr, p_audio->ep_out, (uint8_t *)buffer, len); + return usbh_edpt_xfer(p_audio->daddr, as->ep_addr, (uint8_t *)buffer, len); } //--------------------------------------------------------------------+ // Set Interface //--------------------------------------------------------------------+ -bool tuh_audio_set_interface(uint8_t daddr, uint8_t itf_num, uint8_t alt_setting, - tuh_xfer_cb_t complete_cb, uintptr_t user_data) { - tusb_control_request_t const request = { - .bmRequestType_bit = { - .recipient = TUSB_REQ_RCPT_INTERFACE, - .type = TUSB_REQ_TYPE_STANDARD, - .direction = TUSB_DIR_OUT - }, - .bRequest = TUSB_REQ_SET_INTERFACE, - .wValue = alt_setting, - .wIndex = itf_num, - .wLength = 0 - }; - - tuh_xfer_t xfer = { - .daddr = daddr, - .ep_addr = 0, - .setup = &request, - .buffer = NULL, - .complete_cb = complete_cb, - .user_data = user_data - }; +bool tuh_audio_set_interface(uint8_t daddr, uint8_t itf_num, uint8_t alt_setting, tuh_xfer_cb_t complete_cb, + uintptr_t user_data) { + const tusb_control_request_t request = {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, + .type = TUSB_REQ_TYPE_STANDARD, + .direction = TUSB_DIR_OUT}, + .bRequest = TUSB_REQ_SET_INTERFACE, + .wValue = alt_setting, + .wIndex = itf_num, + .wLength = 0}; + + tuh_xfer_t xfer = {.daddr = daddr, + .ep_addr = 0, + .setup = &request, + .buffer = NULL, + .complete_cb = complete_cb, + .user_data = user_data}; return tuh_control_xfer(&xfer); } diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h index 5c5a8b3df..aafd58f43 100644 --- a/src/class/audio/audio_host.h +++ b/src/class/audio/audio_host.h @@ -17,14 +17,15 @@ extern "C" { //--------------------------------------------------------------------+ // Class Driver Configuration //--------------------------------------------------------------------+ +// Maximum number of Audio interfaces per Audio device #ifndef CFG_TUH_AUDIO_MAX #define CFG_TUH_AUDIO_MAX 1 #endif - +// Maximum number of Audio Streaming interfaces per Audio device #ifndef CFG_TUH_AUDIO_MAX_SAM_FREQ #define CFG_TUH_AUDIO_MAX_SAM_FREQ 5 #endif - +// Maximum number of Audio Streaming interfaces per Audio device #ifndef CFG_TUH_AUDIO_MAX_AS #define CFG_TUH_AUDIO_MAX_AS 4 #endif @@ -33,18 +34,18 @@ extern "C" { // AS Interface Info (per-interface independent storage) //--------------------------------------------------------------------+ typedef struct { - uint8_t interface_num; // AS interface number - uint8_t alt_setting; // Current alt setting - uint8_t ep_addr; // Endpoint address - uint16_t ep_size; // Max packet size - uint8_t ep_dir; // TUSB_DIR_IN or TUSB_DIR_OUT + uint8_t interface_num; // AS interface number + uint8_t alt_setting; // Current alt setting + uint8_t ep_addr; // Endpoint address + uint16_t ep_size; // Max packet size + uint8_t ep_dir; // TUSB_DIR_IN or TUSB_DIR_OUT // Format info - uint8_t format_type; - uint8_t num_channels; - uint8_t sub_frame_size; - uint8_t bit_resolution; - uint8_t sam_freq_type; + uint8_t format_type; + uint8_t num_channels; + uint8_t sub_frame_size; + uint8_t bit_resolution; + uint8_t sam_freq_type; uint32_t sam_freq[CFG_TUH_AUDIO_MAX_SAM_FREQ]; uint32_t sam_freq_lower; uint32_t sam_freq_upper; @@ -58,73 +59,16 @@ typedef struct { #define CFG_TUH_AUDIO_EPOUT_BUFSIZE 192 #endif -//--------------------------------------------------------------------+ -// Descriptor Information -//--------------------------------------------------------------------+ -// Information about parsed UAC 1.0 descriptors passed to the application -// during enumeration (via tuh_audio_descriptor_cb) -typedef struct { - // Audio Control Interface descriptor - const tusb_desc_interface_t *desc_ac_interface; - - // Audio Streaming Interface descriptor (alt setting 0) - const tusb_desc_interface_t *desc_as_interface; - - // Audio Streaming Interface alt setting (with endpoints) - const tusb_desc_interface_t *desc_as_interface_alt; - - // Format Type descriptor - const uint8_t *desc_format_type; - - // Class-Specific AS Interface (AS General) descriptor - const uint8_t *desc_cs_as_general; - - // Standard Isochronous Endpoint descriptor (IN) - const tusb_desc_endpoint_t *desc_ep_in; - - // Standard Isochronous Endpoint descriptor (OUT) - const tusb_desc_endpoint_t *desc_ep_out; - - // Audio function information - uint8_t ac_interface_num; // Audio Control interface number - uint8_t as_interface_num; // Audio Streaming interface number - uint8_t alt_setting; // Current alt setting with endpoints -} tuh_audio_descriptor_cb_t; - -typedef struct { - uint8_t daddr; - uint8_t bInterfaceNumber; - uint8_t bAltSetting; - - // Terminal info (from Audio Control Interface) - uint16_t input_terminal_type; // wTerminalType of Input Terminal (0x0201 = Mic, etc.) - uint8_t input_terminal_id; // bTerminalID of Input Terminal - uint8_t input_terminal_channels; // bNrChannels of Input Terminal - uint16_t output_terminal_type; // wTerminalType of Output Terminal (0x0301 = Speaker, etc.) - uint8_t output_terminal_id; // bTerminalID of Output Terminal - - // Feature Unit info - uint8_t feature_unit_id; // bUnitID of Feature Unit (0 = none) - uint8_t feature_unit_source_id; // bSourceID of Feature Unit - - // Endpoint info - uint8_t ep_in; - uint8_t ep_out; - uint16_t ep_in_size; - uint16_t ep_out_size; - - // Multi-AS support (per AS interface independent storage) - uint8_t as_count; - tuh_audio_as_info_t as_info[CFG_TUH_AUDIO_MAX_AS]; -} tuh_audio_mount_cb_t; - //--------------------------------------------------------------------+ // Application API //--------------------------------------------------------------------+ // Check if Audio interface is mounted bool tuh_audio_mounted(uint8_t idx); - +// Get device address of Audio interface +uint8_t tuh_audio_get_dev_addr(uint8_t idx); +// Get Feature Unit ID +uint8_t tuh_audio_get_feature_unit_id(uint8_t idx); // Get Interface index from device address + interface number // return TUSB_INDEX_INVALID_8 (0xFF) if not found uint8_t tuh_audio_itf_get_index(uint8_t daddr, uint8_t itf_num); @@ -141,8 +85,8 @@ uint8_t tuh_audio_as_get_count(uint8_t idx); bool tuh_audio_as_get_info(uint8_t idx, uint8_t as_idx, tuh_audio_as_info_t *info); // Set Audio Streaming interface alternate setting (to enable/disable endpoints) -bool tuh_audio_set_interface(uint8_t daddr, uint8_t itf_num, uint8_t alt_setting, - tuh_xfer_cb_t complete_cb, uintptr_t user_data); +bool tuh_audio_set_interface(uint8_t daddr, uint8_t itf_num, uint8_t alt_setting, tuh_xfer_cb_t complete_cb, + uintptr_t user_data); //--------------------------------------------------------------------+ // Control Endpoint API @@ -150,54 +94,77 @@ bool tuh_audio_set_interface(uint8_t daddr, uint8_t itf_num, uint8_t alt_setting // Set current sampling frequency on an isochronous endpoint (UAC 1.0) // Sampling frequency is 3 bytes little-endian -bool tuh_audio_set_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t sampling_freq, - tuh_xfer_cb_t complete_cb, uintptr_t user_data); +// In multi-AS scenarios, pass the endpoint address from tuh_audio_as_get_info(). +bool tuh_audio_set_sampling_freq(uint8_t idx, uint8_t as_idx, uint32_t sampling_freq, tuh_xfer_cb_t complete_cb, + uintptr_t user_data); // Get current sampling frequency from an isochronous endpoint (UAC 1.0) -bool tuh_audio_get_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t *sampling_freq, - tuh_xfer_cb_t complete_cb, uintptr_t user_data); +// In multi-AS scenarios, pass the endpoint address from tuh_audio_as_get_info(). +bool tuh_audio_get_sampling_freq(uint8_t idx, uint8_t as_idx, uint32_t *sampling_freq, tuh_xfer_cb_t complete_cb, + uintptr_t user_data); // Set current/mute/volume etc. for a feature unit (UAC 1.0) -bool tuh_audio_feature_unit_set(uint8_t daddr, uint8_t itf_num, uint8_t unit_id, - uint8_t control_selector, uint8_t channel, - uint16_t value, tuh_xfer_cb_t complete_cb, uintptr_t user_data); +bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t value, + tuh_xfer_cb_t complete_cb, uintptr_t user_data); // Get current/mute/volume etc. from a feature unit (UAC 1.0) -bool tuh_audio_feature_unit_get(uint8_t daddr, uint8_t itf_num, uint8_t unit_id, - uint8_t control_selector, uint8_t channel, - void *buffer, uint8_t len, - tuh_xfer_cb_t complete_cb, uintptr_t user_data); +bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t *value, + tuh_xfer_cb_t complete_cb, uintptr_t user_data); + +//--------------------------------------------------------------------+ +// Control Request Sync API +// Each Function will make a USB control transfer request to/from device the function will block until request is +// complete. The function will return the transfer request result +//--------------------------------------------------------------------+ +TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_get_sampling_freq_sync(uint8_t idx, uint8_t as_idx, + uint32_t *sampling_freq) { + TU_API_SYNC(tuh_audio_get_sampling_freq, idx, as_idx, sampling_freq); +} + +TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_set_sampling_freq_sync(uint8_t idx, uint8_t as_idx, + uint32_t sampling_freq) { + TU_API_SYNC(tuh_audio_set_sampling_freq, idx, as_idx, sampling_freq); +} + +TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t +tuh_audio_feature_unit_set_sync(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t value) { + TU_API_SYNC(tuh_audio_feature_unit_set, idx, control_selector, channel, value); +} + +TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t +tuh_audio_feature_unit_get_sync(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t *value) { + TU_API_SYNC(tuh_audio_feature_unit_get, idx, control_selector, channel, value); +} //--------------------------------------------------------------------+ // Interrupt/Isochronous Endpoint API //--------------------------------------------------------------------+ -// Submit an isochronous transfer to receive audio data from IN endpoint -bool tuh_audio_receive(uint8_t daddr, uint8_t idx, uint8_t *buffer, uint16_t len); +// Submit an isochronous transfer to receive audio data from a default IN endpoint. +// In multi-AS scenarios, endpoint selection is implementation-defined default behavior. +// Use tuh_audio_as_get_info() when application needs explicit per-AS endpoint control. +bool tuh_audio_receive(uint8_t idx, uint8_t as_idx, uint8_t *buffer, uint16_t len); -// Submit an isochronous transfer to send audio data to OUT endpoint -bool tuh_audio_send(uint8_t daddr, uint8_t idx, uint8_t *buffer, uint16_t len); +// Submit an isochronous transfer to send audio data to a default OUT endpoint. +// In multi-AS scenarios, endpoint selection is implementation-defined default behavior. +// Use tuh_audio_as_get_info() when application needs explicit per-AS endpoint control. +bool tuh_audio_send(uint8_t idx, uint8_t as_idx, uint8_t *buffer, uint16_t len); //--------------------------------------------------------------------+ // Callbacks (Weak is optional) //--------------------------------------------------------------------+ -// Invoked when Audio interface descriptor is detected during enumeration. -// Application can copy/parse descriptor if needed. -// Note: may be fired before tuh_audio_mount_cb(), therefore audio interface is not mounted/ready. -void tuh_audio_descriptor_cb(uint8_t idx, const tuh_audio_descriptor_cb_t *desc_cb_data); - // Invoked when device with Audio interface is mounted -void tuh_audio_mount_cb(uint8_t idx, const tuh_audio_mount_cb_t *mount_cb_data); +void tuh_audio_mount_cb(uint8_t idx); // Invoked when device with Audio interface is un-mounted void tuh_audio_umount_cb(uint8_t idx); // Invoked when an isochronous IN transfer is complete -void tuh_audio_rx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes); +void tuh_audio_rx_cb(uint8_t dev_addr, uint8_t ep_addr, uint16_t xferred_bytes); // Invoked when an isochronous OUT transfer is complete -void tuh_audio_tx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes); +void tuh_audio_tx_cb(uint8_t dev_addr, uint8_t ep_addr, uint16_t xferred_bytes); //--------------------------------------------------------------------+ // Internal Class Driver API -- cgit v1.3.1 From a2f4786865e85f9cfe7f58c86fbb9355bbd2d701 Mon Sep 17 00:00:00 2001 From: Zixun LI Date: Mon, 27 Jul 2026 14:39:02 +0200 Subject: portable/chipidea: configure LPC USB0 AHB bursts --- src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h | 14 ++++++++++++++ src/portable/chipidea/ci_hs/dcd_ci_hs.c | 4 ++++ src/portable/chipidea/ci_hs/hcd_ci_hs.c | 4 ++++ 3 files changed, 22 insertions(+) (limited to 'src') diff --git a/src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h b/src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h index f2061bd7a..dec3a34b1 100644 --- a/src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h +++ b/src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h @@ -34,4 +34,18 @@ static const ci_hs_controller_t _ci_controller[] = #define CI_HCD_INT_ENABLE(_p) NVIC_EnableIRQ ((IRQn_Type)_ci_controller[_p].irqnum) #define CI_HCD_INT_DISABLE(_p) NVIC_DisableIRQ((IRQn_Type)_ci_controller[_p].irqnum) +enum { + CI_HS_LPC18_43_SBUSCFG_OFFSET = 0x90u, + CI_HS_LPC18_43_AHBBRST_INCR16_UNSPEC = 0x07u, +}; + +TU_ATTR_ALWAYS_INLINE static inline void ci_hs_lpc18_43_set_ahb_burst(uint8_t rhport) { + // USB0 SBUSCFG is at offset 0x90. NXP recommends AHBBRST=0x7: + // INCR16 with non-multiple transfers decomposed into smaller unspecified bursts. + if (rhport == 0) { + volatile uint32_t *sbuscfg = (volatile uint32_t *)(_ci_controller[rhport].reg_base + CI_HS_LPC18_43_SBUSCFG_OFFSET); + *sbuscfg = CI_HS_LPC18_43_AHBBRST_INCR16_UNSPEC; + } +} + #endif diff --git a/src/portable/chipidea/ci_hs/dcd_ci_hs.c b/src/portable/chipidea/ci_hs/dcd_ci_hs.c index fa98d6882..32c701bfa 100644 --- a/src/portable/chipidea/ci_hs/dcd_ci_hs.c +++ b/src/portable/chipidea/ci_hs/dcd_ci_hs.c @@ -237,6 +237,10 @@ bool dcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) { usbmode |= USBMODE_CM_DEVICE; dcd_reg->USBMODE = usbmode; + #if TU_CHECK_MCU(OPT_MCU_LPC18XX, OPT_MCU_LPC43XX) + ci_hs_lpc18_43_set_ahb_burst(rhport); + #endif + #ifdef CFG_TUD_CI_HS_VBUS_CHARGE dcd_reg->OTGSC = OTGSC_VBUS_CHARGE | OTGSC_OTG_TERMINATION; #else diff --git a/src/portable/chipidea/ci_hs/hcd_ci_hs.c b/src/portable/chipidea/ci_hs/hcd_ci_hs.c index 3cb69acfa..c94ce810f 100644 --- a/src/portable/chipidea/ci_hs/hcd_ci_hs.c +++ b/src/portable/chipidea/ci_hs/hcd_ci_hs.c @@ -82,6 +82,10 @@ bool hcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) { hcd_reg->USBMODE = USBMODE_CM_HOST; #endif + #if TU_CHECK_MCU(OPT_MCU_LPC18XX, OPT_MCU_LPC43XX) + ci_hs_lpc18_43_set_ahb_burst(rhport); + #endif + #if !TUH_OPT_HIGH_SPEED hcd_reg->PORTSC1 |= PORTSC1_FORCE_FULL_SPEED; #endif -- cgit v1.3.1 From f0a8a1483bd4e89ab3adf40d8c61777a5ddadc7f Mon Sep 17 00:00:00 2001 From: Zixun LI Date: Mon, 27 Jul 2026 14:58:13 +0200 Subject: portable/chipidea: configure i.MX RT AHB bursts --- src/portable/chipidea/ci_hs/ci_hs_imxrt.h | 10 ++++++++++ src/portable/chipidea/ci_hs/dcd_ci_hs.c | 4 +++- src/portable/chipidea/ci_hs/hcd_ci_hs.c | 4 +++- 3 files changed, 16 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/portable/chipidea/ci_hs/ci_hs_imxrt.h b/src/portable/chipidea/ci_hs/ci_hs_imxrt.h index f0f918fe2..601e4d1c9 100644 --- a/src/portable/chipidea/ci_hs/ci_hs_imxrt.h +++ b/src/portable/chipidea/ci_hs/ci_hs_imxrt.h @@ -36,6 +36,16 @@ static const ci_hs_controller_t _ci_controller[] = #define CI_HS_REG(_port) ((ci_hs_regs_t*) _ci_controller[_port].reg_base) +enum { + // INCR16/8/4 followed by an unspecified-length burst for the remainder. + CI_HS_IMXRT_AHBBRST_INCR16_UNSPEC = 0x07u, +}; + +TU_ATTR_ALWAYS_INLINE static inline void ci_hs_imxrt_set_ahb_burst(uint8_t rhport) { + USB_Type *usb = (USB_Type *)_ci_controller[rhport].reg_base; + usb->SBUSCFG = USB_SBUSCFG_AHBBRST(CI_HS_IMXRT_AHBBRST_INCR16_UNSPEC); +} + //------------- DCD -------------// #define CI_DCD_INT_ENABLE(_p) NVIC_EnableIRQ ((IRQn_Type)_ci_controller[_p].irqnum) #define CI_DCD_INT_DISABLE(_p) NVIC_DisableIRQ((IRQn_Type)_ci_controller[_p].irqnum) diff --git a/src/portable/chipidea/ci_hs/dcd_ci_hs.c b/src/portable/chipidea/ci_hs/dcd_ci_hs.c index 32c701bfa..62d75b4d3 100644 --- a/src/portable/chipidea/ci_hs/dcd_ci_hs.c +++ b/src/portable/chipidea/ci_hs/dcd_ci_hs.c @@ -237,7 +237,9 @@ bool dcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) { usbmode |= USBMODE_CM_DEVICE; dcd_reg->USBMODE = usbmode; - #if TU_CHECK_MCU(OPT_MCU_LPC18XX, OPT_MCU_LPC43XX) + #if CFG_TUSB_MCU == OPT_MCU_MIMXRT1XXX + ci_hs_imxrt_set_ahb_burst(rhport); + #elif TU_CHECK_MCU(OPT_MCU_LPC18XX, OPT_MCU_LPC43XX) ci_hs_lpc18_43_set_ahb_burst(rhport); #endif diff --git a/src/portable/chipidea/ci_hs/hcd_ci_hs.c b/src/portable/chipidea/ci_hs/hcd_ci_hs.c index c94ce810f..0fc8e4d70 100644 --- a/src/portable/chipidea/ci_hs/hcd_ci_hs.c +++ b/src/portable/chipidea/ci_hs/hcd_ci_hs.c @@ -82,7 +82,9 @@ bool hcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) { hcd_reg->USBMODE = USBMODE_CM_HOST; #endif - #if TU_CHECK_MCU(OPT_MCU_LPC18XX, OPT_MCU_LPC43XX) + #if CFG_TUSB_MCU == OPT_MCU_MIMXRT1XXX + ci_hs_imxrt_set_ahb_burst(rhport); + #elif TU_CHECK_MCU(OPT_MCU_LPC18XX, OPT_MCU_LPC43XX) ci_hs_lpc18_43_set_ahb_burst(rhport); #endif -- cgit v1.3.1 From a20cf74e6a62f5b833baacfe1198648b237b3e7f Mon Sep 17 00:00:00 2001 From: Zixun LI Date: Tue, 28 Jul 2026 21:15:38 +0200 Subject: portable/dwc2: rewind DMA on ISO IN retry --- src/portable/synopsys/dwc2/dcd_dwc2.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/portable/synopsys/dwc2/dcd_dwc2.c b/src/portable/synopsys/dwc2/dcd_dwc2.c index 86aa54510..b2f1a93a4 100644 --- a/src/portable/synopsys/dwc2/dcd_dwc2.c +++ b/src/portable/synopsys/dwc2/dcd_dwc2.c @@ -1143,7 +1143,12 @@ static void handle_incomplete_iso_in(uint8_t rhport) { xfer_ctl_t *xfer = XFER_CTL_BASE(epnum, TUSB_DIR_IN); if (xfer->iso_retry > 0) { xfer->iso_retry--; - // Restart ISO transfe: re-write TSIZ and CTL + // Restart ISO transfer: re-write DMA address, TSIZ, and CTL + #if CFG_TUD_DWC2_DMA_ENABLE + if (dma_device_enabled(dwc2)) { + epin->diepdma = (uintptr_t) xfer->buffer; + } + #endif dwc2_ep_tsize_t deptsiz = {.value = 0}; deptsiz.xfer_size = xfer->total_len; deptsiz.packet_count = tu_div_ceil(xfer->total_len, xfer->max_size); -- cgit v1.3.1 From 3e3e9f8a978b274b8fe1f5a9b5fd41a94f928606 Mon Sep 17 00:00:00 2001 From: Zixun LI Date: Wed, 29 Jul 2026 00:34:59 +0200 Subject: class/mtp: preserve final OUT payload before ZLP --- src/class/mtp/mtp_device.c | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/class/mtp/mtp_device.c b/src/class/mtp/mtp_device.c index 7657899ec..275c9f858 100644 --- a/src/class/mtp/mtp_device.c +++ b/src/class/mtp/mtp_device.c @@ -437,8 +437,11 @@ bool mtpd_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t event, uint32_t TU_LOG_DRV(" MTP Data %s CB: xferred_bytes=%lu, xferred_len/total_len=%lu/%lu, is_complete=%d\r\n", is_data_in ? "IN" : "OUT", xferred_bytes, p_mtp->xferred_len, p_mtp->total_len, is_complete ? 1 : 0); - // Send/queue ZLP if packet is full-sized but transfer is complete - if (is_complete && xferred_bytes > 0 && !(xferred_bytes & (threshold - 1))) { + // Send/queue ZLP if packet is full-sized but transfer is complete. + // OUT must deliver this final payload to the application before receiving + // its terminating ZLP below. + const bool need_zlp = is_complete && xferred_bytes > 0 && !(xferred_bytes & (threshold - 1)); + if (is_data_in && need_zlp) { TU_LOG_DRV(" queue ZLP\r\n"); TU_VERIFY(usbd_edpt_claim(p_mtp->rhport, ep_addr)); TU_ASSERT(usbd_edpt_xfer(p_mtp->rhport, ep_addr, NULL, 0, false)); @@ -466,9 +469,16 @@ bool mtpd_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t event, uint32_t cb_data.io_container = headerless_packet; cb_data.io_container.payload_bytes = xferred_bytes; } - tud_mtp_data_xfer_cb(&cb_data); + if (xferred_bytes > 0) { + tud_mtp_data_xfer_cb(&cb_data); + } - if (is_complete) { + if (need_zlp) { + TU_LOG_DRV(" queue ZLP\r\n"); + TU_VERIFY(usbd_edpt_claim(p_mtp->rhport, ep_addr)); + TU_ASSERT(usbd_edpt_xfer(p_mtp->rhport, ep_addr, NULL, 0, false)); + return true; + } else if (is_complete) { // back to header + payload for response cb_data.io_container = headered_packet; cb_data.io_container.header->len = sizeof(mtp_container_header_t); -- cgit v1.3.1 From 45775e9ffcd3b0ccd86c487b5e84d401233cd1c9 Mon Sep 17 00:00:00 2001 From: HiFiPhile Date: Wed, 29 Jul 2026 21:02:05 +0200 Subject: device/dfu: fix transfer buffer overwrite issue Signed-off-by: HiFiPhile --- src/class/dfu/dfu_device.c | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/src/class/dfu/dfu_device.c b/src/class/dfu/dfu_device.c index 006a5bcb7..092abed03 100644 --- a/src/class/dfu/dfu_device.c +++ b/src/class/dfu/dfu_device.c @@ -42,9 +42,8 @@ typedef struct { static dfu_state_ctx_t _dfu_ctx; -#if CFG_TUD_DFU_XFER_BUFSIZE > CFG_TUD_ENDPOINT0_BUFSIZE -TU_ATTR_ALIGNED(4) uint8_t _transfer_buf[CFG_TUD_DFU_XFER_BUFSIZE]; -#endif +// Download data must remain valid across the following GETSTATUS control transfer +TU_ATTR_ALIGNED(4) static uint8_t _transfer_buf[CFG_TUD_DFU_XFER_BUFSIZE]; static void reset_state(void) { _dfu_ctx.state = DFU_IDLE; @@ -52,15 +51,6 @@ static void reset_state(void) { _dfu_ctx.flashing_in_progress = false; } -static inline uint8_t* get_xfer_buffer(void) { - // Use EP0 buffer if it is large enough, otherwise use dedicated buffer - #if CFG_TUD_DFU_XFER_BUFSIZE > CFG_TUD_ENDPOINT0_BUFSIZE - return _transfer_buf; - #else - return usbd_get_ctrl_buf(); - #endif -} - static bool reply_getstatus(uint8_t rhport, const tusb_control_request_t* request, dfu_state_t state, dfu_status_t status, uint32_t timeout); static bool process_download_get_status(uint8_t rhport, uint8_t stage, const tusb_control_request_t* request); static bool process_manifest_get_status(uint8_t rhport, uint8_t stage, const tusb_control_request_t* request); @@ -276,10 +266,10 @@ bool dfu_moded_control_xfer_cb(uint8_t rhport, uint8_t stage, const tusb_control TU_VERIFY(_dfu_ctx.attrs & DFU_ATTR_CAN_UPLOAD); TU_VERIFY(request->wLength <= CFG_TUD_DFU_XFER_BUFSIZE); - const uint16_t xfer_len = tud_dfu_upload_cb(_dfu_ctx.alt, request->wValue, get_xfer_buffer(), + const uint16_t xfer_len = tud_dfu_upload_cb(_dfu_ctx.alt, request->wValue, _transfer_buf, request->wLength); - return tud_control_xfer(rhport, request, get_xfer_buffer(), xfer_len); + return tud_control_xfer(rhport, request, _transfer_buf, xfer_len); } break; @@ -299,7 +289,7 @@ bool dfu_moded_control_xfer_cb(uint8_t rhport, uint8_t stage, const tusb_control if (request->wLength > 0) { // Download with payload -> transition to DOWNLOAD SYNC _dfu_ctx.state = DFU_DNLOAD_SYNC; - return tud_control_xfer(rhport, request, get_xfer_buffer(), request->wLength); + return tud_control_xfer(rhport, request, _transfer_buf, request->wLength); } else { // Download is complete -> transition to MANIFEST SYNC _dfu_ctx.state = DFU_MANIFEST_SYNC; @@ -373,7 +363,7 @@ static bool process_download_get_status(uint8_t rhport, uint8_t stage, const tus } else if (stage == CONTROL_STAGE_ACK) { if (_dfu_ctx.flashing_in_progress) { _dfu_ctx.state = DFU_DNBUSY; - tud_dfu_download_cb(_dfu_ctx.alt, _dfu_ctx.block, get_xfer_buffer(), _dfu_ctx.length); + tud_dfu_download_cb(_dfu_ctx.alt, _dfu_ctx.block, _transfer_buf, _dfu_ctx.length); } else { _dfu_ctx.state = DFU_DNLOAD_IDLE; } -- cgit v1.3.1 From 15dd3120ac4a9dea0d979dc541ef8e0f52f5aa26 Mon Sep 17 00:00:00 2001 From: Javid Khan Date: Thu, 30 Jul 2026 14:13:50 +0530 Subject: clamp committed video payload size to streaming ep buffer Signed-off-by: Javid Khan --- src/class/video/video_device.c | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'src') diff --git a/src/class/video/video_device.c b/src/class/video/video_device.c index 3797e6b2b..390349f13 100644 --- a/src/class/video/video_device.c +++ b/src/class/video/video_device.c @@ -1145,6 +1145,12 @@ static int handle_video_stm_cs_req(uint8_t rhport, uint8_t stage, TU_VERIFY(_update_streaming_parameters(stm, param), VIDEO_ERROR_INVALID_VALUE_WITHIN_RANGE); /* Set the negotiated value */ stm->max_payload_transfer_size = param->dwMaxPayloadTransferSize; + /* A host may commit before the parameters are fully negotiated, in which case + * _update_streaming_parameters returns early without capping the payload size. + * Clamp here so a bulk stream cannot overrun the endpoint buffer. */ + if (CFG_TUD_VIDEO_STREAMING_EP_BUFSIZE < stm->max_payload_transfer_size) { + stm->max_payload_transfer_size = CFG_TUD_VIDEO_STREAMING_EP_BUFSIZE; + } int ret = tud_video_commit_cb(stm->index_vc, stm->index_vs, param); if (VIDEO_ERROR_NONE == ret) { stm->state = VS_STATE_COMMITTED; -- cgit v1.3.1 From 029691afa4dad1309691369de709b2f1226717d9 Mon Sep 17 00:00:00 2001 From: Zixun LI Date: Tue, 4 Aug 2026 16:02:10 +0200 Subject: avoid flushing tx buffer on connection Signed-off-by: Zixun LI --- src/class/cdc/cdc_device.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'src') diff --git a/src/class/cdc/cdc_device.c b/src/class/cdc/cdc_device.c index 56d4aeed9..ed050ad03 100644 --- a/src/class/cdc/cdc_device.c +++ b/src/class/cdc/cdc_device.c @@ -323,9 +323,7 @@ uint16_t cdcd_open(uint8_t rhport, const tusb_desc_interface_t* itf_desc, uint16 tu_edpt_stream_t *stream_tx = &p_cdc->tx_stream; tu_edpt_stream_open(stream_tx, rhport, desc_ep, CFG_TUD_CDC_TX_EPSIZE); - #if CFG_TUD_CDC_TX_PERSISTENT - tu_edpt_stream_write_xfer(stream_tx); // flush pending data - #else + #if !CFG_TUD_CDC_TX_PERSISTENT tu_edpt_stream_clear(stream_tx); #endif } else { -- cgit v1.3.1 From b0738b5949130d5ab175835d72bb793830d09bc9 Mon Sep 17 00:00:00 2001 From: rt-rtos Date: Tue, 4 Aug 2026 21:09:20 +0200 Subject: audio_device: enforce the documented FIFO minimum in the EP-IN flow-control guard The comment above audiod_tx_packet_size() states flow control needs a FIFO of at least 4*Navg, but the guard tests nominal_size[1] <= fifo_depth * 4 - true for any FIFO larger than a quarter packet - instead of nominal_size[1] * 4 <= fifo_depth. As written, flow control engages on FIFOs far below its own documented minimum, where the depth/2 setpoint sits within one packet of empty and the packet_size = 0 branch (a zero-length packet, i.e. an audible 1 ms dropout for audio-class hosts) is reachable from ordinary scheduling jitter rather than only from gross clock deviation. With the guard corrected, undersized FIFOs fall back to the plain min(count, max) path as intended. --- src/class/audio/audio_device.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/class/audio/audio_device.c b/src/class/audio/audio_device.c index 94881521a..bc4c7e544 100644 --- a/src/class/audio/audio_device.c +++ b/src/class/audio/audio_device.c @@ -1841,7 +1841,7 @@ static bool audiod_calc_tx_packet_sz(audiod_function_t *audio) { static uint16_t audiod_tx_packet_size(const uint16_t *nominal_size, uint16_t data_count, uint16_t fifo_depth, uint16_t fifo_threshold, uint16_t max_depth) { // Flow control need a FIFO size of at least 4*Navg - if (nominal_size[1] && nominal_size[1] <= fifo_depth * 4) { + if (nominal_size[1] && nominal_size[1] * 4 <= fifo_depth) { // Use blackout to prioritize normal size packet static int ctrl_blackout = 0; uint16_t packet_size; -- cgit v1.3.1 From cf055c237a93d3308e1670285dfd2b629f0dd8af Mon Sep 17 00:00:00 2001 From: Cedric Van den Bergh Date: Wed, 8 Jul 2026 12:47:03 +0100 Subject: ncm: fix carrier lost on link-state notify collision tud_network_link_state() delivered the NETWORK_CONNECTION notification edge-triggered and fire-once: if a previous notification was still in flight, notification_xmit() returned early and the notification for the new link state was never queued. Because link_is_up is committed before the send, the host could be left reporting a stale carrier state - e.g. a permanent NO-CARRIER after a link up. The notification state was also mutated from both the caller and the notify xfer-completion callback with no serialisation, so on RTOS ports where tud_network_link_state() runs in a task other than tud_task() the two could race. Defer the whole link-state update onto the usbd task, so it can no longer race the completion callback. A collision with an in-flight notification is resolved by re-arming notification_xmit_state and letting the existing completion callback drive it forward on the next xfer completion, rather than adding a separate pending/retry flag. A link toggle does not change the link speed, so strictly only the NETWORK_CONNECTION notification needs (re)sending, but reusing the existing speed-then-connection state machine keeps the fix on a single, already-serialised code path. Closes #3760 --- src/class/net/ncm_device.c | 47 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/class/net/ncm_device.c b/src/class/net/ncm_device.c index 84a524f49..72b592787 100644 --- a/src/class/net/ncm_device.c +++ b/src/class/net/ncm_device.c @@ -800,31 +800,56 @@ static void tud_network_recv_renew_r(uint8_t rhport) { } // tud_network_recv_renew /** - * Set the link state and send notification to host + * usbd-task trampoline for tud_network_link_state(), packing rhport and is_up + * into a single pointer-sized argument. + * + * Runs entirely in the usbd task context, so it cannot race the notify + * xfer-completion callback over the notification state machine. Re-arming + * notification_xmit_state and kicking notification_xmit() (rather than + * sending NETWORK_CONNECTION directly) means a state change that collides + * with an in-flight notification is picked up by the existing completion + * callback instead of being silently dropped - which would otherwise leave + * the host stuck at NO-CARRIER after a link-state change. */ -void tud_network_link_state(uint8_t rhport, bool is_up) { - TU_LOG_DRV("tud_network_link_state(%d, %d)\n", rhport, is_up); +static void ncm_link_state_task(void *param) { + uintptr_t const arg = (uintptr_t) param; + uint8_t const rhport = (uint8_t) (arg >> 1); + bool const is_up = (arg & 1u) != 0; if (ncm_interface.link_is_up == is_up) { - // No change in link state - return; + return; // no change in link state } ncm_interface.link_is_up = is_up; - // Only send notification if we have an active data interface if (ncm_interface.itf_data_alt != 1) { - TU_LOG_DRV(" link state notification skipped (interface not active)\n"); - return; + TU_LOG_DRV(" link state notification deferred (interface not active)\n"); + return; // data interface not active yet; SET_INTERFACE(alt=1) will notify } - // Reset notification state to send speed change notification first, then link state notification + // A link toggle does not change the link speed, so strictly only the + // NETWORK_CONNECTION notification would need (re)sending. Re-running the + // speed-then-connection sequence keeps this on the same state machine the + // completion callback already drives, at the cost of a redundant speed + // notification on every toggle. ncm_interface.notification_xmit_state = NOTIFICATION_SPEED; - - // Trigger notification transmission notification_xmit(rhport, false); } +/** + * Set the link state and notify the host. + * + * Defers onto the usbd task so a caller running in a different task than + * tud_task() cannot race the notification state machine against the notify + * xfer-completion callback. + */ +void tud_network_link_state(uint8_t rhport, bool is_up) { + TU_LOG_DRV("tud_network_link_state(%d, %d)\n", rhport, is_up); + + uintptr_t const arg = ((uintptr_t) rhport << 1) | (is_up ? 1u : 0u); + usbd_defer_func(ncm_link_state_task, (void *) arg, false); +} + //----------------------------------------------------------------------------- // // all the netd_*() stuff (interface TinyUSB -> driver) -- cgit v1.3.1 From f617a208c16ec720b4b5da93b32cbcb8914fcaf7 Mon Sep 17 00:00:00 2001 From: HiFiPhile Date: Tue, 11 Aug 2026 14:41:00 +0200 Subject: fix(dwc2): correct host FIFO allocation --- src/portable/synopsys/dwc2/hcd_dwc2.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/portable/synopsys/dwc2/hcd_dwc2.c b/src/portable/synopsys/dwc2/hcd_dwc2.c index 089b839ae..e8a3d6623 100644 --- a/src/portable/synopsys/dwc2/hcd_dwc2.c +++ b/src/portable/synopsys/dwc2/hcd_dwc2.c @@ -336,13 +336,13 @@ TU_ATTR_ALWAYS_INLINE static inline uint8_t cal_next_pid(uint8_t pid, uint8_t pa static void dfifo_host_init(uint8_t rhport, bool is_hs_phy) { const dwc2_controller_t* dwc2_controller = &_dwc2_controller[rhport]; dwc2_regs_t* dwc2 = DWC2_REG(rhport); - const dwc2_ghwcfg2_t ghwcfg2 = {.value = dwc2->ghwcfg2}; + const uint8_t channel_count = dwc2_channel_count(dwc2); // Scatter/Gather DMA mode is not yet supported. Buffer DMA only need 1 words per channel const bool is_dma = dma_host_enabled(dwc2); uint16_t dfifo_top = dwc2_controller->otg_dfifo_depth; if (is_dma) { - dfifo_top -= ghwcfg2.num_host_ch; + dfifo_top -= channel_count; } // fixed allocation for now, improve later: @@ -358,13 +358,12 @@ static void dfifo_host_init(uint8_t rhport, bool is_hs_phy) { } uint16_t nptxfsiz = 2 * nptx_largest; - uint16_t rxfsiz = 2 * (ptx_largest + 2) + ghwcfg2.num_host_ch; + uint16_t rxfsiz = 2 * (ptx_largest + 2) + channel_count; TU_ASSERT(dfifo_top >= (nptxfsiz + rxfsiz),); uint16_t ptxfsiz = dfifo_top - (nptxfsiz + rxfsiz); dwc2->gdfifocfg = (dfifo_top << GDFIFOCFG_EPINFOBASE_SHIFT) | dfifo_top; - dfifo_top -= rxfsiz; dwc2->grxfsiz = rxfsiz; dfifo_top -= nptxfsiz; -- cgit v1.3.1 From 73e787ae418df76b79ac0fd31bb0674a72e68c88 Mon Sep 17 00:00:00 2001 From: Ryzee119 Date: Wed, 12 Aug 2026 20:40:15 +0930 Subject: ohci: fix double allocation of dummy TDs in gtd_find_free --- src/portable/ohci/ohci.c | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/portable/ohci/ohci.c b/src/portable/ohci/ohci.c index e2c5956b3..a294c5f5f 100644 --- a/src/portable/ohci/ohci.c +++ b/src/portable/ohci/ohci.c @@ -400,6 +400,7 @@ static void ed_list_remove_by_addr(ohci_ed_t * p_head, uint8_t dev_addr) { static ohci_gtd_t* gtd_find_free(void) { for (uint8_t i = 0; i < GTD_MAX; i++) { if (!ohci_data.gtd_pool[i].used) { + ohci_data.gtd_pool[i].used = 1; return &ohci_data.gtd_pool[i]; } } -- cgit v1.3.1 From a0249ada9096365697340031a7b4a285beb18a2b Mon Sep 17 00:00:00 2001 From: hathach Date: Wed, 12 Aug 2026 22:36:49 +0700 Subject: usbd: don't leak the queued-setup counter when the event queue is full A SETUP arriving while the event queue is full is silently dropped by queue_event(), but _usbd_queued_setup has already been incremented. The leaked count makes the event handler skip every subsequent SETUP ("Skipped since there is other SETUP in queue") forever: EP0 stays deaf until tud_init() while the device otherwise looks alive - enumerated, endpoints armed. Undo the increment when the enqueue fails. Unit test: fill the queue so a SETUP is dropped, then verify the next SETUP still completes a GET_DESCRIPTOR control transfer. --- src/device/usbd.c | 6 +++-- test/unit-test/test/device/usbd/test_usbd.c | 38 +++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/device/usbd.c b/src/device/usbd.c index 5471e132d..b77b766dd 100644 --- a/src/device/usbd.c +++ b/src/device/usbd.c @@ -1473,8 +1473,10 @@ TU_ATTR_FAST_FUNC void dcd_event_handler(dcd_event_t const* event, bool in_isr) break; } - if (send) { - queue_event(event, in_isr); + if (send && !queue_event(event, in_isr) && event->event_id == DCD_EVENT_SETUP_RECEIVED) { + // dropped by a full queue: undo the increment, else every later SETUP is skipped as + // "other SETUP in queue" and EP0 is deaf until re-init + _usbd_queued_setup--; } } diff --git a/test/unit-test/test/device/usbd/test_usbd.c b/test/unit-test/test/device/usbd/test_usbd.c index 7f3c3f5b2..935a20221 100644 --- a/test/unit-test/test/device/usbd/test_usbd.c +++ b/test/unit-test/test/device/usbd/test_usbd.c @@ -270,6 +270,44 @@ void test_usbd_control_in_zlp(void) tud_task(); } +//--------------------------------------------------------------------+ +// SETUP dropped by full event queue +//--------------------------------------------------------------------+ + +// When the event queue is full, queue_event() drops the SETUP event. The queued-setup +// counter must not keep the dropped SETUP's increment: a leaked count makes the handler +// skip every later SETUP ("other SETUP in queue") forever, leaving EP0 permanently deaf. +void test_usbd_setup_dropped_by_full_queue_recovers(void) +{ + // fillers drain through usbd_reset -> class reset + mscd_reset_Ignore(); + + // fill the queue to the brim, then post one more SETUP: queue_event() drops it + for (unsigned i = 0; i < CFG_TUD_TASK_QUEUE_SZ; i++) { + dcd_event_bus_signal(rhport, DCD_EVENT_UNPLUGGED, false); + } + dcd_event_setup_received(rhport, (uint8_t*) &req_get_desc_device, false); + + // drain all fillers (each tud_task pass handles at most CFG_TUD_TASK_EVENTS_PER_RUN + // events); the dropped SETUP never arrives + for (unsigned i = 0; i < (CFG_TUD_TASK_QUEUE_SZ / CFG_TUD_TASK_EVENTS_PER_RUN) + 1; i++) { + tud_task(); + } + + // the next SETUP must still be answered + desc_device = (uint8_t const*) &data_desc_device; + dcd_event_setup_received(rhport, (uint8_t*) &req_get_desc_device, false); + + dcd_edpt_xfer_ExpectWithArrayAndReturn(rhport, 0x80, (uint8_t*) &data_desc_device, sizeof(tusb_desc_device_t), sizeof(tusb_desc_device_t), false, true); + dcd_event_xfer_complete(rhport, EDPT_CTRL_IN, sizeof(tusb_desc_device_t), 0, false); + + dcd_edpt_xfer_ExpectAndReturn(rhport, EDPT_CTRL_OUT, NULL, 0, false, true); + dcd_event_xfer_complete(rhport, EDPT_CTRL_OUT, 0, 0, false); + dcd_edpt0_status_complete_ExpectWithArray(rhport, &req_get_desc_device, 1); + + tud_task(); +} + //--------------------------------------------------------------------+ // Control OUT data stage host overrun //--------------------------------------------------------------------+ -- cgit v1.3.1 From a52562b2be7ea728a176ae94d8a18d9ae0a4423a Mon Sep 17 00:00:00 2001 From: hathach Date: Wed, 12 Aug 2026 22:37:06 +0700 Subject: usbd: clear the queued-setup counter on bus reset A SETUP counted before a bus reset must not be carried across it: the consumer would either skip a post-reset SETUP (count drained by the stale entry) or, if the count leaked high for any other reason, skip them all. usbd_reset() now zeroes the counter; the consumer already guards on zero, and any pre-reset SETUP still in the queue is stale by definition and correctly discarded. --- src/device/usbd.c | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src') diff --git a/src/device/usbd.c b/src/device/usbd.c index b77b766dd..79802e70f 100644 --- a/src/device/usbd.c +++ b/src/device/usbd.c @@ -642,6 +642,8 @@ static void configuration_reset(uint8_t rhport) { static void usbd_reset(uint8_t rhport) { configuration_reset(rhport); + // discard any pre-reset SETUP still counted: a stale count skips post-reset SETUPs + _usbd_queued_setup = 0; } bool tud_task_event_ready(void) { -- cgit v1.3.1 From 91fbbd192ca9539221d3dc096f00ce77836a5d3d Mon Sep 17 00:00:00 2001 From: hathach Date: Wed, 12 Aug 2026 23:05:59 +0700 Subject: usbd: clear endpoint busy/claimed when a completion event is dropped An XFER_COMPLETE dropped by a full event queue leaves its endpoint's BUSY|CLAIMED state set forever - the consumer that normally clears it never sees the event, so usbd_edpt_claim()/usbd_edpt_xfer() fail from then on and the class never re-arms the endpoint. Clear both flags when the enqueue fails: the completion is lost either way, but the endpoint stays usable. Unit test: arm a bulk endpoint, drop its completion against a full queue, verify the endpoint can be claimed and re-armed. --- src/device/usbd.c | 16 +++++++--- test/unit-test/test/device/usbd/test_usbd.c | 47 +++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/device/usbd.c b/src/device/usbd.c index 79802e70f..f5c3046d6 100644 --- a/src/device/usbd.c +++ b/src/device/usbd.c @@ -1475,10 +1475,18 @@ TU_ATTR_FAST_FUNC void dcd_event_handler(dcd_event_t const* event, bool in_isr) break; } - if (send && !queue_event(event, in_isr) && event->event_id == DCD_EVENT_SETUP_RECEIVED) { - // dropped by a full queue: undo the increment, else every later SETUP is skipped as - // "other SETUP in queue" and EP0 is deaf until re-init - _usbd_queued_setup--; + if (send && !queue_event(event, in_isr)) { + // event dropped by a full queue: undo state that would otherwise wedge permanently + if (event->event_id == DCD_EVENT_SETUP_RECEIVED) { + // undo the increment, else every later SETUP is skipped as "other SETUP in queue" + // and EP0 is deaf until re-init + _usbd_queued_setup--; + } else if (event->event_id == DCD_EVENT_XFER_COMPLETE) { + // clear busy + claimed, else the endpoint can never be claimed or re-armed again + uint8_t const epnum = tu_edpt_number(event->xfer_complete.ep_addr); + uint8_t const ep_dir = tu_edpt_dir(event->xfer_complete.ep_addr); + _usbd_dev.ep_status[epnum][ep_dir] &= (uint8_t) ~(TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED); + } } } diff --git a/test/unit-test/test/device/usbd/test_usbd.c b/test/unit-test/test/device/usbd/test_usbd.c index 935a20221..849097326 100644 --- a/test/unit-test/test/device/usbd/test_usbd.c +++ b/test/unit-test/test/device/usbd/test_usbd.c @@ -29,6 +29,7 @@ #include "tusb_fifo.h" #include "tusb.h" #include "usbd.h" +#include "device/usbd_pvt.h" TEST_SOURCE_FILE("usbd.c") // Mock File @@ -308,6 +309,52 @@ void test_usbd_setup_dropped_by_full_queue_recovers(void) tud_task(); } +//--------------------------------------------------------------------+ +// Transfer completion dropped by full event queue +//--------------------------------------------------------------------+ + +// When the event queue is full, queue_event() drops the XFER_COMPLETE event. The endpoint's +// busy/claimed state must not survive the dropped completion: a leaked BUSY makes every later +// usbd_edpt_claim()/usbd_edpt_xfer() on that endpoint fail, so the class never re-arms it. +void test_usbd_xfer_complete_dropped_by_full_queue_recovers(void) +{ + // fillers drain through usbd_reset -> class reset + mscd_reset_Ignore(); + + // open + claim + arm a bulk OUT endpoint the way a class driver would + tusb_desc_endpoint_t desc_ep = { + .bLength = sizeof(tusb_desc_endpoint_t), + .bDescriptorType = TUSB_DESC_ENDPOINT, + .bEndpointAddress = 0x01, + .bmAttributes = { .xfer = TUSB_XFER_BULK }, + .wMaxPacketSize = 64, + .bInterval = 0 + }; + static uint8_t xfer_buf[64]; + + dcd_edpt_open_ExpectAndReturn(rhport, &desc_ep, true); + TEST_ASSERT_TRUE(usbd_edpt_open(rhport, &desc_ep)); + TEST_ASSERT_TRUE(usbd_edpt_claim(rhport, 0x01)); + dcd_edpt_xfer_ExpectAndReturn(rhport, 0x01, xfer_buf, 64, false, true); + TEST_ASSERT_TRUE(usbd_edpt_xfer(rhport, 0x01, xfer_buf, 64, false)); + + // fill the queue to the brim, then complete the transfer: queue_event() drops it + for (unsigned i = 0; i < CFG_TUD_TASK_QUEUE_SZ; i++) { + dcd_event_bus_signal(rhport, DCD_EVENT_UNPLUGGED, false); + } + dcd_event_xfer_complete(rhport, 0x01, 64, XFER_RESULT_SUCCESS, false); + + // the endpoint must be re-armable: the dropped completion must not leak busy/claimed + TEST_ASSERT_TRUE(usbd_edpt_claim(rhport, 0x01)); + dcd_edpt_xfer_ExpectAndReturn(rhport, 0x01, xfer_buf, 64, false, true); + TEST_ASSERT_TRUE(usbd_edpt_xfer(rhport, 0x01, xfer_buf, 64, false)); + + // drain the fillers so later tests start from an empty queue + for (unsigned i = 0; i < (CFG_TUD_TASK_QUEUE_SZ / CFG_TUD_TASK_EVENTS_PER_RUN) + 1; i++) { + tud_task(); + } +} + //--------------------------------------------------------------------+ // Control OUT data stage host overrun //--------------------------------------------------------------------+ -- cgit v1.3.1 From c950109bcef6404971c3609a6e4caeb0421120b8 Mon Sep 17 00:00:00 2001 From: "Zhang, Zhenjiang" Date: Fri, 14 Aug 2026 11:10:23 +0800 Subject: feat(class/audio): rework TUH_AUDIO into a WASAPI/ALSA-like stream API Provide a high-level audio streaming API over UAC 1.0 devices while keeping the USB topology private: applications select supported {format, sample_rate, channels} configurations per logical stream, and the driver owns the mapping to AS interface, alternate setting, and endpoint. - One logical stream per direction per instance; multiple AS interfaces and alternate settings in a direction are merged into the stream's configuration list (discrete tuples; continuous ranges exposed as a single configuration at the top rate) - Asynchronous tuh_audio_configure(): SET_INTERFACE to the selected alternate setting, open/reconfigure the endpoint, set the sampling frequency when supported, initialize the FIFO and packet scheduler, then invoke the completion callback - Frame-based FIFO streaming: tuh_audio_read()/tuh_audio_write() queue whole frames; the driver owns transfer replenishment and fractional packet scheduling (44.1 kHz pays back the 0.1 frame/ms remainder via an accumulator for exact average pacing) - tuh_audio_start()/tuh_audio_stop() activate/deactivate the stream interface through SET_INTERFACE (alt n / alt 0) Driver correctness fixes: - Parse only the AC header's interface collection; MIDI Streaming and other subclasses are skipped - Keep every discrete format as a separate configuration; endpoints are opened only for the alternate setting selected by tuh_audio_configure() - Check tuh_interface_set() return values and SET_INTERFACE transfer results instead of ignoring failures - Validate instance state, direction, buffers, and frame counts in every transfer API - Feature Unit requests use the control's real width (mute/AGC/loudness 1 byte, others 2 bytes) and convert multibyte values to host order - Failed/stalled/aborted isochronous transfers reach only the error callback, never the capture/playback callbacks The audio_host example uses the new API: 48 kHz stereo by default, automatic stream restart on error callbacks, a sine test tone on the playback stream, and periodic mic-only / spk-only / echo phase switching. --- examples/host/CMakeLists.txt | 1 + examples/host/audio_host/README.md | 67 +- examples/host/audio_host/src/app.h | 5 +- examples/host/audio_host/src/audio_app.c | 566 +++++++--- examples/host/audio_host/src/main.c | 32 +- examples/host/audio_host/src/tusb_config.h | 8 +- src/class/audio/audio_host.c | 1576 +++++++++++++++++++--------- src/class/audio/audio_host.h | 247 +++-- 8 files changed, 1716 insertions(+), 786 deletions(-) (limited to 'src') diff --git a/examples/host/CMakeLists.txt b/examples/host/CMakeLists.txt index 7c74e3c73..0e877cb78 100644 --- a/examples/host/CMakeLists.txt +++ b/examples/host/CMakeLists.txt @@ -7,6 +7,7 @@ family_initialize_project(tinyusb_host_examples ${CMAKE_CURRENT_LIST_DIR}) # family_add_subdirectory will filter what to actually add based on selected FAMILY set(EXAMPLE_LIST + audio_host bare_api cdc_msc_hid cdc_msc_hid_freertos diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md index 072adbbf9..2ad3f40d6 100644 --- a/examples/host/audio_host/README.md +++ b/examples/host/audio_host/README.md @@ -1,22 +1,25 @@ # USB Audio Host Example -This example demonstrates how to use TinyUSB's USB Audio Host driver (TUH_AUDIO) to communicate with a UAC 1.0 compatible USB Audio Device. +This example demonstrates how to use TinyUSB's USB Audio Host driver (TUH_AUDIO) to capture audio from a UAC 1.0 compatible USB microphone and echo it back to the speaker, using a WASAPI/ALSA-like high-level API. The application never touches USB interfaces, alternate settings, or endpoint addresses — it only selects supported `{format, sample_rate, channels}` configurations by stream index. ## Features - Enumerates and mounts USB Audio Class 1.0 devices -- Receives audio data from IN endpoint (e.g., microphone) -- Sends audio data to OUT endpoint (e.g., speaker) -- Sets sampling frequency via control requests -- Demonstrates isochronous transfer handling +- Discovers the device's logical streams (capture/playback) and their supported configurations (discrete tuples only) +- Configures and starts an S16_LE capture stream (48 kHz preferred, 44.1 kHz fallback; stereo preferred, mono accepted) +- Echoes captured audio to an S16_LE playback stream at the same sample rate (same channel count preferred, mono/stereo conversion otherwise) +- Frame-based FIFO API: `tuh_audio_read()` / `tuh_audio_write()` queue frames; the driver schedules the 1 ms isochronous transfers +- Cycles the streams through three phases (5 s each): mic-only (capture, data dropped), spk-only (sine test tone), and echo (capture looped back to playback) ## Supported Devices -This example supports any UAC 1.0 compliant USB Audio device, such as: +This example supports any UAC 1.0 compliant USB audio device with a discrete sampling-frequency capture stream, such as: - USB microphones -- USB speakers/headphones +- USB headsets (mono microphone + speaker) - USB audio interfaces +The echo needs a matching S16_LE playback stream at the capture sample rate; devices without one run capture-only. The sample rate and channel preferences are configured by the `SAMPLE_RATES` / `AUDIO_MAX_CHANNELS` macros in `src/audio_app.c` (48 kHz stereo by default). Continuous sampling-frequency ranges are exposed as a single configuration at the range's highest frequency (e.g. a 8000–48000 Hz speaker appears as 48000 Hz); non-PCM formats are rejected by the driver. + ## Building ### Using CMake (recommended) @@ -53,46 +56,40 @@ make BOARD= flash 2. Connect a USB Audio device (UAC 1.0) to the USB host port 3. Open a serial terminal to view output 4. The example will: - - Print device information when mounted - - Set sampling frequency based on the device's advertised capabilities - - Receive audio samples from the device (IN endpoint) - - Loop back received audio to the device (OUT endpoint) for testing + - Print each stream's supported configurations when mounted + - Look for an S16_LE capture configuration at a preferred sample rate (48 kHz first, 44.1 kHz fallback; stereo preferred, mono accepted) and configure it + - Echo captured audio to an S16_LE playback configuration at the same sample rate (same channel count preferred, converted otherwise) + - Drain the capture FIFO in `audio_app_task_read()` and queue the frames into the playback FIFO; a sine test tone plays on the playback stream when no capture stream is echoing + - Cycle through the three phases (mic-only / spk-only / echo, 5 s each) with `tuh_audio_start()` / `tuh_audio_stop()`; a failed stream is restarted automatically 100 ms after the error callback ## Serial Output Example ``` TinyUSB Host USB Audio Example Connect a USB Audio Device (UAC 1.0) to test -Audio device mounted: idx=0, daddr=1 - --- Microphone --- - IN EP: 0x81 (max size: 192) - Input Terminal: ID=1, Type=0x0201, Channels=1 - Format Type: 1, Channels: 1, SubFrameSize: 2, BitResolution: 16 - Sampling Freq: Discrete, count=4 - Freq[0]: 44100 Hz - Freq[1]: 48000 Hz - Freq[2]: 96000 Hz - Freq[3]: 192000 Hz - --- Speaker --- - OUT EP: 0x02 (max size: 192) - Output Terminal: ID=2, Type=0x0301 - Format Type: 1, Channels: 2, SubFrameSize: 2, BitResolution: 16 - Sampling Freq: Continuous range 8000 Hz - 48000 Hz - Feature Unit: ID=3, SourceID=1 - Setting IN sampling frequency to 48000 Hz - Setting OUT sampling frequency to 48000 Hz - Sampling frequency set OK, ready for isochronous transfer +Audio device mounted: idx=0 addr=1 + capture stream 1 configurations: 2 + [0] format=1 rate=44100 channels=2 + [1] format=1 rate=48000 channels=2 + playback stream 0 configurations: 2 + [0] format=1 rate=44100 channels=2 + [1] format=1 rate=48000 channels=2 + Configuring 48 kHz S16_LE capture (2 channels) + Microphone configured, starting capture + Configuring 48 kHz S16_LE playback (2 channels) + Speaker configured, starting playback ``` ## Configuration Edit `src/tusb_config.h` to modify: - `CFG_TUH_AUDIO_MAX`: Maximum number of audio devices supported -- `CFG_TUH_AUDIO_EPIN_BUFSIZE`: IN endpoint buffer size -- `CFG_TUH_AUDIO_EPOUT_BUFSIZE`: OUT endpoint buffer size +- `CFG_TUH_AUDIO_EPIN_BUFSIZE`: Maximum size of one capture transfer the driver submits (configurations needing a larger per-poll-interval packet are rejected) +- `CFG_TUH_AUDIO_EPOUT_BUFSIZE`: Maximum size of one playback transfer the driver submits +- `CFG_TUH_AUDIO_STREAM_BUFSIZE`: Per-stream FIFO depth in bytes (default 1024, i.e. four 256 B packets) ## Notes -- This example uses isochronous transfers which require precise timing -- For production applications, synchronize audio transfers with the device's audio clock -- The example sends a simple sine wave for testing; replace with actual audio data in real applications +- While a stream is running, the driver keeps one isochronous transfer in flight and re-submits on completion, so transfers are naturally paced at the 1 ms USB frame rate. `tuh_audio_capture_cb()` / `tuh_audio_playback_cb()` report each completed transfer; `tuh_audio_err_cb()` reports failures. The example restarts the failed stream automatically 100 ms after the error callback. +- `tuh_audio_read()` / `tuh_audio_write()` are non-blocking FIFO operations: they return the number of whole frames actually queued/read (0 when the FIFO is empty/full or the stream is not running), and `tuh_audio_read_available()` / `tuh_audio_write_available()` report the FIFO occupancy in frames. +- Isochronous transfers require the host to poll `tuh_task()` continuously; the capture FIFO absorbs short scheduling gaps, but frames are dropped when it overflows. diff --git a/examples/host/audio_host/src/app.h b/examples/host/audio_host/src/app.h index a807aeaa6..3ebb5c16d 100644 --- a/examples/host/audio_host/src/app.h +++ b/examples/host/audio_host/src/app.h @@ -21,6 +21,7 @@ #include #include -void audio_app_task(void); - +void audio_app_task_read(void); +void audio_app_task_write(void); +void defer_queue_task(void); #endif diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c index d9134f089..40ad620bf 100644 --- a/examples/host/audio_host/src/audio_app.c +++ b/examples/host/audio_host/src/audio_app.c @@ -15,6 +15,7 @@ */ #include +#include #include "bsp/board_api.h" #include "tusb.h" #include "app.h" @@ -23,199 +24,488 @@ // MACRO TYPEDEF CONSTANT ENUM DECLARATION //--------------------------------------------------------------------+ -static bool audio_mounted = false; -static uint8_t audio_dev_addr = 0xFF; -static volatile bool audio_ready = false; // Wait for sampling freq set before starting isochronous transfer -static volatile bool audio_rx_busy = false; // Track IN endpoint transfer state -static volatile bool audio_tx_busy = false; // Track OUT endpoint transfer state -static uint8_t audio_idx = 0xFF; -static uint8_t audiostream_in_idx = 0xFF; -static uint8_t audiostream_out_idx = 0xFF; -static uint32_t sampling_freq = 48000; // Default sampling frequency (Hz) -static uint8_t audio_mic_channels = 1; +// Default configuration of this example, adjust to the target device: +// - AUDIO_MAX_FRAME_COUNT: buffer holds up to 48 frames (1 ms of 48 kHz) +// - AUDIO_MAX_CHANNELS: maximum channels of the capture/playback stream +// - SAMPLE_RATES: sample rates tried in order, first match wins (44.1 kHz stereo by default) +#define AUDIO_MAX_FRAME_COUNT 48 +#define AUDIO_MAX_CHANNELS 2 +#define SAMPLE_RATES {48000, 44100} +static uint8_t audio_idx = TUSB_INDEX_INVALID_8; // index of the selected audio device +static uint8_t cap_stream_idx = TUSB_INDEX_INVALID_8; // capture stream index +static uint8_t spk_stream_idx = TUSB_INDEX_INVALID_8; // playback stream index +static bool mic_ready = false; // capture stream is running +static bool spk_ready = false; // playback stream is running +static int16_t mic_samples[AUDIO_MAX_FRAME_COUNT * AUDIO_MAX_CHANNELS]; // capture FIFO read buffer +static int16_t spk_samples[AUDIO_MAX_FRAME_COUNT * AUDIO_MAX_CHANNELS]; // playback FIFO write buffer +static tuh_audio_stream_config_t mic_config; // selected capture configuration +static tuh_audio_stream_config_t spk_config; // selected playback configuration +static uint32_t audio_frame_count = AUDIO_MAX_FRAME_COUNT; // frames per ms of the selected rate +static uint32_t spk_cb_count = 0; // count of playback callbacks (for debug) +static uint32_t mic_cb_count = 0; // count of capture callbacks (for debug) +static uint32_t err_cb_count = 0; // count of error callbacks (for debug) -static uint8_t audio_rx_buffer[CFG_TUH_AUDIO_EPIN_BUFSIZE] __attribute__((aligned(4))); -static uint8_t audio_tx_buffer[CFG_TUH_AUDIO_EPOUT_BUFSIZE] __attribute__((aligned(4))); //--------------------------------------------------------------------+ // Helper Functions //--------------------------------------------------------------------+ -// Mono (96 bytes, 48 samples) -> Stereo (192 bytes) -static void mono_to_stereo(const uint8_t *mono, uint8_t *stereo, uint16_t mono_samples) { - for (uint16_t i = 0; i < mono_samples; i++) { - // Copy 2 bytes (one int16 sample) to left channel - stereo[i * 4] = mono[i * 2]; - stereo[i * 4 + 1] = mono[i * 2 + 1]; - // Copy same 2 bytes to right channel - stereo[i * 4 + 2] = mono[i * 2]; - stereo[i * 4 + 3] = mono[i * 2 + 1]; +//--------------------------------------------------------------------+ +// Async Deferred Call Queue +//--------------------------------------------------------------------+ +// Schedules one-shot callbacks to be invoked after a given delay in ms. +// Processed by defer_queue_task() in the main loop, no dynamic allocation. + +#define APP_DEFER_QUEUE_SZ 4 + +typedef void (*app_defer_func_t)(uintptr_t param); + +typedef struct { + app_defer_func_t func; + uintptr_t arg; + uint32_t at_ms; +} app_defer_t; + +static app_defer_t _defer_q[APP_DEFER_QUEUE_SZ]; + +// Clear all pending deferred callbacks. +static void app_defer_queue_clear(void) { + memset(_defer_q, 0, sizeof(_defer_q)); +} + +// Schedule func to be called after 'ms' milliseconds, returns false if queue is full +static bool app_defer_ms_async(uint32_t ms, app_defer_func_t func, uintptr_t arg) { + for (uint8_t i = 0; i < APP_DEFER_QUEUE_SZ; i++) { + if (_defer_q[i].func == NULL) { + _defer_q[i].func = func; + _defer_q[i].arg = arg; + // add one to ensure we wait at least 'ms' milliseconds + _defer_q[i].at_ms = tusb_time_millis_api() + ms + 1; + return true; + } } + return false; // queue full } -// Print sampling frequency info for an AS interface -static void print_sampling_freq(const tuh_audio_as_info_t *as) { - if (as->sam_freq_type == 0) { - printf(" Sampling Freq: Continuous range %lu Hz - %lu Hz\r\n", (unsigned long)as->sam_freq_lower, - (unsigned long)as->sam_freq_upper); - } else { - printf(" Sampling Freq: Discrete, count=%u\r\n", as->sam_freq_type); - for (uint8_t j = 0; j < as->sam_freq_type && j < CFG_TUH_AUDIO_MAX_SAM_FREQ; j++) { - printf(" Freq[%u]: %lu Hz\r\n", j, (unsigned long)as->sam_freq[j]); +// Invoke all callbacks whose delay has expired, must be called periodically from main loop +void defer_queue_task(void) { + const uint32_t now_ms = tusb_time_millis_api(); + for (uint8_t i = 0; i < APP_DEFER_QUEUE_SZ; i++) { + if (_defer_q[i].func != NULL && (int32_t)(_defer_q[i].at_ms - now_ms) <= 0) { + const app_defer_func_t func = _defer_q[i].func; + const uintptr_t arg = _defer_q[i].arg; + _defer_q[i].func = NULL; // free slot before invoking, callback may re-schedule + func(arg); } } } -// Print all AS interface info -static void print_as_interfaces(uint8_t idx) { - tuh_audio_as_info_t as = {}; - uint8_t as_count = tuh_audio_as_get_count(idx); - for (uint8_t i = 0; i < as_count; i++) { - tuh_audio_as_get_info(idx, i, &as); - if (as.ep_dir == TUSB_DIR_IN) { - // Save microphone channel count for mono-to-stereo conversion - audio_mic_channels = as.num_channels; - printf(" --- Microphone (AS %u) ---\r\n", i); - printf(" IN EP: 0x%02x (max size: %u)\r\n", as.ep_addr, as.ep_size); - } else { - printf(" --- Speaker (AS %u) ---\r\n", i); - printf(" OUT EP: 0x%02x (max size: %u)\r\n", as.ep_addr, as.ep_size); +// Duplicate each mono sample to both channels (mono mic -> stereo speaker) +static void mono_to_stereo(const int16_t *mono, int16_t *stereo, uint32_t frames) { + for (uint32_t i = 0; i < frames; i++) { + stereo[i * 2] = mono[i]; + stereo[i * 2 + 1] = mono[i]; + } +} + +// Average both channels into one sample (stereo mic -> mono speaker) +static void stereo_to_mono(const int16_t *stereo, int16_t *mono, uint32_t frames) { + for (uint32_t i = 0; i < frames; i++) { + mono[i] = (int16_t)(((int32_t)stereo[i * 2] + stereo[i * 2 + 1]) / 2); + } +} + +// One period of an 8 kHz sine (6 samples at 48 kHz), scaled to ~8-bit +// amplitude. The test tone plays only when no capture stream is echoing. +static const int16_t sine_period[6] = {0, 221, 221, 0, -221, -221}; + +// Precompute a sine wave into the playback buffer +static void spk_init_sine(void) { + for (uint32_t i = 0; i < AUDIO_MAX_FRAME_COUNT; i++) { + const int16_t sample = sine_period[i % 6]; + for (uint8_t ch = 0; ch < spk_config.channels; ch++) { + spk_samples[i * AUDIO_MAX_CHANNELS + ch] = sample; } - printf(" Interface: %u, Alt: %u\r\n", as.interface_num, as.alt_setting); - printf(" Format Type: %u, Channels: %u, SubFrameSize: %u, BitResolution: %u\r\n", as.format_type, - as.num_channels, as.sub_frame_size, as.bit_resolution); - print_sampling_freq(&as); } } +// Frames to queue this millisecond at the given sample rate: rate / 1000, +// with the fractional remainder (0.1 frame per ms at 44.1 kHz) accumulated +// and paid back as one extra frame, matching the driver's playback pacing. +static uint32_t frame_rem_acc = 0; +static uint32_t audio_frames_this_ms(uint32_t sample_rate) { + uint32_t frames = sample_rate / 1000; + frame_rem_acc += sample_rate % 1000; + if (frame_rem_acc >= 1000) { + frame_rem_acc -= 1000; + frames++; + } + return frames; +} + //--------------------------------------------------------------------+ -// Application Task +// Periodic Stream Switching //--------------------------------------------------------------------+ -void audio_app_task(void) { - if (!audio_mounted || !audio_ready) { - return; +// Cycles through three phases with tuh_audio_start()/stop(). The driver +// activates/deactivates the stream's interface (SET_INTERFACE alt setting) +// on each switch. +// 1. mic only (3 s): capture runs, captured data is dropped +// 2. spk only (5 s): playback plays the sine test tone +// 3. echo (5 s): both streams run, captured audio is echoed back +#define APP_PHASE_MIC_ONLY_MS 5000 +#define APP_PHASE_SPK_ONLY_MS 5000 +#define APP_PHASE_ECHO_MS 5000 + +enum { + APP_PHASE_MIC_ONLY = 0, + APP_PHASE_SPK_ONLY, + APP_PHASE_ECHO, + APP_PHASE_COUNT +}; + +static uint8_t app_audio_phase = APP_PHASE_MIC_ONLY; +static const uint32_t app_phase_ms[APP_PHASE_COUNT] = {APP_PHASE_MIC_ONLY_MS, APP_PHASE_SPK_ONLY_MS, APP_PHASE_ECHO_MS}; + +// Start or stop the capture/playback streams according to the current phase. +// The app tasks already behave per phase: with mic_ready false the sine tone +// plays, with the playback stream stopped the echo write returns 0 (dropped). +static void app_audio_phase_apply(void) { + switch (app_audio_phase) { + case APP_PHASE_MIC_ONLY: + if (!mic_ready) { + mic_ready = tuh_audio_start(audio_idx, cap_stream_idx); + } + if (spk_ready) { + spk_ready = !tuh_audio_stop(audio_idx, spk_stream_idx); + } + printf(" Phase %u: mic on, spk off (data dropped)\r\n", app_audio_phase); + break; + case APP_PHASE_SPK_ONLY: + if (mic_ready) { + mic_ready = !tuh_audio_stop(audio_idx, cap_stream_idx); + } + if (!spk_ready) { + spk_ready = tuh_audio_start(audio_idx, spk_stream_idx); + } + printf(" Phase %u: mic off, spk on (sine)\r\n", app_audio_phase); + break; + case APP_PHASE_ECHO: + if (!mic_ready) { + mic_ready = tuh_audio_start(audio_idx, cap_stream_idx); + } + if (!spk_ready) { + spk_ready = tuh_audio_start(audio_idx, spk_stream_idx); + } + printf(" Phase %u: mic + spk on (echo)\r\n", app_audio_phase); + break; + default: + break; } +} - if (!audio_rx_busy) { - if (tuh_audio_receive(audio_idx, audiostream_in_idx, audio_rx_buffer, CFG_TUH_AUDIO_EPIN_BUFSIZE)) { - audio_rx_busy = true; - } +// Enter a phase, then schedule the next switch after this phase's duration +static void app_audio_phase_enter(uintptr_t phase) { + app_audio_phase = (uint8_t)phase; + // Cancel stale deferred callbacks (e.g. a stream restart scheduled on a + // transfer error) so they cannot re-start a stream this phase stops. + app_defer_queue_clear(); + app_audio_phase_apply(); + const uint8_t next_phase = (uint8_t)((app_audio_phase + 1) % APP_PHASE_COUNT); + app_defer_ms_async(app_phase_ms[app_audio_phase], (app_defer_func_t)app_audio_phase_enter, next_phase); +} + + +//--------------------------------------------------------------------+ +// Blinking Task +//--------------------------------------------------------------------+ +void led_blinking_task(void) { + const uint32_t interval_ms = 1000; + static uint32_t start_ms = 0; + + static bool led_state = false; + + // Blink every interval ms + if (tusb_time_millis_api() - start_ms < interval_ms) { + return; // not enough time } + start_ms += interval_ms; + + board_led_write(led_state); + led_state = 1 - led_state; // toggle +#if 1 + printf(" MIC CB=%lu SPK CB=%lu ERR CB=%lu\r\n", (unsigned long)mic_cb_count, (unsigned long)spk_cb_count, + (unsigned long)err_cb_count); + mic_cb_count = 0; + spk_cb_count = 0; + err_cb_count = 0; + +#endif +#if 0 + // Print the current Feature Unit volume, which is set to 0x0600 in mic_configured() and can be changed by the device. + uint16_t volume = 0x0001; + tuh_audio_feature_unit_get_sync(audio_idx, AUDIO10_FU_CTRL_VOLUME, 0, &volume); + printf(" Feature Unit volume get: 0x%04x\r\n", (unsigned int)volume); + uint16_t mute = 0x0000; + tuh_audio_feature_unit_get_sync(audio_idx, AUDIO10_FU_CTRL_MUTE, 0, &mute); + mute=!mute; // toggle mute for demonstration + tuh_audio_feature_unit_set_sync(audio_idx, AUDIO10_FU_CTRL_MUTE, 0, mute); + printf(" Feature Unit mute set: 0x%04x\r\n", (unsigned int)mute); +#endif } //--------------------------------------------------------------------+ -// TinyUSB Callbacks +// Application Task //--------------------------------------------------------------------+ -void tuh_audio_mount_cb(uint8_t idx) { - if (idx >= CFG_TUH_AUDIO_MAX) { - printf("Audio device mount failed: idx=%u exceeds max=%u\r\n", idx, CFG_TUH_AUDIO_MAX); +// Echo the captured audio back to the playback stream: drain the capture +// FIFO into mic_samples, convert, and queue the frames into the playback +// FIFO. The driver schedules the actual isochronous transfers. + +void audio_app_task_read(void) { + if (!mic_ready) { return; } - print_as_interfaces(idx); + const uint32_t frames = + tuh_audio_read(audio_idx, cap_stream_idx, mic_samples, audio_frames_this_ms(mic_config.sample_rate)); + if (frames == 0) { + return; + } - // Save device info - audio_dev_addr = tuh_audio_get_dev_addr(idx); - audio_idx = idx; - audio_mounted = true; + if (spk_config.channels == mic_config.channels) { + memcpy(spk_samples, mic_samples, frames * mic_config.channels * sizeof(int16_t)); + } else if (mic_config.channels == 1 && spk_config.channels == 2) { + mono_to_stereo(mic_samples, spk_samples, frames); + } else { + stereo_to_mono(mic_samples, spk_samples, frames); + } - // Find endpoints and IN sampling frequency - tuh_audio_as_info_t as; - for (uint8_t i = 0; i < tuh_audio_as_get_count(idx); i++) { + (void)tuh_audio_write(audio_idx, spk_stream_idx, spk_samples, frames); +} - tuh_audio_as_get_info(idx, i, &as); - if (as.ep_dir == TUSB_DIR_IN) { - audiostream_in_idx = i; - if (as.sam_freq_type > 0) { - sampling_freq = as.sam_freq[0]; - } - } else { - audiostream_out_idx = i; - } +void audio_app_task_write(void) { + // Fallback: the sine test tone when no capture stream is echoing + if (mic_ready || !spk_ready) { + return; } - // Set IN sampling frequency before starting isochronous transfer - if (audiostream_in_idx != 0xFF && sampling_freq != 0) { - printf(" Setting IN sampling frequency to %lu Hz\r\n", (unsigned long)sampling_freq); - // tuh_audio_set_sampling_freq(audio_idx, audiostream_in_idx, sampling_freq, in_sampling_freq_set_cb, 0); + const uint32_t frames = audio_frames_this_ms(spk_config.sample_rate); + if (tuh_audio_write_available(audio_idx, spk_stream_idx) >= frames) { + (void)tuh_audio_write(audio_idx, spk_stream_idx, spk_samples, frames); + } +} - tusb_xfer_result_t result; - result = tuh_audio_set_sampling_freq_sync(audio_idx, audiostream_in_idx, sampling_freq); - if (result == XFER_RESULT_SUCCESS) { - tuh_audio_get_sampling_freq_sync(audio_idx, audiostream_in_idx, &sampling_freq); - printf(" IN sampling frequency set to %lu Hz\r\n", (unsigned long)sampling_freq); - if (audiostream_out_idx != 0xFF) { - printf(" Setting OUT sampling frequency to %lu Hz\r\n", (unsigned long)sampling_freq); - result = tuh_audio_set_sampling_freq_sync(audio_idx, audiostream_out_idx, sampling_freq); - if (result == XFER_RESULT_SUCCESS) { - tuh_audio_get_sampling_freq_sync(audio_idx, audiostream_out_idx, &sampling_freq); - printf(" OUT sampling frequency set to %lu Hz\r\n", (unsigned long)sampling_freq); - } else { - printf(" Setting OUT sampling frequency FAILED: result=%u\r\n", result); - } - } - } else { - printf(" Setting IN sampling frequency FAILED: result=%u\r\n", result); +// Invoked when an isochronous IN transfer completes: the captured data is +// already queued into the capture FIFO and drained by audio_app_task_read(). +void tuh_audio_capture_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { + (void)idx; + (void)stream_idx; + (void)xferred_bytes; + mic_cb_count++; +} + +// Invoked when an isochronous OUT transfer completes: the next queued packet +// is submitted from the playback FIFO by the driver. +void tuh_audio_playback_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { + (void)idx; + (void)stream_idx; + (void)xferred_bytes; + spk_cb_count++; +} + +// Re-open a stream stopped by a transfer error: the driver keeps the stream +// configured, so tuh_audio_start() resumes it. Invoked deferred so repeated +// errors cannot stall the main loop. +static void audio_app_restart_stream(uintptr_t param) { + const uint8_t idx = (uint8_t)(param >> 8); + const uint8_t stream_idx = (uint8_t)param; + if (!tuh_audio_mounted(idx)) { + return; // device is gone + } + if (stream_idx == cap_stream_idx) { + printf(" Restarting capture stream %u\r\n", stream_idx); + mic_ready = tuh_audio_start(idx, stream_idx); + } else if (stream_idx == spk_stream_idx) { + printf(" Restarting playback stream %u\r\n", stream_idx); + spk_ready = tuh_audio_start(idx, stream_idx); + } +} + +// Invoked when an isochronous transfer fails: the stream was stopped by the +// driver, re-open it after a short delay so the device can recover. +void tuh_audio_err_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { + (void)xferred_bytes; + err_cb_count++; + printf(" AUDIO transfer error: addr=%u stream=%u xferred_bytes=%u\r\n", idx, stream_idx, (unsigned)xferred_bytes); + app_defer_ms_async(100, (app_defer_func_t)audio_app_restart_stream, ((uintptr_t)idx << 8) | stream_idx); +} + +//--------------------------------------------------------------------+ +// TinyUSB Callbacks +//--------------------------------------------------------------------+ + +// Print all supported stream configurations +static void print_stream_configs(uint8_t idx, uint8_t stream_idx) { + const tuh_audio_direction_t dir = tuh_audio_stream_direction(idx, stream_idx); + const char *dir_name = (dir == TUH_AUDIO_STREAM_CAPTURE) ? "capture" : "playback"; + printf(" %s stream %u configurations: %u\r\n", dir_name, stream_idx, tuh_audio_config_count(idx, stream_idx)); + for (uint8_t i = 0; i < tuh_audio_config_count(idx, stream_idx); i++) { + tuh_audio_stream_config_t config; + if (tuh_audio_config_get(idx, stream_idx, i, &config)) { + printf(" [%u] format=%u rate=%lu channels=%u\r\n", i, (unsigned)config.format, + (unsigned long)config.sample_rate, (unsigned)config.channels); } - uint16_t volume = 0x0600; + } +} + +// Invoked when the configuration selected by tuh_audio_configure() completes +static void mic_configured(uint8_t idx, uint8_t stream_idx, tusb_xfer_result_t result, uintptr_t user_data) { + (void)user_data; + + if (idx == audio_idx && stream_idx == cap_stream_idx && result == XFER_RESULT_SUCCESS) { + printf(" Microphone configured, starting capture\r\n"); + mic_ready = tuh_audio_start(idx, stream_idx); - result = tuh_audio_feature_unit_set_sync(audio_idx, AUDIO10_FU_CTRL_VOLUME, 0, volume); + uint16_t volume = 0x0600; + result = tuh_audio_feature_unit_set_sync(idx, AUDIO10_FU_CTRL_VOLUME, 0, volume); if (result == XFER_RESULT_SUCCESS) { printf(" Feature Unit volume set:volume 0x%04x\r\n", (unsigned int)volume); - tuh_audio_feature_unit_get_sync(audio_idx, AUDIO10_FU_CTRL_VOLUME, 0, &volume); + tuh_audio_feature_unit_get_sync(idx, AUDIO10_FU_CTRL_VOLUME, 0, &volume); printf(" Feature Unit volume get: 0x%04x\r\n", (unsigned int)volume); } else { printf(" Setting Feature Unit volume FAILED: result=%u\r\n", result); } + } else { + printf(" Microphone configuration failed: result=%u\r\n", result); } - audio_ready = true; } +// Invoked when the playback configuration selected by tuh_audio_configure() completes +static void spk_configured(uint8_t idx, uint8_t stream_idx, tusb_xfer_result_t result, uintptr_t user_data) { + (void)user_data; + if (idx == audio_idx && stream_idx == spk_stream_idx && result == XFER_RESULT_SUCCESS) { + printf(" Speaker configured, starting playback\r\n"); + spk_ready = tuh_audio_start(idx, stream_idx); + // playback-only device: set the frame cadence from the selected rate + audio_frame_count = spk_config.sample_rate / 1000; + spk_init_sine(); // fallback test tone while no capture stream is echoing + + // both streams running: start the periodic phase switching demo + if (mic_ready && spk_ready) { + app_audio_phase_enter(APP_PHASE_MIC_ONLY); + } + } else { + printf(" Speaker configuration failed: result=%u\r\n", result); + } +} // Invoked when device with Audio interface is un-mounted void tuh_audio_umount_cb(uint8_t idx) { printf("Audio device unmounted: idx=%u\r\n", idx); - if (audio_mounted && audio_idx == idx) { - audio_mounted = false; - audio_ready = false; - audio_rx_busy = false; - audio_tx_busy = false; - audio_dev_addr = 0; - audio_idx = 0; - audiostream_in_idx = 0xFF; - audiostream_out_idx = 0xFF; - } -} - -// Invoked when an isochronous IN transfer is complete -void tuh_audio_rx_cb(uint8_t dev_addr, uint8_t ep_addr, uint16_t xferred_bytes) { - (void)dev_addr; - (void)ep_addr; - audio_rx_busy = false; - - if (xferred_bytes > 0 && audiostream_out_idx != 0xFF && !audio_tx_busy) { - bool ok; - if (audio_mic_channels == 1) { - // Mono microphone, convert to stereo and send to OUT endpoint - uint16_t samples = xferred_bytes / 2; - mono_to_stereo(audio_rx_buffer, audio_tx_buffer, samples); - ok = tuh_audio_send(audio_idx, audiostream_out_idx, audio_tx_buffer, xferred_bytes * 2); - } else { - // Stereo microphone, send directly to OUT endpoint - ok = tuh_audio_send(audio_idx, audiostream_out_idx, audio_rx_buffer, xferred_bytes); + if (idx == audio_idx) { + app_defer_queue_clear(); + audio_idx = TUSB_INDEX_INVALID_8; + cap_stream_idx = TUSB_INDEX_INVALID_8; + spk_stream_idx = TUSB_INDEX_INVALID_8; + mic_ready = false; + spk_ready = false; + } +} + +void tuh_audio_mount_async(uintptr_t param) { + uint8_t idx = (uint8_t)param; + if (idx >= CFG_TUH_AUDIO_MAX) { + printf("Audio device mount failed: idx=%u exceeds max=%u\r\n", idx, CFG_TUH_AUDIO_MAX); + return; + } + + printf("Audio device mounted: idx=%u addr=%u\r\n", idx, tuh_audio_get_dev_addr(idx)); + + // Inspect every stream and print its supported configurations + for (uint8_t stream_idx = 0; stream_idx < tuh_audio_stream_count(idx); stream_idx++) { + if (!tuh_audio_stream_exists(idx, stream_idx)) { + continue; } + print_stream_configs(idx, stream_idx); + } - if (ok) { - audio_tx_busy = true; + // Select a supported 48 kHz S16_LE capture configuration without + // accessing USB interfaces, alternate settings, or endpoint addresses. + // Sample rates are tried in SAMPLE_RATES order (44.1 kHz first), stereo is + // preferred, mono is accepted. + static const uint32_t sample_rates[] = SAMPLE_RATES; + bool capture_found = false; + for (uint8_t r = 0; r < TU_ARRAY_SIZE(sample_rates) && !capture_found; r++) { + const uint32_t sample_rate = sample_rates[r]; + for (uint8_t stream_idx = 0; stream_idx < tuh_audio_stream_count(idx) && !capture_found; stream_idx++) { + // Only consider capture streams, ignore playback streams + if (tuh_audio_stream_direction(idx, stream_idx) != TUH_AUDIO_STREAM_CAPTURE) { + continue; + } + for (uint8_t ch = AUDIO_MAX_CHANNELS; ch >= 1 && !capture_found; ch--) { + for (uint8_t i = 0; i < tuh_audio_config_count(idx, stream_idx); i++) { + tuh_audio_stream_config_t config; + // Check for a matching sample rate S16_LE configuration with the desired channel count + if (tuh_audio_config_get(idx, stream_idx, i, &config) && config.format == TUH_AUDIO_FORMAT_S16_LE && + config.sample_rate == sample_rate && config.channels == ch) { + audio_idx = idx; + cap_stream_idx = stream_idx; + mic_config = config; + // one ms of audio at the selected rate, rounded down to whole frames + audio_frame_count = sample_rate / 1000; + printf(" Configuring %u S16_LE capture (%u channels)\r\n", (unsigned)sample_rate, config.channels); + // Configure the selected capture stream and start it through the callback. + (void)tuh_audio_configure(idx, stream_idx, i, mic_configured, 0); + capture_found = true; + break; + } + } + } + } + } + if (!capture_found) { + printf(" No supported 48/44.1 kHz S16_LE capture configuration found\r\n"); + } + + // The echo needs a playback stream at the capture sample rate (or at any + // preferred rate when no capture stream exists, for the sine fallback). + // Prefer the same channel count as the capture stream (direct echo), then + // the other one (converted). + uint8_t playback_config_idx = TUSB_INDEX_INVALID_8; + for (uint8_t r = 0; r < TU_ARRAY_SIZE(sample_rates) && playback_config_idx == TUSB_INDEX_INVALID_8; r++) { + const uint32_t sample_rate = capture_found ? mic_config.sample_rate : sample_rates[r]; + for (uint8_t stream_idx = 0; + stream_idx < tuh_audio_stream_count(idx) && playback_config_idx == TUSB_INDEX_INVALID_8; stream_idx++) { + // Only consider playback streams, ignore capture streams + if (tuh_audio_stream_direction(idx, stream_idx) != TUH_AUDIO_STREAM_PLAYBACK) { + continue; + } + for (uint8_t n = 0; n < 2 && playback_config_idx == TUSB_INDEX_INVALID_8; n++) { + const uint8_t ch = (n == 0) ? mic_config.channels : (uint8_t)(mic_config.channels == 1 ? 2 : 1); + for (uint8_t i = 0; i < tuh_audio_config_count(idx, stream_idx); i++) { + tuh_audio_stream_config_t config; + if (tuh_audio_config_get(idx, stream_idx, i, &config) && config.format == TUH_AUDIO_FORMAT_S16_LE && + config.sample_rate == sample_rate && config.channels == ch) { + spk_stream_idx = stream_idx; + spk_config = config; + playback_config_idx = i; + break; + } + } + } } } + if (playback_config_idx == TUSB_INDEX_INVALID_8) { + printf(" No supported %u S16_LE playback configuration, echo disabled\r\n", + (unsigned)(capture_found ? mic_config.sample_rate : sample_rates[0])); + return; + } + printf(" Configuring %u S16_LE playback (%u channels)\r\n", (unsigned)spk_config.sample_rate, spk_config.channels); + // Configure the selected playback stream and start it through the callback. + (void)tuh_audio_configure(idx, spk_stream_idx, playback_config_idx, spk_configured, 0); } -// Invoked when an isochronous OUT transfer is complete -void tuh_audio_tx_cb(uint8_t dev_addr, uint8_t ep_addr, uint16_t xferred_bytes) { - (void)dev_addr; - (void)ep_addr; - (void)xferred_bytes; - audio_tx_busy = false; +// Invoked when device with Audio interface is mounted +void tuh_audio_mount_cb(uint8_t idx) { + app_defer_ms_async(100, (app_defer_func_t)tuh_audio_mount_async, idx); } diff --git a/examples/host/audio_host/src/main.c b/examples/host/audio_host/src/main.c index b80cd2938..77c28cf41 100644 --- a/examples/host/audio_host/src/main.c +++ b/examples/host/audio_host/src/main.c @@ -35,39 +35,17 @@ int main(void) { printf("Connect a USB Audio Device (UAC 1.0) to test\r\n"); // init host stack on configured roothub port - tusb_rhport_init_t host_init = { - .role = TUSB_ROLE_HOST, - .speed = TUSB_SPEED_AUTO - }; + tusb_rhport_init_t host_init = {.role = TUSB_ROLE_HOST, .speed = TUSB_SPEED_AUTO}; tusb_init(BOARD_TUH_RHPORT, &host_init); board_init_after_tusb(); - + uint32_t last_ms = tusb_time_millis_api(); while (1) { // tinyusb host task tuh_task(); led_blinking_task(); - audio_app_task(); + audio_app_task_read(); + audio_app_task_write(); + defer_queue_task(); } } - -//--------------------------------------------------------------------+ -// TinyUSB Callbacks -//--------------------------------------------------------------------+ - -//--------------------------------------------------------------------+ -// Blinking Task -//--------------------------------------------------------------------+ -void led_blinking_task(void) { - const uint32_t interval_ms = 1000; - static uint32_t start_ms = 0; - - static bool led_state = false; - - // Blink every interval ms - if ( tusb_time_millis_api() - start_ms < interval_ms) return; // not enough time - start_ms += interval_ms; - - board_led_write(led_state); - led_state = 1 - led_state; // toggle -} diff --git a/examples/host/audio_host/src/tusb_config.h b/examples/host/audio_host/src/tusb_config.h index 4a7a6ad56..9b7f3c94b 100644 --- a/examples/host/audio_host/src/tusb_config.h +++ b/examples/host/audio_host/src/tusb_config.h @@ -81,9 +81,9 @@ extern "C" { #define CFG_TUH_ENUMERATION_BUFSIZE 512 -#define CFG_TUH_HUB 1 +#define CFG_TUH_HUB 0 #define CFG_TUH_CDC 0 -#define CFG_TUH_HID 1 +#define CFG_TUH_HID 0 #define CFG_TUH_MSC 0 #define CFG_TUH_VENDOR 0 #define CFG_TUH_AUDIO 1 @@ -93,8 +93,8 @@ extern "C" { //------------- Audio Host Config -------------// #define CFG_TUH_AUDIO_MAX 2 -#define CFG_TUH_AUDIO_EPIN_BUFSIZE 192 -#define CFG_TUH_AUDIO_EPOUT_BUFSIZE 192 +#define CFG_TUH_AUDIO_EPIN_BUFSIZE 256 // max capture transfer the application submits +#define CFG_TUH_AUDIO_EPOUT_BUFSIZE 256 // max playback transfer the application submits #ifdef __cplusplus } diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index c74a95d48..f943a5b2f 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -6,23 +6,43 @@ */ /* - * This driver implements a USB Audio Host (UAC 1.0) class driver. - * It supports multiple Audio Streaming (AS) interfaces with independent format storage. - * Each AS interface can have its own sample rate, channel count, bit resolution, - * and endpoint configuration. + * This driver implements a USB Audio Host (UAC 1.0) class driver with a + * WASAPI/ALSA-like high-level streaming API. The USB Audio topology (Audio + * Control interface, Audio Streaming interfaces, alternate settings, and + * endpoints) is kept private to the driver. * - * The driver handles: - * 1. Audio Control (AC) interface parsing — Input Terminal, Output Terminal, - * and Feature Unit descriptors. - * 2. Audio Streaming (AS) interface enumeration — multiple AS interfaces with - * alternate settings, each storing its own format information. - * 3. Isochronous IN/OUT endpoint management for audio data transfer. - * 4. Asynchronous control transfers for sample frequency get/set. + * Each instance (Audio Control interface) provides at most one logical stream + * per direction: + * - capture stream (TUSB_DIR_IN): device -> host, filled by isochronous IN + * transfers scheduled by the driver into a FIFO, drained by the application + * with tuh_audio_read() + * - playback stream (TUSB_DIR_OUT): host -> device, drained by isochronous + * OUT transfers from a FIFO filled by the application with tuh_audio_write() * - * In case you need to adjust the number of supported AS interfaces, change - * CFG_TUH_AUDIO_MAX_AS in your tusb_config.h. + * While a stream is running, the driver keeps one isochronous transfer in + * flight (a natural 1 ms frame cadence) and re-submits on completion. The + * FIFO + endpoint-claim pattern is modeled after the tu_edpt_stream helper + * used by the MIDI host driver: the application's frame-based read/write is + * decoupled from the USB transfer cadence, and only whole frames are ever + * queued or transferred. Completion of each transfer is reported through + * tuh_audio_capture_cb()/tuh_audio_playback_cb(), failures through + * tuh_audio_err_cb(). * - * */ + * The supported configurations of all Audio Streaming interfaces and alternate + * settings in one direction are combined into a flat list of discrete + * {format, sample_rate, channels} tuples. The driver keeps the mapping from + * each configuration to its interface, alternate setting, and endpoint, and + * applies it when the application calls tuh_audio_configure(). + * + * Non-PCM formats are rejected explicitly during enumeration. A continuous + * sampling-frequency range is exposed as a single configuration at the + * range's highest sampling frequency. + * + * The driver owns: + * 1. Endpoint selection and opening (only the alternate setting selected by + * tuh_audio_configure() is ever activated). + * 2. Endpoint sampling-frequency control (SET_CUR, 3 bytes little-endian). + */ #include "tusb_option.h" @@ -43,7 +63,6 @@ // Weak stubs: invoked if no strong implementation is available //--------------------------------------------------------------------+ - TU_ATTR_WEAK void tuh_audio_mount_cb(uint8_t idx) { (void)idx; } @@ -52,58 +71,128 @@ TU_ATTR_WEAK void tuh_audio_umount_cb(uint8_t idx) { (void)idx; } -TU_ATTR_WEAK void tuh_audio_rx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes) { +TU_ATTR_WEAK void tuh_audio_capture_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { (void)idx; - (void)ep_addr; + (void)stream_idx; (void)xferred_bytes; } -TU_ATTR_WEAK void tuh_audio_tx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes) { +TU_ATTR_WEAK void tuh_audio_playback_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { (void)idx; - (void)ep_addr; + (void)stream_idx; (void)xferred_bytes; } -//--------------------------------------------------------------------+ -// MACRO CONSTANT TYPEDEF -//--------------------------------------------------------------------+ +TU_ATTR_WEAK void tuh_audio_err_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { + (void)idx; + (void)stream_idx; + (void)xferred_bytes; +} + + //--------------------------------------------------------------------+ + // MACRO CONSTANT TYPEDEF + //--------------------------------------------------------------------+ + + // Maximum number of supported configurations per stream (per direction) + #define AUDIOH_MAX_CONFIGS (CFG_TUH_AUDIO_MAX_AS * CFG_TUH_AUDIO_MAX_SAM_FREQ) + // Maximum number of interfaces in the AC header's interface collection + #define AUDIOH_MAX_COLLECTION 16 -// Per-interface storage +// Stream state machine +enum { + STREAM_STATE_IDLE = 0, // not configured, no configuration in progress + STREAM_STATE_CONFIG, // tuh_audio_configure() sequence in progress + STREAM_STATE_READY // configured, ready to start/stop +}; + +// Hardware mapping of one supported configuration +typedef struct { + uint8_t itf_num; // Audio Streaming interface number + uint8_t alt_setting; // alternate setting that provides this configuration + uint8_t ep_addr; // isochronous endpoint address + uint16_t ep_size; // endpoint max packet size + uint8_t ep_interval; // endpoint bInterval + uint8_t ep_sync; // bmAttributes sync type + uint8_t ep_usage; // bmAttributes usage type + bool sam_freq_ctrl; // endpoint supports sampling-frequency control +} audioh_stream_map_t; + +// One logical stream (capture or playback) +typedef struct { + // instance info (set at init, preserved across close/open) + uint8_t idx; // instance index + uint8_t stream_idx; // logical stream index within the instance + tusb_dir_t dir; // TUSB_DIR_IN = capture, TUSB_DIR_OUT = playback + + // device owning this stream (0 = no device) + uint8_t daddr; + + // Supported configurations (parsed during enumeration) + uint8_t config_count; + tuh_audio_stream_config_t config[AUDIOH_MAX_CONFIGS]; + audioh_stream_map_t map[AUDIOH_MAX_CONFIGS]; + + // Active stream state + uint8_t active_config; // index into config[]/map[], TUSB_INDEX_INVALID_8 when not configured + uint8_t state; // STREAM_STATE_* + bool running; // tuh_audio_start() called, transfers may be submitted + + // Size in bytes of one frame (all channels) of the active configuration + uint8_t frame_bytes; + + // Playback pacing: frames the device consumes per USB frame + // (sample_rate / 1000), with the fractional remainder (0.1 frame per ms at + // 44.1 kHz) accumulated on each submission and paid back as one extra frame + uint16_t frames_per_ms; + uint16_t frames_rem; + uint16_t rem_acc; + + // Configure state machine + tuh_audio_configure_cb_t complete_cb; + uintptr_t user_data; + + // FIFO + endpoint transfer helper (see tu_edpt_stream, used by the MIDI + // host driver): the FIFO decouples the application's frame-based read/write + // from the 1 ms isochronous transfer cadence. ep_buf is bound at init from + // _audioh_epbuf[], the endpoint is bound by tu_edpt_stream_open() when the + // stream is configured. + tu_edpt_stream_t edpt; + uint8_t ff_buf[CFG_TUH_AUDIO_STREAM_BUFSIZE]; + + TUH_EPBUF_DEF(ctrl, 4); // sampling-frequency SET data +} tuh_audio_stream_t; + +// Per-instance (Audio device) storage typedef struct { - uint8_t daddr; // device address + uint8_t daddr; // device address (0 = free slot) uint8_t ac_itf_num; // Audio Control interface number - uint8_t itf_count; // number of interfaces (AC + AS) - // Terminal info (from Audio Control Interface) - uint16_t input_terminal_type; // wTerminalType of Input Terminal - uint8_t input_terminal_id; // bTerminalID of Input Terminal - uint8_t input_terminal_channels; // bNrChannels of Input Terminal - uint16_t output_terminal_type; // wTerminalType of Output Terminal - uint8_t output_terminal_id; // bTerminalID of Output Terminal + // Logical streams: playback first, then capture (stream index order) + tuh_audio_stream_t out_stream; + tuh_audio_stream_t in_stream; + uint8_t stream_count; // number of streams with supported configurations // Feature Unit info - uint8_t feature_unit_id; // bUnitID of Feature Unit (0 = none) - uint8_t feature_unit_source_id; // bSourceID of Feature Unit - - // Multiple AS interfaces support - uint8_t as_count; - uint8_t as_set_idx; - - // Per-AS interface independent storage (new) - tuh_audio_as_info_t as[CFG_TUH_AUDIO_MAX_AS]; // Array of Audio Streaming interface info structures + uint8_t feature_unit_id; // bUnitID of Feature Unit (0 = none) bool mounted; } audioh_interface_t; typedef struct { - TUH_EPBUF_DEF(epin, CFG_TUH_AUDIO_EPIN_BUFSIZE); - TUH_EPBUF_DEF(epout, CFG_TUH_AUDIO_EPOUT_BUFSIZE); - TUH_EPBUF_DEF(ctrl, 8); + TUH_EPBUF_DEF(ctrl, 8); // feature-unit SET data + TUH_EPBUF_DEF(epin, CFG_TUH_AUDIO_EPIN_BUFSIZE); // capture transfer buffer + TUH_EPBUF_DEF(epout, CFG_TUH_AUDIO_EPOUT_BUFSIZE); // playback transfer buffer + // Feature-unit GET chain state: only one GET in flight per device + tuh_xfer_cb_t complete_cb; + uintptr_t user_data; + uint16_t *value; + uint8_t width; } audioh_epbuf_t; static audioh_interface_t _audioh_itf[CFG_TUH_AUDIO_MAX]; static audioh_epbuf_t _audioh_epbuf[CFG_TUH_AUDIO_MAX]; + //--------------------------------------------------------------------+ // Helper //--------------------------------------------------------------------+ @@ -116,29 +205,266 @@ TU_ATTR_ALWAYS_INLINE static inline uint8_t find_new_audio_index(void) { return TUSB_INDEX_INVALID_8; } -static inline uint8_t get_idx_by_ep_addr(uint8_t daddr, uint8_t ep_addr) { +static tuh_audio_stream_t *audioh_get_stream(audioh_interface_t *p_audio, tusb_dir_t direction) { + switch (direction) { + case TUSB_DIR_IN: + return &p_audio->in_stream; + case TUSB_DIR_OUT: + return &p_audio->out_stream; + default: + return NULL; + } +} + +// Look up a stream by its logical index within the instance +static tuh_audio_stream_t *audioh_get_stream_by_idx(audioh_interface_t *p_audio, uint8_t stream_idx) { + for (uint8_t i = 0; i < 2; i++) { + tuh_audio_stream_t *s = (i == 0) ? &p_audio->out_stream : &p_audio->in_stream; + if (s->config_count > 0 && s->stream_idx == stream_idx) { + return s; + } + } + return NULL; +} + +// Map a UAC 1.0 (subframe size, bit resolution) pair to a supported format +static bool audioh_format_from_uac1(uint8_t subframe_size, uint8_t bit_resolution, tuh_audio_format_t *format) { + if (subframe_size == 1 && bit_resolution == 8) { + *format = TUH_AUDIO_FORMAT_S8; + } else if (subframe_size == 2 && bit_resolution == 16) { + *format = TUH_AUDIO_FORMAT_S16_LE; + } else if (subframe_size == 3 && bit_resolution == 24) { + *format = TUH_AUDIO_FORMAT_S24_3LE; + } else if (subframe_size == 4 && bit_resolution == 24) { + *format = TUH_AUDIO_FORMAT_S24_LE; + } else if (subframe_size == 4 && bit_resolution == 32) { + *format = TUH_AUDIO_FORMAT_S32_LE; + } else { + return false; + } + return true; +} + +// Endpoint poll interval in microseconds: full-speed bInterval is in 1 ms +// frames, high-speed isochronous bInterval is a power-of-2 exponent of +// 125 us microframes +static uint32_t audioh_interval_us(uint8_t ep_interval, uint8_t daddr) { + if (tuh_speed_get(daddr) == TUSB_SPEED_HIGH) { + return ((uint32_t)1u << (ep_interval - 1)) * 125u; + } + return (uint32_t)ep_interval * 1000u; +} + +// UAC 1.0 feature-unit control value width: mute/AGC/loudness are 1 byte, the rest 2 bytes +static uint8_t audioh_fu_control_width(uint8_t control_selector) { + switch (control_selector) { + case AUDIO10_FU_CTRL_MUTE: + case AUDIO10_FU_CTRL_AGC: + case AUDIO10_FU_CTRL_LOUDNESS: + return 1; + default: + return 2; + } +} + +// Reset a stream to its unconfigured state (keeps idx, dir, and FIFO configuration) +static void audioh_stream_reset(tuh_audio_stream_t *s) { + s->daddr = 0; + s->stream_idx = TUSB_INDEX_INVALID_8; + s->config_count = 0; + s->active_config = TUSB_INDEX_INVALID_8; + s->state = STREAM_STATE_IDLE; + s->running = false; + s->frame_bytes = 0; + s->frames_per_ms = 0; + s->frames_rem = 0; + s->rem_acc = 0; + s->complete_cb = NULL; + tu_edpt_stream_close(&s->edpt); + tu_edpt_stream_clear(&s->edpt); +} + +// Find the stream owning an endpoint (used to dispatch transfer completion) +static tuh_audio_stream_t *audioh_find_stream(uint8_t dev_addr, uint8_t ep_addr) { for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { - const audioh_interface_t *p_audio = &_audioh_itf[idx]; - if (p_audio->daddr == daddr) { - for (uint8_t as_idx = 0; as_idx < p_audio->as_count; as_idx++) { - if (p_audio->as[as_idx].ep_addr == ep_addr) { - return idx; - } + audioh_interface_t *p_audio = &_audioh_itf[idx]; + for (uint8_t s = 0; s < 2; s++) { + tuh_audio_stream_t *stream = (s == 0) ? &p_audio->in_stream : &p_audio->out_stream; + if (stream->daddr == dev_addr && stream->active_config != TUSB_INDEX_INVALID_8 && + stream->map[stream->active_config].ep_addr == ep_addr) { + return stream; } } } - return TUSB_INDEX_INVALID_8; + return NULL; } -static uint8_t audioh_get_ep_addr_by_dir(const audioh_interface_t *p_audio, uint8_t dir) { - for (uint8_t as_idx = 0; as_idx < p_audio->as_count; as_idx++) { - const tuh_audio_as_info_t *as = &p_audio->as[as_idx]; - if (as->ep_addr != 0 && as->ep_dir == dir) { - return as->ep_addr; - } +//--------------------------------------------------------------------+ +// Packet scheduler +//--------------------------------------------------------------------+ + +// Re-arm the capture endpoint: request one full packet (the device sends at +// most its max packet size per poll interval). Only submit while the whole +// packet fits into the FIFO — otherwise the frame is lost anyway and the +// transfer would be wasted; the stream resumes when tuh_audio_read() frees +// FIFO space. +static void audioh_stream_capture_xfer(tuh_audio_stream_t *s) { + TU_VERIFY(s->state == STREAM_STATE_READY && s->running, ); + + const audioh_stream_map_t *map = &s->map[s->active_config]; + TU_VERIFY(tu_fifo_remaining(&s->edpt.ff) >= map->ep_size, ); + TU_VERIFY(usbh_edpt_claim(s->daddr, map->ep_addr), ); // one transfer in flight + + // ep_size is guaranteed <= CFG_TUH_AUDIO_EPIN_BUFSIZE by enumeration + TU_ASSERT(usbh_edpt_xfer(s->daddr, map->ep_addr, s->edpt.ep_buf, map->ep_size), ); +} + +// Submit the next queued playback packet. The device consumes +// sample_rate / 1000 frames per USB frame; the fractional remainder +// (0.1 frame per ms at 44.1 kHz) is accumulated on each successful +// submission and paid back as one extra frame, keeping the average data +// rate exactly at the sample rate. Whole frames only, limited by the +// queued data, one endpoint packet, and the transfer buffer. +static void audioh_stream_playback_xfer(tuh_audio_stream_t *s) { + TU_VERIFY(s->state == STREAM_STATE_READY && s->running, ); + + const audioh_stream_map_t *map = &s->map[s->active_config]; + TU_VERIFY(usbh_edpt_claim(s->daddr, map->ep_addr), ); // one transfer in flight + + uint16_t frames = s->frames_per_ms; + s->rem_acc += s->frames_rem; + if (s->rem_acc >= 1000) { + s->rem_acc -= 1000; + frames++; + } + + frames = TU_MIN(frames, (uint16_t)(tu_fifo_count(&s->edpt.ff) / s->frame_bytes)); + frames = TU_MIN(frames, (uint16_t)(map->ep_size / s->frame_bytes)); + frames = TU_MIN(frames, (uint16_t)(CFG_TUH_AUDIO_EPOUT_BUFSIZE / s->frame_bytes)); + if (frames == 0) { + // nothing queued: the stream stays idle until the application writes again + usbh_edpt_release(s->daddr, map->ep_addr); + return; } - return 0; + const uint16_t bytes = frames * s->frame_bytes; + tu_fifo_read_n(&s->edpt.ff, s->edpt.ep_buf, bytes); + TU_ASSERT(usbh_edpt_xfer(s->daddr, map->ep_addr, s->edpt.ep_buf, bytes), ); +} + +//--------------------------------------------------------------------+ +// Configure state machine +//--------------------------------------------------------------------+ + +static void audioh_stream_fail(tuh_audio_stream_t *s, tusb_xfer_result_t result) { + s->state = STREAM_STATE_IDLE; + s->active_config = TUSB_INDEX_INVALID_8; + s->running = false; + + tuh_audio_configure_cb_t cb = s->complete_cb; + uintptr_t user_data = s->user_data; + s->complete_cb = NULL; + if (cb != NULL) { + cb(s->idx, s->stream_idx, result, user_data); + } +} + +static void audioh_stream_ready(tuh_audio_stream_t *s) { + s->state = STREAM_STATE_READY; + + tuh_audio_configure_cb_t cb = s->complete_cb; + uintptr_t user_data = s->user_data; + s->complete_cb = NULL; + if (cb != NULL) { + cb(s->idx, s->stream_idx, XFER_RESULT_SUCCESS, user_data); + } +} + +static void audioh_stream_set_freq_complete(tuh_xfer_t *xfer) { + tuh_audio_stream_t *s = (tuh_audio_stream_t *)xfer->user_data; + if (s->daddr != xfer->daddr || s->state != STREAM_STATE_CONFIG) { + return; // device is gone or configuration was aborted + } + + if (xfer->result != XFER_RESULT_SUCCESS) { + TU_LOG_DRV(" AUDIO set sampling frequency failed: result=%u\r\n", xfer->result); + audioh_stream_fail(s, xfer->result); + return; + } + audioh_stream_ready(s); +} + +// Set the endpoint sampling frequency (3 bytes little-endian) when supported +static void audioh_stream_set_freq(tuh_audio_stream_t *s) { + const audioh_stream_map_t *map = &s->map[s->active_config]; + const tuh_audio_stream_config_t *cfg = &s->config[s->active_config]; + + s->ctrl[0] = (uint8_t)(cfg->sample_rate & 0xFF); + s->ctrl[1] = (uint8_t)((cfg->sample_rate >> 8) & 0xFF); + s->ctrl[2] = (uint8_t)((cfg->sample_rate >> 16) & 0xFF); + + const tusb_control_request_t request = + {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_ENDPOINT, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_OUT}, + .bRequest = AUDIO10_CS_REQ_SET_CUR, + .wValue = tu_htole16(tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0)), // control selector, channel 0 + .wIndex = tu_htole16(map->ep_addr), + .wLength = 3}; + + tuh_xfer_t xfer = {.daddr = s->daddr, + .ep_addr = 0, + .setup = &request, + .buffer = s->ctrl, + .complete_cb = audioh_stream_set_freq_complete, + .user_data = (uintptr_t)s}; + if (!tuh_control_xfer(&xfer)) { + audioh_stream_fail(s, XFER_RESULT_FAILED); + } +} + +// Reconstruct the endpoint descriptor of the selected configuration and open it +static void audioh_stream_open_ep(tuh_audio_stream_t *s) { + const audioh_stream_map_t *map = &s->map[s->active_config]; + + const tusb_desc_endpoint_t desc_ep = {.bLength = sizeof(tusb_desc_endpoint_t), + .bDescriptorType = TUSB_DESC_ENDPOINT, + .bEndpointAddress = map->ep_addr, + .bmAttributes = {.xfer = TUSB_XFER_ISOCHRONOUS, + .sync = map->ep_sync, + .usage = map->ep_usage}, + .wMaxPacketSize = tu_htole16(map->ep_size), + .bInterval = map->ep_interval}; + + if (!tuh_edpt_open(s->daddr, &desc_ep)) { + TU_LOG_DRV(" AUDIO open endpoint failed: addr=%u ep=%02x\r\n", s->daddr, map->ep_addr); + audioh_stream_fail(s, XFER_RESULT_FAILED); + return; + } + + // Bind the transfer helper to the endpoint and start with an empty FIFO + const uint16_t xfer_len = (s->dir == TUSB_DIR_IN) ? CFG_TUH_AUDIO_EPIN_BUFSIZE : CFG_TUH_AUDIO_EPOUT_BUFSIZE; + tu_edpt_stream_open(&s->edpt, s->daddr, &desc_ep, xfer_len); + tu_edpt_stream_clear(&s->edpt); + + if (map->sam_freq_ctrl) { + audioh_stream_set_freq(s); + } else { + audioh_stream_ready(s); + } +} + +static void audioh_stream_set_interface_complete(tuh_xfer_t *xfer) { + tuh_audio_stream_t *s = (tuh_audio_stream_t *)xfer->user_data; + if (s->daddr != xfer->daddr || s->state != STREAM_STATE_CONFIG) { + return; // device is gone or configuration was aborted + } + + if (xfer->result != XFER_RESULT_SUCCESS) { + TU_LOG_DRV(" AUDIO SET_INTERFACE failed: itf=%u alt=%u result=%u\r\n", s->map[s->active_config].itf_num, + s->map[s->active_config].alt_setting, xfer->result); + audioh_stream_fail(s, xfer->result); + return; + } + audioh_stream_open_ep(s); } //--------------------------------------------------------------------+ @@ -146,44 +472,352 @@ static uint8_t audioh_get_ep_addr_by_dir(const audioh_interface_t *p_audio, uint //--------------------------------------------------------------------+ bool audioh_init(void) { tu_memclr(&_audioh_itf, sizeof(_audioh_itf)); + + for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { + tuh_audio_stream_t *in = &_audioh_itf[idx].in_stream; + tuh_audio_stream_t *out = &_audioh_itf[idx].out_stream; + + in->idx = idx; + in->dir = TUSB_DIR_IN; + out->idx = idx; + out->dir = TUSB_DIR_OUT; + + // Bind FIFO buffer and transfer buffer (see tu_edpt_stream_init) + TU_VERIFY(tu_edpt_stream_init(&in->edpt, true, false, false, in->ff_buf, CFG_TUH_AUDIO_STREAM_BUFSIZE, + _audioh_epbuf[idx].epin)); + TU_VERIFY(tu_edpt_stream_init(&out->edpt, true, true, false, out->ff_buf, CFG_TUH_AUDIO_STREAM_BUFSIZE, + _audioh_epbuf[idx].epout)); + + audioh_stream_reset(in); + audioh_stream_reset(out); + } return true; } bool audioh_deinit(void) { + for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { + tu_edpt_stream_deinit(&_audioh_itf[idx].in_stream.edpt); + tu_edpt_stream_deinit(&_audioh_itf[idx].out_stream.edpt); + } return true; } void audioh_close(uint8_t daddr) { for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { audioh_interface_t *p_audio = &_audioh_itf[idx]; - if (p_audio->daddr == daddr) { - TU_LOG_DRV(" AUDIO close addr = %u index = %u\r\n", daddr, idx); + if (p_audio->daddr != daddr) { + continue; + } + + TU_LOG_DRV(" AUDIO close addr = %u index = %u\r\n", daddr, idx); + if (p_audio->mounted) { tuh_audio_umount_cb(idx); + } - p_audio->ac_itf_num = 0; - p_audio->daddr = 0; - p_audio->mounted = false; - p_audio->as_count = 0; - p_audio->as_set_idx = 0; - tu_memclr(p_audio->as, sizeof(p_audio->as)); + // Abort a configuration in progress so the application callback still fires + for (uint8_t s = 0; s < 2; s++) { + tuh_audio_stream_t *stream = (s == 0) ? &p_audio->in_stream : &p_audio->out_stream; + if (stream->state == STREAM_STATE_CONFIG && stream->complete_cb != NULL) { + audioh_stream_fail(stream, XFER_RESULT_ABORTED); + } + audioh_stream_reset(stream); } + + _audioh_epbuf[idx].complete_cb = NULL; // drop a pending feature-unit GET + + p_audio->stream_count = 0; + p_audio->daddr = 0; + p_audio->mounted = false; } } bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { - (void)result; - if (tu_edpt_dir(ep_addr) == TUSB_DIR_IN) { - tuh_audio_rx_cb(dev_addr, ep_addr, (uint16_t)xferred_bytes); - } else { - tuh_audio_tx_cb(dev_addr, ep_addr, (uint16_t)xferred_bytes); + tuh_audio_stream_t *s = audioh_find_stream(dev_addr, ep_addr); + if (s == NULL) { + return false; + } + + // Failed, stalled, or aborted transfers never carry valid audio data + if (result != XFER_RESULT_SUCCESS) { + TU_LOG_DRV(" AUDIO transfer failed: addr=%u ep=%02x result=%u\r\n", dev_addr, ep_addr, result); + s->running = false; + tu_edpt_stream_clear(&s->edpt); // discard queued data + tuh_audio_err_cb(s->idx, s->stream_idx, (uint16_t)xferred_bytes); + return true; } + // Stopped stream: the in-flight transfer completes and its data is discarded + if (!s->running) { + return true; + } + + if (s->dir == TUSB_DIR_IN) { + // Capture: move the received bytes into the FIFO (whole frames only), + // notify, then re-arm for the next packet + const uint16_t bytes = (uint16_t)(xferred_bytes - (xferred_bytes % s->frame_bytes)); + if (bytes > 0) { + tu_fifo_write_n(&s->edpt.ff, s->edpt.ep_buf, bytes); + } + tuh_audio_capture_cb(s->idx, s->stream_idx, (uint16_t)xferred_bytes); + audioh_stream_capture_xfer(s); + } else { + // Playback: notify, then submit the next queued packet + tuh_audio_playback_cb(s->idx, s->stream_idx, (uint16_t)xferred_bytes); + audioh_stream_playback_xfer(s); + } return true; } //--------------------------------------------------------------------+ // Enumeration //--------------------------------------------------------------------+ + +// AC header interface collection (baInterfaceNr) bounds-checked +typedef struct TU_ATTR_PACKED { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubType; + uint16_t bcdADC; + uint16_t wTotalLength; + uint8_t bInCollection; + uint8_t baInterfaceNr[AUDIOH_MAX_COLLECTION]; +} audioh_ac_header_t; + +static bool audioh_itf_in_collection(const audioh_ac_header_t *header, uint8_t itf_num) { + for (uint8_t i = 0; i < header->bInCollection; i++) { + if (header->baInterfaceNr[i] == itf_num) { + return true; + } + } + return false; +} + +// Parse one Audio Streaming interface alternate setting and register its +// supported configurations into the matching stream. Returns the descriptor +// pointer of the next interface. +static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_desc_interface_t *desc_itf, + const uint8_t *p_desc, const uint8_t *desc_end) { + const uint8_t itf_num = desc_itf->bInterfaceNumber; + const uint8_t alt = desc_itf->bAlternateSetting; + + p_desc = tu_desc_next(p_desc); + + // Alternate setting 0 has no endpoints: nothing to stream + if (alt == 0 || desc_itf->bNumEndpoints == 0) { + while (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) != TUSB_DESC_INTERFACE) { + p_desc = tu_desc_next(p_desc); + } + return p_desc; + } + + // Parse the class-specific and endpoint descriptors of this alternate setting + uint16_t format_tag = 0; + uint8_t num_channels = 0; + uint8_t subframe_size = 0; + uint8_t bit_res = 0; + uint8_t sam_freq_type = 0; + uint8_t sam_freq_count = 0; // 1 for a continuous range + uint32_t sam_freq[CFG_TUH_AUDIO_MAX_SAM_FREQ] = {0}; + + // An alternate setting can expose an endpoint in each direction + typedef struct { + uint8_t ep_addr; + uint16_t ep_size; + uint8_t ep_interval; + uint8_t ep_sync; + uint8_t ep_usage; + bool sam_freq_ctrl; + } audioh_ep_info_t; + audioh_ep_info_t ep_info[2] = {0}; + uint8_t ep_count = 0; + // The CS_ENDPOINT descriptor carries the sampling-frequency control bit of + // its endpoint. Devices differ in whether it precedes or follows the + // standard endpoint descriptor, so attribute it in either order. + bool pending_sam_freq_ctrl = false; // CS_ENDPOINT seen, applies to the next endpoint + bool unassigned_ep = false; // endpoint seen, applies to the next CS_ENDPOINT + + while (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) != TUSB_DESC_INTERFACE) { + switch (tu_desc_type(p_desc)) { + case TUSB_DESC_CS_INTERFACE: { + switch (tu_desc_subtype(p_desc)) { + case AUDIO10_CS_AS_INTERFACE_AS_GENERAL: { + const audio10_desc_cs_as_interface_t *desc_as_general = (const audio10_desc_cs_as_interface_t *)p_desc; + if (desc_as_general->bLength >= 5) { + format_tag = tu_le16toh(desc_as_general->wFormatTag); + } + break; + } + case AUDIO10_CS_AS_INTERFACE_FORMAT_TYPE: { + TU_ASSERT(p_desc[0] >= 8, p_desc); + if (p_desc[3] != AUDIO10_FORMAT_TYPE_I) { + break; // only Type I (PCM) is supported + } + num_channels = p_desc[4]; + subframe_size = p_desc[5]; + bit_res = p_desc[6]; + sam_freq_type = p_desc[7]; + if (sam_freq_type == 0) { + // Continuous range: expose a single configuration at the + // highest supported sampling frequency (tSamFreq[0] is the + // lower bound, tSamFreq[1] the upper bound) + if (p_desc[0] >= 14) { + sam_freq_count = 1; + sam_freq[0] = ((uint32_t)p_desc[11] | ((uint32_t)p_desc[12] << 8) | ((uint32_t)p_desc[13] << 16)); + TU_LOG_DRV(" AUDIO AS itf %u: continuous range %lu-%lu Hz, using %lu Hz\r\n", itf_num, + (unsigned long)((uint32_t)p_desc[8] | ((uint32_t)p_desc[9] << 8) | + ((uint32_t)p_desc[10] << 16)), + (unsigned long)sam_freq[0], (unsigned long)sam_freq[0]); + } + } else { + sam_freq_count = TU_MIN(sam_freq_type, CFG_TUH_AUDIO_MAX_SAM_FREQ); + for (uint8_t i = 0; i < sam_freq_count && (8 + i * 3 + 2) < p_desc[0]; i++) { + sam_freq[i] = ((uint32_t)p_desc[8 + i * 3] | ((uint32_t)p_desc[9 + i * 3] << 8) | + ((uint32_t)p_desc[10 + i * 3] << 16)); + } + } + break; + } + default: + break; + } + break; + } + case TUSB_DESC_CS_ENDPOINT: { + if (tu_desc_subtype(p_desc) == AUDIO10_CS_EP_SUBTYPE_GENERAL && p_desc[0] >= 4) { + const audio10_desc_cs_as_iso_data_ep_t *desc_ep = (const audio10_desc_cs_as_iso_data_ep_t *)p_desc; + const bool sam_freq_ctrl = (desc_ep->bmAttributes & 0x01) != 0; + if (unassigned_ep) { + // Standard order: the CS_ENDPOINT follows its endpoint descriptor + ep_info[ep_count - 1].sam_freq_ctrl = sam_freq_ctrl; + unassigned_ep = false; + } else { + // Non-standard order: the CS_ENDPOINT precedes its endpoint descriptor + pending_sam_freq_ctrl = sam_freq_ctrl; + } + } + break; + } + case TUSB_DESC_ENDPOINT: { + const tusb_desc_endpoint_t *desc_endpoint = (const tusb_desc_endpoint_t *)p_desc; + if (desc_endpoint->bmAttributes.xfer == TUSB_XFER_ISOCHRONOUS && ep_count < 2) { + audioh_ep_info_t *ep = &ep_info[ep_count]; + ep->ep_addr = desc_endpoint->bEndpointAddress; + ep->ep_size = tu_edpt_packet_size(desc_endpoint); + ep->ep_interval = desc_endpoint->bInterval; + // bInterval must be in [1, 16] for isochronous endpoints + if (ep->ep_interval == 0 || ep->ep_interval > 16) { + ep->ep_interval = 1; + } + ep->ep_sync = desc_endpoint->bmAttributes.sync; + ep->ep_usage = desc_endpoint->bmAttributes.usage; + ep->sam_freq_ctrl = pending_sam_freq_ctrl; + pending_sam_freq_ctrl = false; + unassigned_ep = !ep->sam_freq_ctrl; + ep_count++; + } + break; + } + default: + break; + } + p_desc = tu_desc_next(p_desc); + } + + if (ep_count == 0) { + return p_desc; + } + + // Reject unsupported formats explicitly + if (format_tag != AUDIO10_DATA_FORMAT_TYPE_I_PCM) { + TU_LOG_DRV(" AUDIO AS itf %u: format tag 0x%04x not supported\r\n", itf_num, format_tag); + return p_desc; + } + tuh_audio_format_t format; + if (!audioh_format_from_uac1(subframe_size, bit_res, &format)) { + TU_LOG_DRV(" AUDIO AS itf %u: subframe %u bits %u not supported\r\n", itf_num, subframe_size, bit_res); + return p_desc; + } + if (num_channels == 0) { + TU_LOG_DRV(" AUDIO AS itf %u: zero channels not supported\r\n", itf_num); + return p_desc; + } + + // Register one configuration per (endpoint, discrete sampling frequency) + const uint8_t frame_bytes = num_channels * tuh_audio_format_bytes(format); + for (uint8_t e = 0; e < ep_count; e++) { + const audioh_ep_info_t *ep = &ep_info[e]; + tuh_audio_stream_t *stream = audioh_get_stream(p_audio, tu_edpt_dir(ep->ep_addr)); + if (stream == NULL) { + continue; + } + + const uint16_t epbuf_size = (stream->dir == TUSB_DIR_IN) ? CFG_TUH_AUDIO_EPIN_BUFSIZE : CFG_TUH_AUDIO_EPOUT_BUFSIZE; + + // Capture: the device can deliver up to its max packet size per poll + // interval, the transfer buffer must fit it + if (stream->dir == TUSB_DIR_IN && ep->ep_size > epbuf_size) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: capture ep size %u exceeds transfer buffer %u\r\n", itf_num, alt, + ep->ep_size, epbuf_size); + continue; + } + + for (uint8_t i = 0; i < sam_freq_count; i++) { + if (sam_freq[i] == 0) { + continue; + } + + // Playback: the device accepts any packet up to its max packet size + // (often advertised larger than the audio rate needs), but the largest + // scheduled packet must still fit the transfer buffer + if (stream->dir == TUSB_DIR_OUT) { + const uint64_t per_interval = + (uint64_t)sam_freq[i] * frame_bytes * audioh_interval_us(ep->ep_interval, p_audio->daddr); + const uint32_t need = (uint32_t)((per_interval + 999999u) / 1000000u); + if (need > epbuf_size) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: playback needs %u B per interval, transfer buffer is %u\r\n", itf_num, + alt, (unsigned)need, epbuf_size); + continue; + } + } + + // Skip duplicate configurations + bool duplicate = false; + for (uint8_t j = 0; j < stream->config_count; j++) { + if (stream->config[j].format == format && stream->config[j].sample_rate == sam_freq[i] && + stream->config[j].channels == num_channels) { + duplicate = true; + break; + } + } + if (duplicate) { + continue; + } + + if (stream->config_count >= AUDIOH_MAX_CONFIGS) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: reach max configurations %u\r\n", itf_num, alt, AUDIOH_MAX_CONFIGS); + return p_desc; + } + + stream->config[stream->config_count].dir = + (stream->dir == TUSB_DIR_IN) ? TUH_AUDIO_STREAM_CAPTURE : TUH_AUDIO_STREAM_PLAYBACK; + stream->config[stream->config_count].format = format; + stream->config[stream->config_count].sample_rate = sam_freq[i]; + stream->config[stream->config_count].channels = num_channels; + stream->map[stream->config_count].itf_num = itf_num; + stream->map[stream->config_count].alt_setting = alt; + stream->map[stream->config_count].ep_addr = ep->ep_addr; + stream->map[stream->config_count].ep_size = ep->ep_size; + stream->map[stream->config_count].ep_interval = ep->ep_interval; + stream->map[stream->config_count].ep_sync = ep->ep_sync; + stream->map[stream->config_count].ep_usage = ep->ep_usage; + stream->map[stream->config_count].sam_freq_ctrl = ep->sam_freq_ctrl; + stream->config_count++; + } + } + + return p_desc; +} + uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface_t *desc_itf, uint16_t max_len) { (void)rhport; @@ -197,40 +831,44 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface const uint8_t idx = find_new_audio_index(); TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, 0); audioh_interface_t *p_audio = &_audioh_itf[idx]; - p_audio->itf_count = 0; + p_audio->daddr = dev_addr; + p_audio->ac_itf_num = desc_itf->bInterfaceNumber; + audioh_stream_reset(&p_audio->in_stream); + audioh_stream_reset(&p_audio->out_stream); + p_audio->in_stream.daddr = dev_addr; + p_audio->out_stream.daddr = dev_addr; - // Parse Audio Control Interface TU_LOG_DRV("AUDIO opening AC Interface %u (addr = %u)\r\n", desc_itf->bInterfaceNumber, dev_addr); - p_audio->ac_itf_num = desc_itf->bInterfaceNumber; - p_audio->itf_count = 1; - // Parse Audio Control interface descriptors (Input Terminal, Output Terminal, Feature Unit, etc.) + // Parse the Audio Control interface descriptors and the interface collection + audioh_ac_header_t header = {0}; + bool have_header = false; + p_desc = tu_desc_next(p_desc); while (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) != TUSB_DESC_INTERFACE) { if (tu_desc_type(p_desc) == TUSB_DESC_CS_INTERFACE) { switch (tu_desc_subtype(p_desc)) { - case AUDIO10_CS_AC_INTERFACE_INPUT_TERMINAL: { - const audio10_desc_input_terminal_t *desc_input_terminal = (const audio10_desc_input_terminal_t *)p_desc; - p_audio->input_terminal_type = tu_le16toh(desc_input_terminal->wTerminalType); - p_audio->input_terminal_id = desc_input_terminal->bTerminalID; - p_audio->input_terminal_channels = desc_input_terminal->bNrChannels; - TU_LOG_DRV(" Input Terminal: ID=%u, Type=0x%04x, Channels=%u\r\n", desc_input_terminal->bTerminalID, - tu_le16toh(desc_input_terminal->wTerminalType), desc_input_terminal->bNrChannels); - break; - } - case AUDIO10_CS_AC_INTERFACE_OUTPUT_TERMINAL: { - const audio10_desc_output_terminal_t *desc_output_terminal = (const audio10_desc_output_terminal_t *)p_desc; - p_audio->output_terminal_type = tu_le16toh(desc_output_terminal->wTerminalType); - p_audio->output_terminal_id = desc_output_terminal->bTerminalID; - TU_LOG_DRV(" Output Terminal: ID=%u, Type=0x%04x\r\n", desc_output_terminal->bTerminalID, - tu_le16toh(desc_output_terminal->wTerminalType)); + case AUDIO10_CS_AC_INTERFACE_HEADER: { + const audioh_ac_header_t *desc_header = (const audioh_ac_header_t *)p_desc; + if (desc_header->bLength >= 8) { + header.bInCollection = desc_header->bInCollection; + // The collection array must not extend past the descriptor itself + const uint8_t max_collection = TU_MIN((uint8_t)(desc_header->bLength - 8), (uint8_t)AUDIOH_MAX_COLLECTION); + if (header.bInCollection > max_collection) { + TU_LOG_DRV(" AUDIO AC header collection truncated to %u interfaces\r\n", max_collection); + header.bInCollection = max_collection; + } + if (header.bInCollection > 0) { + memcpy(header.baInterfaceNr, desc_header->baInterfaceNr, header.bInCollection); + // An empty collection falls back to the interface-class heuristic + have_header = true; + } + } break; } case AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT: { - const uint8_t *desc_feature_unit = p_desc; - p_audio->feature_unit_id = desc_feature_unit[3]; // bUnitID - p_audio->feature_unit_source_id = desc_feature_unit[4]; // bSourceID - TU_LOG_DRV(" Feature Unit: ID=%u, SourceID=%u\r\n", desc_feature_unit[3], desc_feature_unit[4]); + p_audio->feature_unit_id = p_desc[3]; // bUnitID + TU_LOG_DRV(" Feature Unit: ID=%u\r\n", p_audio->feature_unit_id); break; } default: @@ -240,246 +878,72 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface p_desc = tu_desc_next(p_desc); } - // Parse all remaining descriptors in this configuration looking for Audio Streaming interfaces + // Parse the Audio Streaming interfaces of this audio function. Interfaces + // outside the AC header's collection (e.g. MIDI Streaming interfaces) are + // left for other class drivers. while (tu_desc_in_bounds(p_desc, desc_end)) { - if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) { - const tusb_desc_interface_t *desc_interface = (const tusb_desc_interface_t *)p_desc; - // Stop at the first non-Audio interface so we don't claim the rest of the configuration - if (desc_interface->bInterfaceClass != TUSB_CLASS_AUDIO) { - break; - } - if (desc_interface->bInterfaceSubClass == AUDIO_SUBCLASS_STREAMING) { - // Found Audio Streaming Interface - TU_LOG_DRV(" Found AS Interface %u (alt = %u)\r\n", desc_interface->bInterfaceNumber, - desc_interface->bAlternateSetting); - - if (desc_interface->bAlternateSetting == 0) { - // Interface descriptor with alt setting 0 (no endpoints) - // Add to AS entries - if (p_audio->as_count < CFG_TUH_AUDIO_MAX_AS) { - p_audio->as[p_audio->as_count].interface_num = desc_interface->bInterfaceNumber; - p_audio->as[p_audio->as_count].alt_setting = 0; - p_audio->as_count++; - } else { - TU_LOG_DRV(" Skip AS Interface %u: reach CFG_TUH_AUDIO_MAX_AS=%u\r\n", desc_interface->bInterfaceNumber, - CFG_TUH_AUDIO_MAX_AS); - } - } else if (desc_interface->bNumEndpoints > 0) { - // Interface descriptor with alt setting > 0 (has endpoints) - // Find matching AS entry and set alt_setting - uint8_t as_entry_idx = CFG_TUH_AUDIO_MAX_AS; - for (uint8_t i = 0; i < p_audio->as_count; i++) { - if (p_audio->as[i].interface_num == desc_interface->bInterfaceNumber) { - as_entry_idx = i; - break; - } - } - if (as_entry_idx >= CFG_TUH_AUDIO_MAX_AS && p_audio->as_count < CFG_TUH_AUDIO_MAX_AS) { - as_entry_idx = p_audio->as_count; - p_audio->as[as_entry_idx].interface_num = desc_interface->bInterfaceNumber; - p_audio->as_count++; - } - if (as_entry_idx < CFG_TUH_AUDIO_MAX_AS) { - p_audio->as[as_entry_idx].alt_setting = desc_interface->bAlternateSetting; - } - - // Parse the interface's descriptors - p_desc = tu_desc_next(p_desc); - // Temporary variables to hold format info until endpoint direction is known - uint8_t tmp_format_type = 0; - uint8_t tmp_num_channels = 0; - uint8_t tmp_sub_frame_size = 0; - uint8_t tmp_bit_resolution = 0; - uint8_t tmp_sam_freq_type = 0; - uint32_t tmp_sam_freq[CFG_TUH_AUDIO_MAX_SAM_FREQ] = {0}; - uint32_t tmp_sam_freq_lower = 0; - uint32_t tmp_sam_freq_upper = 0; - while (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) != TUSB_DESC_INTERFACE) { - switch (tu_desc_type(p_desc)) { - case TUSB_DESC_CS_INTERFACE: { - switch (tu_desc_subtype(p_desc)) { - case AUDIO10_CS_AS_INTERFACE_AS_GENERAL: { - TU_LOG_DRV(" AS General descriptor\r\n"); - break; - } - case AUDIO10_CS_AS_INTERFACE_FORMAT_TYPE: { - TU_LOG_DRV(" Format Type descriptor\r\n"); - TU_ASSERT(p_desc[0] >= 8, 0); - // Parse UAC 1.0 Format Type I descriptor fields into temporary variables - tmp_format_type = p_desc[3]; // bFormatType - tmp_num_channels = p_desc[4]; // bNrChannels - tmp_sub_frame_size = p_desc[5]; // bSubFrameSize - tmp_bit_resolution = p_desc[6]; // bBitResolution - - // Parse sampling frequencies - uint8_t bLength = p_desc[0]; - if (bLength >= 8) { - tmp_sam_freq_type = p_desc[7]; // bSamFreqType - if (tmp_sam_freq_type == 0) { - // Continuous range: tLowerSamFreq, tUpperSamFreq (3 bytes each) - if (bLength >= 14) { - tmp_sam_freq_lower = - ((uint32_t)p_desc[8] | ((uint32_t)p_desc[9] << 8) | ((uint32_t)p_desc[10] << 16)); - tmp_sam_freq_upper = - ((uint32_t)p_desc[11] | ((uint32_t)p_desc[12] << 8) | ((uint32_t)p_desc[13] << 16)); - } - } else { - // Discrete sampling frequencies - uint8_t max_freqs = tmp_sam_freq_type < CFG_TUH_AUDIO_MAX_SAM_FREQ ? tmp_sam_freq_type - : CFG_TUH_AUDIO_MAX_SAM_FREQ; - for (uint8_t i = 0; i < max_freqs && (8 + i * 3 + 2) < bLength; i++) { - tmp_sam_freq[i] = ((uint32_t)p_desc[8 + i * 3] | ((uint32_t)p_desc[9 + i * 3] << 8) | - ((uint32_t)p_desc[10 + i * 3] << 16)); - } - } - } - break; - } - default: - break; - } - break; - } - case TUSB_DESC_ENDPOINT: { - const tusb_desc_endpoint_t *desc_endpoint = (const tusb_desc_endpoint_t *)p_desc; - if (desc_endpoint->bmAttributes.xfer == TUSB_XFER_ISOCHRONOUS) { - TU_LOG_DRV(" Isochronous EP %02x\r\n", desc_endpoint->bEndpointAddress); - if (tu_edpt_dir(desc_endpoint->bEndpointAddress) == TUSB_DIR_IN) { - // Save to per-AS storage - if (as_entry_idx < CFG_TUH_AUDIO_MAX_AS) { - tuh_audio_as_info_t *as = &p_audio->as[as_entry_idx]; - as->ep_addr = desc_endpoint->bEndpointAddress; - as->ep_size = tu_edpt_packet_size(desc_endpoint); - as->ep_dir = TUSB_DIR_IN; - as->format_type = tmp_format_type; - as->num_channels = tmp_num_channels; - as->sub_frame_size = tmp_sub_frame_size; - as->bit_resolution = tmp_bit_resolution; - as->sam_freq_type = tmp_sam_freq_type; - as->sam_freq_lower = tmp_sam_freq_lower; - as->sam_freq_upper = tmp_sam_freq_upper; - for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX_SAM_FREQ; i++) { - as->sam_freq[i] = tmp_sam_freq[i]; - } - } - } else { - // Save to per-AS storage - if (as_entry_idx < CFG_TUH_AUDIO_MAX_AS) { - tuh_audio_as_info_t *as = &p_audio->as[as_entry_idx]; - as->ep_addr = desc_endpoint->bEndpointAddress; - as->ep_size = tu_edpt_packet_size(desc_endpoint); - as->ep_dir = TUSB_DIR_OUT; - as->format_type = tmp_format_type; - as->num_channels = tmp_num_channels; - as->sub_frame_size = tmp_sub_frame_size; - as->bit_resolution = tmp_bit_resolution; - as->sam_freq_type = tmp_sam_freq_type; - as->sam_freq_lower = tmp_sam_freq_lower; - as->sam_freq_upper = tmp_sam_freq_upper; - for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX_SAM_FREQ; i++) { - as->sam_freq[i] = tmp_sam_freq[i]; - } - } - } - TU_ASSERT(tuh_edpt_open(dev_addr, desc_endpoint), 0); - } - break; - } - default: - break; - } - p_desc = tu_desc_next(p_desc); - } - // Continue to parse other AS interfaces (don't break, device may have both IN and OUT) - // break; // Removed: allow parsing multiple AS interfaces (e.g. mic + speaker) - continue; - } - p_audio->itf_count++; - } else if (desc_interface->bInterfaceClass == TUSB_CLASS_AUDIO && - desc_interface->bInterfaceSubClass == AUDIO_SUBCLASS_CONTROL) { - // Another Audio Control interface (shouldn't happen in normal UAC 1.0) - p_audio->itf_count++; - } + if (tu_desc_type(p_desc) != TUSB_DESC_INTERFACE) { + p_desc = tu_desc_next(p_desc); + continue; } - p_desc = tu_desc_next(p_desc); - } - - p_audio->daddr = dev_addr; - return (uint16_t)((uintptr_t)p_desc - (uintptr_t)desc_start); -} - -static void _audioh_mount(uint8_t dev_addr, uint8_t idx); - -static void audioh_set_interface_complete(tuh_xfer_t *xfer) { - uint8_t idx = (uint8_t)xfer->user_data; - audioh_interface_t *p_audio = &_audioh_itf[idx]; + const tusb_desc_interface_t *desc_interface = (const tusb_desc_interface_t *)p_desc; + const bool in_collection = have_header ? audioh_itf_in_collection(&header, desc_interface->bInterfaceNumber) + : desc_interface->bInterfaceClass == TUSB_CLASS_AUDIO; + if (!in_collection) { + break; + } - // Send SET_INTERFACE for next AS interface if any - p_audio->as_set_idx++; - if (p_audio->as_set_idx < p_audio->as_count) { - uint8_t as_idx = p_audio->as_set_idx; - uint8_t itf = p_audio->as[as_idx].interface_num; - uint8_t alt = p_audio->as[as_idx].alt_setting; - if (alt > 0) { - TU_LOG_DRV("AUDIO Set Interface %u Alt %u (addr = %u)\r\n", itf, alt, xfer->daddr); - tuh_interface_set(xfer->daddr, itf, alt, audioh_set_interface_complete, idx); - return; + if (desc_interface->bInterfaceSubClass == AUDIO_SUBCLASS_STREAMING) { + TU_LOG_DRV(" Found AS Interface %u (alt = %u)\r\n", desc_interface->bInterfaceNumber, + desc_interface->bAlternateSetting); + p_desc = audioh_parse_as(p_audio, desc_interface, p_desc, desc_end); + } else { + // MIDI Streaming or another subclass: not our interface + break; } } - // All SET_INTERFACE done, mount the device - _audioh_mount(xfer->daddr, idx); -} - -static void _audioh_mount(uint8_t dev_addr, uint8_t idx) { - audioh_interface_t *p_audio = &_audioh_itf[idx]; - p_audio->mounted = true; - - - tuh_audio_mount_cb(idx); + // Assign stream indices: playback first, then capture, so the application + // can iterate [0, stream_count) without gaps + uint8_t stream_idx = 0; + if (p_audio->out_stream.config_count > 0) { + p_audio->out_stream.stream_idx = stream_idx++; + } + if (p_audio->in_stream.config_count > 0) { + p_audio->in_stream.stream_idx = stream_idx++; + } + p_audio->stream_count = stream_idx; - usbh_driver_set_config_complete(dev_addr, p_audio->ac_itf_num); + return (uint16_t)((uintptr_t)p_desc - (uintptr_t)desc_start); } +//--------------------------------------------------------------------+ +// Set Configuration +//--------------------------------------------------------------------+ bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num) { - uint8_t idx = tuh_audio_itf_get_index(dev_addr, itf_num); - - // If not found, check if this is an AS interface that belongs to a known AC interface - if (idx >= CFG_TUH_AUDIO_MAX) { - for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX; i++) { - const audioh_interface_t *p_audio = &_audioh_itf[i]; - if (p_audio->daddr == dev_addr) { - for (uint8_t as_idx = 0; as_idx < p_audio->as_count; as_idx++) { - if (p_audio->as[as_idx].interface_num == itf_num) { - // AS interface: configuration is driven by the AC interface, so just pass through - usbh_driver_set_config_complete(dev_addr, itf_num); - return true; - } - } - } + uint8_t idx = TUSB_INDEX_INVALID_8; + for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX; i++) { + if (_audioh_itf[i].daddr == dev_addr && _audioh_itf[i].ac_itf_num == itf_num) { + idx = i; + break; } - // Not an Audio interface we own; pass through so enumeration can continue + } + + if (idx == TUSB_INDEX_INVALID_8) { + // Audio Streaming interface (or another driver's interface): nothing to do at mount. + // Alternate settings are activated by tuh_audio_configure(). usbh_driver_set_config_complete(dev_addr, itf_num); return true; } audioh_interface_t *p_audio = &_audioh_itf[idx]; - TU_VERIFY(p_audio->as_count <= CFG_TUH_AUDIO_MAX_AS, false); - - // Send SET_INTERFACE for all AS interfaces with alt_setting > 0 - if (p_audio->as_count > 0) { - p_audio->as_set_idx = 0; - uint8_t itf = p_audio->as[0].interface_num; - uint8_t alt = p_audio->as[0].alt_setting; - if (alt > 0) { - TU_LOG_DRV("AUDIO Set Interface %u Alt %u (addr = %u)\r\n", itf, alt, dev_addr); - tuh_interface_set(dev_addr, itf, alt, audioh_set_interface_complete, idx); - return true; - } - } + p_audio->mounted = true; + TU_LOG_DRV(" AUDIO mounted: addr = %u index = %u\r\n", dev_addr, idx); + + tuh_audio_mount_cb(idx); - _audioh_mount(dev_addr, idx); + usbh_driver_set_config_complete(dev_addr, itf_num); return true; } @@ -488,143 +952,308 @@ bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num) { //--------------------------------------------------------------------+ bool tuh_audio_mounted(uint8_t idx) { TU_VERIFY(idx < CFG_TUH_AUDIO_MAX); - audioh_interface_t *p_audio = &_audioh_itf[idx]; - return p_audio->mounted; + return _audioh_itf[idx].mounted; } uint8_t tuh_audio_get_dev_addr(uint8_t idx) { - audioh_interface_t *p_audio = &_audioh_itf[idx]; - return p_audio->daddr; + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, 0); + return _audioh_itf[idx].daddr; } - uint8_t tuh_audio_get_feature_unit_id(uint8_t idx) { - audioh_interface_t *p_audio = &_audioh_itf[idx]; - return p_audio->feature_unit_id; + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, 0); + return _audioh_itf[idx].feature_unit_id; } -uint8_t tuh_audio_itf_get_index(uint8_t daddr, uint8_t itf_num) { - for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { - const audioh_interface_t *p_audio = &_audioh_itf[idx]; - if (p_audio->daddr == daddr && p_audio->ac_itf_num == itf_num) { - return idx; +uint8_t tuh_audio_stream_count(uint8_t dev_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, 0); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->daddr != 0, 0); + return p_audio->stream_count; +} + +bool tuh_audio_stream_exists(uint8_t dev_idx, uint8_t stream_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, false); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->daddr != 0, false); + return audioh_get_stream_by_idx(p_audio, stream_idx) != NULL; +} + +tuh_audio_direction_t tuh_audio_stream_direction(uint8_t dev_idx, uint8_t stream_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, TUH_AUDIO_STREAM_DIRECTION_COUNT); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->daddr != 0, TUH_AUDIO_STREAM_DIRECTION_COUNT); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s, TUH_AUDIO_STREAM_DIRECTION_COUNT); + return (s->dir == TUSB_DIR_IN) ? TUH_AUDIO_STREAM_CAPTURE : TUH_AUDIO_STREAM_PLAYBACK; +} + +uint8_t tuh_audio_config_count(uint8_t dev_idx, uint8_t stream_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, 0); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->daddr != 0, 0); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s, 0); + return s->config_count; +} +uint8_t tuh_audio_active_config(uint8_t dev_idx, uint8_t stream_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, TUSB_INDEX_INVALID_8); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->daddr != 0, TUSB_INDEX_INVALID_8); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s, TUSB_INDEX_INVALID_8); + return s->active_config; +} +bool tuh_audio_config_get(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx, tuh_audio_stream_config_t *config) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, false); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->daddr != 0, false); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s && config, false); + TU_VERIFY(config_idx < s->config_count, false); + + *config = s->config[config_idx]; + return true; +} + +bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx, tuh_audio_configure_cb_t complete_cb, + uintptr_t user_data) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, false); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->mounted, false); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s && complete_cb, false); + TU_VERIFY(config_idx < s->config_count, false); + // Reconfiguration is allowed from a stopped stream; only one configuration + // may be in progress + TU_VERIFY(s->state != STREAM_STATE_CONFIG && !s->running, false); + if (s->state == STREAM_STATE_READY) { + // Wait for any in-flight transfer to complete and be discarded + TU_VERIFY(!usbh_edpt_busy(s->daddr, s->map[s->active_config].ep_addr), false); + } + + // A shared AS interface must not be left in two different alternate settings + tuh_audio_stream_t *other = (s == &p_audio->out_stream) ? &p_audio->in_stream : &p_audio->out_stream; + if (other->active_config != TUSB_INDEX_INVALID_8) { + const audioh_stream_map_t *m1 = &s->map[config_idx]; + const audioh_stream_map_t *m2 = &other->map[other->active_config]; + if (m1->itf_num == m2->itf_num && m1->alt_setting != m2->alt_setting) { + TU_LOG_DRV(" AUDIO configure failed: shared AS itf %u in conflicting alt settings\r\n", m1->itf_num); + return false; } } - return TUSB_INDEX_INVALID_8; + + s->active_config = config_idx; + s->frame_bytes = (uint8_t)tuh_audio_config_frame_size(&s->config[config_idx]); + s->frames_per_ms = (uint16_t)(s->config[config_idx].sample_rate / 1000); + s->frames_rem = (uint16_t)(s->config[config_idx].sample_rate % 1000); + s->rem_acc = 0; + s->complete_cb = complete_cb; + s->user_data = user_data; + s->state = STREAM_STATE_CONFIG; + + const audioh_stream_map_t *map = &s->map[config_idx]; + TU_LOG_DRV(" AUDIO configure %s stream %u: itf %u alt %u ep %02x\r\n", + (s->dir == TUSB_DIR_IN) ? "capture" : "playback", s->stream_idx, map->itf_num, map->alt_setting, + map->ep_addr); + + if (!tuh_interface_set(s->daddr, map->itf_num, map->alt_setting, audioh_stream_set_interface_complete, + (uintptr_t)s)) { + audioh_stream_fail(s, XFER_RESULT_FAILED); + return false; + } + return true; } -bool tuh_audio_itf_get_info(uint8_t idx, tuh_itf_info_t *info) { - audioh_interface_t *p_audio = &_audioh_itf[idx]; - TU_VERIFY(p_audio && info); +// Invoked when the SET_INTERFACE activating the stream's interface completes: +// the interface is active, start submitting transfers +static void audioh_stream_start_complete(tuh_xfer_t *xfer) { + tuh_audio_stream_t *s = (tuh_audio_stream_t *)xfer->user_data; + if (s->daddr != xfer->daddr || !s->running) { + return; // device is gone or the stream was stopped meanwhile + } + if (xfer->result != XFER_RESULT_SUCCESS) { + TU_LOG_DRV(" AUDIO SET_INTERFACE activate failed: result=%u\r\n", xfer->result); + s->running = false; + return; + } + if (s->dir == TUSB_DIR_IN) { + audioh_stream_capture_xfer(s); // feed the capture endpoint + } else { + audioh_stream_playback_xfer(s); // flush queued frames, if any + } +} - info->daddr = p_audio->daddr; +bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, false); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->mounted, false); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s, false); + TU_VERIFY(s->state == STREAM_STATE_READY && !s->running, false); + // Wait for any in-flight transfer to complete and be discarded + TU_VERIFY(!usbh_edpt_busy(s->daddr, s->map[s->active_config].ep_addr), false); + + // Activate the interface's alternate setting asynchronously: transfers + // begin once SET_INTERFACE completes (audioh_stream_start_complete) + s->running = true; + const audioh_stream_map_t *map = &s->map[s->active_config]; + if (!tuh_interface_set(s->daddr, map->itf_num, map->alt_setting, audioh_stream_start_complete, (uintptr_t)s)) { + s->running = false; + return false; + } + return true; +} - // re-construct descriptor - tusb_desc_interface_t *desc_interface = &info->desc; - desc_interface->bLength = sizeof(tusb_desc_interface_t); - desc_interface->bDescriptorType = TUSB_DESC_INTERFACE; +// Invoked when the SET_INTERFACE deactivating the stream's interface (alt 0) +// completes +static void audioh_stream_stop_complete(tuh_xfer_t *xfer) { + tuh_audio_stream_t *s = (tuh_audio_stream_t *)xfer->user_data; + if (s->daddr != xfer->daddr) { + return; + } + TU_LOG_DRV(" AUDIO SET_INTERFACE deactivate done: result=%u\r\n", xfer->result); +} - uint8_t ep_in = audioh_get_ep_addr_by_dir(p_audio, TUSB_DIR_IN); - uint8_t ep_out = audioh_get_ep_addr_by_dir(p_audio, TUSB_DIR_OUT); +bool tuh_audio_stop(uint8_t dev_idx, uint8_t stream_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, false); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->mounted, false); - desc_interface->bInterfaceNumber = p_audio->ac_itf_num; - desc_interface->bAlternateSetting = 0; - desc_interface->bNumEndpoints = (uint8_t)((ep_in ? 1u : 0u) + (ep_out ? 1u : 0u)); - desc_interface->bInterfaceClass = TUSB_CLASS_AUDIO; - desc_interface->bInterfaceSubClass = AUDIO_SUBCLASS_CONTROL; - desc_interface->bInterfaceProtocol = 0; - desc_interface->iInterface = 0; + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s && s->running, false); - return true; + // The in-flight transfer (if any) completes and its data is discarded; + // queued frames are dropped as well. The interface is deactivated (alt 0) + // so the device stops transferring. + s->running = false; + tu_edpt_stream_clear(&s->edpt); + s->rem_acc = 0; // restart the pacing accumulator on the next tuh_audio_start() + + const audioh_stream_map_t *map = &s->map[s->active_config]; + return tuh_interface_set(s->daddr, map->itf_num, 0, audioh_stream_stop_complete, (uintptr_t)s); } -//--------------------------------------------------------------------+ -// Control Endpoint API -//--------------------------------------------------------------------+ -bool tuh_audio_set_sampling_freq(uint8_t idx, uint8_t as_idx, uint32_t sampling_freq, tuh_xfer_cb_t complete_cb, - uintptr_t user_data) { - TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); - audioh_interface_t *p_audio = &_audioh_itf[idx]; - TU_VERIFY(p_audio && as_idx < p_audio->as_count, false); +uint32_t tuh_audio_write(uint8_t dev_idx, uint8_t stream_idx, const void *buffer, uint32_t frame_count) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, 0); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->mounted && buffer, 0); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + // Writes are only accepted by the playback stream + TU_VERIFY(s && s->dir == TUSB_DIR_OUT, 0); + TU_VERIFY(s->state == STREAM_STATE_READY && s->running, 0); + TU_VERIFY(frame_count > 0, 0); + + // Queue as many whole frames as the FIFO can hold + const uint32_t frames = TU_MIN(frame_count, tu_fifo_remaining(&s->edpt.ff) / s->frame_bytes); + if (frames == 0) { + return 0; + } + tu_fifo_write_n(&s->edpt.ff, buffer, (uint16_t)(frames * s->frame_bytes)); - uint8_t ep_addr = p_audio->as[as_idx].ep_addr; - uint8_t daddr = p_audio->daddr; - uint8_t *freq_buf = _audioh_epbuf[idx].ctrl; + // Flush a packet when the FIFO holds at least one; the scheduler drains + // the rest on completion + audioh_stream_playback_xfer(s); - const tusb_control_request_t request = - {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_ENDPOINT, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_OUT}, - .bRequest = AUDIO10_CS_REQ_SET_CUR, - .wValue = tu_htole16(tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0)), // Control Selector = Sampling Freq, Channel = 0 - .wIndex = tu_htole16((uint16_t)ep_addr), - .wLength = 3}; - - // UAC 1.0 sampling frequency is 3 bytes little-endian - // uint8_t freq_buf[3] = { - // (uint8_t)(sampling_freq & 0xFF), - // (uint8_t)((sampling_freq >> 8) & 0xFF), - // (uint8_t)((sampling_freq >> 16) & 0xFF) - // }; - freq_buf[0] = (uint8_t)(sampling_freq & 0xFF); - freq_buf[1] = (uint8_t)((sampling_freq >> 8) & 0xFF); - freq_buf[2] = (uint8_t)((sampling_freq >> 16) & 0xFF); - tuh_xfer_t xfer = {.daddr = daddr, - .ep_addr = 0, - .setup = &request, - .buffer = freq_buf, - .complete_cb = complete_cb, - .user_data = user_data}; + return frames; +} - return tuh_control_xfer(&xfer); +uint32_t tuh_audio_read(uint8_t dev_idx, uint8_t stream_idx, void *buffer, uint32_t frame_count) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, 0); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->mounted && buffer, 0); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + // Reads are only accepted by the capture stream + TU_VERIFY(s && s->dir == TUSB_DIR_IN, 0); + TU_VERIFY(s->state == STREAM_STATE_READY && s->running, 0); + TU_VERIFY(frame_count > 0, 0); + + // Drain as many whole frames as are queued + const uint32_t frames = TU_MIN(frame_count, tu_fifo_count(&s->edpt.ff) / s->frame_bytes); + if (frames > 0) { + tu_fifo_read_n(&s->edpt.ff, buffer, (uint16_t)(frames * s->frame_bytes)); + audioh_stream_capture_xfer(s); // re-arm: the FIFO has room again + } + return frames; } -bool tuh_audio_get_sampling_freq(uint8_t idx, uint8_t as_idx, uint32_t *sampling_freq, tuh_xfer_cb_t complete_cb, - uintptr_t user_data) { - TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); - audioh_interface_t *p_audio = &_audioh_itf[idx]; - TU_VERIFY(p_audio && as_idx < p_audio->as_count && sampling_freq, false); - uint8_t ep_addr = p_audio->as[as_idx].ep_addr; - uint8_t daddr = p_audio->daddr; +uint32_t tuh_audio_write_available(uint8_t dev_idx, uint8_t stream_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, 0); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->daddr != 0, 0); - *sampling_freq = 0; + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s && s->dir == TUSB_DIR_OUT, 0); + TU_VERIFY(s->state == STREAM_STATE_READY && s->running, 0); + return tu_edpt_stream_write_available(&s->edpt) / s->frame_bytes; +} - const tusb_control_request_t request = - {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_ENDPOINT, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_IN}, - .bRequest = AUDIO10_CS_REQ_GET_CUR, - .wValue = tu_htole16(tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0)), // Control Selector = Sampling Freq, Channel = 0 - .wIndex = tu_htole16((uint16_t)ep_addr), - .wLength = 3}; - - // Application needs to parse 3-byte little-endian sampling frequency from buffer - tuh_xfer_t xfer = {.daddr = daddr, - .ep_addr = 0, - .setup = &request, - .buffer = (uint8_t *)sampling_freq, - .complete_cb = complete_cb, - .user_data = user_data}; +uint32_t tuh_audio_read_available(uint8_t dev_idx, uint8_t stream_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, 0); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->daddr != 0, 0); - return tuh_control_xfer(&xfer); + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s && s->dir == TUSB_DIR_IN, 0); + TU_VERIFY(s->state == STREAM_STATE_READY && s->running, 0); + return tu_edpt_stream_read_available(&s->edpt) / s->frame_bytes; +} + +//--------------------------------------------------------------------+ +// Feature Unit Control API +//--------------------------------------------------------------------+ + +// Convert the raw control value to host order and chain to the application callback +static void audioh_fu_get_complete(tuh_xfer_t *xfer) { + const uint8_t idx = (uint8_t)xfer->user_data; + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + tuh_xfer_cb_t app_cb = epbuf->complete_cb; + uintptr_t user_data = epbuf->user_data; + uint16_t *value = epbuf->value; + const uint8_t width = epbuf->width; + epbuf->complete_cb = NULL; + + if (app_cb != NULL && value != NULL && xfer->result == XFER_RESULT_SUCCESS) { + const uint8_t *raw = (const uint8_t *)value; + // The raw bytes are little-endian on the wire: rebuild the host-order value + *value = (width == 1) ? (uint16_t)raw[0] : (uint16_t)((uint16_t)raw[0] | ((uint16_t)raw[1] << 8)); + } + + xfer->user_data = user_data; + if (app_cb != NULL) { + app_cb(xfer); + } } bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t value, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); - uint8_t daddr = _audioh_itf[idx].daddr; - uint8_t itf_num = _audioh_itf[idx].ac_itf_num; - uint8_t unit_id = _audioh_itf[idx].feature_unit_id; + audioh_interface_t *p_audio = &_audioh_itf[idx]; + TU_VERIFY(p_audio->mounted && p_audio->feature_unit_id != 0, false); + + const uint8_t width = audioh_fu_control_width(control_selector); const tusb_control_request_t request = {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_OUT}, .bRequest = AUDIO10_CS_REQ_SET_CUR, .wValue = tu_htole16(tu_u16(control_selector, channel)), - .wIndex = tu_htole16(tu_u16(unit_id, itf_num)), - .wLength = 2}; + .wIndex = tu_htole16(tu_u16(p_audio->feature_unit_id, p_audio->ac_itf_num)), + .wLength = width}; uint8_t *val_buf = _audioh_epbuf[idx].ctrl; val_buf[0] = (uint8_t)(value & 0xFF); val_buf[1] = (uint8_t)((value >> 8) & 0xFF); - tuh_xfer_t xfer = {.daddr = daddr, + tuh_xfer_t xfer = {.daddr = p_audio->daddr, .ep_addr = 0, .setup = &request, .buffer = val_buf, @@ -634,103 +1263,62 @@ bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t control_selector, uint8_t c return tuh_control_xfer(&xfer); } -bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t *buffer, +bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t *value, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); - uint8_t daddr = _audioh_itf[idx].daddr; - uint8_t itf_num = _audioh_itf[idx].ac_itf_num; - uint8_t unit_id = _audioh_itf[idx].feature_unit_id; + audioh_interface_t *p_audio = &_audioh_itf[idx]; + TU_VERIFY(p_audio->mounted && p_audio->feature_unit_id != 0 && value, false); + + const uint8_t width = audioh_fu_control_width(control_selector); const tusb_control_request_t request = {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_IN}, .bRequest = AUDIO10_CS_REQ_GET_CUR, .wValue = tu_htole16(tu_u16(control_selector, channel)), - .wIndex = tu_htole16(tu_u16(unit_id, itf_num)), - .wLength = 2}; - - tuh_xfer_t xfer = {.daddr = daddr, - .ep_addr = 0, - .setup = &request, - .buffer = (uint8_t *)buffer, - .complete_cb = complete_cb, - .user_data = user_data}; - - return tuh_control_xfer(&xfer); -} - -//--------------------------------------------------------------------+ -// Multi-AS interface API -//--------------------------------------------------------------------+ -uint8_t tuh_audio_as_get_count(uint8_t idx) { - TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, 0); - return _audioh_itf[idx].as_count; -} - -bool tuh_audio_as_get_info(uint8_t idx, uint8_t as_idx, tuh_audio_as_info_t *info) { - TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); - TU_VERIFY(as_idx < _audioh_itf[idx].as_count, false); - TU_VERIFY(info, false); - - tuh_audio_as_info_t *as = &_audioh_itf[idx].as[as_idx]; - info->interface_num = as->interface_num; - info->alt_setting = as->alt_setting; - info->ep_addr = as->ep_addr; - info->ep_size = as->ep_size; - info->ep_dir = as->ep_dir; - info->format_type = as->format_type; - info->num_channels = as->num_channels; - info->sub_frame_size = as->sub_frame_size; - info->bit_resolution = as->bit_resolution; - info->sam_freq_type = as->sam_freq_type; - info->sam_freq_lower = as->sam_freq_lower; - info->sam_freq_upper = as->sam_freq_upper; - memcpy(info->sam_freq, as->sam_freq, sizeof(info->sam_freq)); - return true; -} - -//--------------------------------------------------------------------+ -// Isochronous Endpoint API -//--------------------------------------------------------------------+ -bool tuh_audio_receive(uint8_t idx, uint8_t as_idx, uint8_t *buffer, uint16_t len) { - TU_VERIFY(idx < CFG_TUH_AUDIO_MAX); - audioh_interface_t *p_audio = &_audioh_itf[idx]; - tuh_audio_as_info_t *as = &p_audio->as[as_idx]; - TU_VERIFY(as->ep_addr != 0); - - return usbh_edpt_xfer(p_audio->daddr, as->ep_addr, buffer, len); -} + .wIndex = tu_htole16(tu_u16(p_audio->feature_unit_id, p_audio->ac_itf_num)), + .wLength = width}; + + if (complete_cb == NULL) { + // Sync (blocking) path: user_data points to a tusb_xfer_result_t, the raw + // bytes are converted to host order after the transfer completes + tuh_xfer_t xfer = {.daddr = p_audio->daddr, + .ep_addr = 0, + .setup = &request, + .buffer = (uint8_t *)value, + .complete_cb = NULL, + .user_data = user_data}; + if (!tuh_control_xfer(&xfer)) { + return false; + } + if (xfer.result == XFER_RESULT_SUCCESS) { + const uint8_t *raw = (const uint8_t *)value; + *value = (width == 1) ? (uint16_t)raw[0] : (uint16_t)((uint16_t)raw[0] | ((uint16_t)raw[1] << 8)); + } + return true; + } -bool tuh_audio_send(uint8_t idx, uint8_t as_idx, uint8_t *buffer, uint16_t len) { - TU_VERIFY(idx < CFG_TUH_AUDIO_MAX); - audioh_interface_t *p_audio = &_audioh_itf[idx]; - tuh_audio_as_info_t *as = &p_audio->as[as_idx]; - TU_VERIFY(as->ep_addr != 0); + // Async path: chain the host-order conversion to the application callback + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + TU_VERIFY(epbuf->complete_cb == NULL, false); // one feature-unit GET in flight per device - return usbh_edpt_xfer(p_audio->daddr, as->ep_addr, (uint8_t *)buffer, len); -} + epbuf->complete_cb = complete_cb; + epbuf->user_data = user_data; + epbuf->value = value; + epbuf->width = width; -//--------------------------------------------------------------------+ -// Set Interface -//--------------------------------------------------------------------+ -bool tuh_audio_set_interface(uint8_t daddr, uint8_t itf_num, uint8_t alt_setting, tuh_xfer_cb_t complete_cb, - uintptr_t user_data) { - const tusb_control_request_t request = {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, - .type = TUSB_REQ_TYPE_STANDARD, - .direction = TUSB_DIR_OUT}, - .bRequest = TUSB_REQ_SET_INTERFACE, - .wValue = alt_setting, - .wIndex = itf_num, - .wLength = 0}; - - tuh_xfer_t xfer = {.daddr = daddr, + tuh_xfer_t xfer = {.daddr = p_audio->daddr, .ep_addr = 0, .setup = &request, - .buffer = NULL, - .complete_cb = complete_cb, - .user_data = user_data}; + .buffer = (uint8_t *)value, // raw bytes, converted in audioh_fu_get_complete() + .complete_cb = audioh_fu_get_complete, + .user_data = (uintptr_t)idx}; - return tuh_control_xfer(&xfer); + if (!tuh_control_xfer(&xfer)) { + epbuf->complete_cb = NULL; + return false; + } + return true; } #endif diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h index aafd58f43..aaa65b671 100644 --- a/src/class/audio/audio_host.h +++ b/src/class/audio/audio_host.h @@ -17,11 +17,11 @@ extern "C" { //--------------------------------------------------------------------+ // Class Driver Configuration //--------------------------------------------------------------------+ -// Maximum number of Audio interfaces per Audio device +// Maximum number of Audio devices #ifndef CFG_TUH_AUDIO_MAX #define CFG_TUH_AUDIO_MAX 1 #endif -// Maximum number of Audio Streaming interfaces per Audio device +// Maximum number of discrete sampling frequencies per Audio Streaming interface #ifndef CFG_TUH_AUDIO_MAX_SAM_FREQ #define CFG_TUH_AUDIO_MAX_SAM_FREQ 5 #endif @@ -30,84 +30,177 @@ extern "C" { #define CFG_TUH_AUDIO_MAX_AS 4 #endif -//--------------------------------------------------------------------+ -// AS Interface Info (per-interface independent storage) -//--------------------------------------------------------------------+ -typedef struct { - uint8_t interface_num; // AS interface number - uint8_t alt_setting; // Current alt setting - uint8_t ep_addr; // Endpoint address - uint16_t ep_size; // Max packet size - uint8_t ep_dir; // TUSB_DIR_IN or TUSB_DIR_OUT - - // Format info - uint8_t format_type; - uint8_t num_channels; - uint8_t sub_frame_size; - uint8_t bit_resolution; - uint8_t sam_freq_type; - uint32_t sam_freq[CFG_TUH_AUDIO_MAX_SAM_FREQ]; - uint32_t sam_freq_lower; - uint32_t sam_freq_upper; -} tuh_audio_as_info_t; - +// Maximum size of one capture (IN) isochronous transfer the driver submits. +// Configurations needing a larger per-poll-interval packet are rejected. +// 256 covers 2-ch 48 kHz S16_LE (192 B) and common endpoint padding (208 B). #ifndef CFG_TUH_AUDIO_EPIN_BUFSIZE - #define CFG_TUH_AUDIO_EPIN_BUFSIZE 192 + #define CFG_TUH_AUDIO_EPIN_BUFSIZE 256 #endif +// Maximum size of one playback (OUT) isochronous transfer the driver submits. +// Configurations needing a larger per-poll-interval packet are rejected. #ifndef CFG_TUH_AUDIO_EPOUT_BUFSIZE - #define CFG_TUH_AUDIO_EPOUT_BUFSIZE 192 + #define CFG_TUH_AUDIO_EPOUT_BUFSIZE 256 +#endif + +// Depth in bytes of the per-stream data FIFO. The FIFO decouples the +// application's read/write calls from the 1 ms isochronous transfer cadence +// and absorbs rate differences. 1024 bytes hold 4 default (256 B) packets. +#ifndef CFG_TUH_AUDIO_STREAM_BUFSIZE + #define CFG_TUH_AUDIO_STREAM_BUFSIZE 1024 #endif //--------------------------------------------------------------------+ -// Application API +// Types //--------------------------------------------------------------------+ -// Check if Audio interface is mounted -bool tuh_audio_mounted(uint8_t idx); -// Get device address of Audio interface -uint8_t tuh_audio_get_dev_addr(uint8_t idx); -// Get Feature Unit ID -uint8_t tuh_audio_get_feature_unit_id(uint8_t idx); -// Get Interface index from device address + interface number -// return TUSB_INDEX_INVALID_8 (0xFF) if not found -uint8_t tuh_audio_itf_get_index(uint8_t daddr, uint8_t itf_num); +// Fixed transfer direction of a logical stream. +typedef enum { + TUH_AUDIO_STREAM_PLAYBACK = 0, // Host -> Device (OUT) + TUH_AUDIO_STREAM_CAPTURE = 1, // Device -> Host (IN) + TUH_AUDIO_STREAM_DIRECTION_COUNT +} tuh_audio_direction_t; + +// Discrete sample format. Only discrete configurations are supported +// initially; continuous sample-rate ranges are ignored by the driver. +typedef enum { + TUH_AUDIO_FORMAT_S8 = 0, // signed 8-bit + TUH_AUDIO_FORMAT_S16_LE, // signed 16-bit little-endian + TUH_AUDIO_FORMAT_S24_3LE, // signed 24-bit packed in 3 bytes, LE + TUH_AUDIO_FORMAT_S24_LE, // signed 24-bit in 32-bit container, LE + TUH_AUDIO_FORMAT_S32_LE, // signed 32-bit little-endian + TUH_AUDIO_FORMAT_COUNT +} tuh_audio_format_t; + +// One complete supported discrete configuration tuple. +// Each entry is a full (format, sample_rate, channels) combination, +// avoiding invalid mixes between independent format/rate/channel lists. +// dir is constant for all configs of a given (dev_idx, stream_idx) and +// equals the result of tuh_audio_stream_direction(). +typedef struct { + tuh_audio_direction_t dir; + tuh_audio_format_t format; + uint32_t sample_rate; + uint8_t channels; +} tuh_audio_stream_config_t; + +// Asynchronous completion callback of tuh_audio_configure(). +typedef void (*tuh_audio_configure_cb_t)(uint8_t dev_idx, uint8_t stream_idx, tusb_xfer_result_t result, + uintptr_t user_data); + +//--------------------------------------------------------------------+ +// Stream Enumeration +//--------------------------------------------------------------------+ + +// Number of logical audio streams exposed by one mounted device. The +// application iterates stream indices [0, tuh_audio_stream_count()) and +// inspects each with tuh_audio_stream_exists()/tuh_audio_stream_direction(). +uint8_t tuh_audio_stream_count(uint8_t dev_idx); + +// True if (dev_idx, stream_idx) identifies an existing stream. +bool tuh_audio_stream_exists(uint8_t dev_idx, uint8_t stream_idx); + +// Fixed transfer direction of the stream. +tuh_audio_direction_t tuh_audio_stream_direction(uint8_t dev_idx, uint8_t stream_idx); + +//--------------------------------------------------------------------+ +// Configuration Enumeration +//--------------------------------------------------------------------+ + +// Number of supported discrete configurations of the stream. +uint8_t tuh_audio_config_count(uint8_t dev_idx, uint8_t stream_idx); + +// Active configuration index of the stream, or TUSB_INDEX_INVALID_8 if none. +uint8_t tuh_audio_active_config(uint8_t dev_idx, uint8_t stream_idx); + +// Retrieve one discrete configuration tuple into *config. +bool tuh_audio_config_get(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx, tuh_audio_stream_config_t *config); + +//--------------------------------------------------------------------+ +// Configuration (ALSA hw_params analogue, asynchronous) +//--------------------------------------------------------------------+ + +// Configure the stream with the discrete configuration identified by +// config_idx. The driver asynchronously: +// 1. resolves the AS interface and alternate setting, +// 2. issues SET_INTERFACE (checking submission and transfer result), +// 3. opens / reconfigures only the selected endpoint, +// 4. sets the endpoint sampling frequency when supported, +// 5. initializes the FIFO and packet scheduler. +// complete_cb is invoked with the final XFER_RESULT_* status. +bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx, tuh_audio_configure_cb_t complete_cb, + uintptr_t user_data); -// Get Interface information -// return true if index is correct and interface is currently mounted -bool tuh_audio_itf_get_info(uint8_t idx, tuh_itf_info_t *info); +//--------------------------------------------------------------------+ +// Stream Control / Frame-based Data +//--------------------------------------------------------------------+ -// Get number of AS interfaces for an audio device -uint8_t tuh_audio_as_get_count(uint8_t idx); +// Start/stop transferring data on a configured stream. +bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx); +bool tuh_audio_stop(uint8_t dev_idx, uint8_t stream_idx); -// Get AS interface info by index -// as_idx: 0 to (as_count - 1) -bool tuh_audio_as_get_info(uint8_t idx, uint8_t as_idx, tuh_audio_as_info_t *info); +// Frame-based transfer. One frame = channels * bytes per sample. +// tuh_audio_write() is valid only for TUH_AUDIO_STREAM_PLAYBACK streams, +// tuh_audio_read() only for TUH_AUDIO_STREAM_CAPTURE streams. +// Returns the number of frames actually written/read (0 on any error, +// including wrong direction, unconfigured/stopped stream, or full/empty FIFO). +uint32_t tuh_audio_write(uint8_t dev_idx, uint8_t stream_idx, const void *buffer, uint32_t frame_count); +uint32_t tuh_audio_read(uint8_t dev_idx, uint8_t stream_idx, void *buffer, uint32_t frame_count); -// Set Audio Streaming interface alternate setting (to enable/disable endpoints) -bool tuh_audio_set_interface(uint8_t daddr, uint8_t itf_num, uint8_t alt_setting, tuh_xfer_cb_t complete_cb, - uintptr_t user_data); +// FIFO occupancy in frames available for a non-blocking write/read. +uint32_t tuh_audio_write_available(uint8_t dev_idx, uint8_t stream_idx); +uint32_t tuh_audio_read_available(uint8_t dev_idx, uint8_t stream_idx); //--------------------------------------------------------------------+ -// Control Endpoint API +// Helpers //--------------------------------------------------------------------+ -// Set current sampling frequency on an isochronous endpoint (UAC 1.0) -// Sampling frequency is 3 bytes little-endian -// In multi-AS scenarios, pass the endpoint address from tuh_audio_as_get_info(). -bool tuh_audio_set_sampling_freq(uint8_t idx, uint8_t as_idx, uint32_t sampling_freq, tuh_xfer_cb_t complete_cb, - uintptr_t user_data); +// Container size in bytes of one sample for a given format. +static inline uint8_t tuh_audio_format_bytes(tuh_audio_format_t format) { + switch (format) { + case TUH_AUDIO_FORMAT_S8: + return 1; + case TUH_AUDIO_FORMAT_S16_LE: + return 2; + case TUH_AUDIO_FORMAT_S24_3LE: + return 3; + case TUH_AUDIO_FORMAT_S24_LE: + case TUH_AUDIO_FORMAT_S32_LE: + return 4; + default: + return 0; + } +} + +// Size in bytes of one frame (all channels) for a configuration. +static inline uint32_t tuh_audio_config_frame_size(const tuh_audio_stream_config_t *config) { + TU_ASSERT(config != NULL); + return (uint32_t)tuh_audio_format_bytes(config->format) * config->channels; +} + +//--------------------------------------------------------------------+ +// Device Info +//--------------------------------------------------------------------+ -// Get current sampling frequency from an isochronous endpoint (UAC 1.0) -// In multi-AS scenarios, pass the endpoint address from tuh_audio_as_get_info(). -bool tuh_audio_get_sampling_freq(uint8_t idx, uint8_t as_idx, uint32_t *sampling_freq, tuh_xfer_cb_t complete_cb, - uintptr_t user_data); +// Check if Audio device is mounted +bool tuh_audio_mounted(uint8_t idx); +// Get device address of Audio device +uint8_t tuh_audio_get_dev_addr(uint8_t idx); +// Get Feature Unit ID +uint8_t tuh_audio_get_feature_unit_id(uint8_t idx); -// Set current/mute/volume etc. for a feature unit (UAC 1.0) +//--------------------------------------------------------------------+ +// Control Request API +//--------------------------------------------------------------------+ + +// Set a Feature Unit control (mute, volume, ...) of the Audio device (UAC 1.0) +// The request length follows the control selector: mute/AGC/loudness are 1 byte, the rest are 2 bytes bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t value, tuh_xfer_cb_t complete_cb, uintptr_t user_data); -// Get current/mute/volume etc. from a feature unit (UAC 1.0) +// Get a Feature Unit control (mute, volume, ...) of the Audio device (UAC 1.0) +// The value is converted to host byte order before complete_cb is invoked. +// Only one feature unit GET may be in flight per device. bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t *value, tuh_xfer_cb_t complete_cb, uintptr_t user_data); @@ -116,16 +209,6 @@ bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t control_selector, uint8_t c // Each Function will make a USB control transfer request to/from device the function will block until request is // complete. The function will return the transfer request result //--------------------------------------------------------------------+ -TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_get_sampling_freq_sync(uint8_t idx, uint8_t as_idx, - uint32_t *sampling_freq) { - TU_API_SYNC(tuh_audio_get_sampling_freq, idx, as_idx, sampling_freq); -} - -TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_set_sampling_freq_sync(uint8_t idx, uint8_t as_idx, - uint32_t sampling_freq) { - TU_API_SYNC(tuh_audio_set_sampling_freq, idx, as_idx, sampling_freq); -} - TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_feature_unit_set_sync(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t value) { TU_API_SYNC(tuh_audio_feature_unit_set, idx, control_selector, channel, value); @@ -136,20 +219,6 @@ tuh_audio_feature_unit_get_sync(uint8_t idx, uint8_t control_selector, uint8_t c TU_API_SYNC(tuh_audio_feature_unit_get, idx, control_selector, channel, value); } -//--------------------------------------------------------------------+ -// Interrupt/Isochronous Endpoint API -//--------------------------------------------------------------------+ - -// Submit an isochronous transfer to receive audio data from a default IN endpoint. -// In multi-AS scenarios, endpoint selection is implementation-defined default behavior. -// Use tuh_audio_as_get_info() when application needs explicit per-AS endpoint control. -bool tuh_audio_receive(uint8_t idx, uint8_t as_idx, uint8_t *buffer, uint16_t len); - -// Submit an isochronous transfer to send audio data to a default OUT endpoint. -// In multi-AS scenarios, endpoint selection is implementation-defined default behavior. -// Use tuh_audio_as_get_info() when application needs explicit per-AS endpoint control. -bool tuh_audio_send(uint8_t idx, uint8_t as_idx, uint8_t *buffer, uint16_t len); - //--------------------------------------------------------------------+ // Callbacks (Weak is optional) //--------------------------------------------------------------------+ @@ -160,11 +229,17 @@ void tuh_audio_mount_cb(uint8_t idx); // Invoked when device with Audio interface is un-mounted void tuh_audio_umount_cb(uint8_t idx); -// Invoked when an isochronous IN transfer is complete -void tuh_audio_rx_cb(uint8_t dev_addr, uint8_t ep_addr, uint16_t xferred_bytes); +// Invoked when an isochronous IN transfer completes successfully: the +// received data is already queued into the stream's capture FIFO. +void tuh_audio_capture_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes); + +// Invoked when an isochronous OUT transfer completes successfully: the +// next queued packet is submitted from the stream's playback FIFO. +void tuh_audio_playback_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes); -// Invoked when an isochronous OUT transfer is complete -void tuh_audio_tx_cb(uint8_t dev_addr, uint8_t ep_addr, uint16_t xferred_bytes); +// Invoked when an isochronous transfer fails. The stream is stopped +// (tuh_audio_start() must be called again to resume). +void tuh_audio_err_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes); //--------------------------------------------------------------------+ // Internal Class Driver API -- cgit v1.3.1 From 282d46e68d9100af0dfdcc01e7689bb63bbf8419 Mon Sep 17 00:00:00 2001 From: ice458 <85405449+ice458@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:00:06 +0900 Subject: usbtmc: re-arm (or stall) the bulk-OUT endpoint after a USB488 TRIGGER A single USB488 TRIGGER message left the bulk-OUT endpoint un-armed, so the host's next bulk-OUT transfer timed out. The trigger itself succeeded silently, so the failure surfaced on a later, unrelated command; only a USBTMC device clear recovered it. The bundled examples/device/usbtmc reproduced this as shipped. Every other branch of the STATE_IDLE dispatch in usbtmcd_xfer_cb() leaves the endpoint in a defined state: it either transitions out of STATE_IDLE so a later tud_usbtmc_start_bus_read() can re-arm it, or it stalls and lets the CLEAR_FEATURE(ENDPOINT_HALT) handler recover it. USBTMC_MSGID_USB488_TRIGGER did neither, and because the state stayed STATE_IDLE, even an application following the contract documented in usbtmc_device.h got a silent no-op from tud_usbtmc_start_bus_read(). Transition to STATE_NAK so the re-arm can take effect, and stall the endpoint when trigger is unsupported or the application callback rejects it, matching the existing handling for messages the driver cannot process. The callback result is deliberately not wrapped in TU_VERIFY(), which would return before the stall/re-arm and reintroduce the same hang. Since the driver now re-arms after a trigger, drop tud_usbtmc_msg_trigger_cb from the list of callbacks after which the application must do so. Fixes #3821 Co-Authored-By: Claude Opus 5 --- src/class/usbtmc/usbtmc_device.c | 18 +++++++++++++++--- src/class/usbtmc/usbtmc_device.h | 1 - 2 files changed, 15 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/class/usbtmc/usbtmc_device.c b/src/class/usbtmc/usbtmc_device.c index 07190d89f..e248341ac 100644 --- a/src/class/usbtmc/usbtmc_device.c +++ b/src/class/usbtmc/usbtmc_device.c @@ -497,9 +497,21 @@ bool usbtmcd_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint #if (CFG_TUD_USBTMC_ENABLE_488) case USBTMC_MSGID_USB488_TRIGGER: - // Spec says we halt the EP if we didn't declare we support it. - TU_VERIFY(usbtmc_state.capabilities->bmIntfcCapabilities488.supportsTrigger); - TU_VERIFY(tud_usbtmc_msg_trigger_cb(msg)); + // Unlike the messages above, TRIGGER is complete on arrival and has no response, so nothing else + // will move us out of STATE_IDLE. Do it here, otherwise the tud_usbtmc_start_bus_read() below (and + // any call the application makes from its callback) is a no-op and the bulk-OUT endpoint is left + // un-armed, silently timing out every subsequent host transfer. + TU_VERIFY(atomicChangeState(STATE_IDLE, STATE_NAK)); + + // Spec says we halt the EP if we didn't declare we support it; do the same when the application + // rejects the trigger. The callback result must not be wrapped in TU_VERIFY() here: returning + // early would skip both the stall and the re-arm below. + if (!usbtmc_state.capabilities->bmIntfcCapabilities488.supportsTrigger || + !tud_usbtmc_msg_trigger_cb(msg)) { + usbd_edpt_stall(rhport, usbtmc_state.ep_bulk_out); + return false; + } + tud_usbtmc_start_bus_read(); break; #endif diff --git a/src/class/usbtmc/usbtmc_device.h b/src/class/usbtmc/usbtmc_device.h index 3dc700876..efda84f16 100644 --- a/src/class/usbtmc/usbtmc_device.h +++ b/src/class/usbtmc/usbtmc_device.h @@ -25,7 +25,6 @@ // * tud_usbtmc_open_cb // * tud_usbtmc_msg_data_cb // * tud_usbtmc_msgBulkIn_complete_cb -// * tud_usbtmc_msg_trigger_cb // * (successful) tud_usbtmc_check_abort_bulk_out_cb // * (successful) tud_usbtmc_check_abort_bulk_in_cb // * (successful) tud_usmtmc_bulkOut_clearFeature_cb -- cgit v1.3.1 From af81f9ef42254301c2239eed657b0adce8b466b0 Mon Sep 17 00:00:00 2001 From: ice458 <85405449+ice458@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:23:38 +0900 Subject: usbtmc: document why the trigger re-arm result is ignored A false return from tud_usbtmc_start_bus_read() here does not mean arming failed: it means the endpoint is already armed, either because the application re-armed it from its trigger callback or because a transfer is still queued (usbd_edpt_xfer() reports failure when the endpoint is busy). Both cases end in STATE_IDLE, so the state cannot disambiguate them either, and stalling on the result would halt a healthy endpoint. Co-Authored-By: Claude Opus 5 --- src/class/usbtmc/usbtmc_device.c | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src') diff --git a/src/class/usbtmc/usbtmc_device.c b/src/class/usbtmc/usbtmc_device.c index e248341ac..0e9978a81 100644 --- a/src/class/usbtmc/usbtmc_device.c +++ b/src/class/usbtmc/usbtmc_device.c @@ -511,6 +511,9 @@ bool usbtmcd_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint usbd_edpt_stall(rhport, usbtmc_state.ep_bulk_out); return false; } + // Result deliberately ignored: false here means the endpoint is already armed - either the + // application re-armed it from its callback, or a transfer is still queued - not that arming + // failed. Stalling on it would halt a healthy endpoint. tud_usbtmc_start_bus_read(); break; -- cgit v1.3.1 From 16629759cd26973cd8e26bee632b339e718f83ba Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Fri, 14 Aug 2026 15:21:18 -0300 Subject: feat(midi2): complete the UMP stream discovery responder Adds the Device Identity Notification with an app callback, MIDI-CI version and SysEx8 stream count in FB Info, honors the Endpoint Discovery filter bitmap, and paces discovery replies by TX FIFO room. --- src/class/midi/midi2_device.c | 129 +++++++++++++++++++++++++++++++++++++----- src/class/midi/midi2_device.h | 28 +++++++++ 2 files changed, 143 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/src/class/midi/midi2_device.c b/src/class/midi/midi2_device.c index 1d40a2efa..e03717992 100644 --- a/src/class/midi/midi2_device.c +++ b/src/class/midi/midi2_device.c @@ -36,6 +36,9 @@ TU_ATTR_WEAK const char* tud_midi2_fb_name_cb(uint8_t itf, uint8_t fb_idx) { TU_ATTR_WEAK tud_midi2_stream_result_t tud_midi2_stream_msg_cb(uint8_t itf, const uint32_t* ump_words) { (void) itf; (void) ump_words; return MIDI2_STREAM_PASS; } +TU_ATTR_WEAK bool tud_midi2_device_identity_cb(uint8_t itf, tud_midi2_device_identity_t* identity) { + (void) itf; (void) identity; return false; +} //--------------------------------------------------------------------+ // Byte order note @@ -59,6 +62,7 @@ enum { enum { STREAM_ENDPOINT_DISCOVERY = 0x000, STREAM_ENDPOINT_INFO = 0x001, + STREAM_DEVICE_IDENTITY = 0x002, STREAM_EP_NAME = 0x003, STREAM_PROD_INSTANCE_ID = 0x004, STREAM_CONFIG_REQUEST = 0x005, @@ -103,6 +107,12 @@ typedef struct { uint8_t protocol; bool negotiated; + // Discovery reply bits waiting for TX FIFO room, drained on TX complete + uint8_t nego_pending_ep_filter; + uint8_t nego_pending_fb_filter; + uint8_t nego_pending_fb_num; // block requested by the pending discovery, 0xFF = all + uint8_t nego_pending_fb_next; // next block index to reply for + /*------------- From this point, data is not cleared by bus reset -------------*/ struct { midi2d_tx_t tx; @@ -380,6 +390,33 @@ static void _nego_send_config_notify(midi2d_interface_t* p_midi, uint8_t protoco _nego_send_ump(p_midi, msg, 4); } +static void _nego_send_device_identity(midi2d_interface_t* p_midi) { + tud_midi2_device_identity_t id; + tu_memclr(&id, sizeof(id)); + if (!tud_midi2_device_identity_cb(_itf_idx(p_midi), &id)) return; + + // Every field is a run of bytes, each carrying 7 bits, laid out in the same + // order as the MIDI 1.0 Device Inquiry reply this message mirrors. A 1-byte + // manufacturer ID occupies the first of the three bytes, the other two stay + // zero, so the caller passes it as 0x7D0000 and not 0x00007D. + uint32_t msg[4] = {0}; + msg[0] = ((uint32_t) MT_STREAM << 28) + | ((uint32_t) STREAM_DEVICE_IDENTITY << 16); + msg[1] = id.manufacturer & UINT32_C(0x7F7F7F); + // Family and model are 14-bit numbers sent least significant byte first, + // as in the Device Inquiry reply. Manufacturer above is a byte sequence + // rather than a number, so it keeps its own order. + msg[2] = ((uint32_t) (id.family & 0x7F) << 24) + | ((uint32_t) ((id.family >> 7) & 0x7F) << 16) + | ((uint32_t) (id.model & 0x7F) << 8) + | ((uint32_t) ((id.model >> 7) & 0x7F)); + msg[3] = ((uint32_t) ((id.sw_revision >> 24) & 0x7F) << 24) + | ((uint32_t) ((id.sw_revision >> 16) & 0x7F) << 16) + | ((uint32_t) ((id.sw_revision >> 8) & 0x7F) << 8) + | ((uint32_t) (id.sw_revision & 0x7F)); + _nego_send_ump(p_midi, msg, 4); +} + static void _nego_send_fb_info(midi2d_interface_t* p_midi, uint8_t fb_idx) { // Derive direction and group span for this block from the GTB descriptor. uint16_t gtb_len = 0; @@ -395,10 +432,75 @@ static void _nego_send_fb_info(midi2d_interface_t* p_midi, uint8_t fb_idx) { | ((uint32_t) fb_idx << 8) | _fb_dir_byte(type); // UI hint + bDirection from the GTB block type msg[1] = ((uint32_t) first_group << 24) - | ((uint32_t) num_groups << 16); + | ((uint32_t) num_groups << 16) + | ((uint32_t) (CFG_TUD_MIDI2_FB_CI_VERSION & 0xFF) << 8) + | ((uint32_t) (CFG_TUD_MIDI2_FB_SYSEX8_STREAMS & 0xFF)); _nego_send_ump(p_midi, msg, 4); } +// Byte cost of one stream text reply (name or product id), all packets included. +static uint16_t _nego_stream_text_bytes(bool has_index, const char* str) { + if (!str || str[0] == '\0') return 0; + const uint8_t per_pkt = has_index ? 13 : 14; + const uint16_t len = (uint16_t) strlen(str); + return (uint16_t)(((len + per_pkt - 1) / per_pkt) * 16); +} + +// Send pending discovery replies, one whole reply at a time and only when the +// TX FIFO can take it. A full-filter Endpoint Discovery asks for more bytes +// than the default FIFO holds; replies that do not fit stay pending and are +// retried from the TX complete path, paced by the transfer flow. +static void _nego_send_pending(midi2d_interface_t* p_midi) { + tu_fifo_t* tx_ff = &p_midi->ep_stream.tx.ff; + const uint16_t depth = tu_fifo_depth(tx_ff); + const uint8_t itf = _itf_idx(p_midi); + + while (p_midi->nego_pending_ep_filter) { + const uint8_t bit = (uint8_t)(p_midi->nego_pending_ep_filter & (uint8_t)(-p_midi->nego_pending_ep_filter)); + uint16_t needed; + switch (bit) { + case 0x04: needed = _nego_stream_text_bytes(false, tud_midi2_ep_name_cb(itf)); break; + case 0x08: needed = _nego_stream_text_bytes(false, tud_midi2_product_id_cb(itf)); break; + default: needed = 16; break; // endpoint info, device identity, config notify + } + if (needed > depth) needed = depth; // oversized reply: send best effort, never stall + if (tu_fifo_remaining(tx_ff) < needed) return; + + switch (bit) { + case 0x01: _nego_send_endpoint_info(p_midi); break; + case 0x02: _nego_send_device_identity(p_midi); break; + case 0x04: _nego_send_stream_text(p_midi, STREAM_EP_NAME, false, 0, tud_midi2_ep_name_cb(itf)); break; + case 0x08: _nego_send_stream_text(p_midi, STREAM_PROD_INSTANCE_ID, false, 0, tud_midi2_product_id_cb(itf)); break; + case 0x10: _nego_send_config_notify(p_midi, p_midi->protocol); break; + default: break; + } + p_midi->nego_pending_ep_filter &= (uint8_t) ~bit; + } + + const uint8_t fb_count = _gtb_block_count(p_midi); + while (p_midi->nego_pending_fb_filter && p_midi->nego_pending_fb_next < fb_count) { + const uint8_t f = p_midi->nego_pending_fb_next; + if (p_midi->nego_pending_fb_num != 0xFF && p_midi->nego_pending_fb_num != f) { + p_midi->nego_pending_fb_next++; + continue; + } + // Info and name for one block go out together to keep per-block ordering. + uint16_t needed = (p_midi->nego_pending_fb_filter & 0x01) ? 16 : 0; + if (p_midi->nego_pending_fb_filter & 0x02) { + needed = (uint16_t)(needed + _nego_stream_text_bytes(true, tud_midi2_fb_name_cb(itf, f))); + } + if (needed > depth) needed = depth; + if (tu_fifo_remaining(tx_ff) < needed) return; + + if (p_midi->nego_pending_fb_filter & 0x01) _nego_send_fb_info(p_midi, f); + if (p_midi->nego_pending_fb_filter & 0x02) { + _nego_send_stream_text(p_midi, STREAM_FB_NAME, true, f, tud_midi2_fb_name_cb(itf, f)); + } + p_midi->nego_pending_fb_next++; + } + if (p_midi->nego_pending_fb_next >= fb_count) p_midi->nego_pending_fb_filter = 0; +} + static void _nego_handle_stream_msg(midi2d_interface_t* p_midi, const uint32_t* words) { // Let the application override this message before the built-in responder. switch (tud_midi2_stream_msg_cb(_itf_idx(p_midi), words)) { @@ -421,9 +523,9 @@ static void _nego_handle_stream_msg(midi2d_interface_t* p_midi, const uint32_t* switch (status) { case STREAM_ENDPOINT_DISCOVERY: - _nego_send_endpoint_info(p_midi); - _nego_send_stream_text(p_midi, STREAM_EP_NAME, false, 0, tud_midi2_ep_name_cb(_itf_idx(p_midi))); - _nego_send_stream_text(p_midi, STREAM_PROD_INSTANCE_ID, false, 0, tud_midi2_product_id_cb(_itf_idx(p_midi))); + // Filter bitmap: each bit set asks for one individual reply. + p_midi->nego_pending_ep_filter |= (uint8_t)(words[1] & 0x1F); + _nego_send_pending(p_midi); break; case STREAM_CONFIG_REQUEST: { @@ -436,17 +538,12 @@ static void _nego_handle_stream_msg(midi2d_interface_t* p_midi, const uint32_t* break; } - case STREAM_FB_DISCOVERY: { - uint8_t fb_idx = (words[0] >> 8) & 0xFF; - uint8_t filter = words[0] & 0xFF; // bit 0: FB Info, bit 1: FB Name - uint8_t fb_count = _gtb_block_count(p_midi); - for (uint8_t f = 0; f < fb_count; f++) { - if (fb_idx != 0xFF && fb_idx != f) continue; - if (filter & 0x01) _nego_send_fb_info(p_midi, f); - if (filter & 0x02) _nego_send_stream_text(p_midi, STREAM_FB_NAME, true, f, tud_midi2_fb_name_cb(_itf_idx(p_midi), f)); - } + case STREAM_FB_DISCOVERY: + p_midi->nego_pending_fb_num = (uint8_t)((words[0] >> 8) & 0xFF); + p_midi->nego_pending_fb_filter = (uint8_t)(words[0] & 0x03); // bit 0: FB Info, bit 1: FB Name + p_midi->nego_pending_fb_next = 0; + _nego_send_pending(p_midi); break; - } default: break; @@ -824,6 +921,10 @@ bool midi2d_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint3 } tu_edpt_stream_read_xfer(ep_rx); } else if (ep_addr == ep_tx->ep_addr && result == XFER_RESULT_SUCCESS) { + // Completed transfer freed FIFO room: flush discovery replies still pending. + if (p_midi->alt_setting == 1) { + _nego_send_pending(p_midi); + } uint16_t queued = _tx_start_xfer(p_midi); // Send ZLP if no more data is queued but the last transfer was exactly mps if (queued == 0 && tu_fifo_count(&ep_tx->ff) == 0 && xferred_bytes > 0 && diff --git a/src/class/midi/midi2_device.h b/src/class/midi/midi2_device.h index 171b404b7..e3eb084d9 100644 --- a/src/class/midi/midi2_device.h +++ b/src/class/midi/midi2_device.h @@ -58,6 +58,17 @@ extern "C" { #define CFG_TUD_MIDI2_PRODUCT_ID "TinyUSB-MIDI2" #endif +// Function Block capabilities reported in Function Block Info Notification. +// The GTB descriptor carries direction and group span, but not these: they +// depend on what the application implements, so they default to "none". +#ifndef CFG_TUD_MIDI2_FB_CI_VERSION + #define CFG_TUD_MIDI2_FB_CI_VERSION 0 // 0: none or unknown, 1 or higher: MIDI-CI version +#endif + +#ifndef CFG_TUD_MIDI2_FB_SYSEX8_STREAMS + #define CFG_TUD_MIDI2_FB_SYSEX8_STREAMS 0 // 0: unsupported, 1: single, 2-255: simultaneous streams +#endif + // String descriptor index for the Group Terminal Block (iBlockItem, Table 5-6). // 0 = no string descriptor (default, spec-allowed). #ifndef CFG_TUD_MIDI2_BLOCK_STRIDX @@ -118,6 +129,17 @@ typedef enum { MIDI2_STREAM_NEGOTIATED_MIDI2, } tud_midi2_stream_result_t; +// Device identity fields, as defined for the MIDI 1.0 Device Inquiry reply and +// reused by the Device Identity Notification. Every byte carries 7 bits. +// A 1-byte System Exclusive ID goes in the first of the three manufacturer +// bytes, so 0x7D is passed as 0x7D0000. +typedef struct { + uint32_t manufacturer; // 3 bytes, first byte is most significant + uint16_t family; // 2 bytes + uint16_t model; // 2 bytes + uint32_t sw_revision; // 4 bytes +} tud_midi2_device_identity_t; + //--------------------------------------------------------------------+ // Application Callback API (weak, optional) //--------------------------------------------------------------------+ @@ -138,6 +160,12 @@ const uint8_t* tud_midi2_gtb_desc_cb(uint8_t itf, uint16_t* len); // discovery. Return NULL or "" for no name. const char* tud_midi2_fb_name_cb(uint8_t itf, uint8_t fb_idx); +// Optional device identity, sent as a Device Identity Notification when the +// host sets the 'd' bit in the Endpoint Discovery filter. Same four fields as +// the MIDI 1.0 Device Inquiry reply. Return false to skip the notification, +// which is the default. All values are 7-bit per byte. +bool tud_midi2_device_identity_cb(uint8_t itf, tud_midi2_device_identity_t* identity); + // Optional: intercept an incoming UMP Stream message (MT 0xF). Return PASS to // let the built-in responder handle it, or HANDLED / NEGOTIATED_* if the app // answered it (e.g. via tud_midi2_n_ump_write). Lets an app override a single -- cgit v1.3.1 From 0504faf29825deb130bfeb88dba46bb1c1bdec75 Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Fri, 14 Aug 2026 16:43:36 -0300 Subject: fix(midi2): keep discovery replies valid under TX pressure Text replies resume instead of dropping their tail packets, which used to leave a Start/Continue sequence without an End. A new Function Block Discovery now merges with a pending one instead of replacing it. --- src/class/midi/midi2_device.c | 90 ++++++++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 40 deletions(-) (limited to 'src') diff --git a/src/class/midi/midi2_device.c b/src/class/midi/midi2_device.c index e03717992..369d380c5 100644 --- a/src/class/midi/midi2_device.c +++ b/src/class/midi/midi2_device.c @@ -112,6 +112,7 @@ typedef struct { uint8_t nego_pending_fb_filter; uint8_t nego_pending_fb_num; // block requested by the pending discovery, 0xFF = all uint8_t nego_pending_fb_next; // next block index to reply for + uint16_t nego_text_offset; // progress into the text reply being sent /*------------- From this point, data is not cleared by bus reset -------------*/ struct { @@ -337,16 +338,20 @@ static void _nego_send_endpoint_info(midi2d_interface_t* p_midi) { // index byte (the Function Block number for FB Name) and 13 chars fit per // packet; otherwise the text starts there and 14 chars fit (Endpoint Name, // Product Instance Id). -static void _nego_send_stream_text(midi2d_interface_t* p_midi, uint16_t status, - bool has_index, uint8_t index, const char* str) { - if (!str || str[0] == '\0') return; +// Sends a stream text from `offset` and returns how far it got. Resuming keeps +// the End packet, which dropping the tail would lose. +static uint16_t _nego_send_stream_text(midi2d_interface_t* p_midi, uint16_t status, + bool has_index, uint8_t index, const char* str, + uint16_t offset) { + if (!str || str[0] == '\0') return 0; - uint16_t total_len = (uint16_t) strlen(str); - uint16_t offset = 0; + const uint16_t total_len = (uint16_t) strlen(str); const uint8_t per_pkt = has_index ? 13 : 14; const uint8_t head_chars = has_index ? 1 : 2; // chars carried in word0 + if (offset >= total_len) return total_len; while (offset < total_len) { + if (tu_fifo_remaining(&p_midi->ep_stream.tx.ff) < 16) break; uint16_t remaining = total_len - offset; uint8_t n = (uint8_t)((remaining > per_pkt) ? per_pkt : remaining); bool is_first = (offset == 0); @@ -380,6 +385,7 @@ static void _nego_send_stream_text(midi2d_interface_t* p_midi, uint16_t status, _nego_send_ump(p_midi, msg, 4); offset += n; } + return offset; } static void _nego_send_config_notify(midi2d_interface_t* p_midi, uint8_t protocol) { @@ -438,41 +444,37 @@ static void _nego_send_fb_info(midi2d_interface_t* p_midi, uint8_t fb_idx) { _nego_send_ump(p_midi, msg, 4); } -// Byte cost of one stream text reply (name or product id), all packets included. -static uint16_t _nego_stream_text_bytes(bool has_index, const char* str) { - if (!str || str[0] == '\0') return 0; - const uint8_t per_pkt = has_index ? 13 : 14; - const uint16_t len = (uint16_t) strlen(str); - return (uint16_t)(((len + per_pkt - 1) / per_pkt) * 16); -} - // Send pending discovery replies, one whole reply at a time and only when the // TX FIFO can take it. A full-filter Endpoint Discovery asks for more bytes // than the default FIFO holds; replies that do not fit stay pending and are // retried from the TX complete path, paced by the transfer flow. static void _nego_send_pending(midi2d_interface_t* p_midi) { tu_fifo_t* tx_ff = &p_midi->ep_stream.tx.ff; - const uint16_t depth = tu_fifo_depth(tx_ff); const uint8_t itf = _itf_idx(p_midi); while (p_midi->nego_pending_ep_filter) { const uint8_t bit = (uint8_t)(p_midi->nego_pending_ep_filter & (uint8_t)(-p_midi->nego_pending_ep_filter)); - uint16_t needed; + const char* text = NULL; + uint16_t status = 0; switch (bit) { - case 0x04: needed = _nego_stream_text_bytes(false, tud_midi2_ep_name_cb(itf)); break; - case 0x08: needed = _nego_stream_text_bytes(false, tud_midi2_product_id_cb(itf)); break; - default: needed = 16; break; // endpoint info, device identity, config notify + case 0x04: text = tud_midi2_ep_name_cb(itf); status = STREAM_EP_NAME; break; + case 0x08: text = tud_midi2_product_id_cb(itf); status = STREAM_PROD_INSTANCE_ID; break; + default: break; } - if (needed > depth) needed = depth; // oversized reply: send best effort, never stall - if (tu_fifo_remaining(tx_ff) < needed) return; - switch (bit) { - case 0x01: _nego_send_endpoint_info(p_midi); break; - case 0x02: _nego_send_device_identity(p_midi); break; - case 0x04: _nego_send_stream_text(p_midi, STREAM_EP_NAME, false, 0, tud_midi2_ep_name_cb(itf)); break; - case 0x08: _nego_send_stream_text(p_midi, STREAM_PROD_INSTANCE_ID, false, 0, tud_midi2_product_id_cb(itf)); break; - case 0x10: _nego_send_config_notify(p_midi, p_midi->protocol); break; - default: break; + if (text != NULL) { + p_midi->nego_text_offset = _nego_send_stream_text(p_midi, status, false, 0, text, + p_midi->nego_text_offset); + if (p_midi->nego_text_offset < (uint16_t) strlen(text)) return; // resume on TX complete + p_midi->nego_text_offset = 0; + } else { + if (tu_fifo_remaining(tx_ff) < 16) return; + switch (bit) { + case 0x01: _nego_send_endpoint_info(p_midi); break; + case 0x02: _nego_send_device_identity(p_midi); break; + case 0x10: _nego_send_config_notify(p_midi, p_midi->protocol); break; + default: break; + } } p_midi->nego_pending_ep_filter &= (uint8_t) ~bit; } @@ -484,17 +486,16 @@ static void _nego_send_pending(midi2d_interface_t* p_midi) { p_midi->nego_pending_fb_next++; continue; } - // Info and name for one block go out together to keep per-block ordering. - uint16_t needed = (p_midi->nego_pending_fb_filter & 0x01) ? 16 : 0; - if (p_midi->nego_pending_fb_filter & 0x02) { - needed = (uint16_t)(needed + _nego_stream_text_bytes(true, tud_midi2_fb_name_cb(itf, f))); + if ((p_midi->nego_pending_fb_filter & 0x01) && p_midi->nego_text_offset == 0) { + if (tu_fifo_remaining(tx_ff) < 16) return; + _nego_send_fb_info(p_midi, f); } - if (needed > depth) needed = depth; - if (tu_fifo_remaining(tx_ff) < needed) return; - - if (p_midi->nego_pending_fb_filter & 0x01) _nego_send_fb_info(p_midi, f); if (p_midi->nego_pending_fb_filter & 0x02) { - _nego_send_stream_text(p_midi, STREAM_FB_NAME, true, f, tud_midi2_fb_name_cb(itf, f)); + const char* name = tud_midi2_fb_name_cb(itf, f); + p_midi->nego_text_offset = _nego_send_stream_text(p_midi, STREAM_FB_NAME, true, f, name, + p_midi->nego_text_offset); + if (name != NULL && p_midi->nego_text_offset < (uint16_t) strlen(name)) return; + p_midi->nego_text_offset = 0; } p_midi->nego_pending_fb_next++; } @@ -538,12 +539,21 @@ static void _nego_handle_stream_msg(midi2d_interface_t* p_midi, const uint32_t* break; } - case STREAM_FB_DISCOVERY: - p_midi->nego_pending_fb_num = (uint8_t)((words[0] >> 8) & 0xFF); - p_midi->nego_pending_fb_filter = (uint8_t)(words[0] & 0x03); // bit 0: FB Info, bit 1: FB Name - p_midi->nego_pending_fb_next = 0; + case STREAM_FB_DISCOVERY: { + const uint8_t req_num = (uint8_t)((words[0] >> 8) & 0xFF); + // Merge with a pending request: repeating a Function Block Info is allowed + // at any time, losing a requested one is not. + if (p_midi->nego_pending_fb_filter && p_midi->nego_pending_fb_num != req_num) { + p_midi->nego_pending_fb_num = 0xFF; + p_midi->nego_pending_fb_next = 0; + } else if (!p_midi->nego_pending_fb_filter) { + p_midi->nego_pending_fb_num = req_num; + p_midi->nego_pending_fb_next = 0; + } + p_midi->nego_pending_fb_filter |= (uint8_t)(words[0] & 0x03); // bit 0: FB Info, bit 1: FB Name _nego_send_pending(p_midi); break; + } default: break; -- cgit v1.3.1 From dfd197ff0c83a01ac55a99b85f2e8f3794ea0a47 Mon Sep 17 00:00:00 2001 From: HiFiPhile Date: Sat, 15 Aug 2026 05:11:55 +0200 Subject: fix(midi2): fix discovery response racing Signed-off-by: HiFiPhile --- src/class/midi/midi2_device.c | 97 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 79 insertions(+), 18 deletions(-) (limited to 'src') diff --git a/src/class/midi/midi2_device.c b/src/class/midi/midi2_device.c index 369d380c5..b0a9e2503 100644 --- a/src/class/midi/midi2_device.c +++ b/src/class/midi/midi2_device.c @@ -112,7 +112,10 @@ typedef struct { uint8_t nego_pending_fb_filter; uint8_t nego_pending_fb_num; // block requested by the pending discovery, 0xFF = all uint8_t nego_pending_fb_next; // next block index to reply for + bool nego_pending_fb_restart; // restart after the active FB name when requests merge + uint16_t nego_text_status; // text reply owning nego_text_offset, 0 = none uint16_t nego_text_offset; // progress into the text reply being sent + uint8_t nego_text_index; // Function Block index for an active FB name /*------------- From this point, data is not cleared by bus reset -------------*/ struct { @@ -444,29 +447,85 @@ static void _nego_send_fb_info(midi2d_interface_t* p_midi, uint8_t fb_idx) { _nego_send_ump(p_midi, msg, 4); } +static void _nego_clear_pending(midi2d_interface_t* p_midi) { + p_midi->nego_pending_ep_filter = 0; + p_midi->nego_pending_fb_filter = 0; + p_midi->nego_pending_fb_num = 0; + p_midi->nego_pending_fb_next = 0; + p_midi->nego_pending_fb_restart = false; + p_midi->nego_text_status = 0; + p_midi->nego_text_offset = 0; + p_midi->nego_text_index = 0; +} + +static const char* _nego_text_cb(midi2d_interface_t* p_midi, uint16_t status, uint8_t index) { + const uint8_t itf = _itf_idx(p_midi); + switch (status) { + case STREAM_EP_NAME: return tud_midi2_ep_name_cb(itf); + case STREAM_PROD_INSTANCE_ID: return tud_midi2_product_id_cb(itf); + case STREAM_FB_NAME: return tud_midi2_fb_name_cb(itf, index); + default: return NULL; + } +} + +// Send or resume one text reply. While it is incomplete, its status and index +// identify the sole owner of nego_text_offset so another discovery request +// cannot resume a different string from the same offset. +static bool _nego_send_text(midi2d_interface_t* p_midi, uint16_t status, uint8_t index) { + const char* text = _nego_text_cb(p_midi, status, index); + const uint16_t len = text ? (uint16_t) strlen(text) : 0; + + p_midi->nego_text_status = status; + p_midi->nego_text_index = index; + p_midi->nego_text_offset = _nego_send_stream_text(p_midi, status, status == STREAM_FB_NAME, + index, text, p_midi->nego_text_offset); + if (p_midi->nego_text_offset < len) return false; + + p_midi->nego_text_status = 0; + p_midi->nego_text_offset = 0; + p_midi->nego_text_index = 0; + return true; +} + // Send pending discovery replies, one whole reply at a time and only when the // TX FIFO can take it. A full-filter Endpoint Discovery asks for more bytes // than the default FIFO holds; replies that do not fit stay pending and are // retried from the TX complete path, paced by the transfer flow. static void _nego_send_pending(midi2d_interface_t* p_midi) { tu_fifo_t* tx_ff = &p_midi->ep_stream.tx.ff; - const uint8_t itf = _itf_idx(p_midi); + + // An incomplete text sequence must finish before any newly arrived request + // is serviced; otherwise its Continue/End packets could be attached to a + // different Endpoint or Function Block string. + if (p_midi->nego_text_status) { + const uint16_t status = p_midi->nego_text_status; + const uint8_t index = p_midi->nego_text_index; + if (!_nego_send_text(p_midi, status, index)) return; + + if (status == STREAM_FB_NAME) { + if (p_midi->nego_pending_fb_restart) { + p_midi->nego_pending_fb_next = 0; + p_midi->nego_pending_fb_restart = false; + } else { + p_midi->nego_pending_fb_next++; + } + } else { + const uint8_t bit = (status == STREAM_EP_NAME) ? 0x04 : 0x08; + p_midi->nego_pending_ep_filter &= (uint8_t) ~bit; + } + } while (p_midi->nego_pending_ep_filter) { const uint8_t bit = (uint8_t)(p_midi->nego_pending_ep_filter & (uint8_t)(-p_midi->nego_pending_ep_filter)); - const char* text = NULL; uint16_t status = 0; switch (bit) { - case 0x04: text = tud_midi2_ep_name_cb(itf); status = STREAM_EP_NAME; break; - case 0x08: text = tud_midi2_product_id_cb(itf); status = STREAM_PROD_INSTANCE_ID; break; + case 0x04: status = STREAM_EP_NAME; break; + case 0x08: status = STREAM_PROD_INSTANCE_ID; break; default: break; } - if (text != NULL) { - p_midi->nego_text_offset = _nego_send_stream_text(p_midi, status, false, 0, text, - p_midi->nego_text_offset); - if (p_midi->nego_text_offset < (uint16_t) strlen(text)) return; // resume on TX complete - p_midi->nego_text_offset = 0; + if (status != 0) { + if (!_nego_send_text(p_midi, status, 0)) return; } else { if (tu_fifo_remaining(tx_ff) < 16) return; switch (bit) { @@ -491,11 +550,7 @@ static void _nego_send_pending(midi2d_interface_t* p_midi) { _nego_send_fb_info(p_midi, f); } if (p_midi->nego_pending_fb_filter & 0x02) { - const char* name = tud_midi2_fb_name_cb(itf, f); - p_midi->nego_text_offset = _nego_send_stream_text(p_midi, STREAM_FB_NAME, true, f, name, - p_midi->nego_text_offset); - if (name != NULL && p_midi->nego_text_offset < (uint16_t) strlen(name)) return; - p_midi->nego_text_offset = 0; + if (!_nego_send_text(p_midi, STREAM_FB_NAME, f)) return; } p_midi->nego_pending_fb_next++; } @@ -541,16 +596,21 @@ static void _nego_handle_stream_msg(midi2d_interface_t* p_midi, const uint32_t* case STREAM_FB_DISCOVERY: { const uint8_t req_num = (uint8_t)((words[0] >> 8) & 0xFF); + const uint8_t req_filter = (uint8_t)(words[0] & 0x03); // Merge with a pending request: repeating a Function Block Info is allowed // at any time, losing a requested one is not. - if (p_midi->nego_pending_fb_filter && p_midi->nego_pending_fb_num != req_num) { - p_midi->nego_pending_fb_num = 0xFF; - p_midi->nego_pending_fb_next = 0; + if (req_filter && p_midi->nego_pending_fb_filter) { + if (p_midi->nego_pending_fb_num != req_num) p_midi->nego_pending_fb_num = 0xFF; + if (p_midi->nego_text_status == STREAM_FB_NAME) { + p_midi->nego_pending_fb_restart = true; + } else { + p_midi->nego_pending_fb_next = 0; + } } else if (!p_midi->nego_pending_fb_filter) { p_midi->nego_pending_fb_num = req_num; p_midi->nego_pending_fb_next = 0; } - p_midi->nego_pending_fb_filter |= (uint8_t)(words[0] & 0x03); // bit 0: FB Info, bit 1: FB Name + p_midi->nego_pending_fb_filter |= req_filter; // bit 0: FB Info, bit 1: FB Name _nego_send_pending(p_midi); break; } @@ -861,6 +921,7 @@ bool midi2d_control_xfer_cb(uint8_t rhport, uint8_t stage, const tusb_control_re tu_edpt_stream_clear(&p_midi->ep_stream.rx); tu_fifo_clear(&p_midi->ep_stream.tx.ff); + _nego_clear_pending(p_midi); if (alt == 1) { p_midi->negotiated = false; -- cgit v1.3.1 From 8737c5adfca7e51003e743bcc8bcefed837fb1d8 Mon Sep 17 00:00:00 2001 From: Zixun LI Date: Sat, 15 Aug 2026 05:51:43 +0200 Subject: Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: HiFiPhile --- src/class/video/video_device.c | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/class/video/video_device.c b/src/class/video/video_device.c index 390349f13..770595178 100644 --- a/src/class/video/video_device.c +++ b/src/class/video/video_device.c @@ -1144,13 +1144,10 @@ static int handle_video_stm_cs_req(uint8_t rhport, uint8_t stage, video_probe_and_commit_control_t *param = &stm->probe_commit_payload; TU_VERIFY(_update_streaming_parameters(stm, param), VIDEO_ERROR_INVALID_VALUE_WITHIN_RANGE); /* Set the negotiated value */ - stm->max_payload_transfer_size = param->dwMaxPayloadTransferSize; - /* A host may commit before the parameters are fully negotiated, in which case - * _update_streaming_parameters returns early without capping the payload size. - * Clamp here so a bulk stream cannot overrun the endpoint buffer. */ - if (CFG_TUD_VIDEO_STREAMING_EP_BUFSIZE < stm->max_payload_transfer_size) { - stm->max_payload_transfer_size = CFG_TUD_VIDEO_STREAMING_EP_BUFSIZE; + if (CFG_TUD_VIDEO_STREAMING_EP_BUFSIZE < param->dwMaxPayloadTransferSize) { + param->dwMaxPayloadTransferSize = CFG_TUD_VIDEO_STREAMING_EP_BUFSIZE; } + stm->max_payload_transfer_size = param->dwMaxPayloadTransferSize; int ret = tud_video_commit_cb(stm->index_vc, stm->index_vs, param); if (VIDEO_ERROR_NONE == ret) { stm->state = VS_STATE_COMMITTED; -- cgit v1.3.1 From 8ccd0d549798c66d484e5a4b4c57edf49e8bb097 Mon Sep 17 00:00:00 2001 From: hathach Date: Thu, 13 Aug 2026 14:35:01 +0700 Subject: portable/chipidea: name SBUSCFG in ci_hs_regs_t, unify AHB burst hook Replace the duplicated per-MCU dispatch in dcd_init/hcd_init and the two helper flavors (USB_Type access on iMX RT, raw offset 0x90 on LPC18/43) with one SBUSCFG register field plus a per-header CI_HS_SET_AHB_BURST() hook, compiled only where defined. The LPC USB0-only policy is now visible at the macro definition. --- src/portable/chipidea/ci_hs/ci_hs_imxrt.h | 11 ++--------- src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h | 17 ++++------------- src/portable/chipidea/ci_hs/ci_hs_type.h | 9 ++++++++- src/portable/chipidea/ci_hs/dcd_ci_hs.c | 6 ++---- src/portable/chipidea/ci_hs/hcd_ci_hs.c | 6 ++---- 5 files changed, 18 insertions(+), 31 deletions(-) (limited to 'src') diff --git a/src/portable/chipidea/ci_hs/ci_hs_imxrt.h b/src/portable/chipidea/ci_hs/ci_hs_imxrt.h index 601e4d1c9..8f0d6083e 100644 --- a/src/portable/chipidea/ci_hs/ci_hs_imxrt.h +++ b/src/portable/chipidea/ci_hs/ci_hs_imxrt.h @@ -36,15 +36,8 @@ static const ci_hs_controller_t _ci_controller[] = #define CI_HS_REG(_port) ((ci_hs_regs_t*) _ci_controller[_port].reg_base) -enum { - // INCR16/8/4 followed by an unspecified-length burst for the remainder. - CI_HS_IMXRT_AHBBRST_INCR16_UNSPEC = 0x07u, -}; - -TU_ATTR_ALWAYS_INLINE static inline void ci_hs_imxrt_set_ahb_burst(uint8_t rhport) { - USB_Type *usb = (USB_Type *)_ci_controller[rhport].reg_base; - usb->SBUSCFG = USB_SBUSCFG_AHBBRST(CI_HS_IMXRT_AHBBRST_INCR16_UNSPEC); -} +// NXP recommends AHBBRST = INCR16 (remainder as unspecified-length bursts) +#define CI_HS_SET_AHB_BURST(_p) (CI_HS_REG(_p)->SBUSCFG = SBUSCFG_AHBBRST_INCR16_UNSPEC) //------------- DCD -------------// #define CI_DCD_INT_ENABLE(_p) NVIC_EnableIRQ ((IRQn_Type)_ci_controller[_p].irqnum) diff --git a/src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h b/src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h index dec3a34b1..c7dc7e69f 100644 --- a/src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h +++ b/src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h @@ -34,18 +34,9 @@ static const ci_hs_controller_t _ci_controller[] = #define CI_HCD_INT_ENABLE(_p) NVIC_EnableIRQ ((IRQn_Type)_ci_controller[_p].irqnum) #define CI_HCD_INT_DISABLE(_p) NVIC_DisableIRQ((IRQn_Type)_ci_controller[_p].irqnum) -enum { - CI_HS_LPC18_43_SBUSCFG_OFFSET = 0x90u, - CI_HS_LPC18_43_AHBBRST_INCR16_UNSPEC = 0x07u, -}; - -TU_ATTR_ALWAYS_INLINE static inline void ci_hs_lpc18_43_set_ahb_burst(uint8_t rhport) { - // USB0 SBUSCFG is at offset 0x90. NXP recommends AHBBRST=0x7: - // INCR16 with non-multiple transfers decomposed into smaller unspecified bursts. - if (rhport == 0) { - volatile uint32_t *sbuscfg = (volatile uint32_t *)(_ci_controller[rhport].reg_base + CI_HS_LPC18_43_SBUSCFG_OFFSET); - *sbuscfg = CI_HS_LPC18_43_AHBBRST_INCR16_UNSPEC; - } -} +// USB0 (high-speed) only: NXP recommends AHBBRST = INCR16 (remainder as +// unspecified-length bursts) +#define CI_HS_SET_AHB_BURST(_p) \ + do { if ((_p) == 0) { CI_HS_REG(_p)->SBUSCFG = SBUSCFG_AHBBRST_INCR16_UNSPEC; } } while (0) #endif diff --git a/src/portable/chipidea/ci_hs/ci_hs_type.h b/src/portable/chipidea/ci_hs/ci_hs_type.h index 70817a6e3..b209c7545 100644 --- a/src/portable/chipidea/ci_hs/ci_hs_type.h +++ b/src/portable/chipidea/ci_hs/ci_hs_type.h @@ -71,11 +71,18 @@ enum { USBMODE_VBUS_POWER_SELECT = TU_BIT(5), // Need to be enabled for LPC18XX/43XX in host mode }; +// SBUSCFG +enum { + SBUSCFG_AHBBRST_INCR16_UNSPEC = 7, // INCR16 burst, remainder as unspecified-length bursts +}; + // Device Registers typedef struct { //------------- ID + HW Parameter Registers-------------// - volatile uint32_t TU_RESERVED[64]; ///< For iMX RT10xx, but not used by LPC18XX/LPC43XX + volatile uint32_t TU_RESERVED[36]; ///< ID/HW parameter registers, not used by this driver + volatile uint32_t SBUSCFG; ///< System Bus Interface Configuration (not present on every MCU) + volatile uint32_t TU_RESERVED[27]; //------------- Capability Registers-------------// volatile uint8_t CAPLENGTH; ///< Capability Registers Length diff --git a/src/portable/chipidea/ci_hs/dcd_ci_hs.c b/src/portable/chipidea/ci_hs/dcd_ci_hs.c index 62d75b4d3..8c08c6bd5 100644 --- a/src/portable/chipidea/ci_hs/dcd_ci_hs.c +++ b/src/portable/chipidea/ci_hs/dcd_ci_hs.c @@ -237,10 +237,8 @@ bool dcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) { usbmode |= USBMODE_CM_DEVICE; dcd_reg->USBMODE = usbmode; - #if CFG_TUSB_MCU == OPT_MCU_MIMXRT1XXX - ci_hs_imxrt_set_ahb_burst(rhport); - #elif TU_CHECK_MCU(OPT_MCU_LPC18XX, OPT_MCU_LPC43XX) - ci_hs_lpc18_43_set_ahb_burst(rhport); + #ifdef CI_HS_SET_AHB_BURST + CI_HS_SET_AHB_BURST(rhport); #endif #ifdef CFG_TUD_CI_HS_VBUS_CHARGE diff --git a/src/portable/chipidea/ci_hs/hcd_ci_hs.c b/src/portable/chipidea/ci_hs/hcd_ci_hs.c index 0fc8e4d70..0f24f5bb6 100644 --- a/src/portable/chipidea/ci_hs/hcd_ci_hs.c +++ b/src/portable/chipidea/ci_hs/hcd_ci_hs.c @@ -82,10 +82,8 @@ bool hcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) { hcd_reg->USBMODE = USBMODE_CM_HOST; #endif - #if CFG_TUSB_MCU == OPT_MCU_MIMXRT1XXX - ci_hs_imxrt_set_ahb_burst(rhport); - #elif TU_CHECK_MCU(OPT_MCU_LPC18XX, OPT_MCU_LPC43XX) - ci_hs_lpc18_43_set_ahb_burst(rhport); + #ifdef CI_HS_SET_AHB_BURST + CI_HS_SET_AHB_BURST(rhport); #endif #if !TUH_OPT_HIGH_SPEED -- cgit v1.3.1 From 18bb2d650404432d0c6c61c26e98c7074f46ec6b Mon Sep 17 00:00:00 2001 From: Ryzee119 Date: Wed, 12 Aug 2026 21:49:00 +0930 Subject: ohci: reclaim orphaned TDs on device disconnect --- src/portable/ohci/ohci.c | 77 ++++++++++++++++++++++++++++++++++++++++++++---- src/portable/ohci/ohci.h | 3 +- 2 files changed, 73 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/portable/ohci/ohci.c b/src/portable/ohci/ohci.c index a294c5f5f..7ede1ac72 100644 --- a/src/portable/ohci/ohci.c +++ b/src/portable/ohci/ohci.c @@ -378,7 +378,7 @@ static void ed_list_remove_by_addr(ohci_ed_t * p_head, uint8_t dev_addr) { ohci_ed_t* p_prev = p_head; while (p_prev->next) { - ohci_ed_t* ed = (ohci_ed_t*)_virt_addr((void*)p_prev->next); + ohci_ed_t* ed = hcd_dcache_uncached((ohci_ed_t*)_virt_addr((void*)p_prev->next)); if (ed->w0.dev_addr == dev_addr) { // Prevent Host Controller from processing this ED while we remove it @@ -387,12 +387,24 @@ static void ed_list_remove_by_addr(ohci_ed_t * p_head, uint8_t dev_addr) { // unlink ed, will also move up p_prev p_prev->next = ed->next; - // point the removed ED's next pointer to list head to make sure HC can always safely move away from this ED - ed->next = (uint32_t)_phys_addr(p_head); - ed->w0.used = 0; - ed->w0.skip = 0; + // Control endpoints (EP number 0) are statically allocated with the device which are only reused + // after connection of another device long after HC has finished with them now, these can be freed immediately. + if (ed->w0.ep_number != 0) { + ed->w0.is_reclaiming = 1; + + // 5.2.7.1.2 Removing. Disable list processing for bulk + if (p_head == p_ed_head[TUSB_XFER_BULK]) { + OHCI_REG->control &= ~OHCI_CONTROL_LIST_BULK_ENABLE_MASK; + } + + // Temporarily enable SOF IRQ. ED and TD Memory will be reclaimed in the SOF IRQ. + OHCI_REG->interrupt_enable = OHCI_INT_SOF_MASK; + } else { + ed->w0.used = 0; + ed->w0.skip = 0; + } } else { - p_prev = (ohci_ed_t*)_virt_addr((void*)p_prev->next); + p_prev = ed; } } } @@ -653,6 +665,59 @@ void hcd_int_handler(uint8_t hostid, bool in_isr) { // Disable MIE as per OHCI spec 5.3 OHCI_REG->interrupt_disable = OHCI_INT_MASTER_ENABLE_MASK; + // Start of frame (SOF) + if (int_status & OHCI_INT_SOF_MASK) { + OHCI_REG->interrupt_disable = OHCI_INT_SOF_MASK; + + bool re_enable_lists = false; + + for (size_t i = 0; i < ED_MAX; i++) { + ohci_ed_t* ed = hcd_dcache_uncached(&ohci_data.ed_pool[i]); + if (ed->w0.used && ed->w0.is_reclaiming) { + TU_ASSERT(ed->w0.skip == 1, ); + TU_ASSERT(ed->w0.ep_number != 0, ); + + // Reclaim orphaned TDs + uint32_t td_addr = ed->td_head.address & ~0x0F; + while (td_addr) { + if (!ed->w0.is_iso) { + ohci_gtd_t *gtd = (ohci_gtd_t*)_virt_addr((void*)(uintptr_t)td_addr); + gtd->used = 0; + } else { + // TODO: Free ITD once implemented + } + + if (td_addr == ed->td_tail) { + break; + } + td_addr = ((ohci_td_item_t*)_virt_addr((void*)(uintptr_t)td_addr))->next; + } + + ed->w0.is_reclaiming = 0; + ed->w0.used = 0; + ed->w0.skip = 0; + + re_enable_lists = true; + } + } + + if (re_enable_lists) { + // 5.2.7.1.2 Removing + // Reset current ED pointers and re-enable lists + // Once the next frame has started, the HcControlCurrentED or HcBulkCurrentED register should be adjusted so + // that it does not point to the Endpoint Descriptor being removed (for simplicity you may just write + // a zero to the register); + if (!(OHCI_REG->control & OHCI_CONTROL_LIST_CONTROL_ENABLE_MASK)) { + OHCI_REG->control_current_ed = 0; + OHCI_REG->control |= OHCI_CONTROL_LIST_CONTROL_ENABLE_MASK; + } + if (!(OHCI_REG->control & OHCI_CONTROL_LIST_BULK_ENABLE_MASK)) { + OHCI_REG->bulk_current_ed = 0; + OHCI_REG->control |= OHCI_CONTROL_LIST_BULK_ENABLE_MASK; + } + } + } + // Frame number overflow if (int_status & OHCI_INT_FRAME_OVERFLOW_MASK) { ohci_data.frame_number_hi++; diff --git a/src/portable/ohci/ohci.h b/src/portable/ohci/ohci.h index 84ae04b0f..c66954502 100644 --- a/src/portable/ohci/ohci.h +++ b/src/portable/ohci/ohci.h @@ -107,7 +107,8 @@ typedef union { // HCD: make use of 5 reserved bits uint32_t used : 1; uint32_t is_interrupt_xfer : 1; - uint32_t : 3; + uint32_t is_reclaiming : 1; + uint32_t : 2; }; uint32_t value; } ohci_ed_word0_t; -- cgit v1.3.1 From 53567226fce0e7be7e7b5e7a019d0f4d14064c33 Mon Sep 17 00:00:00 2001 From: Javid Khan Date: Mon, 17 Aug 2026 20:56:27 +0530 Subject: bound endpoint number in tu_bind_driver_to_ep_itf --- src/common/tusb_private.h | 2 +- src/device/usbd.c | 4 ++-- src/host/usbh.c | 3 ++- src/tusb.c | 3 ++- 4 files changed, 7 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/common/tusb_private.h b/src/common/tusb_private.h index 0bbc119fd..b91fc0608 100644 --- a/src/common/tusb_private.h +++ b/src/common/tusb_private.h @@ -63,7 +63,7 @@ TU_ATTR_ALWAYS_INLINE static inline bool tu_edpt_validate(const tusb_desc_endpoi // Bind drivers to all interfaces and endpoints in the provided configuration descriptor bool tu_bind_driver_to_ep_itf(uint8_t driver_id, uint8_t ep2drv[][2], uint8_t itf2drv[], uint8_t itf_max, - const uint8_t *p_desc, uint16_t desc_len); + uint8_t ep_max, const uint8_t *p_desc, uint16_t desc_len); // Claim an endpoint with provided mutex bool tu_edpt_claim(volatile uint8_t* ep_state, osal_mutex_t mutex); diff --git a/src/device/usbd.c b/src/device/usbd.c index f5c3046d6..5b20d0870 100644 --- a/src/device/usbd.c +++ b/src/device/usbd.c @@ -1281,8 +1281,8 @@ static bool process_set_config(uint8_t rhport, uint8_t cfg_num) { TU_LOG_USBD(" %s opened\r\n", driver->name); // bind found driver to all interfaces and endpoint within drv_len - TU_ASSERT(tu_bind_driver_to_ep_itf(drv_id, _usbd_dev.ep2drv, _usbd_dev.itf2drv, CFG_TUD_INTERFACE_MAX, p_desc, - drv_len)); + TU_ASSERT(tu_bind_driver_to_ep_itf(drv_id, _usbd_dev.ep2drv, _usbd_dev.itf2drv, CFG_TUD_INTERFACE_MAX, + CFG_TUD_ENDPPOINT_MAX, p_desc, drv_len)); p_desc += drv_len; // next Interface break; // exit driver find loop diff --git a/src/host/usbh.c b/src/host/usbh.c index e307bb5e5..cb4977dd2 100644 --- a/src/host/usbh.c +++ b/src/host/usbh.c @@ -2160,7 +2160,8 @@ static bool enum_parse_configuration_desc(uint8_t dev_addr, tusb_desc_configurat TU_LOG_USBH(" %s opened\r\n", driver->name); // bind found driver to all interfaces and endpoint within drv_len - tu_bind_driver_to_ep_itf(drv_id, dev->ep2drv, dev->itf2drv, CFG_TUH_INTERFACE_MAX, p_desc, drv_len); + tu_bind_driver_to_ep_itf(drv_id, dev->ep2drv, dev->itf2drv, CFG_TUH_INTERFACE_MAX, CFG_TUH_ENDPOINT_MAX, + p_desc, drv_len); p_desc += drv_len; // next Interface break; // exit driver find loop diff --git a/src/tusb.c b/src/tusb.c index 78ee7aeda..e1548e8c1 100644 --- a/src/tusb.c +++ b/src/tusb.c @@ -274,7 +274,7 @@ bool tu_edpt_validate(const tusb_desc_endpoint_t *desc_ep, tusb_speed_t speed) { #endif bool tu_bind_driver_to_ep_itf(uint8_t driver_id, uint8_t ep2drv[][2], uint8_t itf2drv[], uint8_t itf_max, - const uint8_t *p_desc, uint16_t desc_len) { + uint8_t ep_max, const uint8_t *p_desc, uint16_t desc_len) { const uint8_t *desc_end = p_desc + desc_len; while (tu_desc_in_bounds(p_desc, desc_end)) { const uint8_t desc_type = tu_desc_type(p_desc); @@ -283,6 +283,7 @@ bool tu_bind_driver_to_ep_itf(uint8_t driver_id, uint8_t ep2drv[][2], uint8_t it const uint8_t ep_addr = ((const tusb_desc_endpoint_t *)p_desc)->bEndpointAddress; const uint8_t ep_num = tu_edpt_number(ep_addr); const uint8_t ep_dir = tu_edpt_dir(ep_addr); + TU_ASSERT(ep_num < ep_max); ep2drv[ep_num][ep_dir] = driver_id; } else if (desc_type == TUSB_DESC_INTERFACE) { const tusb_desc_interface_t *desc_itf = (const tusb_desc_interface_t *)p_desc; -- cgit v1.3.1 From 9b05c706272c5344284f7f8b4b53b1aefe082968 Mon Sep 17 00:00:00 2001 From: Jerzy Kasenberg Date: Tue, 18 Aug 2026 15:37:18 +0200 Subject: UAC2: Add more terminal types This adds more standard terminal types that can be used in descriptors to improve end user experience when operating system can present more acurate image for audio device terminal. Signed-off-by: Jerzy Kasenberg --- src/class/audio/audio.h | 65 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) (limited to 'src') diff --git a/src/class/audio/audio.h b/src/class/audio/audio.h index 7981396c2..4a47bbe60 100644 --- a/src/class/audio/audio.h +++ b/src/class/audio/audio.h @@ -83,6 +83,71 @@ typedef enum { AUDIO_TERM_TYPE_OUT_LOW_FRQ_EFFECTS_SPEAKER = 0x0307, } audio_terminal_output_type_t; +/// 2.4 - Audio Class-Bi-directional Terminal Types UAC2 +typedef enum +{ + AUDIO_TERM_TYPE_BI_DIRECTORAL_UNDEFINED = 0x0400, + AUDIO_TERM_TYPE_BI_DIRECTORAL_HEADSET_HAND_HELD = 0x0401, + AUDIO_TERM_TYPE_BI_DIRECTORAL_HEADSET_MOUNTED = 0x0402, + AUDIO_TERM_TYPE_BI_DIRECTORAL_SPEAKERPHONE = 0x0403, + AUDIO_TERM_TYPE_BI_DIRECTORAL_SPEAKERPHONE_ECHO_SUPPRESS = 0x0404, + AUDIO_TERM_TYPE_BI_DIRECTORAL_SPEAKERPHONE_ECHO_CANCLE = 0x0405, +} audio_terminal_bi_directorial_type_t; + +/// 2.5 - Audio Class-Telephone Terminal Types UAC2 +typedef enum +{ + AUDIO_TERM_TYPE_TELEPHONE_UNDEFINED = 0x0500, + AUDIO_TERM_TYPE_TELEPHONE_PHONE_LINE = 0x0501, + AUDIO_TERM_TYPE_TELEPHONE_TELEPHONE = 0x0502, + AUDIO_TERM_TYPE_TELEPHONE_DOWN_LINE_PHONE = 0x0503, +} audio_terminal_telephony_type_t; + +/// 2.6 - Audio Class-External Types UAC2 +typedef enum +{ + AUDIO_TERM_TYPE_EXTERNAL_UNDEFINED = 0x0600, + AUDIO_TERM_TYPE_EXTERNAL_ANALOG_CONECTOR = 0x0601, + AUDIO_TERM_TYPE_EXTERNAL_DIGITAL_AUDIO = 0x0602, + AUDIO_TERM_TYPE_EXTERNAL_LINE_CONNECTOR = 0x0603, + AUDIO_TERM_TYPE_EXTERNAL_LEGACY_ADUIO_CONNECTOR = 0x0604, + AUDIO_TERM_TYPE_EXTERNAL_SPDIF_INTERFACE = 0x0605, + AUDIO_TERM_TYPE_EXTERNAL_1394_DA_STREAM = 0x0606, + AUDIO_TERM_TYPE_EXTERNAL_1394_DA_STREAM_SOUNDTRACK = 0x0607, + AUDIO_TERM_TYPE_EXTERNAL_ADAT_LIGHTPIPE = 0x0608, + AUDIO_TERM_TYPE_EXTERNAL_TDIF = 0x0609, + AUDIO_TERM_TYPE_EXTERNAL_MADI = 0x060A, +} audio_terminal_external_type_t; + +/// 2.7 - Audio Class-Embedded Types UAC2 +typedef enum +{ + AUDIO_TERM_TYPE_EMBEDDED_UNDEFINED = 0x0700, + AUDIO_TERM_TYPE_EMBEDDED_LEVEL_CALIBRATION_NOISE_SOURCE = 0x0701, + AUDIO_TERM_TYPE_EMBEDDED_EQUALIZATION_NOISE = 0x0702, + AUDIO_TERM_TYPE_EMBEDDED_CD_PLAYER = 0x0703, + AUDIO_TERM_TYPE_EMBEDDED_DAT = 0x0704, + AUDIO_TERM_TYPE_EMBEDDED_DCC = 0x0705, + AUDIO_TERM_TYPE_EMBEDDED_COMPRESSED_AUDIO_PLAYER = 0x0706, + AUDIO_TERM_TYPE_EMBEDDED_ANALOG_TAPE = 0x0707, + AUDIO_TERM_TYPE_EMBEDDED_PHONOGRAPH = 0x0708, + AUDIO_TERM_TYPE_EMBEDDED_VCR_AUDIO = 0x0709, + AUDIO_TERM_TYPE_EMBEDDED_VIDEO_DISC_AUDIO = 0x070A, + AUDIO_TERM_TYPE_EMBEDDED_DVD_AUDIO = 0x070B, + AUDIO_TERM_TYPE_EMBEDDED_TV_TUNER_AUDIO = 0x070C, + AUDIO_TERM_TYPE_EMBEDDED_SATELITE_RECEIVER_AUDIO = 0x070D, + AUDIO_TERM_TYPE_EMBEDDED_CABLE_TUNER_AUDIO = 0x070E, + AUDIO_TERM_TYPE_EMBEDDED_DSS_AUDIO = 0x070F, + AUDIO_TERM_TYPE_EMBEDDED_RADIO_RECEIVER = 0x0710, + AUDIO_TERM_TYPE_EMBEDDED_RADIO_TRANSMITTER = 0x0711, + AUDIO_TERM_TYPE_EMBEDDED_MULTI_TRACK_RECORDER = 0x0712, + AUDIO_TERM_TYPE_EMBEDDED_SYNTHESIZER = 0x0713, + AUDIO_TERM_TYPE_EMBEDDED_PIANO = 0x0714, + AUDIO_TERM_TYPE_EMBEDDED_GUITAR = 0x0715, + AUDIO_TERM_TYPE_EMBEDDED_DRUMS = 0x0716, + AUDIO_TERM_TYPE_EMBEDDED_OTHER_MUSICAL_INSTRUMENT = 0x0717, +} audio_terminal_embedded_type_t; + /// Rest is yet to be implemented //--------------------------------------------------------------------+ -- cgit v1.3.1 From 073942589355676980ba401cb88c0eb9f065e468 Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 17 Aug 2026 01:01:37 +0700 Subject: usbd: split bus reset into start/end edge events A driver that can see reset signalling begin has no way to say so: the only event carries the negotiated speed, which does not exist until the reset ends. On ChipIdea that left the stack believing it was still configured for the whole reset window - 3 ms at minimum, tens of milliseconds in practice - while the controller had already torn its endpoints down, so a class driver writing in that window primed a disabled endpoint over a zeroed queue head. Add DCD_EVENT_BUS_RESET_START for the leading edge and rename the existing event to DCD_EVENT_BUS_RESET_END, keeping DCD_EVENT_BUS_RESET as an alias. START is optional and END stays self-sufficient, so every other driver and the unit tests are untouched. --- src/device/dcd.h | 26 +++++++++++++++++--------- src/device/usbd.c | 12 ++++++++++-- 2 files changed, 27 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/device/dcd.h b/src/device/dcd.h index f005e9620..a4006ae0c 100644 --- a/src/device/dcd.h +++ b/src/device/dcd.h @@ -20,19 +20,27 @@ // MACRO CONSTANT TYPEDEF PROTYPES //--------------------------------------------------------------------+ +// Bus reset is reported as two edges. BUS_RESET_START is optional: a controller that +// cannot tell the edges apart emits only BUS_RESET_END, which stays self-sufficient (it +// performs the full teardown with or without a preceding START). Emit START when reset +// signaling is detected - the link is unusable and the speed is not negotiated yet - so +// the stack stops using endpoints immediately instead of at the end of the reset. typedef enum { - DCD_EVENT_INVALID = 0, // 0 - DCD_EVENT_BUS_RESET, // 1 - DCD_EVENT_UNPLUGGED, // 2 - DCD_EVENT_SOF, // 3 - DCD_EVENT_SUSPEND, // 4 TODO LPM Sleep L1 support - DCD_EVENT_RESUME, // 5 - DCD_EVENT_SETUP_RECEIVED, // 6 - DCD_EVENT_XFER_COMPLETE, // 7 - USBD_EVENT_FUNC_CALL, // 8 Not an DCD event, just a convenient way to defer ISR function + DCD_EVENT_INVALID = 0, // 0 + DCD_EVENT_BUS_RESET_START, // 1 + DCD_EVENT_BUS_RESET_END, // 2 with negotiated speed + DCD_EVENT_UNPLUGGED, // 3 + DCD_EVENT_SOF, // 4 + DCD_EVENT_SUSPEND, // 5 TODO LPM Sleep L1 support + DCD_EVENT_RESUME, // 6 + DCD_EVENT_SETUP_RECEIVED, // 7 + DCD_EVENT_XFER_COMPLETE, // 8 + USBD_EVENT_FUNC_CALL, // 9 Not an DCD event, just a convenient way to defer ISR function DCD_EVENT_COUNT } dcd_eventid_t; +#define DCD_EVENT_BUS_RESET DCD_EVENT_BUS_RESET_END // backward compatibility + typedef struct TU_ATTR_ALIGNED(4) { uint8_t rhport; uint8_t event_id; diff --git a/src/device/usbd.c b/src/device/usbd.c index f5c3046d6..7215a8dc5 100644 --- a/src/device/usbd.c +++ b/src/device/usbd.c @@ -456,7 +456,8 @@ TU_ATTR_WEAK bool dcd_configure(uint8_t rhport, uint32_t cfg_id, const void* cfg #if CFG_TUSB_DEBUG >= CFG_TUD_LOG_LEVEL static char const *const _usbd_event_str[DCD_EVENT_COUNT] = { "Invalid", - "Bus Reset", + "Bus Reset Start", + "Bus Reset End", "Unplugged", "SOF", "Suspend", @@ -697,8 +698,15 @@ void tud_task_ext(uint32_t timeout_ms, bool in_isr) { #endif switch (event.event_id) { - case DCD_EVENT_BUS_RESET: + case DCD_EVENT_BUS_RESET_START: + TU_LOG_USBD("\r\n"); + usbd_reset(event.rhport); + break; + + case DCD_EVENT_BUS_RESET_END: TU_LOG_USBD(": %s Speed\r\n", tu_str_speed[event.bus_reset.speed]); + // TODO a DCD that reports both edges pays for two teardowns: track a per-rhport + // "start seen" flag and skip this reset, keeping it for the single-event DCDs. usbd_reset(event.rhport); _usbd_dev.speed = event.bus_reset.speed; break; -- cgit v1.3.1 From 2fda873fa5f6ef0c893f4f138b5c54e49c24e0a9 Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 17 Aug 2026 01:01:54 +0700 Subject: dcd(ci_hs): rework bus reset handling and bound the register waits A bus reset was detected only from the port change that ends it, which is late: the manual asks the DCD to clear the endpoint semaphores, cancel every prime and free the dTDs while the reset is still being driven. Enable the reset interrupt and do all of that there, in the manual's order (IMXRT1060RM 42.5.6.2.1, p.2394), including the two steps that were missing - confirming the port is still being reset, and freeing the dTDs. A failed check means the cleanup arrived late and the controller may be in an undefined state, so the manual's remedy is carried out rather than noted: a controller reset, followed by the full re-initialisation it then requires, since the reset detaches the device. The port change that ends the reset is left with what the manual gives it, the negotiated speed, which the new BUS_RESET_END event carries. A port change is classified by the interrupt that preceded it: a suspend raises no port change of its own, the resume that ends it does. Every unbounded register spin is now bounded. They waited on bits the hardware clears within a frame, but each could hang an interrupt handler outright on a controller that had stopped responding. The endpoint flush follows all three steps of IMXRT1060RM 42.5.6.6.5 (p.2413), repeating a flush the controller refuses while a packet is in progress - previously reported as success. EP0 setup handling is hardened alongside: the payload is copied out of the queue head through the volatile qualifier before ENDPTSETUPSTAT is cleared, since that clear releases the setup lockout and a back-to-back setup can overwrite the buffer immediately after, and C orders volatile accesses only against each other, so a plain memcpy may legally be sunk past the store. There is deliberately no unplug detection. IMXRT1060RM 42.7.31 (p.2470) states a zero Current Connect Status means the device "did not attach successfully or was forcibly disconnected by the software writing a zero to the Run bit ... It does not state the device being disconnected or suspended", so a cable pull raises no port change at all; VBUS via OTGSC is the manual's disconnect indicator and is board dependent. Verified on mimxrt1064_evk: 30 forced bus resets each re-enumerating at high speed with no descriptor errors, plus repeated full usbtest batteries at 30/30 across the series. --- src/portable/chipidea/ci_hs/ci_hs_type.h | 8 + src/portable/chipidea/ci_hs/dcd_ci_hs.c | 247 +++++++++++++++++++++---------- 2 files changed, 180 insertions(+), 75 deletions(-) (limited to 'src') diff --git a/src/portable/chipidea/ci_hs/ci_hs_type.h b/src/portable/chipidea/ci_hs/ci_hs_type.h index b209c7545..5baa14821 100644 --- a/src/portable/chipidea/ci_hs/ci_hs_type.h +++ b/src/portable/chipidea/ci_hs/ci_hs_type.h @@ -36,10 +36,18 @@ enum { PORTSC1_CURRENT_CONNECT_STATUS = TU_BIT(0), PORTSC1_FORCE_PORT_RESUME = TU_BIT(6), PORTSC1_SUSPEND = TU_BIT(7), + PORTSC1_PORT_RESET = TU_BIT(8), // read-only in device mode: a reset is being driven PORTSC1_FORCE_FULL_SPEED = TU_BIT(24), PORTSC1_PORT_SPEED = TU_BIT(26) | TU_BIT(27) }; +// PORTSC1 PSPD field values, once shifted down by PORTSC1_PORT_SPEED_POS. 3 is undefined. +enum { + PORTSC1_PORT_SPEED_FULL = 0, + PORTSC1_PORT_SPEED_LOW = 1, + PORTSC1_PORT_SPEED_HIGH = 2, +}; + // OTGSC enum { OTGSC_VBUS_DISCHARGE = TU_BIT(0), diff --git a/src/portable/chipidea/ci_hs/dcd_ci_hs.c b/src/portable/chipidea/ci_hs/dcd_ci_hs.c index 8c08c6bd5..6ab28e0be 100644 --- a/src/portable/chipidea/ci_hs/dcd_ci_hs.c +++ b/src/portable/chipidea/ci_hs/dcd_ci_hs.c @@ -154,6 +154,14 @@ TU_VERIFY_STATIC(sizeof(dcd_qhd_t) == 64, "size is not correct"); #define QTD_NEXT_INVALID 0x01 +// Bounded spin for register waits. The longest legitimate wait is a flush held off by a packet +// already in progress: ~50 us for a full-speed 64-byte packet, a low thousands of dependent +// register reads, so healthy hardware never approaches this bound. Exceeding it means the +// controller has stopped responding, and the spin then only serves to keep an ISR (or an +// IRQ-masked caller) from hanging outright - the 3 ms reset-cleanup window of IMXRT1060RM 42.5.6.2.1 (p.2394) +// is already unreachable in that state, and the manual's remedy there is a controller reset. +#define CI_HS_BUSY_SPIN 10000u + typedef struct { // Must be at 2K alignment // Each endpoint with direction (IN/OUT) occupies a queue head @@ -164,6 +172,17 @@ typedef struct { CFG_TUD_MEM_SECTION TU_ATTR_ALIGNED(2048) static dcd_data_t _dcd_data; +// What the next Port Change Detect will be. Each one is preceded by the interrupt that causes it: +// a reset interrupt for the end of a bus reset - where the speed first becomes final - or a +// suspend interrupt for the resume that ends the suspend. A suspend itself raises no port change, +// which is why there is no such value here. Indexed by rhport, which is 0 or 1 on every ci_hs +// variant (NOT the controller count: mcx/rw61x map rhport 1 to controller 0). +enum { + PORT_CHANGE_REASON_RESET = 0, + PORT_CHANGE_REASON_RESUME = 1, +}; +static volatile uint8_t _port_change_reason[2]; + //--------------------------------------------------------------------+ // Prototypes and Helper Functions //--------------------------------------------------------------------+ @@ -172,12 +191,37 @@ TU_ATTR_ALWAYS_INLINE static inline uint8_t ci_ep_count(const ci_hs_regs_t *dcd_ return dcd_reg->DCCPARAMS & DCCPARAMS_DEN_MASK; } +static bool controller_reset(uint8_t rhport); + //--------------------------------------------------------------------+ // Controller API //--------------------------------------------------------------------+ -/// follows LPC43xx User Manual 23.10.3 -static void bus_reset(uint8_t rhport) { +// Flush endpoint buffers, following IMXRT1060RM 42.5.6.6.5 Flushing/De-priming an Endpoint +// (p.2413): write ENDPTFLUSH, wait for the controller +// to acknowledge, then confirm ENDPTSTAT went to zero. The controller refuses the flush when a +// packet is in progress, and the manual requires the procedure be repeated until it takes. +// Callers proceed regardless of the result; the bound only prevents an ISR-context hang on dead +// hardware. +static bool flush_endpoints(ci_hs_regs_t *dcd_reg, uint32_t mask) { + uint32_t guard = CI_HS_BUSY_SPIN; + do { + dcd_reg->ENDPTFLUSH = mask; + while (dcd_reg->ENDPTFLUSH & mask) { + if (!guard--) { + return false; + } + } + } while ((dcd_reg->ENDPTSTAT & mask) && guard--); + + return !(dcd_reg->ENDPTSTAT & mask); +} + +/// Everything the manual asks of the DCD when a reset is detected, in its order: clear the setup +/// and completion semaphores, cancel every prime, check the reset is still being driven, and free +/// the dTDs. All of it belongs inside the reset window (IMXRT1060RM 42.5.6.2.1, p.2394); nothing +/// is left for the port change that ends the reset, which only reports the negotiated speed. +static void bus_reset_begin(uint8_t rhport) { ci_hs_regs_t *dcd_reg = CI_HS_REG(rhport); // The reset value for all endpoint types is the control endpoint. If one endpoint @@ -193,17 +237,24 @@ static void bus_reset(uint8_t rhport) { //------------- Clear All Registers -------------// dcd_reg->ENDPTNAK = dcd_reg->ENDPTNAK; dcd_reg->ENDPTNAKEN = 0; - dcd_reg->USBSTS = dcd_reg->USBSTS; dcd_reg->ENDPTSETUPSTAT = dcd_reg->ENDPTSETUPSTAT; dcd_reg->ENDPTCOMPLETE = dcd_reg->ENDPTCOMPLETE; - while (dcd_reg->ENDPTPRIME) {} - dcd_reg->ENDPTFLUSH = 0xFFFFFFFF; - while (dcd_reg->ENDPTFLUSH) {} - - // read reset bit in portsc + uint32_t guard = CI_HS_BUSY_SPIN; + while (dcd_reg->ENDPTPRIME && guard--) {} + dcd_reg->ENDPTFLUSH = 0xFFFFFFFFUL; + + // All of the above must land while the reset is still being driven - it lasts at least 3 ms. + // Arriving late leaves the controller in an undefined state, and the manual's remedy is to + // hardware-reset it. That clears Run/Stop, so the device detaches and the host will drive a + // fresh reset and enumeration - which is why nothing below this point is worth doing here. + if (!(dcd_reg->PORTSC1 & PORTSC1_PORT_RESET)) { + TU_LOG1("ci_hs: reset cleanup ran past the end of the reset, resetting controller\r\n"); + controller_reset(rhport); + return; // the controller detached; the host's next reset redoes everything below + } - //------------- Queue Head & Queue TD -------------// + //------------- Free all allocated dTDs: the controller will not execute them again -------------// tu_memclr(&_dcd_data, sizeof(dcd_data_t)); //------------- Set up Control Endpoints (0 OUT, 1 IN) -------------// @@ -216,21 +267,19 @@ static void bus_reset(uint8_t rhport) { dcd_dcache_clean_invalidate(&_dcd_data, sizeof(dcd_data_t)); } -bool dcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) { - (void)rh_init; - tu_memclr(&_dcd_data, sizeof(dcd_data_t)); - +/// Reset the controller and bring it back up in device mode. Also the manual's remedy when the +/// reset cleanup misses its window: the controller reset clears Run/Stop and detaches the device, +/// so it must be re-initialised completely afterwards (IMXRT1060RM 42.5.6.2.1, p.2394). +static bool controller_reset(uint8_t rhport) { ci_hs_regs_t *dcd_reg = CI_HS_REG(rhport); - TU_ASSERT(ci_ep_count(dcd_reg) <= TUP_DCD_ENDPOINT_MAX); - - #if TU_CHECK_MCU(OPT_MCU_HPM) - usb_phy_init((USB_Type *)dcd_reg, false); - #endif + tu_memclr(&_dcd_data, sizeof(dcd_data_t)); // Reset controller dcd_reg->USBCMD |= USBCMD_RESET; - while (dcd_reg->USBCMD & USBCMD_RESET) {} + uint32_t guard = CI_HS_BUSY_SPIN; + while ((dcd_reg->USBCMD & USBCMD_RESET) && guard--) {} + TU_VERIFY(!(dcd_reg->USBCMD & USBCMD_RESET)); // reached from the ISR too, so never halt here // Set mode to device, must be set immediately after reset uint32_t usbmode = dcd_reg->USBMODE & ~USBMOD_CM_MASK; @@ -257,9 +306,11 @@ bool dcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) { dcd_dcache_clean_invalidate(&_dcd_data, sizeof(dcd_data_t)); + _port_change_reason[rhport] = PORT_CHANGE_REASON_RESET; + dcd_reg->ENDPTLISTADDR = (uint32_t)_dcd_data.qhd; // Endpoint List Address has to be 2K alignment dcd_reg->USBSTS = dcd_reg->USBSTS; - dcd_reg->USBINTR = INTR_USB | INTR_ERROR | INTR_PORT_CHANGE | INTR_SUSPEND; + dcd_reg->USBINTR = INTR_USB | INTR_ERROR | INTR_PORT_CHANGE | INTR_RESET | INTR_SUSPEND; uint32_t usbcmd = dcd_reg->USBCMD; usbcmd &= ~USBCMD_INTR_THRESHOLD_MASK; // Interrupt Threshold Interval = 0 @@ -270,8 +321,22 @@ bool dcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) { return true; } +bool dcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) { + (void)rh_init; + ci_hs_regs_t *dcd_reg = CI_HS_REG(rhport); + + TU_ASSERT(ci_ep_count(dcd_reg) <= TUP_DCD_ENDPOINT_MAX); + + #if TU_CHECK_MCU(OPT_MCU_HPM) + usb_phy_init((USB_Type *)dcd_reg, false); + #endif + + return controller_reset(rhport); +} + bool dcd_deinit(uint8_t rhport) { ci_hs_regs_t* dcd_reg = CI_HS_REG(rhport); + _port_change_reason[rhport] = PORT_CHANGE_REASON_RESET; // disable all interrupt dcd_reg->USBINTR = 0; @@ -280,9 +345,9 @@ bool dcd_deinit(uint8_t rhport) { dcd_reg->USBCMD &= ~USBCMD_RUN_STOP; // flush all endpoints - while (dcd_reg->ENDPTPRIME) {} - dcd_reg->ENDPTFLUSH = 0xFFFFFFFF; - while (dcd_reg->ENDPTFLUSH) {} + uint32_t guard = CI_HS_BUSY_SPIN; + while (dcd_reg->ENDPTPRIME && guard--) {} + flush_endpoints(dcd_reg, 0xFFFFFFFF); return true; } @@ -296,11 +361,13 @@ void dcd_int_disable(uint8_t rhport) { } void dcd_set_address(uint8_t rhport, uint8_t dev_addr) { - // Response with status first before changing device address - dcd_edpt_xfer(rhport, tu_edpt_addr(0, TUSB_DIR_IN), NULL, 0, false); - - ci_hs_regs_t *dcd_reg = CI_HS_REG(rhport); - dcd_reg->DEVICEADDR = (dev_addr << 25) | TU_BIT(24); + // Response with status first before changing device address. A refused prime means a new + // setup superseded this transfer; staging an address whose ACK will never arrive would + // leave the device answering on it, so only arm the address when the status went out. + if (dcd_edpt_xfer(rhport, tu_edpt_addr(0, TUSB_DIR_IN), NULL, 0, false)) { + ci_hs_regs_t *dcd_reg = CI_HS_REG(rhport); + dcd_reg->DEVICEADDR = (dev_addr << 25) | TU_BIT(24); + } } void dcd_remote_wakeup(uint8_t rhport) { @@ -468,9 +535,7 @@ bool dcd_edpt_iso_activate(uint8_t rhport, const tusb_desc_endpoint_t *desc_ep) // dcd_dcache_clean_invalidate(&_dcd_data, sizeof(dcd_data_t)); // Flush EP - const uint32_t flush_mask = TU_BIT(epnum + (dir ? 16 : 0)); - dcd_reg->ENDPTFLUSH = flush_mask; - while (dcd_reg->ENDPTFLUSH & flush_mask) {} + flush_endpoints(dcd_reg, TU_BIT(epnum + (dir ? 16 : 0))); // disable to change max packet size ep_ctrl_clear(endptctrl, dir, ENDPTCTRL_ENABLE); @@ -496,7 +561,7 @@ void dcd_edpt_close_all(uint8_t rhport) { } } -static void qhd_start_xfer(uint8_t rhport, uint8_t epnum, uint8_t dir) { +static bool qhd_start_xfer(uint8_t rhport, uint8_t epnum, uint8_t dir) { ci_hs_regs_t *dcd_reg = CI_HS_REG(rhport); dcd_qhd_t *p_qhd = &_dcd_data.qhd[epnum][dir]; dcd_qtd_t *p_qtd = &_dcd_data.qtd[epnum][dir]; @@ -509,13 +574,22 @@ static void qhd_start_xfer(uint8_t rhport, uint8_t epnum, uint8_t dir) { dcd_dcache_clean_invalidate(&_dcd_data, sizeof(dcd_data_t)); if (epnum == 0) { - // follows UM 24.10.8.1.1 Setup packet handling using setup lockout mechanism - // wait until ENDPTSETUPSTAT before priming data/status in response TODO add time out - while (dcd_reg->ENDPTSETUPSTAT & TU_BIT(0)) {} + // Setup lockout (IMXRT1060RM 42.5.6.4.2.1 Setup Phase, p.2403): never prime EP0 while a new + // SETUP is pending. The ISR + // normally consumes ENDPTSETUPSTAT quickly; if the guard trips, fail the transfer so usbd + // releases the endpoint (a pending SETUP supersedes this response anyway; without one, usbd + // stalls EP0 and the host recovers with a fresh control transfer). + uint32_t guard = CI_HS_BUSY_SPIN; + while (dcd_reg->ENDPTSETUPSTAT & TU_BIT(0)) { + if (!guard--) { + return false; + } + } } // start transfer dcd_reg->ENDPTPRIME = TU_BIT(epnum + (dir ? 16 : 0)); + return true; } bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t *buffer, uint16_t total_bytes, bool is_isr) { @@ -531,9 +605,7 @@ bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t *buffer, uint16_t to // Start qhd transfer p_qhd->ff = NULL; - qhd_start_xfer(rhport, epnum, dir); - - return true; + return qhd_start_xfer(rhport, epnum, dir); } #if !CFG_TUD_MEM_DCACHE_ENABLE @@ -584,9 +656,7 @@ bool dcd_edpt_xfer_fifo(uint8_t rhport, uint8_t ep_addr, tu_fifo_t *ff, uint16_t // Start qhd transfer p_qhd->ff = ff; - qhd_start_xfer(rhport, epnum, dir); - - return true; + return qhd_start_xfer(rhport, epnum, dir); } #endif @@ -634,43 +704,43 @@ void dcd_int_handler(uint8_t rhport) { return; } - // Set if the port controller enters the full or high-speed operational state. - // either from Bus Reset or Suspended state - if (int_status & INTR_PORT_CHANGE) { - // TU_LOG2("PortChange %08lx\r\n", dcd_reg->PORTSC1); - - // Reset interrupt is not enabled, we manually check if Port Change is due - // to connection / disconnection - if (dcd_reg->USBSTS & INTR_RESET) { - dcd_reg->USBSTS = INTR_RESET; - - if (dcd_reg->PORTSC1 & PORTSC1_CURRENT_CONNECT_STATUS) { - const uint32_t speed = (dcd_reg->PORTSC1 & PORTSC1_PORT_SPEED) >> PORTSC1_PORT_SPEED_POS; - bus_reset(rhport); - dcd_event_bus_reset(rhport, (tusb_speed_t)speed, true); - } else { - dcd_event_bus_signal(rhport, DCD_EVENT_UNPLUGGED, true); - } - } else { - // Triggered by resuming from suspended state - if (!(dcd_reg->PORTSC1 & PORTSC1_SUSPEND)) { - dcd_event_bus_signal(rhport, DCD_EVENT_RESUME, true); - } - } - } + const uint8_t pci_reason = _port_change_reason[rhport]; // save current pci_reason if (int_status & INTR_SUSPEND) { - // TU_LOG2("Suspend %08lx\r\n", dcd_reg->PORTSC1); + _port_change_reason[rhport] = PORT_CHANGE_REASON_RESUME; // next PCI is resume + dcd_event_bus_signal(rhport, DCD_EVENT_SUSPEND, true); + } - if (dcd_reg->PORTSC1 & PORTSC1_SUSPEND) { - // Note: Host may delay more than 3 ms before and/or after bus reset before doing enumeration. - // Skip suspend event if we are not addressed - if ((dcd_reg->DEVICEADDR >> 25) & 0x0f) { - dcd_event_bus_signal(rhport, DCD_EVENT_SUSPEND, true); - } + // USB Reset Received: register cleanup runs here within the reset window (IMXRT1060RM 42.5.6.2.1, p.2394) + // and BUS_RESET_START fires now; BUS_RESET_END, with the final speed, is triggered later by PCI. + if (int_status & INTR_RESET) { + _port_change_reason[rhport] = PORT_CHANGE_REASON_RESET; + bus_reset_begin(rhport); + dcd_event_bus_signal(rhport, DCD_EVENT_BUS_RESET_START, true); + } + + // Port entered the full/high-speed operational state: the end of a bus reset, or a resume. + if (int_status & INTR_PORT_CHANGE) { + if (pci_reason == PORT_CHANGE_REASON_RESUME) { + dcd_event_bus_signal(rhport, DCD_EVENT_RESUME, true); + } else { + // the undefined encoding falls back to full speed + const uint32_t pspd = (dcd_reg->PORTSC1 & PORTSC1_PORT_SPEED) >> PORTSC1_PORT_SPEED_POS; + const tusb_speed_t speed = (pspd == PORTSC1_PORT_SPEED_LOW) ? TUSB_SPEED_LOW : + (pspd == PORTSC1_PORT_SPEED_HIGH) ? TUSB_SPEED_HIGH : TUSB_SPEED_FULL; + dcd_event_bus_reset(rhport, speed, true); + // This reset is over, so the next port change is a resume. Leaving it at RESET instead would + // dispatch every later resume as another end-of-reset, clearing the queue heads mid-session. + _port_change_reason[rhport] = PORT_CHANGE_REASON_RESUME; } } + // No unplug detection yet, by the manual rather than by omission: IMXRT1060RM 42.7.31 (p.2470) says a zero + // Current Connect Status means the device "did not attach successfully or was forcibly + // disconnected by the software writing a zero to the Run bit ... It does not state the device + // being disconnected or suspended", so a cable pull raises no port change at all. VBUS via + // OTGSC BSV is the manual's disconnect indicator, and it is board dependent. + if (int_status & INTR_USB) { // Make sure we read the latest version of _dcd_data. dcd_dcache_clean_invalidate(&_dcd_data, sizeof(dcd_data_t)); @@ -678,7 +748,7 @@ void dcd_int_handler(uint8_t rhport) { const uint32_t edpt_complete = dcd_reg->ENDPTCOMPLETE; dcd_reg->ENDPTCOMPLETE = edpt_complete; // acknowledge - // 23.10.12.3 Failed QTD also get ENDPTCOMPLETE set + // 42.5.6.6.4 Transfer Completion (p.2413): a failed dTD also sets ENDPTCOMPLETE // nothing to do, we will submit xfer as error to usbd // if (int_status & INTR_ERROR) { } @@ -694,12 +764,39 @@ void dcd_int_handler(uint8_t rhport) { } // Set up Received - // 23.10.10.2 Operational model for setup transfers + // 42.5.6.4.2 Control Endpoint Operation Model (p.2403) // Must be after normal transfer complete since it is possible to have both previous control status + new setup // in the same frame and we should handle previous status first. if (dcd_reg->ENDPTSETUPSTAT) { + // 42.5.6.4.2.1 Setup Phase (p.2403) steps 1-2: duplicate the setup payload BEFORE clearing + // ENDPTSETUPSTAT - + // the clear releases the setup lockout and a back-to-back SETUP (usbtest case 10) can + // overwrite the queue-head buffer immediately after. The copy is read through the volatile + // qualifier rather than memcpy'd because C orders volatile accesses only against each + // other: a plain copy may legally be sunk past the lockout-releasing store below. + union { + tusb_control_request_t request; + uint8_t byte[8]; + } setup; + const volatile uint8_t *setup_src = (const volatile uint8_t *)&_dcd_data.qhd[0][0].setup_request; + for (uint8_t i = 0; i < sizeof(setup.request); i++) { + setup.byte[i] = setup_src[i]; + } dcd_reg->ENDPTSETUPSTAT = dcd_reg->ENDPTSETUPSTAT; - dcd_event_setup_received(rhport, (uint8_t *)(uintptr_t)&_dcd_data.qhd[0][0].setup_request, true); + + // Retire a status/handshake phase left primed by the previous control sequence + // (IMXRT1060RM 42.5.6.4.2.1, p.2403), which would otherwise retire the response the task is about to + // prime for this setup. Skipped when EP0 has nothing primed or priming, since the manual + // does not want the flush wait in an interrupt handler when it has nothing to do. + // One volatile read per statement: C leaves their order unspecified within a single + // expression, which IAR rejects outright (Pa082). + const uint32_t ep0_mask = TU_BIT(0) | TU_BIT(16); + const uint32_t ep0_stat = dcd_reg->ENDPTSTAT; + const uint32_t ep0_prime = dcd_reg->ENDPTPRIME; + if ((ep0_stat | ep0_prime) & ep0_mask) { + flush_endpoints(dcd_reg, ep0_mask); + } + dcd_event_setup_received(rhport, setup.byte, true); } } -- cgit v1.3.1 From a85a6afc6d98726f5edfb2d7606527c87c963dba Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 17 Aug 2026 01:02:07 +0700 Subject: usbd: handle a refused transfer without halting, and report it A refused transfer is a recoverable condition - a new setup superseding a control response, for instance - rather than a bug, but every failure path treated it as one. TU_ASSERT carries TU_BREAKPOINT, which is gated on a debugger being attached rather than on CFG_TUSB_DEBUG, so on a rig where a probe is always attached it halted the CPU even in release builds. Use TU_VERIFY on the control transfer paths, including the multi-packet data stage continuation, and drop the breakpoint from the endpoint transfer failure arm, which already marks the endpoint ready again so the next transfer can proceed. The result of usbd_control_xfer_cb() was separately dropped on the floor, leaving EP0 neither armed nor stalled and nothing recorded. It is logged now, and deliberately not stalled: a DCD refuses an EP0 prime when a newer setup is already latched, and EP0 stalls are cleared by hardware when that setup arrives, so a stall issued here would land after the auto-clear and stall the transfer that superseded this one. The pending setup re-drives EP0 by itself. --- src/device/usbd.c | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/device/usbd.c b/src/device/usbd.c index 7215a8dc5..e84d72fa4 100644 --- a/src/device/usbd.c +++ b/src/device/usbd.c @@ -757,7 +757,14 @@ void tud_task_ext(uint32_t timeout_ms, bool in_isr) { _usbd_dev.ep_status[epnum][ep_dir] &= (uint8_t) ~(TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED); if (0 == epnum) { - usbd_control_xfer_cb(event.rhport, ep_addr, (xfer_result_t) event.xfer_complete.result, event.xfer_complete.len); + // Not stalled on failure: a DCD refuses an EP0 prime when a newer setup is already + // latched, and EP0 stalls are cleared by hardware when that setup arrives - so a stall + // issued here lands after the auto-clear and would stall the transfer that superseded + // this one. The pending setup re-drives EP0 by itself. + if (!usbd_control_xfer_cb(event.rhport, ep_addr, (xfer_result_t) event.xfer_complete.result, + event.xfer_complete.len)) { + TU_LOG_USBD(" Control stage not continued\r\n"); + } } else { usbd_class_driver_t const* driver = get_driver(_usbd_dev.ep2drv[epnum][ep_dir]); TU_ASSERT(driver,); @@ -875,10 +882,10 @@ bool tud_control_xfer(uint8_t rhport, const tusb_control_request_t* request, voi if (ctrl_xfer->data_len > 0U) { TU_ASSERT(buffer); } - TU_ASSERT(data_stage_xact(rhport)); + TU_VERIFY(data_stage_xact(rhport)); } else { // wLength == 0: Status stage is always IN per USB 2.0 §9.3.1 - TU_ASSERT(status_stage_xact(rhport, TU_EP0_IN)); + TU_VERIFY(status_stage_xact(rhport, TU_EP0_IN)); } return true; @@ -929,7 +936,7 @@ static bool usbd_control_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t } if (is_ok) { - TU_ASSERT(status_stage_xact(rhport, ep_status)); + TU_VERIFY(status_stage_xact(rhport, ep_status)); } else { // Stall both IN and OUT control endpoint dcd_edpt_stall(rhport, TU_EP0_OUT); @@ -937,7 +944,7 @@ static bool usbd_control_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t } } else { // More data to transfer - TU_ASSERT(data_stage_xact(rhport)); + TU_VERIFY(data_stage_xact(rhport)); } return true; @@ -1608,10 +1615,12 @@ bool usbd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t* buffer, uint16_t t if (dcd_edpt_xfer(rhport, ep_addr, buffer, total_bytes, is_isr)) { return true; } else { - // DCD error, mark endpoint as ready to allow next transfer + // Driver refused the transfer, mark endpoint as ready to allow next transfer. This is a + // recoverable condition (e.g. a new setup superseding a control response), not a bug, so + // do not break into the debugger - TU_BREAKPOINT() halts the CPU whenever a probe is + // attached, which on a test rig is always. _usbd_dev.ep_status[epnum][dir] &= (uint8_t) ~(TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED); TU_LOG_USBD("FAILED\r\n"); - TU_BREAKPOINT(); return false; } } -- cgit v1.3.1 From 5baf5925c8b6a033de85e3b5537ea879de75e3da Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 17 Aug 2026 01:02:22 +0700 Subject: dcd(ip3511): fix DEVCMDSTAT write-1-to-clear handling and EP0 setup races DEVCMDSTAT mixes read/write fields with write-1-to-clear latches, so a blind read-modify-write writes a pending latch back as a one and silently clears it - a setup consumed that way strands EP0. Mask the latches on every update. The setup path follows the manual's order: acknowledge the latch, then read the payload. The EP0 IN interrupt is cleared along with EP0 OUT, as the control endpoint flowchart requires - a control IN completion latched before the setup must not reach usbd after it, where it would be applied to the request the setup just started and arm its status stage early. The payload is copied a byte at a time out of a buffer now declared volatile: the controller DMAs a new setup packet into it as soon as the latch is cleared, and C orders volatile accesses only against each other, so gcc sinks a plain memcpy below the guard read that follows at -O2 and -O3 - leaving only -Os, the level CI builds, correct. --- src/portable/nxp/lpc_ip3511/dcd_lpc_ip3511.c | 106 +++++++++++++++++++++------ 1 file changed, 85 insertions(+), 21 deletions(-) (limited to 'src') diff --git a/src/portable/nxp/lpc_ip3511/dcd_lpc_ip3511.c b/src/portable/nxp/lpc_ip3511/dcd_lpc_ip3511.c index d5b03e4b1..42f6750b1 100644 --- a/src/portable/nxp/lpc_ip3511/dcd_lpc_ip3511.c +++ b/src/portable/nxp/lpc_ip3511/dcd_lpc_ip3511.c @@ -87,6 +87,10 @@ enum { DEVCMDSTAT_SUSPEND_CHANGE_MASK = TU_BIT(25), DEVCMDSTAT_RESET_CHANGE_MASK = TU_BIT(26), DEVCMDSTAT_VBUS_DEBOUNCED_MASK = TU_BIT(28), + + // write-1-to-clear latches + DEVCMDSTAT_W1C_MASK = DEVCMDSTAT_SETUP_RECEIVED_MASK | DEVCMDSTAT_CONNECT_CHANGE_MASK | + DEVCMDSTAT_SUSPEND_CHANGE_MASK | DEVCMDSTAT_RESET_CHANGE_MASK, }; enum { @@ -171,7 +175,9 @@ typedef struct ep_cmd_sts_t ep[2*MAX_EP_PAIRS][2]; xfer_dma_t dma[2*MAX_EP_PAIRS]; - TU_ATTR_ALIGNED(64) uint8_t setup_packet[8]; + // volatile: the controller DMAs a new setup packet into this buffer as soon as the SETUP + // latch is cleared, so reads of it must stay ordered against the register accesses around them + TU_ATTR_ALIGNED(64) volatile uint8_t setup_packet[8]; }dcd_data_t; // EP list must be 256-byte aligned @@ -180,8 +186,12 @@ typedef struct // Use CFG_TUD_MEM_SECTION to place it accordingly. CFG_TUD_MEM_SECTION TU_ATTR_ALIGNED(256) static dcd_data_t _dcd; -// Dummy buffer to fix ZLPs overwriting the buffer (probably an USB/DMA controller bug) -// TODO find way to save memory +// Dummy buffer to fix ZLPs overwriting the buffer: Errata LPC55S6x USB.5 / LPC55S2x USB.4 - the +// HS device controller always DMA-writes OUT data in 8-byte units, so up to 7 bytes land past the +// received length. This redirects the ZLP case; the general short-OUT case is unhandled here +// (TinyUSB's own endpoint buffers are sized/aligned so the spill stays inside them, but a tight +// caller buffer can be overrun by up to 7 bytes - the SDK's documented workaround is a bounce +// buffer). TODO find way to save memory CFG_TUD_MEM_SECTION TU_ATTR_ALIGNED(64) static uint8_t dummy[8]; //--------------------------------------------------------------------+ @@ -221,7 +231,7 @@ static const dcd_controller_t _dcd_controller[] = { // INTERNAL OBJECT & FUNCTION DECLARATION //--------------------------------------------------------------------+ -TU_ATTR_ALWAYS_INLINE static inline uint16_t get_buf_offset(void const * buffer) { +TU_ATTR_ALWAYS_INLINE static inline uint16_t get_buf_offset(void const volatile * buffer) { uint32_t addr = (uint32_t) buffer; TU_ASSERT( (addr & 0x3f) == 0, 0 ); return ( (addr >> 6) & 0xFFFFUL ) ; @@ -247,6 +257,16 @@ TU_ATTR_ALWAYS_INLINE static inline bool rhport_is_highspeed(uint8_t rhport) { return _dcd_controller[rhport].is_highspeed; } + +// DEVCMDSTAT mixes RW fields with write-1-to-clear latches (SETUP + the 3 change bits): a blind +// RMW writes a pending latch back as 1 and silently clears it (a SETUP eaten this way strands +// EP0). Mask the latches on every update; pass one in set_mask only to clear it. +TU_ATTR_ALWAYS_INLINE static inline void devcmdstat_update(dcd_registers_t* dcd_reg, + uint32_t clear_mask, uint32_t set_mask) { + const uint32_t v = dcd_reg->DEVCMDSTAT & ~(DEVCMDSTAT_W1C_MASK | clear_mask); + dcd_reg->DEVCMDSTAT = v | set_mask; +} + //--------------------------------------------------------------------+ // CONTROLLER API //--------------------------------------------------------------------+ @@ -284,8 +304,10 @@ bool dcd_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) { dcd_reg->DATABUFSTART = tu_align((uint32_t) &_dcd, TU_BIT(22)); // 22-bit alignment dcd_reg->INTSTAT = dcd_reg->INTSTAT; // clear all pending interrupt dcd_reg->INTEN = INT_DEVICE_STATUS_MASK; - dcd_reg->DEVCMDSTAT |= DEVCMDSTAT_DEVICE_ENABLE_MASK | DEVCMDSTAT_DEVICE_CONNECT_MASK | - DEVCMDSTAT_RESET_CHANGE_MASK | DEVCMDSTAT_CONNECT_CHANGE_MASK | DEVCMDSTAT_SUSPEND_CHANGE_MASK; + // deliberately clear every latch (incl. a SETUP left by a bootloader/warm start) for a + // deterministic init state + devcmdstat_update(dcd_reg, 0, DEVCMDSTAT_DEVICE_ENABLE_MASK | DEVCMDSTAT_DEVICE_CONNECT_MASK | + DEVCMDSTAT_W1C_MASK); NVIC_ClearPendingIRQ(_dcd_controller[rhport].irqnum); @@ -309,8 +331,7 @@ void dcd_set_address(uint8_t rhport, uint8_t dev_addr) // Response with status first before changing device address dcd_edpt_xfer(rhport, tu_edpt_addr(0, TUSB_DIR_IN), NULL, 0, false); - dcd_reg->DEVCMDSTAT &= ~DEVCMDSTAT_DEVICE_ADDR_MASK; - dcd_reg->DEVCMDSTAT |= dev_addr; + devcmdstat_update(dcd_reg, DEVCMDSTAT_DEVICE_ADDR_MASK, dev_addr); } void dcd_remote_wakeup(uint8_t rhport) @@ -321,13 +342,13 @@ void dcd_remote_wakeup(uint8_t rhport) void dcd_connect(uint8_t rhport) { dcd_registers_t* dcd_reg = _dcd_controller[rhport].regs; - dcd_reg->DEVCMDSTAT |= DEVCMDSTAT_DEVICE_CONNECT_MASK; + devcmdstat_update(dcd_reg, 0, DEVCMDSTAT_DEVICE_CONNECT_MASK); } void dcd_disconnect(uint8_t rhport) { dcd_registers_t* dcd_reg = _dcd_controller[rhport].regs; - dcd_reg->DEVCMDSTAT &= ~DEVCMDSTAT_DEVICE_CONNECT_MASK; + devcmdstat_update(dcd_reg, DEVCMDSTAT_DEVICE_CONNECT_MASK, 0); } void dcd_sof_enable(uint8_t rhport, bool en) @@ -380,9 +401,17 @@ void dcd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr) uint8_t const ep_id = ep_addr2id(ep_addr); + // Preserve rf_tv: for non-control endpoints it is a TYPE bit, not the toggle value (UM11126: + // T=1 + RF 1/0 = interrupt/iso). Zeroing it here turned HS periodic interrupt endpoints into + // isochronous - no handshake on OUT, dead IN (usbtest cases 25/26 on lpc55 HS port). + // TODO implement the Errata LPC546xx USB.13 work-around (same semantics in UM11126): with RF/TV preserved at 1, TR + // loads the toggle from TV, so an HS interrupt endpoint restarts on DATA1 after clear-halt and + // the host discards one packet as a retransmission. The documented workaround needs an + // interrupt-on-NAK state machine (park as generic TR=1/TV=0, wait for a NAKed token to latch + // toggle 0 via EPTOGGLE, restore the type) - deferred; one lost packet beats the fully broken + // endpoint the old rf_tv clear caused. _dcd.ep[ep_id][0].cmd_sts.stall = 0; _dcd.ep[ep_id][0].cmd_sts.toggle_reset = 1; - _dcd.ep[ep_id][0].cmd_sts.rf_tv = 0; } bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * p_endpoint_desc) @@ -432,7 +461,7 @@ void dcd_edpt_close_all (uint8_t rhport) { for (uint8_t ep_id = 0; ep_id < 2*_dcd_controller[rhport].ep_pairs; ++ep_id) { - _dcd.ep[ep_id][0].cmd_sts.active = _dcd.ep[ep_id][0].cmd_sts.active = 0; // TODO proper way is to EPSKIP then wait ep[][].active then write ep[][].disable (see table 778 in LPC55S69 Use Manual) + _dcd.ep[ep_id][0].cmd_sts.active = _dcd.ep[ep_id][1].cmd_sts.active = 0; // TODO proper way is to EPSKIP then wait ep[][].active then write ep[][].disable (see table 778 in LPC55S69 Use Manual) _dcd.ep[ep_id][0].cmd_sts.disable = _dcd.ep[ep_id][1].cmd_sts.disable = 1; } } @@ -538,7 +567,7 @@ static void bus_reset(uint8_t rhport) dcd_reg->EPSKIP = 0xFFFFFFFF; dcd_reg->INTSTAT = dcd_reg->INTSTAT; // clear all pending interrupt - dcd_reg->DEVCMDSTAT |= DEVCMDSTAT_SETUP_RECEIVED_MASK; // clear setup received interrupt + devcmdstat_update(dcd_reg, 0, DEVCMDSTAT_SETUP_RECEIVED_MASK); // clear setup received interrupt dcd_reg->INTEN = INT_DEVICE_STATUS_MASK | TU_BIT(0) | TU_BIT(1); // enable device status & control endpoints } @@ -597,18 +626,25 @@ void dcd_int_handler(uint8_t rhport) { dcd_registers_t* dcd_reg = _dcd_controller[rhport].regs; - uint32_t const cmd_stat = dcd_reg->DEVCMDSTAT; - uint32_t int_status = dcd_reg->INTSTAT; - int_status &= dcd_reg->INTEN; + int_status &= dcd_reg->INTEN; dcd_reg->INTSTAT = int_status; // Acknowledge handled interrupt if (int_status == 0) return; + // Snapshot after the INTSTAT ack: latch bits persist (RWC) so nothing is lost, while the reverse + // order could consume INTSTAT bit0 for a SETUP not yet visible in the snapshot - stranding the + // SETUP (INTSTAT is edge-latched) and feeding bit0 to process_xfer_isr as a bogus completion. + uint32_t const cmd_stat = dcd_reg->DEVCMDSTAT; + //------------- Device Status -------------// if ( int_status & INT_DEVICE_STATUS_MASK ) { - dcd_reg->DEVCMDSTAT |= DEVCMDSTAT_RESET_CHANGE_MASK | DEVCMDSTAT_CONNECT_CHANGE_MASK | DEVCMDSTAT_SUSPEND_CHANGE_MASK; + // clear only the change latches observed in the snapshot: one latched by hardware between the + // snapshot and this write would be acknowledged unseen (its DEV_INT re-latches and dispatches + // next pass instead) + devcmdstat_update(dcd_reg, 0, cmd_stat & + (DEVCMDSTAT_RESET_CHANGE_MASK | DEVCMDSTAT_CONNECT_CHANGE_MASK | DEVCMDSTAT_SUSPEND_CHANGE_MASK)); if ( cmd_stat & DEVCMDSTAT_RESET_CHANGE_MASK) // bus reset { @@ -653,15 +689,43 @@ void dcd_int_handler(uint8_t rhport) _dcd.ep[0][0].cmd_sts.active = _dcd.ep[1][0].cmd_sts.active = 0; _dcd.ep[0][0].cmd_sts.stall = _dcd.ep[1][0].cmd_sts.stall = 0; - dcd_reg->DEVCMDSTAT |= DEVCMDSTAT_SETUP_RECEIVED_MASK; + // UM flow: ack the latch FIRST, then read the payload. This IP has no setup lockout, so a + // back-to-back SETUP can overwrite _dcd.setup_packet at any time - but with the latch already + // released, any such overwrite re-latches SETUP_RECEIVED and is redelivered (worst case a + // superseded duplicate, absorbed by usbd's queued-setup counter). The reverse order can + // consume the newer SETUP's latch unseen and lose it. + devcmdstat_update(dcd_reg, 0, DEVCMDSTAT_SETUP_RECEIVED_MASK); + + // UM11126 Fig 163 (control EP0 flowchart) requires clearing the EP0IN interrupt here: a + // control IN completion latched before this SETUP must not reach usbd after it, where it + // would be applied to the new request and arm its status stage early. EP0OUT goes with it - + // bit0 is set by SETUP reception too, and left set it would replay next pass as a phantom + // completion. Neither can discard live work: the SETUP latch NAKs all EP0 traffic until the + // update above, and both EP0 Active bits were cleared a few lines up. + dcd_reg->INTSTAT = TU_BIT(0) | TU_BIT(1); + + // Copied a byte at a time rather than with memcpy: C orders volatile accesses only against + // each other, so a non-volatile copy of this buffer may be sunk below the guard read that + // follows - gcc does exactly that at -O2 and -O3, leaving only -Os correct. + uint8_t setup_copy[8]; + for (uint8_t i = 0; i < sizeof(setup_copy); i++) { + setup_copy[i] = _dcd.setup_packet[i]; + } - dcd_event_setup_received(rhport, _dcd.setup_packet, true); + // a SETUP that raced in after the acks (its bit0 consumed above) makes this copy suspect: + // its latch is visible again, so re-raise the endpoint interrupt and let the next pass + // deliver the newer payload rather than passing up bytes that may be torn between the two + if (dcd_reg->DEVCMDSTAT & DEVCMDSTAT_SETUP_RECEIVED_MASK) { + dcd_reg->INTSETSTAT = TU_BIT(0); + } else { + dcd_event_setup_received(rhport, setup_copy, true); + } // keep waiting for next setup prepare_setup_packet(rhport); - // clear bit0 - int_status = tu_bit_clear(int_status, 0); + // drop both EP0 bits: acked above, and neither belongs to the request this SETUP starts + int_status &= ~(TU_BIT(0) | TU_BIT(1)); } // Endpoint transfer complete interrupt -- cgit v1.3.1 From 19ff2ed615e4a97984aab5551ac8835ead53b9e7 Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 17 Aug 2026 01:02:54 +0700 Subject: examples: document and work around the i.MX RT and LPC55 USB errata ERR050101: while an isochronous IN endpoint is active, an IN token addressed to that same endpoint number on ANOTHER device sharing the host can silently unprime one of this device's OUT endpoints - control, bulk, interrupt or isochronous alike. NXP states it cannot be detected by software and raises no interrupt, so the endpoint simply stops answering and the transfer never completes. The workaround is a uniqueness requirement rather than a particular number: the isochronous IN endpoint must not share its number with any IN endpoint in use on the bus. One family-wide constant therefore defeats it, since two affected boards on the same hub then pick the same number and each becomes the other's aggressor. CFG_TUSB_MIMXRT1XXX_ERRATA_ERR050101 is set only for the parts whose errata list it - RT1015, RT1020, RT1024 and RT1050, where it is marked no fix scheduled, plus RT1060 and RT1064 rev A - so RT1010 and the RT11xx family keep the ordinary number and cannot collide with an affected board beside them. Several affected boards on one hub can still be given distinct numbers with -DEPNUM_ISO_IN. The guard covers every example that has an isochronous IN endpoint: audio_test, audio_4_channel_mic, uac2_headset, cdc_uac2, usbtest, video_capture and video_capture_2ch. The video examples move the endpoint only when streaming isochronously, since the bulk configuration is unaffected, and video_capture_2ch takes two numbers because it has two streams. The macro name follows CFG_TUSB_RP2_ERRATA_E2/E4/E15 already in tree, and its is fixed, and which cannot be told apart at compile time - a way to define it to 0. device_issues.rst records ERR050101 against every affected part with a link to each errata sheet, and adds the LPC55S2x USB.3 speed-detection and USB.5 isochronous IN entries, neither of which TinyUSB works around. The branch's design notes are included under docs/superpowers. Verified: 340 wedge-free runs on mimxrt1064_evk, which previously wedged within hours, and the macro resolving to endpoint 0x87 on mimxrt1064_evk against 0x83 on mimxrt1010_evk and stm32f407disco. --- docs/reference/device_issues.rst | 45 ++ .../plans/2026-08-15-ci-hs-reset-edges.md | 782 +++++++++++++++++++++ .../plans/2026-08-16-drop-ep0-prime-verify.md | 314 +++++++++ .../specs/2026-08-15-ci-hs-reset-edges-design.md | 162 +++++ .../2026-08-16-drop-ep0-prime-verify-design.md | 90 +++ .../audio_4_channel_mic/src/usb_descriptors.c | 4 + examples/device/audio_test/src/usb_descriptors.c | 4 + examples/device/cdc_uac2/src/usb_descriptors.c | 10 + examples/device/uac2_headset/src/usb_descriptors.c | 7 + examples/device/usbtest/src/usb_descriptors.c | 16 + .../device/video_capture/src/usb_descriptors.c | 4 + .../device/video_capture_2ch/src/usb_descriptors.c | 11 +- src/common/tusb_mcu.h | 19 + 13 files changed, 1466 insertions(+), 2 deletions(-) create mode 100644 docs/superpowers/plans/2026-08-15-ci-hs-reset-edges.md create mode 100644 docs/superpowers/plans/2026-08-16-drop-ep0-prime-verify.md create mode 100644 docs/superpowers/specs/2026-08-15-ci-hs-reset-edges-design.md create mode 100644 docs/superpowers/specs/2026-08-16-drop-ep0-prime-verify-design.md (limited to 'src') diff --git a/docs/reference/device_issues.rst b/docs/reference/device_issues.rst index 0850409cb..b95a3fc1e 100644 --- a/docs/reference/device_issues.rst +++ b/docs/reference/device_issues.rst @@ -20,6 +20,51 @@ Most severe issues are: - USB.5: In USB full-speed host mode, linked list on done queue is broken. - USB.15: USB high-speed device in endpoint TX data corruption +NXP i.MX RT1015/RT1020/RT1024/RT1050/RT1060/RT1064 +----------------------------------------------------- +**Severity: High** when an isochronous IN endpoint is used behind a hub + +Reference: ERR050101 "USB: Endpoint conflict issue in device mode", listed in the errata sheet of +every part above - `IMXRT1015CE`_, `IMXRT1020CE`_, `IMXRT1024CE`_, `IMXRT1050CE`_, `IMXRT1060CE`_ +and `IMXRT1064CE`_. On RT1060 and RT1064 it applies to rev A silicon only and is fixed in rev B; on +RT1015, RT1020, RT1024 and RT1050 it is marked *no fix scheduled*, so all silicon is affected. +RT1010, RT116x, RT117x and RT118x do not list it. + +.. _IMXRT1015CE: https://www.nxp.com/docs/en/errata/IMXRT1015CE.pdf +.. _IMXRT1020CE: https://www.nxp.com/docs/en/errata/IMXRT1020CE.pdf +.. _IMXRT1024CE: https://www.nxp.com/docs/en/errata/IMXRT1024CE.pdf +.. _IMXRT1050CE: https://www.nxp.com/docs/en/errata/IMXRT1050CE.pdf +.. _IMXRT1060CE: https://www.nxp.com/docs/en/errata/IMXRT1060CE.pdf +.. _IMXRT1064CE: https://www.nxp.com/docs/en/errata/IMXRT1064CE.pdf + +While an isochronous IN endpoint is active, an IN token addressed to *that same endpoint number on +another device sharing the host* can silently unprime one of this device's OUT endpoints - control, +bulk, interrupt or isochronous alike. NXP states the unpriming cannot be detected by software and +raises no interrupt, so the endpoint simply stops answering OUT tokens and the transfer never +completes. Typically seen when the device is behind a hub with other devices attached. + +Workaround: give isochronous IN endpoints a number that no other device on the same host uses for +any IN endpoint - endpoints 1-3 are used by nearly every composite device, so choose a high number +(``examples/device/usbtest`` uses endpoint 7 on this family for that reason). Devices without an +isochronous IN endpoint are unaffected. + +NXP LPC55S2x/LPC552x +--------------------------------- +**Severity: Low** (both need specific conditions) + +Reference: `LPC55S2x Errata Sheet`_ USB.3, USB.5 + +.. _LPC55S2x Errata Sheet: https://www.nxp.com/docs/en/errata/ES_LPC55S2x.pdf + +USB.3: As a high-speed device behind certain full-speed hubs, the device does not correctly detect +the host's KJ chirp sequence and can behave erratically due to wrong speed detection. The documented +workaround is to set the FORCE_FS bit in DEVCMDSTAT on bus reset when the reported link speed is +full speed. TinyUSB does not implement this workaround. + +USB.5: An isochronous IN endpoint sending a 1024-byte maximum-packet-size packet raises no endpoint +interrupt and its command/status entry is not updated. Workaround: cap the isochronous IN maximum +packet size at 1023 bytes in the descriptor. + WCH CH32F20x/CH32V20x/CH32V30x --------------------------------- **Severity: Medium** diff --git a/docs/superpowers/plans/2026-08-15-ci-hs-reset-edges.md b/docs/superpowers/plans/2026-08-15-ci-hs-reset-edges.md new file mode 100644 index 000000000..ec0834e55 --- /dev/null +++ b/docs/superpowers/plans/2026-08-15-ci-hs-reset-edges.md @@ -0,0 +1,782 @@ +# Bus-Reset Edge Events + Review Fix Wave Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Give the device stack a "bus reset started" event so ci_hs can tell usbd to stand down at the URI interrupt instead of up to 50 ms later, and clear the ten findings agreed from the max review. + +**Architecture:** `DCD_EVENT_BUS_RESET` splits into `DCD_EVENT_BUS_RESET_START` / `_END` with a compatibility alias, so every other port stays byte-identical. `dcd_ci_hs.c`'s `bus_reset()` splits along the register/software line — registers at URI (`_START`), software structures at the port-change ending the reset (`_END`) — which eliminates the window where usbd believes it is configured over zeroed queue heads. A single bounded-flush helper absorbs the five flush sites. Seven mechanical fixes follow. + +**Tech Stack:** C99, TinyUSB device stack (`src/device/`), ChipIdea HS DCD (`src/portable/chipidea/ci_hs/`), NXP IP3511 DCD (`src/portable/nxp/lpc_ip3511/`), CMake+Ninja and Make builds, J-Link flashing, `test/hil/` HIL harness. + +## Global Constraints + +- Branch `fix-ci-hs` in worktree `/home/hathach/.herdr/worktrees/tinyusb/fix-ci-hs`. Do NOT push; the user pushes. +- C99, 2-space indent, no tabs. Match each file's surrounding style (`dcd_lpc_ip3511.c` mixes styles — follow the immediate neighbourhood). +- Commit messages: imperative mood, no `Co-Authored-By:` or `Claude-Session:` trailers (repo rule: hathach is sole author). +- The repo pre-commit hook (trailing-whitespace, end-of-file-fixer, codespell, unique-PIDs, ceedling unit tests) must pass. If it rewrites a file, re-stage and retry the commit once. +- Comments: short, only the non-obvious "why". Cite manuals as `UM10503 25.10.3` / `Errata LPC546xx USB.13` style — never `ES_` prefixes. +- Never edit anything under `hw/mcu/` or `lib/` (vendor code). +- Build commands used throughout (each ~30-60 s): + `cmake --build examples/cmake-build-` for `mimxrt1064_evk`, `lpcxpresso18s37`, `lpcxpresso11u37`, `lpcxpresso55s28`. +- Design source of truth: `docs/superpowers/specs/2026-08-15-ci-hs-reset-edges-design.md`. + +## File Structure + +| File | Responsibility in this plan | +|---|---| +| `src/device/dcd.h` | Event enum + compatibility alias + contract comment | +| `src/device/usbd.c` | Handle both reset edges; log strings; stop breakpointing on DCD refusal | +| `src/portable/chipidea/ci_hs/dcd_ci_hs.c` | Flush helper; `bus_reset()` split; setup-flush wait; `dcd_set_address`; RESUME guard | +| `src/portable/nxp/lpc_ip3511/dcd_lpc_ip3511.c` | Torn-setup delivery; USB.13 TODO token | +| `hw/bsp/lpc55/boards/lpcxpresso55s28/board.cmake` | Delete dead RHPORT block | +| `hw/bsp/lpc11/boards/lpcxpresso11u37/lpc11u37.ld` | Correct stale comment; relabel ASSERT | + +Tasks 1-3 are ordered (each builds on the previous); Tasks 4-6 are independent of each other. + +--- + +### Task 1: Split the bus-reset event into START/END edges + +**Files:** +- Modify: `src/device/dcd.h` (enum at lines 23-34; contract comment above it) +- Modify: `src/device/usbd.c` (`_usbd_event_str[]` at line 457; the `DCD_EVENT_BUS_RESET` case at line 700) + +**Interfaces:** +- Produces: `DCD_EVENT_BUS_RESET_START` and `DCD_EVENT_BUS_RESET_END` enum members; `#define DCD_EVENT_BUS_RESET DCD_EVENT_BUS_RESET_END`. Task 2 emits `_START` via the existing `dcd_event_bus_signal(uint8_t rhport, dcd_eventid_t eid, bool in_isr)` and `_END` via the existing `dcd_event_bus_reset(uint8_t rhport, tusb_speed_t speed, bool in_isr)`. + +- [ ] **Step 1: Replace the enum member in `src/device/dcd.h`** + +Replace: + +```c +typedef enum { + DCD_EVENT_INVALID = 0, // 0 + DCD_EVENT_BUS_RESET, // 1 + DCD_EVENT_UNPLUGGED, // 2 + DCD_EVENT_SOF, // 3 + DCD_EVENT_SUSPEND, // 4 TODO LPM Sleep L1 support + DCD_EVENT_RESUME, // 5 + DCD_EVENT_SETUP_RECEIVED, // 6 + DCD_EVENT_XFER_COMPLETE, // 7 + USBD_EVENT_FUNC_CALL, // 8 Not an DCD event, just a convenient way to defer ISR function + DCD_EVENT_COUNT +} dcd_eventid_t; +``` + +with: + +```c +// Bus reset is reported as two edges. BUS_RESET_START is optional: a controller that +// cannot tell the edges apart emits only BUS_RESET_END, which stays self-sufficient (it +// performs the full teardown with or without a preceding START). Emit START when reset +// signaling is detected - the link is unusable and the speed is not negotiated yet - so +// the stack stops using endpoints immediately instead of at the end of the reset. +typedef enum { + DCD_EVENT_INVALID = 0, // 0 + DCD_EVENT_BUS_RESET_START, // 1 + DCD_EVENT_BUS_RESET_END, // 2 with negotiated speed + DCD_EVENT_UNPLUGGED, // 3 + DCD_EVENT_SOF, // 4 + DCD_EVENT_SUSPEND, // 5 TODO LPM Sleep L1 support + DCD_EVENT_RESUME, // 6 + DCD_EVENT_SETUP_RECEIVED, // 7 + DCD_EVENT_XFER_COMPLETE, // 8 + USBD_EVENT_FUNC_CALL, // 9 Not an DCD event, just a convenient way to defer ISR function + DCD_EVENT_COUNT +} dcd_eventid_t; + +#define DCD_EVENT_BUS_RESET DCD_EVENT_BUS_RESET_END // backward compatibility +``` + +- [ ] **Step 2: Update the log-string table in `src/device/usbd.c`** + +At line 457 the table is indexed by event id and MUST stay in enum order. Replace the +`"Bus Reset",` entry (line 459) with two entries: + +```c + "Bus Reset Start", + "Bus Reset End", +``` + +- [ ] **Step 3: Handle both edges in the usbd task loop** + +Replace the case at `src/device/usbd.c:700`: + +```c + case DCD_EVENT_BUS_RESET: + TU_LOG_USBD(": %s Speed\r\n", tu_str_speed[event.bus_reset.speed]); + usbd_reset(event.rhport); + _usbd_dev.speed = event.bus_reset.speed; + break; +``` + +with: + +```c + case DCD_EVENT_BUS_RESET_START: + TU_LOG_USBD("\r\n"); + usbd_reset(event.rhport); + break; + + case DCD_EVENT_BUS_RESET_END: + TU_LOG_USBD(": %s Speed\r\n", tu_str_speed[event.bus_reset.speed]); + // TODO a DCD that reports both edges pays for two teardowns: track a per-rhport + // "start seen" flag and skip this reset, keeping it for the single-event DCDs. + usbd_reset(event.rhport); + _usbd_dev.speed = event.bus_reset.speed; + break; +``` + +- [ ] **Step 4: Verify legacy ports still build (the alias must carry them)** + +Run: + +```bash +cd examples && cmake -B cmake-build-stm32f407disco -DBOARD=stm32f407disco -G Ninja -DCMAKE_BUILD_TYPE=MinSizeRel . && cmake --build cmake-build-stm32f407disco +``` + +Expected: builds clean. This board's DCD (dwc2) still calls `dcd_event_bus_reset()`, which +now resolves to `_END` through the unchanged helper — proving the alias works. + +- [ ] **Step 5: Verify the unit tests still build and pass** + +Run: `cd test/unit-test && ceedling test:all` +Expected: all tests pass (they reference `DCD_EVENT_BUS_RESET` via the alias). + +- [ ] **Step 6: Commit** + +```bash +git add src/device/dcd.h src/device/usbd.c +git commit -m "usbd: split bus reset into start/end edge events + +A DCD that can see reset signaling begin has no way to say so: the only +event carries the negotiated speed, which does not exist until the reset +ends. On ChipIdea that leaves the stack believing it is configured for the +whole reset window (3 ms minimum, tens of ms in practice) while the +controller has already torn its endpoints down. + +Add DCD_EVENT_BUS_RESET_START for the leading edge and rename the existing +event to DCD_EVENT_BUS_RESET_END, keeping DCD_EVENT_BUS_RESET as an alias +so every other port and the unit tests are untouched. START is optional and +END stays self-sufficient, so single-event drivers keep working unchanged." +``` + +--- + +### Task 2: Split ci_hs `bus_reset()` across the two edges, behind one flush helper + +**Files:** +- Modify: `src/portable/chipidea/ci_hs/dcd_ci_hs.c` (`bus_reset()`; `dcd_deinit()`; `dcd_edpt_iso_activate()`; the `INTR_RESET` and `INTR_PORT_CHANGE` branches of `dcd_int_handler()`) + +**Interfaces:** +- Consumes: `DCD_EVENT_BUS_RESET_START` (Task 1), `dcd_event_bus_signal()`, `dcd_event_bus_reset()`. +- Produces: `static bool flush_endpoints(ci_hs_regs_t *dcd_reg, uint32_t mask)` — writes `ENDPTFLUSH = mask`, spins bounded by `CI_HS_BUSY_SPIN`, returns `true` if the bits cleared. Used by Task 3. + +- [ ] **Step 1: Add the flush helper next to `bus_reset()`** + +Insert above `bus_reset()`: + +```c +// Flush endpoint buffers and wait for the controller to acknowledge. Callers proceed +// regardless of the result; the bound only prevents an ISR-context hang on dead hardware. +static bool flush_endpoints(ci_hs_regs_t *dcd_reg, uint32_t mask) { + dcd_reg->ENDPTFLUSH = mask; + uint32_t guard = CI_HS_BUSY_SPIN; + while (dcd_reg->ENDPTFLUSH & mask) { + if (!guard--) { + return false; + } + } + return true; +} +``` + +- [ ] **Step 2: Split `bus_reset()` into begin/complete** + +Replace the whole `bus_reset()` function with these two. `bus_reset_begin()` keeps only +register work; `bus_reset_complete()` owns everything that touches `_dcd_data`: + +```c +/// Register-side reset handling, must run inside the reset window (UM10503 25.10.3) +static void bus_reset_begin(uint8_t rhport) { + ci_hs_regs_t *dcd_reg = CI_HS_REG(rhport); + + // The reset value for all endpoint types is the control endpoint. If one endpoint + // direction is enabled and the paired endpoint of opposite direction is disabled, then the + // endpoint type of the unused direction must be changed from the control type to any other + // type (e.g. bulk). Leaving an un-configured endpoint control will cause undefined behavior + // for the data PID tracking on the active endpoint. + const uint8_t ep_count = ci_ep_count(dcd_reg); + for (uint8_t i = 1; i < ep_count; i++) { + dcd_reg->ENDPTCTRL[i] = ENDPTCTRL_RESET_MASK; + } + + //------------- Clear All Registers -------------// + dcd_reg->ENDPTNAK = dcd_reg->ENDPTNAK; + dcd_reg->ENDPTNAKEN = 0; + dcd_reg->ENDPTSETUPSTAT = dcd_reg->ENDPTSETUPSTAT; + dcd_reg->ENDPTCOMPLETE = dcd_reg->ENDPTCOMPLETE; + + uint32_t guard = CI_HS_BUSY_SPIN; + while (dcd_reg->ENDPTPRIME && guard--) {} + flush_endpoints(dcd_reg, 0xFFFFFFFF); +} + +/// Software-side reset handling, deferred to the port change ending the reset so the queue +/// heads stay coherent until the stack is told - and so a prime issued by a task that had +/// not yet seen BUS_RESET_START is flushed here rather than surviving re-enumeration. +static void bus_reset_complete(uint8_t rhport) { + ci_hs_regs_t *dcd_reg = CI_HS_REG(rhport); + flush_endpoints(dcd_reg, 0xFFFFFFFF); + + //------------- Queue Head & Queue TD -------------// + tu_memclr(&_dcd_data, sizeof(dcd_data_t)); + + //------------- Set up Control Endpoints (0 OUT, 1 IN) -------------// + _dcd_data.qhd[0][0].zero_length_termination = _dcd_data.qhd[0][1].zero_length_termination = 1; + _dcd_data.qhd[0][0].max_packet_size = _dcd_data.qhd[0][1].max_packet_size = CFG_TUD_ENDPOINT0_SIZE; + _dcd_data.qhd[0][0].qtd_overlay.next = _dcd_data.qhd[0][1].qtd_overlay.next = QTD_NEXT_INVALID; + + _dcd_data.qhd[0][0].int_on_setup = 1; // OUT only + + dcd_dcache_clean_invalidate(&_dcd_data, sizeof(dcd_data_t)); +} +``` + +- [ ] **Step 3: Route the two ISR branches to the new functions** + +In `dcd_int_handler()`, the `INTR_RESET` branch becomes: + +```c + if (int_status & INTR_RESET) { + bus_reset_begin(rhport); + _port_change_reason[rhport] = PORT_CHANGE_REASON_RESET; + dcd_event_bus_signal(rhport, DCD_EVENT_BUS_RESET_START, true); + } +``` + +and inside the `INTR_PORT_CHANGE` branch, the reset arm (the `else` of the resume test) +becomes: + +```c + } else { + bus_reset_complete(rhport); + // PSPD: 0 full, 1 low, 2 high, 3 undefined (treated as full) + const uint32_t pspd = (dcd_reg->PORTSC1 & PORTSC1_PORT_SPEED) >> PORTSC1_PORT_SPEED_POS; + const tusb_speed_t speed = (pspd == 1) ? TUSB_SPEED_LOW : (pspd == 2) ? TUSB_SPEED_HIGH : TUSB_SPEED_FULL; + dcd_event_bus_reset(rhport, speed, true); + } +``` + +Delete the now-unused EP0 `ENDPTFLUSH` line that previously sat at the top of that arm — +`bus_reset_complete()` flushes all endpoints. + +- [ ] **Step 4: Route the remaining flush sites through the helper** + +In `dcd_deinit()`, replace the flush block with: + +```c + // flush all endpoints + uint32_t guard = CI_HS_BUSY_SPIN; + while (dcd_reg->ENDPTPRIME && guard--) {} + flush_endpoints(dcd_reg, 0xFFFFFFFF); +``` + +In `dcd_edpt_iso_activate()`, replace the flush + spin with: + +```c + // Flush EP + flush_endpoints(dcd_reg, TU_BIT(epnum + (dir ? 16 : 0))); +``` + +- [ ] **Step 5: Build both ci_hs board families** + +Run: + +```bash +cmake --build examples/cmake-build-mimxrt1064_evk && cmake --build examples/cmake-build-lpcxpresso18s37 +``` + +Expected: both succeed with no new warnings. + +- [ ] **Step 6: Commit** + +```bash +git add src/portable/chipidea/ci_hs/dcd_ci_hs.c +git commit -m "dcd(ci_hs): report bus reset start at URI, finish at port change + +The RM wants the reset cleanup inside the reset window, but the negotiated +speed only exists once the port reaches its operational state, so the stack +was told nothing for the whole window - it kept believing it was configured +while the queue heads had been zeroed under it, and a transfer a class +driver started in that gap stayed primed across re-enumeration. + +Split the work along the register/software line: bus_reset_begin() does the +register cleanup at URI and signals BUS_RESET_START, bus_reset_complete() +re-flushes, resets the queue heads and reports BUS_RESET_END with the final +speed at the port change. Zeroing the queue heads now happens in the same +breath as telling the stack, and the second flush retires anything primed +in between. + +Fold the five hand-rolled endpoint flushes into one bounded helper while +the reset path is open." +``` + +--- + +### Task 3: Make the setup-time EP0 flush wait, and stop dropping the SET_ADDRESS status prime + +**Files:** +- Modify: `src/portable/chipidea/ci_hs/dcd_ci_hs.c` (`dcd_set_address()`; the `ENDPTSETUPSTAT` branch inside `dcd_int_handler()`) + +**Interfaces:** +- Consumes: `flush_endpoints()` (Task 2); `qhd_start_xfer()` returning `bool`, already propagated by `dcd_edpt_xfer()`. + +- [ ] **Step 1: Wait for the setup-time flush to complete** + +In the ISR's setup branch, replace the fire-and-forget flush line + +```c + dcd_reg->ENDPTFLUSH = TU_BIT(0) | TU_BIT(16); +``` + +with + +```c + // Wait it out: the flush retires a status/handshake phase left primed by the previous + // control sequence (UM10503 25.10.8.1.1), and an unfinished flush would otherwise + // still be asserted when the task primes the response to this setup and would retire + // that instead. A flush waits for any packet already in progress - microseconds at + // high speed - and the guard caps wedged hardware. + flush_endpoints(dcd_reg, TU_BIT(0) | TU_BIT(16)); +``` + +- [ ] **Step 2: Honour the status-prime result in `dcd_set_address`** + +Replace the body of `dcd_set_address()`: + +```c +void dcd_set_address(uint8_t rhport, uint8_t dev_addr) { + // Response with status first before changing device address. A refused prime means a new + // setup superseded this transfer; staging an address whose ACK will never arrive would + // leave the device answering on it, so only arm the address when the status went out. + if (dcd_edpt_xfer(rhport, tu_edpt_addr(0, TUSB_DIR_IN), NULL, 0, false)) { + ci_hs_regs_t *dcd_reg = CI_HS_REG(rhport); + dcd_reg->DEVICEADDR = (dev_addr << 25) | TU_BIT(24); + } +} +``` + +- [ ] **Step 3: Build and commit** + +Run: `cmake --build examples/cmake-build-mimxrt1064_evk && cmake --build examples/cmake-build-lpcxpresso18s37` +Expected: both succeed. + +```bash +git add src/portable/chipidea/ci_hs/dcd_ci_hs.c +git commit -m "dcd(ci_hs): wait out the setup flush, honour the set-address prime + +The flush issued on every new setup was fire-and-forget. A flush waits for +a packet already in progress, so it could still be asserted when the task +primed the response to that setup and retire the fresh prime instead - +leaving EP0 silent until the host gave up. + +dcd_set_address() also armed DEVICEADDR unconditionally, but the status +prime can now be refused when a newer setup supersedes the transfer; the +address was then staged behind an ACK that never came and the device sat at +address 0. Only arm it when the status transfer actually started." +``` + +--- + +### Task 4: Emit RESUME only when the port really left suspend + +**Files:** +- Modify: `src/portable/chipidea/ci_hs/dcd_ci_hs.c` (the resume arm of the `INTR_PORT_CHANGE` branch in `dcd_int_handler()`) + +**Interfaces:** none consumed or produced. + +- [ ] **Step 1: Restore the hardware guard** + +In the `INTR_PORT_CHANGE` branch, the resume arm currently reads: + +```c + if (pci_reason == PORT_CHANGE_REASON_SUSPEND) { + dcd_event_bus_signal(rhport, DCD_EVENT_RESUME, true); + } else { +``` + +Replace that condition with one that also consults live hardware: + +```c + if (pci_reason == PORT_CHANGE_REASON_SUSPEND) { + // Only when the port actually left suspend: a starved snapshot can hold the resume's + // port change together with a second suspend, and reporting a resume there would + // leave the stack awake on a sleeping bus with no further event to correct it. + if (!(dcd_reg->PORTSC1 & PORTSC1_SUSPEND)) { + dcd_event_bus_signal(rhport, DCD_EVENT_RESUME, true); + } + } else { +``` + +- [ ] **Step 2: Build and commit** + +Run: `cmake --build examples/cmake-build-mimxrt1064_evk && cmake --build examples/cmake-build-lpcxpresso18s37` +Expected: both succeed. + +```bash +git add src/portable/chipidea/ci_hs/dcd_ci_hs.c +git commit -m "dcd(ci_hs): only report resume when the port left suspend + +A suspend, resume and second suspend collapsed into one interrupt pass +queued suspend then resume from the recorded cause alone, so the stack +ended up awake while the bus was still suspended and nothing arrived to +correct it. Consult PORTSC1 before reporting the resume." +``` + +--- + +### Task 5: ip3511 — never deliver a knowingly-torn setup packet + +**Files:** +- Modify: `src/portable/nxp/lpc_ip3511/dcd_lpc_ip3511.c` (setup branch of `dcd_int_handler()`; the `dcd_edpt_clear_stall()` comment) + +**Interfaces:** none consumed or produced. + +- [ ] **Step 1: Deliver only when the copy is known good** + +Replace: + +```c + // a SETUP that raced in after the acks (its bit0 consumed above, this copy possibly torn): + // its latch is visible again - re-raise the endpoint interrupt so the next pass redelivers + // the newer payload + if (dcd_reg->DEVCMDSTAT & DEVCMDSTAT_SETUP_RECEIVED_MASK) { + dcd_reg->INTSETSTAT = TU_BIT(0); + } + + dcd_event_setup_received(rhport, setup_copy, true); +``` + +with: + +```c + // a SETUP that raced in after the acks (its bit0 consumed above) makes this copy suspect: + // its latch is visible again, so re-raise the endpoint interrupt and let the next pass + // deliver the newer payload rather than passing up bytes that may be torn between the two + if (dcd_reg->DEVCMDSTAT & DEVCMDSTAT_SETUP_RECEIVED_MASK) { + dcd_reg->INTSETSTAT = TU_BIT(0); + } else { + dcd_event_setup_received(rhport, setup_copy, true); + } +``` + +- [ ] **Step 2: Add the TODO token to the USB.13 deferral** + +In `dcd_edpt_clear_stall()`, change the caveat's opening line from + +```c + // Known caveat (Errata LPC546xx USB.13, same semantics in UM11126): with RF/TV preserved at 1, TR +``` + +to + +```c + // TODO implement the Errata LPC546xx USB.13 work-around (same semantics in UM11126): with RF/TV preserved at 1, TR +``` + +- [ ] **Step 3: Build and commit** + +Run: `cmake --build examples/cmake-build-lpcxpresso11u37 && cmake --build examples/cmake-build-lpcxpresso55s28` +Expected: both succeed. + +```bash +git add src/portable/nxp/lpc_ip3511/dcd_lpc_ip3511.c +git commit -m "dcd(ip3511): drop a setup packet the hardware may have overwritten + +The handler already notices when a new setup landed while it was copying +the previous one, and re-raises the endpoint interrupt so the newer payload +is delivered next pass - but it then passed the suspect copy up anyway. +Usually harmless, since the redelivery supersedes it, but if that second +event cannot be queued the torn bytes are processed as a real request. +Deliver the copy only when no newer setup is pending." +``` + +--- + +### Task 6: BSP cleanups — dead RHPORT block and the stale linker comment + +**Files:** +- Modify: `hw/bsp/lpc55/boards/lpcxpresso55s28/board.cmake` +- Modify: `hw/bsp/lpc11/boards/lpcxpresso11u37/lpc11u37.ld` + +**Interfaces:** none consumed or produced. + +- [ ] **Step 1: Delete the redundant RHPORT block** + +`hw/bsp/lpc55/family.cmake` already applies the identical guarded defaults (`RHPORT_DEVICE 1`, +`RHPORT_HOST 0`) after including the board file, so remove these lines from +`board.cmake` entirely: + +```cmake +# device highspeed, host fullspeed; guarded so a -D override on the cmake command line wins +if (NOT DEFINED RHPORT_DEVICE) + set(RHPORT_DEVICE 1) +endif () +if (NOT DEFINED RHPORT_HOST) + set(RHPORT_HOST 0) +endif () +``` + +Leave `board.mk`'s `RHPORT_DEVICE ?= 1` / `RHPORT_HOST ?= 0` alone — `?=` is the idiomatic +Make form and matches sibling boards. + +- [ ] **Step 2: Prove the defaults and the override still work** + +Run: + +```bash +cd examples && rm -rf /tmp/rh-default /tmp/rh-override +cmake -B /tmp/rh-default -DBOARD=lpcxpresso55s28 -G Ninja . > /tmp/rh-default.log 2>&1 +grep -m1 "RHPORT_DEVICE" /tmp/rh-default.log || cmake -B /tmp/rh-default -DBOARD=lpcxpresso55s28 -G Ninja -LA . | grep -E "^RHPORT_(DEVICE|HOST)" +cmake -B /tmp/rh-override -DBOARD=lpcxpresso55s28 -DRHPORT_DEVICE=0 -DRHPORT_HOST=1 -G Ninja -LA . | grep -E "^RHPORT_(DEVICE|HOST)" +``` + +Expected: the default configure yields device 1 / host 0; the override configure yields +device 0 / host 1. Then rebuild the real tree: `cmake --build cmake-build-lpcxpresso55s28`. + +- [ ] **Step 3: Correct the linker-script comment and relabel the ASSERT** + +In `lpc11u37.ld`, replace the comment block above `__user_stack_top` and the ASSERT with: + +```text + /* Main (MSP/ISR) stack lives at the top of the USB SRAM bank: the 8K main bank is packed so + tight that only ~280 B remained above .bss, and ISR frames overflowed into the topmost task + stack (cdc_msc_freertos hard fault). Nothing else is placed in this bank in either build + system, so the stack owns all 2 KB; the ASSERT is future-proofing in case USB buffers are + ever mapped here again. */ + __user_stack_top = ORIGIN(RamUsb2) + LENGTH(RamUsb2); + ASSERT(__user_stack_top - (ADDR(.noinit_RAM2) + SIZEOF(.noinit_RAM2)) >= 0x200, + "main stack headroom in RamUsb2 below 512 bytes") +``` + +- [ ] **Step 4: Build both build systems for lpc11u37** + +Run: + +```bash +cmake --build examples/cmake-build-lpcxpresso11u37 +cd examples/device/cdc_msc_freertos && make -j8 BOARD=lpcxpresso11u37 all && cd ../../.. +``` + +Expected: both succeed. + +- [ ] **Step 5: Commit** + +```bash +git add hw/bsp/lpc55/boards/lpcxpresso55s28/board.cmake hw/bsp/lpc11/boards/lpcxpresso11u37/lpc11u37.ld +git commit -m "bsp: drop duplicated lpc55s28 rhport defaults, fix lpc11u37 comment + +hw/bsp/lpc55/family.cmake already applies the same guarded rhport defaults +after including the board file, so the board-level copy only added a second +place to keep in sync. + +The lpc11u37 linker comment still described USB buffers living in RamUsb2, +a placement the same branch removed; nothing lands there now, so say so and +label the headroom assert as future-proofing." +``` + +--- + +### Task 7: Stop halting the target when a DCD legitimately refuses a transfer + +**Files:** +- Modify: `src/device/usbd.c` (`usbd_edpt_xfer()` failure arm) + +**Interfaces:** none consumed or produced. + +- [ ] **Step 1: Remove the breakpoint from the DCD-refusal path** + +Replace the failure arm of `usbd_edpt_xfer()`: + +```c + } else { + // DCD error, mark endpoint as ready to allow next transfer + _usbd_dev.ep_status[epnum][dir] &= (uint8_t) ~(TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED); + TU_LOG_USBD("FAILED\r\n"); + TU_BREAKPOINT(); + return false; + } +``` + +with: + +```c + } else { + // Driver refused the transfer, mark endpoint as ready to allow next transfer. This is a + // recoverable condition (e.g. a new setup superseding a control response), not a bug, so + // do not break into the debugger - TU_BREAKPOINT() halts the CPU whenever a probe is + // attached, which on a test rig is always. + _usbd_dev.ep_status[epnum][dir] &= (uint8_t) ~(TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED); + TU_LOG_USBD("FAILED\r\n"); + return false; + } +``` + +- [ ] **Step 2: Confirm no other stack path relies on that breakpoint** + +Run: `grep -n "TU_BREAKPOINT" src/device/*.c src/device/*.h` +Expected: no remaining hits inside `usbd_edpt_xfer`; other occurrences (if any) are in +unrelated assert macros and stay as they are. + +- [ ] **Step 3: Build and run unit tests** + +Run: + +```bash +cmake --build examples/cmake-build-mimxrt1064_evk +cd test/unit-test && ceedling test:all && cd ../.. +``` + +Expected: build succeeds, all unit tests pass. + +- [ ] **Step 4: Commit** + +```bash +git add src/device/usbd.c +git commit -m "usbd: do not breakpoint when a driver refuses a transfer + +TU_BREAKPOINT() is not gated on CFG_TUSB_DEBUG - it halts the CPU whenever +a debugger is attached, which on a test rig is always. A driver declining a +transfer is recoverable (a new setup superseding a control response, for +one) and the endpoint is already released for the retry, so a halted target +turns a self-healing case into a dead board." +``` + +--- + +### Task 8: Full validation on hardware + +**Files:** none modified — this task produces the evidence for the PR description. + +**Interfaces:** consumes the firmware built by Tasks 1-7. + +- [ ] **Step 1: Software gate** + +Run: + +```bash +pre-commit run --all-files +cd examples +for b in mimxrt1064_evk lpcxpresso18s37 lpcxpresso11u37 lpcxpresso55s28; do + rm -rf cmake-build-$b && cmake -B cmake-build-$b -DBOARD=$b -G Ninja -DCMAKE_BUILD_TYPE=MinSizeRel . && cmake --build cmake-build-$b || echo "FAILED $b" +done +cd .. +``` + +Expected: pre-commit all green; all four boards build every example. + +- [ ] **Step 2: Make-build regression checks** + +Run: + +```bash +cd examples/host/cdc_msc_hid && make -j8 BOARD=lpcxpresso55s28 all && cd ../../.. +cd examples/device/cdc_msc_throughput && make -j8 BOARD=lpcxpresso11u37 all && cd ../../.. +``` + +Expected: both link (these two were broken earlier in the branch and are the regression +canaries for the BSP changes). + +- [ ] **Step 3: Flash with verification (mandatory)** + +The mimxrt1064_evk has twice accepted a flash that silently did not take, so every load in +this task uses `verifyfile`. For each board, write a J-Link script of this shape and run it: + +``` +r +h +loadfile examples/cmake-build-/device/usbtest/usbtest.elf +verifyfile examples/cmake-build-/device/usbtest/usbtest.elf +r +g +qc +``` + +Probes and devices: `mimxrt1064_evk` = `-USB 000725299165 -device MIMXRT1064xxx6A`, +`lpcxpresso55s28` = `-USB 000727031389 -device LPC55S28`, +`lpcxpresso11u37` = `-USB 000724441579 -device LPC11U37/401`. +Invoke as `JLinkExe -if swd -speed 4000 -autoconnect 1 -NoGui 1 -CommandFile