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. --- examples/host/audio_host/CMakeLists.txt | 30 ++++ examples/host/audio_host/Makefile | 14 ++ examples/host/audio_host/README.md | 98 +++++++++++++ examples/host/audio_host/src/app.h | 26 ++++ examples/host/audio_host/src/audio_app.c | 226 +++++++++++++++++++++++++++++ examples/host/audio_host/src/main.c | 73 ++++++++++ examples/host/audio_host/src/tusb_config.h | 103 +++++++++++++ 7 files changed, 570 insertions(+) create mode 100644 examples/host/audio_host/CMakeLists.txt create mode 100644 examples/host/audio_host/Makefile create mode 100644 examples/host/audio_host/README.md create mode 100644 examples/host/audio_host/src/app.h create mode 100644 examples/host/audio_host/src/audio_app.c create mode 100644 examples/host/audio_host/src/main.c create mode 100644 examples/host/audio_host/src/tusb_config.h (limited to 'examples') diff --git a/examples/host/audio_host/CMakeLists.txt b/examples/host/audio_host/CMakeLists.txt new file mode 100644 index 000000000..0891f5829 --- /dev/null +++ b/examples/host/audio_host/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.20) + +include(${CMAKE_CURRENT_SOURCE_DIR}/../../../hw/bsp/family_support.cmake) + +project(audio_host C CXX ASM) + +# Checks this example is valid for the family and initializes the project +family_initialize_project(${PROJECT_NAME} ${CMAKE_CURRENT_LIST_DIR}) + +# Espressif has its own cmake build system +if(FAMILY STREQUAL "espressif") + return() +endif() + +add_executable(${PROJECT_NAME}) + +# Example source +target_sources(${PROJECT_NAME} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src/audio_app.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/main.c + ) + +# Example include +target_include_directories(${PROJECT_NAME} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src + ) + +# Configure compilation flags and libraries for the example without RTOS. +# See the corresponding function in hw/bsp/FAMILY/family.cmake for details. +family_configure_host_example(${PROJECT_NAME} noos) diff --git a/examples/host/audio_host/Makefile b/examples/host/audio_host/Makefile new file mode 100644 index 000000000..5c2e23184 --- /dev/null +++ b/examples/host/audio_host/Makefile @@ -0,0 +1,14 @@ +include ../../../hw/bsp/family_support.mk + +INC += \ + src \ + + +# Example source +EXAMPLE_SOURCE += \ + src/audio_app.c \ + src/main.c + +SRC_C += $(addprefix $(EXAMPLE_PATH)/, $(EXAMPLE_SOURCE)) + +include ../../../hw/bsp/family_rules.mk diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md new file mode 100644 index 000000000..4d8f66b71 --- /dev/null +++ b/examples/host/audio_host/README.md @@ -0,0 +1,98 @@ +# 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. + +## 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 + +## Supported Devices + +This example supports any UAC 1.0 compliant USB Audio device, such as: +- USB microphones +- USB speakers/headphones +- USB audio interfaces + +## Building + +### Using CMake (recommended) + +```bash +cd examples/host/audio_host +mkdir -p build && cd build +cmake -DBOARD= -G Ninja .. +cmake --build . +``` + +Replace `` with your target board name (e.g., `raspberry_pi_pico`, `stm32f407disco`, etc.) + +### Using Make + +```bash +cd examples/host/audio_host +make BOARD= all +``` + +## Flashing + +```bash +# Using CMake +ninja flash + +# Using Make +make BOARD= flash +``` + +## Usage + +1. Build and flash the example to your board +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 to 48kHz + - Receive audio samples from the device (IN endpoint) + - Send test sine wave audio to the device (OUT endpoint) + +## 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 +``` + +## 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 + +## 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 diff --git a/examples/host/audio_host/src/app.h b/examples/host/audio_host/src/app.h new file mode 100644 index 000000000..a807aeaa6 --- /dev/null +++ b/examples/host/audio_host/src/app.h @@ -0,0 +1,26 @@ +/* + * 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. + */ + +#ifndef TUSB_TINYUSB_EXAMPLES_APP_H +#define TUSB_TINYUSB_EXAMPLES_APP_H + +#include +#include +#include + +void audio_app_task(void); + +#endif diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c new file mode 100644 index 000000000..ac7596d91 --- /dev/null +++ b/examples/host/audio_host/src/audio_app.c @@ -0,0 +1,226 @@ +/* + * 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. + */ + +#include +#include "bsp/board_api.h" +#include "tusb.h" +#include "app.h" + +//--------------------------------------------------------------------+ +// 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 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]; + } +} + +// 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]); + } + } +} + +// 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) { + // 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); + } + 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 +//--------------------------------------------------------------------+ +void audio_app_task(void) { + if (!audio_mounted || !audio_ready) { + return; + } + + if (!audio_rx_busy) { + if (tuh_audio_receive(audio_dev_addr, audio_idx, audio_rx_buffer, CFG_TUH_AUDIO_EPIN_BUFSIZE)) { + audio_rx_busy = true; + } + } +} + +//--------------------------------------------------------------------+ +// TinyUSB Callbacks +//--------------------------------------------------------------------+ + + +// 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) { + 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); + } + + // 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; + + // Find endpoints and IN sampling frequency + uint32_t in_sam_freq = find_audio_endpoints(mount_cb_data); + + // Set IN sampling frequency before starting isochronous transfer + if (audio_ep_in != 0 && in_sam_freq != 0) { + sampling_freq = in_sam_freq; + 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); + } +} + +// 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; + } +} + +// 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)ep_addr; + audio_rx_busy = false; + + if (xferred_bytes > 0) { + 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); + tuh_audio_send(audio_dev_addr, audio_idx, audio_tx_buffer, xferred_bytes * 2); + } else { + // Stereo microphone, send directly to OUT endpoint + tuh_audio_send(audio_dev_addr, audio_idx, audio_rx_buffer, 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)ep_addr; + (void)xferred_bytes; + audio_tx_busy = false; +} diff --git a/examples/host/audio_host/src/main.c b/examples/host/audio_host/src/main.c new file mode 100644 index 000000000..b80cd2938 --- /dev/null +++ b/examples/host/audio_host/src/main.c @@ -0,0 +1,73 @@ +/* + * 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. + */ + +#include +#include +#include + +#include "bsp/board_api.h" +#include "tusb.h" +#include "app.h" + +//--------------------------------------------------------------------+ +// MACRO CONSTANT TYPEDEF PROTYPES +//--------------------------------------------------------------------+ +void led_blinking_task(void); + +/*------------- MAIN -------------*/ +int main(void) { + board_init(); + + printf("TinyUSB Host USB Audio Example\r\n"); + 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_init(BOARD_TUH_RHPORT, &host_init); + + board_init_after_tusb(); + + while (1) { + // tinyusb host task + tuh_task(); + led_blinking_task(); + audio_app_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 new file mode 100644 index 000000000..a4fb17fa5 --- /dev/null +++ b/examples/host/audio_host/src/tusb_config.h @@ -0,0 +1,103 @@ +/* + * 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. + */ + +#ifndef TUSB_CONFIG_H_ +#define TUSB_CONFIG_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +//-------------------------------------------------------------------- +// Common Configuration +//-------------------------------------------------------------------- + +#ifndef CFG_TUSB_MCU + #error CFG_TUSB_MCU must be defined +#endif + +#ifndef CFG_TUSB_OS + #define CFG_TUSB_OS OPT_OS_NONE +#endif + +#ifndef CFG_TUSB_DEBUG + #define CFG_TUSB_DEBUG 0 +#endif + +#ifndef CFG_TUH_MEM_SECTION + #define CFG_TUH_MEM_SECTION +#endif + +#ifndef CFG_TUH_MEM_ALIGN + #define CFG_TUH_MEM_ALIGN __attribute__((aligned(4))) +#endif + +//-------------------------------------------------------------------- +// Host Configuration +//-------------------------------------------------------------------- + +#define CFG_TUH_ENABLED 1 + +#if CFG_TUSB_MCU == OPT_MCU_RP2040 + #if (defined(CFG_TUH_RPI_PIO_USB) && CFG_TUH_RPI_PIO_USB) || (defined(CFG_TUH_MAX3421) && CFG_TUH_MAX3421) + #define BOARD_TUH_RHPORT 1 + #endif +#endif + +#define CFG_TUH_MAX_SPEED BOARD_TUH_MAX_SPEED + +#ifndef BOARD_TUH_RHPORT + #define BOARD_TUH_RHPORT 1 +#endif + +#ifndef BOARD_TUH_MAX_SPEED + #define BOARD_TUH_MAX_SPEED OPT_MODE_DEFAULT_SPEED +#endif + +//-------------------------------------------------------------------- +// Driver Configuration +//-------------------------------------------------------------------- + +#define CFG_TUH_ENUMERATION_BUFSIZE 512 + +#define CFG_TUH_HUB 1 +#define CFG_TUH_CDC 0 +#define CFG_TUH_HID 1 +#define CFG_TUH_MSC 0 +#define CFG_TUH_VENDOR 0 +#define CFG_TUH_AUDIO 1 + +// max device support (excluding hub device): 1 hub typically has 4 ports +#define CFG_TUH_DEVICE_MAX (3 * CFG_TUH_HUB + 1) + +//------------- Audio Host Config -------------// +#define CFG_TUH_AUDIO_MAX 2 +#define CFG_TUH_AUDIO_EPIN_BUFSIZE 192 +#define CFG_TUH_AUDIO_EPOUT_BUFSIZE 192 + +#ifdef __cplusplus +} +#endif + +#endif /* TUSB_CONFIG_H_ */ -- cgit v1.3.1 From f76a0f2c0addb4988da2ef7d993d3253a1667d1a Mon Sep 17 00:00:00 2001 From: 代码人生 Date: Thu, 16 Jul 2026 16:09:19 +0800 Subject: Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- examples/host/audio_host/src/audio_app.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) (limited to 'examples') diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c index ac7596d91..d264426fd 100644 --- a/examples/host/audio_host/src/audio_app.c +++ b/examples/host/audio_host/src/audio_app.c @@ -204,15 +204,20 @@ void tuh_audio_rx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes) { (void)ep_addr; audio_rx_busy = false; - if (xferred_bytes > 0) { + if (xferred_bytes > 0 && audio_ep_out != 0 && !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); - tuh_audio_send(audio_dev_addr, audio_idx, audio_tx_buffer, xferred_bytes * 2); + ok = tuh_audio_send(audio_dev_addr, audio_idx, audio_tx_buffer, xferred_bytes * 2); } else { // Stereo microphone, send directly to OUT endpoint - tuh_audio_send(audio_dev_addr, audio_idx, audio_rx_buffer, xferred_bytes); + ok = tuh_audio_send(audio_dev_addr, audio_idx, audio_rx_buffer, xferred_bytes); + } + + if (ok) { + audio_tx_busy = true; } } } -- 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 'examples') 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 'examples') 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 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 'examples') 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 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 'examples') 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 3c9e92c60abf3959ef0367f5965b53b2def28d58 Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Fri, 14 Aug 2026 15:21:24 -0300 Subject: example(midi2): report device identity in midi2_device --- examples/device/midi2_device/src/main.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'examples') diff --git a/examples/device/midi2_device/src/main.c b/examples/device/midi2_device/src/main.c index 2c77652dc..f0162ad04 100644 --- a/examples/device/midi2_device/src/main.c +++ b/examples/device/midi2_device/src/main.c @@ -565,6 +565,19 @@ const char* tud_midi2_fb_name_cb(uint8_t itf, uint8_t fb_idx) { return (fb_idx == 0) ? "Synth Out" : "Keys In"; } +// Sent when the host asks for a Device Identity Notification (Endpoint +// Discovery 'd' filter bit). Same four fields as the MIDI 1.0 Device Inquiry +// reply; 0x7D is the prototyping SysEx ID, placed in the first of the three +// manufacturer bytes. +bool tud_midi2_device_identity_cb(uint8_t itf, tud_midi2_device_identity_t* identity) { + (void)itf; + identity->manufacturer = 0x7D0000; + identity->family = 0x0001; + identity->model = 0x0001; + identity->sw_revision = 0x00010000; + return true; +} + //--------------------------------------------------------------------+ // Initial Setup - Program Change, CC, Per-Note Management //--------------------------------------------------------------------+ -- 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 'examples') 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