blob: 2b0b99101d9d5223716c33cef396a2b9a4820715 [file]
// SPDX-License-Identifier: MIT
#include <assert.h>
#include <stdio.h>
#include "tusb_option.h"
#include "common/tusb_common.h"
// Only the QH software tail contains a native pointer. On a 64-bit test host
// its size differs from the 32-bit target ABI. Recheck hardware layouts below.
#undef TU_VERIFY_STATIC
#define TU_VERIFY_STATIC(condition, ...) \
_Static_assert((condition) || sizeof(void*) == 8, "EHCI ABI")
#include "portable/ehci/ehci.h"
#undef TU_VERIFY_STATIC
#define TU_VERIFY_STATIC(condition, ...) _Static_assert(condition, __VA_ARGS__)
#include "portable/ehci/ehci.c"
_Static_assert(CFG_TUH_XFER_QUEUE_DEPTH == TEST_EXPECTED_QUEUE_DEPTH, "queue depth selection");
_Static_assert(sizeof(ehci_link_t) == 4, "link ABI");
_Static_assert(sizeof(ehci_qtd_t) == 32, "qTD ABI");
_Static_assert(offsetof(ehci_qhd_t, qtd_overlay) == 16, "QH hardware prefix");
_Static_assert(sizeof(ehci_itd_t) == 64, "iTD ABI");
_Static_assert(sizeof(ehci_sitd_t) == 32, "siTD ABI");
_Static_assert(sizeof(ehci_cap_registers_t) == 16, "capability register ABI");
static ehci_registers_t regs;
static ehci_cap_registers_t caps;
static tuh_bus_info_t buses[8];
static hcd_event_t event;
static unsigned events;
static unsigned queued_events;
static hcd_event_t terminal_events[16];
static uint8_t buffer[8192] TU_ATTR_ALIGNED(4096);
void hcd_int_enable(uint8_t rhport) { (void) rhport; }
void hcd_int_disable(uint8_t rhport) { (void) rhport; }
void usbh_spin_lock(bool in_isr) { (void) in_isr; }
void usbh_spin_unlock(bool in_isr) { (void) in_isr; }
bool tuh_bus_info_get(uint8_t daddr, tuh_bus_info_t* bus) {
memset(bus, 0, sizeof(*bus));
if (daddr >= TU_ARRAY_SIZE(buses)) {
return false;
}
*bus = buses[daddr];
return true;
}
void hcd_event_handler(hcd_event_t const* e, bool in_isr) {
(void) in_isr;
if (e->event_id == HCD_EVENT_XFER_COMPLETE && e->xfer_complete.result == XFER_RESULT_QUEUED) {
queued_events++;
return;
}
event = *e;
assert(events < TU_ARRAY_SIZE(terminal_events));
terminal_events[events++] = *e;
}
static void reset(uint8_t root_speed) {
memset(&ehci_data, 0, sizeof(ehci_data));
memset((void*)&regs, 0, sizeof(regs));
memset((void*)&caps, 0, sizeof(caps));
memset(buses, 0, sizeof(buses));
ehci_data.regs = &regs;
ehci_data.cap_regs = &caps;
regs.portsc = (uint32_t)root_speed << 26;
regs.frame_index = 800;
regs.command_bm.int_threshold = 8;
init_periodic_list(0);
events = queued_events = 0;
}
static void test_attach_debounce(void) {
reset(TUSB_SPEED_FULL);
regs.portsc |= EHCI_PORTSC_MASK_CURRENT_CONNECT_STATUS;
uint32_t const before = regs.portsc;
port_connect_status_change_isr(0);
assert(events == 1 && event.event_id == HCD_EVENT_DEVICE_ATTACH);
assert(regs.portsc == before); // Attach must not reset the port before USBH debounces it.
}
#if defined(TUP_USBIP_CHIPIDEA_HS) && CFG_TUH_CHIPIDEA_ISO_ENABLE && !CFG_TUH_MAX3421
#define TEST_ISO_STREAM_EP_COUNT 4
static bool open_ep(uint8_t addr, uint8_t speed, uint16_t size, uint8_t interval) {
buses[1].speed = speed;
tusb_desc_endpoint_t desc = {
.bLength = sizeof(desc), .bDescriptorType = TUSB_DESC_ENDPOINT,
.bEndpointAddress = addr, .bmAttributes = {.xfer = TUSB_XFER_ISOCHRONOUS},
.wMaxPacketSize = size, .bInterval = interval
};
return iso_ep_open(0, 1, &desc);
}
static void test_shared_pools(void) {
reset(TUSB_SPEED_HIGH);
buses[1].speed = TUSB_SPEED_HIGH;
tusb_desc_endpoint_t const intr_desc = {
.bLength = sizeof(intr_desc), .bDescriptorType = TUSB_DESC_ENDPOINT,
.bEndpointAddress = 0x83, .bmAttributes = {.xfer = TUSB_XFER_INTERRUPT},
.wMaxPacketSize = 64, .bInterval = 1
};
assert(hcd_edpt_open(0, 1, &intr_desc));
assert(hcd_edpt_xfer(0, 1, 0x83, buffer, 64));
ehci_qhd_t* qhd = qhd_get_from_addr(1, 0x83);
ehci_qtd_t* qtd = qhd->attached_qtd;
assert(qtd == &ehci_data.qtd_pool[0].qtd[0]);
// A live qTD in either half prevents the whole pair becoming an ISO TD.
assert(open_ep(0x81, TUSB_SPEED_HIGH, 64, 1));
iso_ep_t* ep = iso_ep_find(1, 0x81);
assert(qhd_get_from_addr(1, 0x81) == NULL);
assert(ep->req[0].td == &ehci_data.qtd_pool[1].iso);
assert(qtd_find_free() == &ehci_data.qtd_pool[0].qtd[1]);
for (size_t i = 0; i < CFG_TUH_XFER_QUEUE_DEPTH; i++) {
assert(ep->req[i].td == &ehci_data.qtd_pool[i + 1].iso);
assert(((uintptr_t) ep->req[i].td & 63) == 0);
}
assert(qtd->active && qtd->expected_bytes == 64);
// Async reclamation must not interpret ISO software state as a QH.
iso_ep_t const saved = *ep;
async_advance_isr(0);
assert(memcmp(ep, &saved, sizeof(saved)) == 0);
assert(qhd_get_from_addr(1, 0x83) == qhd);
// Release unpublished ISO descriptors and reuse their space as qTDs.
iso_ep_free(ep);
assert(iso_ep_find(1, 0x81) == NULL);
ehci_qtd_t* second = qtd_find_free();
qtd_init(second, buffer + 64, 32);
ehci_qtd_t* recycled = qtd_find_free();
assert(recycled == &ehci_data.qtd_pool[1].qtd[0]);
qtd_init(recycled, buffer + 128, 16);
assert(open_ep(0x81, TUSB_SPEED_HIGH, 64, 1));
assert(iso_ep_find(1, 0x81)->req[0].td == &ehci_data.qtd_pool[2].iso);
assert(second->active && recycled->active && qtd->active);
qtd->active = 0;
qtd->total_bytes = 0;
qhd_xfer_complete_isr(qhd);
assert(events == 1 && event.xfer_complete.len == 64 && !qtd->used);
assert(second->active && recycled->active);
// Exhaust the TD pool, allowing a partial ISO allocation before failure.
reset(TUSB_SPEED_HIGH);
for (size_t i = 0; i < QTD_MAX; i++) {
qtd = qtd_find_free();
assert(qtd != NULL);
qtd_init(qtd, buffer, 64);
}
assert(qtd_find_free() == NULL);
for (size_t i = 0; i + 1 < CFG_TUH_XFER_QUEUE_DEPTH; i++) {
ehci_data.qtd_pool[i].qtd[0].used = 0;
ehci_data.qtd_pool[i].qtd[1].used = 0;
}
assert(!open_ep(0x81, TUSB_SPEED_HIGH, 64, 1));
assert(iso_ep_find(1, 0x81) == NULL);
assert(qhd_find_free() == &ehci_data.qhd_pool[0].qhd);
for (size_t i = 0; i < TU_ARRAY_SIZE(ehci_data.qtd_is_iso); i++) {
assert(!ehci_data.qtd_is_iso[i]);
}
for (size_t i = 2 * (CFG_TUH_XFER_QUEUE_DEPTH - 1); i < QTD_MAX; i++) {
qtd = &ehci_data.qtd_pool[i / 2].qtd[i % 2];
assert(qtd->used && qtd->active && qtd->expected_bytes == 64);
}
}
static void test_native_fs(void) {
reset(TUSB_SPEED_FULL);
assert(open_ep(0x81, TUSB_SPEED_FULL, 1023, 1));
iso_ep_t* ep = iso_ep_find(1, 0x81);
assert(iso_xfer(0, ep, buffer + 4090, 1023));
ehci_sitd_t* td = &ep->req[ep->head].td->sitd;
assert(ep->req[ep->head].scheduled_uframe == 800);
assert(td->active && td->int_on_complete && td->total_bytes == 1023);
assert(td->int_smask == 0 && td->fl_int_cmask == 0);
assert(td->buffer[0] == (uint32_t)(uintptr_t)(buffer + 4090));
assert(td->buffer[1] == (uint32_t)(uintptr_t)(buffer + 4096));
td->active = 0;
td->total_bytes = 23;
regs.frame_index = 808;
iso_process(true);
assert(events == 1 && event.xfer_complete.len == 1000);
assert(event.xfer_complete.result == XFER_RESULT_SUCCESS);
iso_process(true);
assert(events == 1);
assert(iso_xfer(0, ep, buffer, 0));
td = &ep->req[ep->head].td->sitd;
td->active = 0;
regs.frame_index += 8;
iso_process(true);
assert(events == 2 && event.xfer_complete.len == 0);
assert(event.xfer_complete.result == XFER_RESULT_SUCCESS);
}
static void test_split(void) {
reset(TUSB_SPEED_HIGH);
buses[1].hub_addr = 2;
buses[1].hub_port = 3;
buses[2].speed = TUSB_SPEED_FULL;
buses[2].hub_addr = 3;
buses[2].hub_port = 4;
buses[3].speed = TUSB_SPEED_HIGH;
assert(!open_ep(0x81, TUSB_SPEED_FULL, 565, 1));
assert(open_ep(0x81, TUSB_SPEED_FULL, 564, 1));
iso_ep_t* ep = iso_ep_find(1, 0x81);
assert(iso_xfer(0, ep, buffer, 564));
ehci_sitd_t* td = &ep->req[ep->head].td->sitd;
assert(td->hub_addr == 3 && td->port_number == 4);
assert(td->int_smask == 4 && td->fl_int_cmask == 0xf0);
assert(open_ep(1, TUSB_SPEED_FULL, 1023, 1));
ep = iso_ep_find(1, 1);
assert(iso_xfer(0, ep, buffer, 1023));
td = &ep->req[ep->head].td->sitd;
assert(td->int_smask == 0x3f && td->fl_int_cmask == 0);
assert((td->buffer[1] & 0xfff) == (6 | 8));
}
static void test_split_audio(void) {
reset(TUSB_SPEED_HIGH);
buses[1].hub_addr = 2;
buses[1].hub_port = 1;
buses[2].speed = TUSB_SPEED_HIGH;
assert(open_ep(0x81, TUSB_SPEED_FULL, 98, 1));
assert(open_ep(0x01, TUSB_SPEED_FULL, 196, 1));
iso_ep_t* in = iso_ep_find(1, 0x81);
iso_ep_t* out = iso_ep_find(1, 0x01);
assert(iso_xfer(0, in, buffer, 98));
assert(iso_xfer(0, out, buffer + 128, 192));
ehci_sitd_t* in_td = &in->req[in->head].td->sitd;
ehci_sitd_t* out_td = &out->req[out->head].td->sitd;
// 48 kHz stereo needs two start-splits. Do not interleave an IN start
// with the OUT Begin/End sequence on the same transaction translator.
assert(out_td->int_smask == 3);
assert((out_td->buffer[1] & 0x1f) == (2 | 8));
assert((in_td->int_smask & out_td->int_smask) == 0);
assert(in_td->int_smask > out_td->int_smask);
}
static void test_hs(void) {
reset(TUSB_SPEED_HIGH);
assert(open_ep(0x82, TUSB_SPEED_HIGH, 1024 | (2 << 11), 1));
iso_ep_t* ep = iso_ep_find(1, 0x82);
assert(!iso_xfer(0, ep, buffer, 3073));
assert(iso_xfer(0, ep, buffer + 4095, 3072));
ehci_itd_t* td = &ep->req[ep->head].td->itd;
uint8_t slot = ep->req[ep->head].scheduled_uframe & 7;
assert(ep->req[ep->head].scheduled_uframe == 802);
assert(((uintptr_t)td & 63) == 0);
assert(td->xact[slot].offset == 4095 && td->xact[slot].length == 3072);
assert((td->BufferPointer[0] & 0xfff) == 0x201);
assert((td->BufferPointer[1] & 0xfff) == 0xc00);
assert((td->BufferPointer[2] & 0xfff) == 3);
assert((td->BufferPointer[0] & ~0xfffu) == (uint32_t)(uintptr_t)buffer);
assert((td->BufferPointer[1] & ~0xfffu) == (uint32_t)(uintptr_t)(buffer + 4096));
assert((td->BufferPointer[2] & ~0xfffu) == (uint32_t)(uintptr_t)(buffer + 8192));
for (unsigned i = 0; i < 8; i++) {
assert(td->xact[i].active == (i == slot));
}
td->xact[slot].active = 0;
td->xact[slot].length = 2048;
regs.frame_index = 803;
iso_process(true);
assert(events == 1 && event.xfer_complete.len == 2048);
assert(iso_xfer(0, ep, buffer, 10));
td = &ep->req[ep->head].td->itd;
slot = ep->req[ep->head].scheduled_uframe & 7;
td->xact[slot].active = 0;
td->xact[slot].babble_err = 1;
regs.frame_index = ep->req[ep->head].scheduled_uframe + 1;
iso_process(true);
assert(events == 2 && event.xfer_complete.result == XFER_RESULT_FAILED);
assert(event.xfer_complete.len == 0);
}
static void test_long_interval_and_wrap(void) {
reset(TUSB_SPEED_HIGH);
assert(open_ep(1, TUSB_SPEED_HIGH, 64, 10));
iso_ep_t* ep = iso_ep_find(1, 1);
assert(iso_xfer(0, ep, buffer, 64));
assert(!ep->req[ep->head].armed && ep->req[ep->head].scheduled_uframe == 1312);
regs.frame_index = 1264;
iso_process(true);
assert(ep->req[ep->head].armed && events == 0);
ep->req[ep->head].td->itd.xact[0].active = 0;
regs.frame_index = 1313;
iso_process(true);
assert(events == 1 && event.xfer_complete.len == 64);
assert(iso_xfer(0, ep, buffer, 64));
assert(!ep->req[ep->head].armed && iso_abort(0, ep));
assert(ep->count == 0 && events == 1);
// Start a separate long interval request and let its arm window expire.
ep->next_uframe = regs.frame_index + ep->interval;
assert(iso_xfer(0, ep, buffer, 64));
regs.frame_index = ep->req[ep->head].scheduled_uframe + 1;
iso_process(true);
assert(ep->count == 0 && events == 2 && event.xfer_complete.result == XFER_RESULT_FAILED);
ehci_data.iso_last_frindex = 16380;
ehci_data.iso_uframe = 0xfffffffcu;
regs.frame_index = 4;
assert(iso_now() == 4);
}
static void test_iso_status_errors(void) {
for (unsigned hs = 0; hs < 2; hs++) {
for (unsigned err = 0; err < (hs ? 3u : 5u); err++) {
reset(hs ? TUSB_SPEED_HIGH : TUSB_SPEED_FULL);
assert(open_ep(0x81, hs ? TUSB_SPEED_HIGH : TUSB_SPEED_FULL, 64, 1));
iso_ep_t* ep = iso_ep_find(1, 0x81);
assert(iso_xfer(0, ep, buffer, 64));
iso_req_t* req = &ep->req[ep->head];
iso_td_t* td = req->td;
// Active work must remain queued, even if status bits are already set.
if (hs) {
unsigned const slot = req->scheduled_uframe & 7;
switch (err) {
case 0: td->itd.xact[slot].error = 1; break;
case 1: td->itd.xact[slot].babble_err = 1; break;
case 2: td->itd.xact[slot].buffer_err = 1; break;
}
iso_process(true);
assert(events == 0);
td->itd.xact[slot].active = 0;
} else {
switch (err) {
case 0: td->sitd.error = 1; break;
case 1: td->sitd.buffer_err = 1; break;
case 2: td->sitd.babble_err = 1; break;
case 3: td->sitd.xact_err = 1; break;
case 4: td->sitd.missed_uframe = 1; break;
}
iso_process(true);
assert(events == 0);
td->sitd.active = 0;
}
regs.frame_index = (req->scheduled_uframe + ehci_data.iso_frame_offset) & 0x3fff;
iso_process(true);
assert(events == 1 && event.xfer_complete.result == XFER_RESULT_FAILED);
assert(event.xfer_complete.len == 0);
}
}
}
static void test_iso_clock_config(void) {
static uint16_t const due[] = {802, 802, 803, 804, 805, 806, 807, 808,
808, 808, 808, 808, 808, 808, 808, 808};
for (unsigned threshold = 0; threshold < TU_ARRAY_SIZE(due); threshold++) {
reset(TUSB_SPEED_HIGH);
caps.hccparams_bm.iso_schedule_threshold = threshold;
assert(open_ep(0x81, TUSB_SPEED_HIGH, 64, 1));
assert(iso_earliest(iso_now()) == due[threshold]);
// Model completed teardown; this fixture cannot emulate the DMA stop/start handshake.
iso_ep_free(iso_ep_find(1, 0x81));
regs.inten &= ~EHCI_INT_MASK_NXP_SOF;
init_periodic_list(0);
// A new root connection must refresh both cached scheduling attributes.
regs.portsc = (uint32_t) TUSB_SPEED_FULL << 26;
caps.hccparams_bm.iso_schedule_threshold = 0;
assert(open_ep(0x81, TUSB_SPEED_FULL, 64, 1));
assert(iso_now() == 792 && iso_earliest(iso_now()) == 794);
}
}
#endif
static void test_qtd_retirement(void) {
reset(TUSB_SPEED_FULL);
ehci_qhd_t* qh = &ehci_data.control[1].qhd;
ehci_qtd_t* td = &ehci_data.control[1].qtd;
qh->dev_addr = 1;
qh->attached_qtd = td;
td->active = 1;
td->expected_bytes = 3;
qhd_xfer_complete_isr(qh);
assert(events == 0 && qh->attached_qtd == td);
td->active = 0;
qhd_xfer_complete_isr(qh);
assert(events == 1 && event.xfer_complete.len == 3);
qhd_xfer_complete_isr(qh);
assert(events == 1);
}
#if defined(TUP_USBIP_CHIPIDEA_HS) && CFG_TUH_CHIPIDEA_ISO_ENABLE && !CFG_TUH_MAX3421
static void test_limits_and_late_completion(void) {
reset(TUSB_SPEED_HIGH);
assert(!open_ep(0x80, TUSB_SPEED_HIGH, 64, 1));
assert(!open_ep(0x81, TUSB_SPEED_LOW, 64, 1));
assert(!open_ep(0x81, TUSB_SPEED_HIGH, 64, 0));
assert(!open_ep(0x81, TUSB_SPEED_HIGH, 64, 17));
assert(!open_ep(0x81, TUSB_SPEED_HIGH, 0, 1));
assert(!open_ep(0x81, TUSB_SPEED_HIGH, 1025, 1));
assert(!open_ep(0x81, TUSB_SPEED_HIGH, 64 | (3 << 11), 1));
unsigned const capacity = (QTD_MAX / 2) / CFG_TUH_XFER_QUEUE_DEPTH;
for (unsigned i = 0; i < capacity; i++) {
assert(open_ep(tu_edpt_addr(i % 15 + 1, i / 15), TUSB_SPEED_HIGH, 64, 1));
}
assert(!open_ep(tu_edpt_addr(capacity % 15 + 1, capacity / 15), TUSB_SPEED_HIGH, 64, 1));
iso_ep_t* ep = iso_ep_find(1, 1);
assert(iso_xfer(0, ep, buffer, 64));
ep->req[ep->head].td->itd.xact[ep->req[ep->head].scheduled_uframe & 7].active = 0;
regs.frame_index = (ep->req[ep->head].scheduled_uframe & ~7u) + FRAMELIST_SIZE * 8u;
iso_process(true);
assert(events == 1 && event.xfer_complete.result == XFER_RESULT_FAILED);
assert(ep->count == 0);
}
#if CFG_TUH_XFER_QUEUE_DEPTH > 1
static void test_queue(void) {
reset(TUSB_SPEED_HIGH);
assert(open_ep(0x81, TUSB_SPEED_HIGH, 64, 1));
iso_ep_t* ep = iso_ep_find(1, 0x81);
for (unsigned round = 0; round < 3; round++) {
iso_req_t* first = &ep->req[ep->head];
for (unsigned slot = 0; slot < CFG_TUH_XFER_QUEUE_DEPTH; slot++) {
assert(iso_xfer(0, ep, buffer + slot * 64, 24 + slot * 4));
iso_req_t* req = &ep->req[(ep->head + slot) % CFG_TUH_XFER_QUEUE_DEPTH];
assert(req->scheduled_uframe == first->scheduled_uframe + slot);
assert(ep->count == slot + 1);
assert(queued_events == round * (CFG_TUH_XFER_QUEUE_DEPTH - 1) +
tu_min32(slot + 1, CFG_TUH_XFER_QUEUE_DEPTH - 1));
assert(req->armed);
if (slot != 0) {
assert(req->td != first->td && req->buffer != first->buffer);
// Tail completions must never bypass the FIFO head.
req->td->itd.xact[req->scheduled_uframe & 7].active = 0;
}
}
assert(!iso_xfer(0, ep, buffer + CFG_TUH_XFER_QUEUE_DEPTH * 64, 32));
iso_process(true);
assert(events == round * CFG_TUH_XFER_QUEUE_DEPTH && ep->count == CFG_TUH_XFER_QUEUE_DEPTH);
first->td->itd.xact[first->scheduled_uframe & 7].active = 0;
regs.frame_index = first->scheduled_uframe + CFG_TUH_XFER_QUEUE_DEPTH - 1;
iso_process(true);
assert(events == (round + 1) * CFG_TUH_XFER_QUEUE_DEPTH && ep->count == 0);
for (unsigned slot = 0; slot < CFG_TUH_XFER_QUEUE_DEPTH; slot++) {
assert(terminal_events[round * CFG_TUH_XFER_QUEUE_DEPTH + slot].xfer_complete.len == 24 + slot * 4);
}
}
}
#endif
static void test_schedule_sweep(void) {
// Exercise every descriptor slot, all intervals and frame-counter wrap.
for (uint8_t interval = 1; interval <= 16; interval++) {
for (uint32_t start = 16368; start < 16400; start++) {
reset(TUSB_SPEED_HIGH);
regs.frame_index = start & 0x3fff;
assert(open_ep(0x81, TUSB_SPEED_HIGH, 64, interval));
iso_ep_t* ep = iso_ep_find(1, 0x81);
assert(iso_xfer(0, ep, buffer, 64));
uint32_t const due = ep->req[ep->head].scheduled_uframe;
uint32_t const now = iso_now();
assert((int32_t)(due - now) >= 2);
assert((due - now) % ep->interval == 0);
assert(ep->req[ep->head].armed == (due - now < (FRAMELIST_SIZE - 1) * 8));
if (!ep->req[ep->head].armed) {
assert(iso_abort(0, ep));
} else {
ep->req[ep->head].td->itd.xact[due & 7].active = 0;
regs.frame_index = (due + 1) & 0x3fff;
iso_process(true);
assert(events == 1 && event.xfer_complete.result == XFER_RESULT_SUCCESS);
}
}
}
}
static void test_descriptor_reuse(void) {
// Revisit the same descriptor with new lengths/pages and (for HS) microframes.
for (unsigned mode = 0; mode < 3; mode++) {
for (unsigned dir = 0; dir < 2; dir++) {
bool const hs = mode == 0;
reset(mode == 1 ? TUSB_SPEED_FULL : TUSB_SPEED_HIGH);
if (mode == 2) {
buses[1].hub_addr = 2;
buses[1].hub_port = 3;
buses[2].speed = TUSB_SPEED_HIGH;
}
uint8_t const addr = tu_edpt_addr(1, dir);
uint16_t const mps = hs ? 1024 : (dir ? 564 : 1023);
assert(open_ep(addr, hs ? TUSB_SPEED_HIGH : TUSB_SPEED_FULL,
hs ? mps | (2 << 11) : mps, 1));
iso_ep_t* ep = iso_ep_find(1, addr);
for (unsigned queue = 0; queue < CFG_TUH_XFER_QUEUE_DEPTH; queue++) {
iso_req_t* req = &ep->req[queue];
for (unsigned round = 0; round < 16; round++) {
req->scheduled_uframe = round * FRAMELIST_SIZE * 8 + (hs ? round & 7 : 0);
req->buffer = buffer + ((round & 1) ? 0 : 4095);
req->buflen = round % 3 == 0 ? mps * (hs ? 3 : 1) : (round % 3 == 1 ? 192 : 0);
req->armed = false;
iso_arm(ep, req, req->scheduled_uframe - 2);
assert(req->armed);
iso_td_t* td = req->td;
uint32_t const ptr = (uint32_t)(uintptr_t)req->buffer;
uint32_t const page = ptr & ~0xfffu;
if (hs) {
unsigned const slot = req->scheduled_uframe & 7;
for (unsigned i = 0; i < 8; i++) {
assert(td->itd.xact[i].active == (i == slot));
if (i != slot) { assert(td->words[1 + i] == 0); }
}
assert(td->itd.xact[slot].length == req->buflen);
assert(td->itd.xact[slot].offset == (ptr & 0xfff));
assert(td->itd.xact[slot].page_select == 0 && td->itd.xact[slot].int_on_complete);
assert(td->itd.BufferPointer[0] == (page | 0x101));
assert(td->itd.BufferPointer[1] == ((page + 4096) | mps | (dir << 11)));
assert(td->itd.BufferPointer[2] == ((page + 8192) | 3));
// Retired hardware status, including changed page/offset and errors.
td->words[1 + slot] = 0x7fffffff;
} else {
ehci_sitd_t* s = &td->sitd;
assert(s->dev_addr == 1 && s->ep_number == 1 && s->direction == dir);
assert(s->hub_addr == (mode == 2 ? 2 : 0));
assert(s->port_number == (mode == 2 ? 3 : 0));
assert(s->back.terminate && s->active && s->int_on_complete);
assert(s->total_bytes == req->buflen && !s->cmask_progress && !s->page_select);
assert(!s->split_state && !s->missed_uframe && !s->xact_err && !s->error);
assert(!s->buffer_err && !s->babble_err && s->buffer[0] == ptr);
unsigned const count = req->buflen ? (req->buflen + 187) / 188 : 1;
assert(s->int_smask == (mode == 1 ? 0 : (dir ? 4 : (1u << count) - 1)));
assert(s->fl_int_cmask == (mode == 2 && dir ? 0xf0 : 0));
assert(s->buffer[1] == ((page + 4096) |
(mode == 2 && !dir ? count | (count > 1 ? 8 : 0) : 0)));
td->words[3] = 0xffffff7f; // retired, all other status/progress bits set
s->buffer[0] += 100;
s->buffer[1] ^= 0x1f; // hardware advances OUT split count/position
}
iso_td_unlink(req);
req->armed = false;
}
}
}
}
}
static void test_late_schedule_phase(void) {
uint32_t const starts[] = {800, 16383, 0xfffffffe};
uint32_t const delays[] = {0, 1, 19, 1031};
for (unsigned interval = 1; interval <= 16; interval++) {
for (unsigned s = 0; s < TU_ARRAY_SIZE(starts); s++) {
for (unsigned d = 0; d < TU_ARRAY_SIZE(delays); d++) {
reset(TUSB_SPEED_HIGH);
assert(open_ep(0x81, TUSB_SPEED_HIGH, 64, interval));
iso_ep_t* ep = iso_ep_find(1, 0x81);
uint32_t const now = starts[s];
ehci_data.iso_uframe = now;
ehci_data.iso_last_frindex = now & 0x3fff;
regs.frame_index = now & 0x3fff;
ep->next_uframe = now - delays[d];
// Reference: advance one endpoint interval at a time, including wrap.
uint32_t expected = ep->next_uframe;
while ((int32_t)(expected - (now + 2)) < 0) {
expected += ep->interval;
}
assert(iso_xfer(0, ep, buffer, 64));
assert(ep->req[ep->head].scheduled_uframe == expected);
}
}
}
}
static void test_pool_hs_interval(uint8_t interval) {
// Walk the actual DMA chains while four endpoints reuse TDs across both
// frame-list and extended-clock wrap. This also detects orphaned active TDs.
reset(TUSB_SPEED_HIGH);
for (unsigned i = 0; i < TEST_ISO_STREAM_EP_COUNT; i++) {
assert(open_ep((uint8_t)(0x81 + i), TUSB_SPEED_HIGH, 64, interval));
}
uint32_t const start = 0xfffffff0;
ehci_data.iso_uframe = start;
ehci_data.iso_last_frindex = start & 0x3fff;
regs.frame_index = start & 0x3fff;
for (unsigned i = 0; i < TEST_ISO_STREAM_EP_COUNT; i++) {
ehci_data.qhd_pool[i].iso.next_uframe = start;
}
unsigned transferred = 0;
unsigned const ticks = tu_max32(1024, (1u << (interval - 1)) * 8);
for (unsigned tick = 0; tick < ticks; tick++) {
uint32_t const now = start + tick;
regs.frame_index = now & 0x3fff;
ehci_link_t link = ehci_data.period_framelist[(now >> 3) % FRAMELIST_SIZE];
unsigned visited = 0, completed = 0;
while (!link.terminate && link.type != EHCI_QTYPE_QHD) {
assert(link.type == EHCI_QTYPE_ITD);
assert(++visited <= TEST_ISO_STREAM_EP_COUNT * CFG_TUH_XFER_QUEUE_DEPTH);
iso_td_t* td = (iso_td_t*)(uintptr_t)tu_align32(link.address);
if (td->itd.xact[now & 7].active) {
unsigned owners = 0;
for (unsigned i = 0; i < TEST_ISO_STREAM_EP_COUNT; i++) {
iso_ep_t* ep = &ehci_data.qhd_pool[i].iso;
for (unsigned j = 0; j < ep->count; j++) {
iso_req_t* req = &ep->req[(ep->head + j) % CFG_TUH_XFER_QUEUE_DEPTH];
if (req->armed && req->td == td) {
assert(req->scheduled_uframe == now);
owners++;
}
}
}
assert(owners == 1);
td->itd.xact[now & 7].active = 0;
td->itd.xact[now & 7].length = 24;
completed++;
}
link = td->itd.next;
}
assert(link.terminate || link.type == EHCI_QTYPE_QHD);
events = 0;
iso_process(true);
assert(events == completed);
for (unsigned i = 0; i < events; i++) {
assert(terminal_events[i].xfer_complete.result == XFER_RESULT_SUCCESS);
assert(terminal_events[i].xfer_complete.len == 24);
}
transferred += completed;
for (unsigned i = 0; i < TEST_ISO_STREAM_EP_COUNT; i++) {
iso_ep_t* ep = &ehci_data.qhd_pool[i].iso;
while (ep->count < CFG_TUH_XFER_QUEUE_DEPTH) {
assert(iso_xfer(0, ep, buffer + i * 128 + ep->count * 64, 24));
}
}
}
#if CFG_TUH_XFER_QUEUE_DEPTH > 1
if (interval == 1) {
assert(transferred == (ticks - 2) * TEST_ISO_STREAM_EP_COUNT);
}
#endif
assert(transferred != 0);
}
static void test_pool_fs_interval(uint8_t interval) {
for (unsigned hub = 0; hub < 2; hub++) {
reset(hub ? TUSB_SPEED_HIGH : TUSB_SPEED_FULL);
for (unsigned i = 0; i < TEST_ISO_STREAM_EP_COUNT; i++) {
if (hub) {
buses[1].hub_addr = 2;
buses[1].hub_port = 3;
buses[2].speed = TUSB_SPEED_HIGH;
}
assert(open_ep((uint8_t)(0x81 + i), TUSB_SPEED_FULL, 64, interval));
}
uint32_t const start = iso_now();
unsigned transferred = 0;
unsigned const ticks = tu_max32(512, (1u << (interval - 1)) * 64);
for (unsigned tick = 0; tick < ticks; tick += hub ? 1 : 8) {
uint32_t const now = start + tick;
regs.frame_index = (now + (hub ? 0 : 8)) & 0x3fff;
unsigned completed = 0;
if (!hub || (now & 7) == 7) {
ehci_link_t link = ehci_data.period_framelist[(now >> 3) % FRAMELIST_SIZE];
unsigned visited = 0;
while (!link.terminate && link.type != EHCI_QTYPE_QHD) {
assert(link.type == EHCI_QTYPE_SITD);
assert(++visited <= TEST_ISO_STREAM_EP_COUNT * CFG_TUH_XFER_QUEUE_DEPTH);
iso_td_t* td = (iso_td_t*)(uintptr_t)tu_align32(link.address);
if (td->sitd.active) {
unsigned owners = 0;
for (unsigned i = 0; i < TEST_ISO_STREAM_EP_COUNT; i++) {
iso_ep_t* ep = &ehci_data.qhd_pool[i].iso;
for (unsigned j = 0; j < ep->count; j++) {
iso_req_t* req = &ep->req[(ep->head + j) % CFG_TUH_XFER_QUEUE_DEPTH];
if (req->armed && req->td == td) {
assert(req->scheduled_uframe == (now & ~7u));
owners++;
}
}
}
assert(owners == 1);
td->sitd.active = 0;
td->sitd.total_bytes = 0;
completed++;
}
link = td->itd.next;
}
}
events = 0;
iso_process(true);
if (events != completed) {
fprintf(stderr, "FS pool hub=%u tick=%u now=%u completed=%u events=%u result=%u\n",
hub, tick, now, completed, events, event.xfer_complete.result);
}
assert(events == completed);
for (unsigned i = 0; i < events; i++) {
assert(terminal_events[i].xfer_complete.result == XFER_RESULT_SUCCESS);
}
transferred += completed;
for (unsigned i = 0; i < TEST_ISO_STREAM_EP_COUNT; i++) {
iso_ep_t* ep = &ehci_data.qhd_pool[i].iso;
while (ep->count < CFG_TUH_XFER_QUEUE_DEPTH) {
assert(iso_xfer(0, ep, buffer + i * 128 + ep->count * 64, 64));
}
}
}
#if CFG_TUH_XFER_QUEUE_DEPTH > 1
if (interval == 1) {
assert(transferred == 63 * TEST_ISO_STREAM_EP_COUNT);
}
#endif
assert(transferred != 0);
}
}
static void test_pool_stream(void) {
for (uint8_t interval = 1; interval <= 16; interval++) {
test_pool_hs_interval(interval);
}
}
static void test_pool_fs_stream(void) {
for (uint8_t interval = 1; interval <= 16; interval++) {
test_pool_fs_interval(interval);
}
}
static void test_completion_unlink(bool hs) {
reset(TUSB_SPEED_HIGH);
if (!hs) {
buses[1].hub_addr = 2;
buses[1].hub_port = 1;
buses[2].speed = TUSB_SPEED_HIGH;
}
uint32_t const due = hs ? 802 : 808;
size_t const frame = (due >> 3) % FRAMELIST_SIZE;
unsigned const status_word = hs ? 1 + (due & 7) : 3;
uint32_t const active = TU_BIT(hs ? 31 : 7);
unsigned const type = hs ? EHCI_QTYPE_ITD : EHCI_QTYPE_SITD;
ehci_link_t const original = ehci_data.period_framelist[frame];
assert(open_ep(0x81, hs ? TUSB_SPEED_HIGH : TUSB_SPEED_FULL, 64, 1));
assert(open_ep(0x82, hs ? TUSB_SPEED_HIGH : TUSB_SPEED_FULL, 64, 1));
iso_ep_t* first = iso_ep_find(1, 0x81);
iso_ep_t* second = iso_ep_find(1, 0x82);
assert(iso_xfer(0, first, buffer, 24));
assert(iso_xfer(0, second, buffer + 64, 24));
iso_td_t* first_td = first->req[0].td;
iso_td_t* second_td = second->req[0].td;
first_td->words[status_word] &= ~active;
regs.frame_index = due;
iso_process(true);
assert(events == 1 && first->count == 0 && !first->req[0].armed);
// Completion removes a middle TD immediately, preserving the active sibling.
assert(second_td->words[status_word] & active);
assert(second_td->itd.next.address == original.address);
assert(ehci_data.period_framelist[frame].address == ((uint32_t)(uintptr_t)second_td | (type << 1)));
uint32_t const next_due = hs ? 804 : 816;
size_t const next_frame = (next_due >> 3) % FRAMELIST_SIZE;
ehci_link_t const next_original = hs ? original : ehci_data.period_framelist[next_frame];
assert(iso_xfer(0, first, buffer, 24));
iso_req_t* req = &first->req[first->head];
iso_td_t* replacement = req->td;
assert(req->armed && req->scheduled_uframe == next_due);
second_td->words[status_word] &= ~active;
iso_process(true);
assert(events == 2 && second->count == 0);
// Preserve the replacement, including when it precedes the completed HS TD.
unsigned const next_status_word = hs ? 1 + (next_due & 7) : 3;
assert(replacement->words[next_status_word] & active);
assert(replacement->itd.next.address == next_original.address);
replacement->words[next_status_word] &= ~active;
regs.frame_index = next_due;
iso_process(true);
assert(events == 3 && first->count == 0);
assert(ehci_data.period_framelist[frame].address == original.address);
assert(ehci_data.period_framelist[next_frame].address == next_original.address);
}
static void test_future_completion(void) {
uint32_t const starts[] = {800, 0xfffffff8};
for (unsigned mode = 0; mode < 3; mode++) {
for (unsigned i = 0; i < TU_ARRAY_SIZE(starts); i++) {
reset(mode == 1 ? TUSB_SPEED_FULL : TUSB_SPEED_HIGH);
if (mode == 2) {
buses[1].hub_addr = 2;
buses[1].hub_port = 1;
buses[2].speed = TUSB_SPEED_HIGH;
}
assert(open_ep(0x81, mode == 0 ? TUSB_SPEED_HIGH : TUSB_SPEED_FULL, 64, 1));
iso_ep_t* ep = iso_ep_find(1, 0x81);
ep->next_uframe = starts[i];
ehci_data.iso_uframe = starts[i] + ehci_data.iso_frame_offset;
regs.frame_index = ehci_data.iso_last_frindex = ehci_data.iso_uframe & 0x3fff;
assert(iso_xfer(0, ep, buffer, 64));
iso_req_t* req = &ep->req[ep->head];
assert(req->armed);
uint32_t const due = req->scheduled_uframe;
regs.frame_index = (due - 1 + ehci_data.iso_frame_offset) & 0x3fff;
iso_process(true);
assert(events == 0 && ep->count == 1);
// Once the interval starts, inspect status and allow immediate completion.
regs.frame_index = (due + ehci_data.iso_frame_offset) & 0x3fff;
iso_process(true);
assert(events == 0);
iso_td_t* td = req->td;
if (mode == 0) {
td->itd.xact[due & 7].active = 0;
} else {
td->sitd.active = 0;
td->sitd.total_bytes = 0;
}
iso_process(true);
assert(events == 1 && ep->count == 0);
assert(event.xfer_complete.result == XFER_RESULT_SUCCESS && event.xfer_complete.len == 64);
}
}
}
#endif
int main(void) {
// Hardware links are 32-bit. The runner places static fixtures below 4 GiB.
assert((uintptr_t)&ehci_data <= UINT32_MAX && (uintptr_t)buffer <= UINT32_MAX);
test_attach_debounce();
test_qtd_retirement();
#if defined(TUP_USBIP_CHIPIDEA_HS) && CFG_TUH_CHIPIDEA_ISO_ENABLE && !CFG_TUH_MAX3421
test_shared_pools();
test_native_fs();
test_split();
test_split_audio();
test_hs();
test_iso_status_errors();
test_iso_clock_config();
test_long_interval_and_wrap();
test_limits_and_late_completion();
test_schedule_sweep();
test_descriptor_reuse();
test_late_schedule_phase();
test_pool_stream();
test_pool_fs_stream();
test_completion_unlink(true);
test_completion_unlink(false);
test_future_completion();
#if CFG_TUH_XFER_QUEUE_DEPTH > 1
test_queue();
#endif
#else
reset(TUSB_SPEED_HIGH);
tusb_desc_endpoint_t const iso_desc = {
.bLength = sizeof(iso_desc), .bDescriptorType = TUSB_DESC_ENDPOINT,
.bEndpointAddress = 0x81, .bmAttributes = {.xfer = TUSB_XFER_ISOCHRONOUS},
.wMaxPacketSize = 64, .bInterval = 1
};
assert(!hcd_edpt_open(0, 1, &iso_desc));
assert(regs.command_bm.int_threshold == 8);
#endif
puts("EHCI ISO regression tests passed");
return 0;
}