From 72f00d7c6d1124d90587e53f0ceadac5a3258cbd Mon Sep 17 00:00:00 2001 From: Tomas Rezucha Date: Thu, 7 Dec 2023 10:11:41 +0100 Subject: [PATCH] feat(usb/host): Update ISOC scheduler for HS endpoints USB-OTG uses 'sched_info' field of HCTSIZ register to schedule transactions in USB microframes. --- components/hal/include/hal/usb_dwc_hal.h | 16 +-- components/hal/include/hal/usb_dwc_ll.h | 34 ++++++ components/hal/usb_dwc_hal.c | 53 +++++++-- components/usb/hcd_dwc.c | 96 ++++++++-------- .../usb/test_apps/hcd/main/test_hcd_common.c | 1 - .../usb/test_apps/hcd/main/test_hcd_isoc.c | 106 +++++++++++++++++- 6 files changed, 238 insertions(+), 68 deletions(-) diff --git a/components/hal/include/hal/usb_dwc_hal.h b/components/hal/include/hal/usb_dwc_hal.h index 2ee7104b6f..9b2a2c3bc3 100644 --- a/components/hal/include/hal/usb_dwc_hal.h +++ b/components/hal/include/hal/usb_dwc_hal.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2020-2023 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2020-2024 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -141,7 +141,8 @@ typedef struct { }; struct { unsigned int interval; /**< The interval of the endpoint in frames (FS) or microframes (HS) */ - uint32_t phase_offset_frames; /**< Phase offset in number of frames */ + uint32_t offset; /**< Offset of this channel in the periodic scheduler */ + bool is_hs; /**< This endpoint is HighSpeed. Needed for Periodic Frame List (HAL layer) scheduling */ } periodic; /**< Characteristic for periodic (interrupt/isochronous) endpoints only */ } usb_dwc_hal_ep_char_t; @@ -425,17 +426,6 @@ static inline void usb_dwc_hal_port_set_frame_list(usb_dwc_hal_context_t *hal, u hal->frame_list_len = len; } -/** - * @brief Get the pointer to the periodic scheduling frame list - * - * @param hal Context of the HAL layer - * @return uint32_t* Base address of the periodic scheduling frame list - */ -static inline uint32_t *usb_dwc_hal_port_get_frame_list(usb_dwc_hal_context_t *hal) -{ - return hal->periodic_frame_list; -} - /** * @brief Enable periodic scheduling * diff --git a/components/hal/include/hal/usb_dwc_ll.h b/components/hal/include/hal/usb_dwc_ll.h index 44c7fca824..49ade50405 100644 --- a/components/hal/include/hal/usb_dwc_ll.h +++ b/components/hal/include/hal/usb_dwc_ll.h @@ -818,6 +818,40 @@ static inline void usb_dwc_ll_hctsiz_init(volatile usb_dwc_host_chan_regs_t *cha chan->hctsiz_reg.val = hctsiz.val; } +static inline void usb_dwc_ll_hctsiz_set_sched_info(volatile usb_dwc_host_chan_regs_t *chan, int tokens_per_frame, int offset) +{ + // @see USB-OTG databook: Table 5-47 + // This function is relevant only for HS + usb_dwc_hctsiz_reg_t hctsiz; + hctsiz.val = chan->hctsiz_reg.val; + uint8_t sched_info_val; + switch (tokens_per_frame) { + case 1: + offset %= 8; // If the required offset > 8, we must wrap around to SCHED_INFO size = 8 + sched_info_val = 0b00000001; + break; + case 2: + offset %= 4; + sched_info_val = 0b00010001; + break; + case 4: + offset %= 2; + sched_info_val = 0b01010101; + break; + case 8: + offset = 0; + sched_info_val = 0b11111111; + break; + default: + abort(); + break; + } + sched_info_val <<= offset; + hctsiz.xfersize &= ~(0xFF); + hctsiz.xfersize |= sched_info_val; + chan->hctsiz_reg.val = hctsiz.val; +} + // ---------------------------- HCDMAi Register -------------------------------- static inline void usb_dwc_ll_hcdma_set_qtd_list_addr(volatile usb_dwc_host_chan_regs_t *chan, void *dmaaddr, uint32_t qtd_idx) diff --git a/components/hal/usb_dwc_hal.c b/components/hal/usb_dwc_hal.c index 7cc4ae7f83..bcc8870ed4 100644 --- a/components/hal/usb_dwc_hal.c +++ b/components/hal/usb_dwc_hal.c @@ -6,7 +6,8 @@ #include #include -#include +#include // For memset() +#include // For abort() #include "sdkconfig.h" #include "soc/chip_revision.h" #include "soc/usb_dwc_cfg.h" @@ -335,12 +336,48 @@ void usb_dwc_hal_chan_set_ep_char(usb_dwc_hal_context_t *hal, usb_dwc_hal_chan_t chan_obj->type = ep_char->type; //If this is a periodic endpoint/channel, set its schedule in the frame list if (ep_char->type == USB_DWC_XFER_TYPE_ISOCHRONOUS || ep_char->type == USB_DWC_XFER_TYPE_INTR) { - HAL_ASSERT((int)ep_char->periodic.interval <= (int)hal->frame_list_len); //Interval cannot exceed the length of the frame list - //Find the effective offset in the frame list (in case the phase_offset_frames > interval) - int offset = ep_char->periodic.phase_offset_frames % ep_char->periodic.interval; - //Schedule the channel in the frame list - for (int i = offset; i < hal->frame_list_len; i+= ep_char->periodic.interval) { - hal->periodic_frame_list[i] |= 1 << chan_obj->flags.chan_idx; + unsigned int interval_frame_list = ep_char->periodic.interval; + unsigned int offset_frame_list = ep_char->periodic.offset; + // Periodic Frame List works with USB frames. For HS endpoints we must divide interval[microframes] by 8 to get interval[frames] + if (ep_char->periodic.is_hs) { + interval_frame_list /= 8; + offset_frame_list /= 8; + } + // Interval in Periodic Frame List must be power of 2. + // This is not a HW restriction. It is just a lot easier to schedule channels like this. + if (interval_frame_list >= (int)hal->frame_list_len) { // Upper limits is Periodic Frame List length + interval_frame_list = (int)hal->frame_list_len; + } else if (interval_frame_list >= 32) { + interval_frame_list = 32; + } else if (interval_frame_list >= 16) { + interval_frame_list = 16; + } else if (interval_frame_list >= 8) { + interval_frame_list = 8; + } else if (interval_frame_list >= 4) { + interval_frame_list = 4; + } else if (interval_frame_list >= 2) { + interval_frame_list = 2; + } else { // Lower limit is 1 + interval_frame_list = 1; + } + // Schedule the channel in the frame list + for (int i = 0; i < hal->frame_list_len; i+= interval_frame_list) { + int index = (offset_frame_list + i) % hal->frame_list_len; + hal->periodic_frame_list[index] |= 1 << chan_obj->flags.chan_idx; + } + // For HS endpoints we must write to sched_info field of HCTSIZ register to schedule microframes + if (ep_char->periodic.is_hs) { + unsigned int tokens_per_frame; + if (ep_char->periodic.interval >= 8) { + tokens_per_frame = 1; // 1 token every 8 microframes + } else if (ep_char->periodic.interval >= 4) { + tokens_per_frame = 2; // 1 token every 4 microframes + } else if (ep_char->periodic.interval >= 2) { + tokens_per_frame = 4; // 1 token every 2 microframes + } else { + tokens_per_frame = 8; // 1 token every microframe + } + usb_dwc_ll_hctsiz_set_sched_info(chan_obj->regs, tokens_per_frame, ep_char->periodic.offset); } } } @@ -490,7 +527,7 @@ usb_dwc_hal_chan_event_t usb_dwc_hal_chan_decode_intr(usb_dwc_hal_chan_t *chan_o */ chan_event = USB_DWC_HAL_CHAN_EVENT_NONE; } else { - abort(); //Should never reach this point + abort(); } return chan_event; } diff --git a/components/usb/hcd_dwc.c b/components/usb/hcd_dwc.c index f529ff1b3e..3171467e72 100644 --- a/components/usb/hcd_dwc.c +++ b/components/usb/hcd_dwc.c @@ -60,8 +60,10 @@ #define XFER_LIST_LEN_CTRL 3 // One descriptor for each stage #define XFER_LIST_LEN_BULK 2 // One descriptor for transfer, one to support an extra zero length packet +// Same length as the frame list makes it easier to schedule. Must be power of 2 +// FS: Must be 2-64. HS: Must be 8-256. See USB-OTG databook Table 5-47 #define XFER_LIST_LEN_INTR FRAME_LIST_LEN -#define XFER_LIST_LEN_ISOC FRAME_LIST_LEN // Same length as the frame list makes it easier to schedule. Must be power of 2 +#define XFER_LIST_LEN_ISOC FRAME_LIST_LEN // ------------------------ Flags -------------------------- @@ -1745,27 +1747,14 @@ static void pipe_set_ep_char(const hcd_pipe_config_t *pipe_config, usb_transfer_ } else { interval_value = (1 << (pipe_config->ep_desc->bInterval - 1)); } - // Round down interval to nearest power of 2 - if (interval_value >= 32) { - interval_value = 32; - } else if (interval_value >= 16) { - interval_value = 16; - } else if (interval_value >= 8) { - interval_value = 8; - } else if (interval_value >= 4) { - interval_value = 4; - } else if (interval_value >= 2) { - interval_value = 2; - } else if (interval_value >= 1) { - interval_value = 1; - } ep_char->periodic.interval = interval_value; // We are the Nth pipe to be allocated. Use N as a phase offset unsigned int xfer_list_len = (type == USB_TRANSFER_TYPE_INTR) ? XFER_LIST_LEN_INTR : XFER_LIST_LEN_ISOC; - ep_char->periodic.phase_offset_frames = pipe_idx & (xfer_list_len - 1); + ep_char->periodic.offset = (pipe_idx % xfer_list_len) % interval_value; + ep_char->periodic.is_hs = (pipe_config->dev_speed == USB_SPEED_HIGH); } else { ep_char->periodic.interval = 0; - ep_char->periodic.phase_offset_frames = 0; + ep_char->periodic.offset = 0; } } @@ -2219,14 +2208,16 @@ static inline void _buffer_fill_intr(dma_buffer_block_t *buffer, usb_transfer_t buffer->flags.intr.zero_len_packet = zero_len_packet; } -static inline void _buffer_fill_isoc(dma_buffer_block_t *buffer, usb_transfer_t *transfer, bool is_in, int mps, int interval, int start_idx) +static inline void IRAM_ATTR _buffer_fill_isoc(dma_buffer_block_t *buffer, usb_transfer_t *transfer, bool is_in, int mps, int interval, int start_idx) { assert(interval > 0); + assert(__builtin_popcount(interval) == 1); // Isochronous interval must be power of 2 according to USB2.0 specification int total_num_desc = transfer->num_isoc_packets * interval; assert(total_num_desc <= XFER_LIST_LEN_ISOC); int desc_idx = start_idx; int bytes_filled = 0; - // For each packet, fill in a descriptor and a interval-1 blank descriptor after it + // Zeroize the whole QTD, so we can focus only on the active descriptors + memset(buffer->xfer_desc_list, 0, XFER_LIST_LEN_ISOC * sizeof(usb_dwc_ll_dma_qtd_t)); for (int pkt_idx = 0; pkt_idx < transfer->num_isoc_packets; pkt_idx++) { int xfer_len = transfer->isoc_packet_desc[pkt_idx].num_bytes; uint32_t flags = (is_in) ? USB_DWC_HAL_XFER_DESC_FLAG_IN : 0; @@ -2236,16 +2227,8 @@ static inline void _buffer_fill_isoc(dma_buffer_block_t *buffer, usb_transfer_t } usb_dwc_hal_xfer_desc_fill(buffer->xfer_desc_list, desc_idx, &transfer->data_buffer[bytes_filled], xfer_len, flags); bytes_filled += xfer_len; - if (++desc_idx >= XFER_LIST_LEN_ISOC) { - desc_idx = 0; - } - // Clear descriptors for unscheduled frames - for (int i = 0; i < interval - 1; i++) { - usb_dwc_hal_xfer_desc_clear(buffer->xfer_desc_list, desc_idx); - if (++desc_idx >= XFER_LIST_LEN_ISOC) { - desc_idx = 0; - } - } + desc_idx += interval; + desc_idx %= XFER_LIST_LEN_ISOC; } // Update buffer members and flags buffer->flags.isoc.num_qtds = total_num_desc; @@ -2254,7 +2237,7 @@ static inline void _buffer_fill_isoc(dma_buffer_block_t *buffer, usb_transfer_t buffer->flags.isoc.next_start_idx = desc_idx; } -static void _buffer_fill(pipe_t *pipe) +static void IRAM_ATTR _buffer_fill(pipe_t *pipe) { // Get an URB from the pending tailq urb_t *urb = TAILQ_FIRST(&pipe->pending_urb_tailq); @@ -2276,29 +2259,46 @@ static void _buffer_fill(pipe_t *pipe) break; } case USB_DWC_XFER_TYPE_ISOCHRONOUS: { - uint32_t start_idx; + uint16_t start_idx; + // Interval in frames (FS) or microframes (HS). But it does not matter here, as each QTD represents one transaction in a frame or microframe + unsigned int interval = pipe->ep_char.periodic.interval; + if (interval > XFER_LIST_LEN_ISOC) { + // Each QTD in the list corresponds to one frame/microframe. Interval > Descriptor_list does not make sense here. + interval = XFER_LIST_LEN_ISOC; + } if (pipe->multi_buffer_control.buffer_num_to_exec == 0) { // There are no more previously filled buffers to execute. We need to calculate a new start index based on HFNUM and the pipe's schedule - uint32_t cur_frame_num = usb_dwc_hal_port_get_cur_frame_num(pipe->port->hal); - uint32_t cur_mod_idx_no_offset = (cur_frame_num - pipe->ep_char.periodic.phase_offset_frames) & (XFER_LIST_LEN_ISOC - 1); // Get the modulated index (i.e., the Nth desc in the descriptor list) - // This is the non-offset modulated QTD index of the last scheduled interval - uint32_t last_interval_mod_idx_no_offset = (cur_mod_idx_no_offset / pipe->ep_char.periodic.interval) * pipe->ep_char.periodic.interval; // Floor divide and the multiply again - uint32_t next_interval_idx_no_offset = (last_interval_mod_idx_no_offset + pipe->ep_char.periodic.interval); - // We want at least a half interval or 2 frames of buffer space - if (next_interval_idx_no_offset - cur_mod_idx_no_offset > (pipe->ep_char.periodic.interval / 2) - && next_interval_idx_no_offset - cur_mod_idx_no_offset >= 2) { - start_idx = (next_interval_idx_no_offset + pipe->ep_char.periodic.phase_offset_frames) & (XFER_LIST_LEN_ISOC - 1); - } else { - // Not enough time until the next schedule, add another interval to it. - start_idx = (next_interval_idx_no_offset + pipe->ep_char.periodic.interval + pipe->ep_char.periodic.phase_offset_frames) & (XFER_LIST_LEN_ISOC - 1); + uint16_t cur_frame_num = usb_dwc_hal_port_get_cur_frame_num(pipe->port->hal); + start_idx = cur_frame_num + 1; // This is the next frame that the periodic scheduler will fetch + uint16_t rem_time = usb_dwc_ll_hfnum_get_frame_time_rem(pipe->port->hal->dev); + + // If there is not enough time remaining in this frame, consider the next frame as start index + // The remaining time is in USB PHY clocks. The threshold value is time between buffer fill and execute (6-11us) = 180 + 5 x num_packets + if (rem_time < 195 + 5 * transfer->num_isoc_packets) { + if (rem_time > 165 + 5 * transfer->num_isoc_packets) { + // If the remaining time is +-15 PHY clocks around the threshold value we cannot be certain whether we will schedule it in time for this frame + // Busy wait 10us to be sure that we are at the beginning of next frame/microframe + esp_rom_delay_us(10); + } + start_idx++; } + + // Only every (interval + offset) transfer belongs to this channel + // Following calculation effectively rounds up to nearest (interval + offset) + if (interval > 1) { + uint32_t interval_offset = (start_idx - pipe->ep_char.periodic.offset) % interval; // Can be <0, interval) + if (interval_offset > 0) { + start_idx += interval - interval_offset; + } + } + start_idx %= XFER_LIST_LEN_ISOC; } else { // Start index is based on previously filled buffer uint32_t prev_buffer_idx = (pipe->multi_buffer_control.wr_idx - 1) & (NUM_BUFFERS - 1); dma_buffer_block_t *prev_filled_buffer = pipe->buffers[prev_buffer_idx]; start_idx = prev_filled_buffer->flags.isoc.next_start_idx; } - _buffer_fill_isoc(buffer_to_fill, transfer, is_in, mps, (int)pipe->ep_char.periodic.interval, start_idx); + _buffer_fill_isoc(buffer_to_fill, transfer, is_in, mps, (int)interval, start_idx); break; } case USB_DWC_XFER_TYPE_BULK: { @@ -2324,7 +2324,7 @@ static void _buffer_fill(pipe_t *pipe) pipe->multi_buffer_control.buffer_num_to_exec++; } -static void _buffer_exec(pipe_t *pipe) +static void IRAM_ATTR _buffer_exec(pipe_t *pipe) { assert(pipe->multi_buffer_control.rd_idx != pipe->multi_buffer_control.wr_idx || pipe->multi_buffer_control.buffer_num_to_exec > 0); dma_buffer_block_t *buffer_to_exec = pipe->buffers[pipe->multi_buffer_control.rd_idx]; @@ -2621,6 +2621,12 @@ esp_err_t hcd_urb_enqueue(hcd_pipe_handle_t pipe_hdl, urb_t *urb) // Check that URB has not already been enqueued HCD_CHECK(urb->hcd_ptr == NULL && urb->hcd_var == URB_HCD_STATE_IDLE, ESP_ERR_INVALID_STATE); pipe_t *pipe = (pipe_t *)pipe_hdl; + // Check if the ISOC pipe can handle all packets: + // In case the pipe's interval is too long and there are too many ISOC packets, they might not fit into the transfer descriptor list + HCD_CHECK( + !((pipe->ep_char.type == USB_DWC_XFER_TYPE_ISOCHRONOUS) && (urb->transfer.num_isoc_packets * pipe->ep_char.periodic.interval > XFER_LIST_LEN_ISOC)), + ESP_ERR_INVALID_SIZE + ); // Sync user's data from cache to memory. For OUT and CTRL transfers CACHE_SYNC_DATA_BUFFER_C2M(pipe, urb); diff --git a/components/usb/test_apps/hcd/main/test_hcd_common.c b/components/usb/test_apps/hcd/main/test_hcd_common.c index 219e552cc4..25145c3f24 100644 --- a/components/usb/test_apps/hcd/main/test_hcd_common.c +++ b/components/usb/test_apps/hcd/main/test_hcd_common.c @@ -236,7 +236,6 @@ hcd_pipe_handle_t test_hcd_pipe_alloc(hcd_port_handle_t port_hdl, const usb_ep_d //Create a queue for pipe callback to queue up pipe events QueueHandle_t pipe_evt_queue = xQueueCreate(EVENT_QUEUE_LEN, sizeof(pipe_event_msg_t)); TEST_ASSERT_NOT_NULL(pipe_evt_queue); - printf("Creating pipe\n"); hcd_pipe_config_t pipe_config = { .callback = pipe_callback, .callback_arg = (void *)pipe_evt_queue, diff --git a/components/usb/test_apps/hcd/main/test_hcd_isoc.c b/components/usb/test_apps/hcd/main/test_hcd_isoc.c index 1e0124246b..aff0440b49 100644 --- a/components/usb/test_apps/hcd/main/test_hcd_isoc.c +++ b/components/usb/test_apps/hcd/main/test_hcd_isoc.c @@ -1,11 +1,12 @@ /* - * SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ #include #include +#include "soc/usb_dwc_cfg.h" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include "unity.h" @@ -18,6 +19,7 @@ #define ISOC_PACKET_SIZE MOCK_ISOC_EP_MPS #define URB_DATA_BUFF_SIZE (NUM_PACKETS_PER_URB * ISOC_PACKET_SIZE) #define POST_ENQUEUE_DELAY_US 20 +#define ENQUEUE_DELAY (OTG_HSPHY_INTERFACE ? 100 : 500) // With this delay we want to enqueue the URBs at different times /* Test HCD ISOC pipe URBs @@ -95,6 +97,108 @@ TEST_CASE("Test HCD isochronous pipe URBs", "[isoc][full_speed]") test_hcd_wait_for_disconn(port_hdl, false); } +/* +Test HCD ISOC pipe URBs with all channels and intervals combinations + +Purpose: + - Test that the ISOC scheduler correctly schedules all channels and intervals + +Procedure: + - Setup HCD and wait for connection + - Allocate default pipe and enumerate the device + - Allocate an isochronous pipe and multiple URBs. Each URB should contain multiple packets to test HCD's ability to + schedule an URB across multiple intervals. + - Repeat for all channels and intervals + - Enqueue those URBs + - Expect HCD_PIPE_EVENT_URB_DONE for each URB. Verify that data is correct using logic analyzer + - Deallocate URBs + - Teardown +*/ +TEST_CASE("Test HCD isochronous pipe URBs all", "[isoc][full_speed]") +{ + usb_speed_t port_speed = test_hcd_wait_for_conn(port_hdl); //Trigger a connection + //The MPS of the ISOC OUT pipe is quite large, so we need to bias the FIFO sizing + TEST_ASSERT_EQUAL(ESP_OK, hcd_port_set_fifo_bias(port_hdl, HCD_PORT_FIFO_BIAS_PTX)); + vTaskDelay(pdMS_TO_TICKS(100)); //Short delay send of SOF (for FS) or EOPs (for LS) + + //Enumerate and reset device + hcd_pipe_handle_t default_pipe = test_hcd_pipe_alloc(port_hdl, NULL, 0, port_speed); //Create a default pipe (using a NULL EP descriptor) + uint8_t dev_addr = test_hcd_enum_device(default_pipe); + + urb_t *urb_list[NUM_URBS]; + hcd_pipe_handle_t unused_pipes[OTG_NUM_HOST_CHAN]; + + // For all channels + for (int channel = 0; channel < OTG_NUM_HOST_CHAN - 1; channel++) { + // Allocate unused pipes, so the active isoc_out_pipe uses different channel index + for (int ch = 0; ch < channel; ch++) { + unused_pipes[ch] = test_hcd_pipe_alloc(port_hdl, &mock_isoc_out_ep_desc, dev_addr + 1, port_speed); + } + + // For all intervals + for (int interval = 1; interval <= 6; interval++) { + + vTaskDelay(5); + unsigned num_packets_per_urb = 32; // This is maximum number of packets if interval = 1. This is limited by FRAME_LIST_LEN + num_packets_per_urb >>= (interval - 1); + //Create ISOC OUT pipe + usb_ep_desc_t isoc_out_ep = mock_isoc_out_ep_desc; // Implicit copy + isoc_out_ep.bInterval = interval; + isoc_out_ep.bEndpointAddress = interval; // So you can see the bInterval value in trace + hcd_pipe_handle_t isoc_out_pipe = test_hcd_pipe_alloc(port_hdl, &isoc_out_ep, channel + 1, port_speed); // Channel number represented in dev_num, so you can see it in trace + + //Initialize URBs + for (int urb_idx = 0; urb_idx < NUM_URBS; urb_idx++) { + urb_list[urb_idx] = test_hcd_alloc_urb(num_packets_per_urb, num_packets_per_urb * ISOC_PACKET_SIZE); + urb_list[urb_idx]->transfer.num_bytes = num_packets_per_urb * ISOC_PACKET_SIZE; + urb_list[urb_idx]->transfer.context = URB_CONTEXT_VAL; + for (int pkt_idx = 0; pkt_idx < num_packets_per_urb; pkt_idx++) { + urb_list[urb_idx]->transfer.isoc_packet_desc[pkt_idx].num_bytes = ISOC_PACKET_SIZE; + //Each packet will consist of the same byte, but each subsequent packet's byte will increment (i.e., packet 0 transmits all 0x0, packet 1 transmits all 0x1) + memset(&urb_list[urb_idx]->transfer.data_buffer[pkt_idx * ISOC_PACKET_SIZE], (urb_idx * num_packets_per_urb) + pkt_idx, ISOC_PACKET_SIZE); + } + } + + // Add a delay so we start scheduling the transactions at different time in USB frame + esp_rom_delay_us(ENQUEUE_DELAY * interval + ENQUEUE_DELAY * channel); + + //Enqueue URBs + for (int i = 0; i < NUM_URBS; i++) { + TEST_ASSERT_EQUAL(ESP_OK, hcd_urb_enqueue(isoc_out_pipe, urb_list[i])); + } + //Wait for each done event from each URB + for (int i = 0; i < NUM_URBS; i++) { + test_hcd_expect_pipe_event(isoc_out_pipe, HCD_PIPE_EVENT_URB_DONE); + } + //Dequeue URBs + for (int urb_idx = 0; urb_idx < NUM_URBS; urb_idx++) { + urb_t *urb = hcd_urb_dequeue(isoc_out_pipe); + TEST_ASSERT_EQUAL(urb_list[urb_idx], urb); + TEST_ASSERT_EQUAL(URB_CONTEXT_VAL, urb->transfer.context); + //Overall URB status and overall number of bytes + TEST_ASSERT_EQUAL(num_packets_per_urb * ISOC_PACKET_SIZE, urb->transfer.actual_num_bytes); + TEST_ASSERT_EQUAL_MESSAGE(USB_TRANSFER_STATUS_COMPLETED, urb->transfer.status, "Transfer NOT completed"); + for (int pkt_idx = 0; pkt_idx < num_packets_per_urb; pkt_idx++) { + TEST_ASSERT_EQUAL_MESSAGE(USB_TRANSFER_STATUS_COMPLETED, urb->transfer.isoc_packet_desc[pkt_idx].status, "Transfer NOT completed"); + } + } + //Free URB list and pipe + for (int i = 0; i < NUM_URBS; i++) { + test_hcd_free_urb(urb_list[i]); + } + test_hcd_pipe_free(isoc_out_pipe); + } + + // Free unused pipes + for (int ch = 0; ch < channel; ch++) { + test_hcd_pipe_free(unused_pipes[ch]); + } + } + test_hcd_pipe_free(default_pipe); + //Cleanup + test_hcd_wait_for_disconn(port_hdl, false); +} + /* Test a port sudden disconnect with an active ISOC pipe