switch-pico/tests/native_hub_trace_test.c
Joey Yakimowich-Payne 80b788099b feat(native-usb): add 32-frame HD gameplay rumble and USB flight diagnostics
Preserve native frequency/amplitude timelines for DualSense PCM output,
remove the unsupported 64-frame path, and retain compatibility rumble for
other controllers. Use the validated 300 MHz sampling phase and restrict
non-bondable Classic discovery autoconnect to previously paired peers.

Buffer native-hub UART stdout and release USB IRQs around port-reset
callbacks. Add observer liveness and pre-SETUP root-response observations
without changing recovery behavior. Cover transport and haptics boundaries.

Record the user-accepted 0.108 trial: controls remained responsive and
rumble felt fine. Instrumentation changes timing; the disconnect root
cause and long-term reliability remain unqualified.

Validation: 624 tests, 11 affected firmware/probe builds, and on-device
concurrent USB and HD-auto-start checks. Private captures, generated
images, and unrelated working-tree files are intentionally excluded.
2026-09-19 17:03:46 -06:00

2298 lines
104 KiB
C

#include <assert.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "hardware_stub.h"
static sio_hw_t* trace_test_sio(void);
static bool trace_test_lock_busy;
static bool trace_test_trylock(spin_lock_t* lock) {
return !trace_test_lock_busy && spin_try_lock_unsafe(lock);
}
#undef sio_hw
#define sio_hw trace_test_sio()
#define spin_try_lock_unsafe trace_test_trylock
#define NATIVE_TEST_EXTERNAL_LOG 1
#include "native_hub_transport_fixture.c"
// Each scenario runs in a fresh process: the real recorder's BSS is its reset.
// Hardware registers model host/IRQ observations, not packets on a USB wire.
#define TEST_STALL_US 200000u
#define TEST_LINE_US 50000u
#define TEST_RETAIN 64u
#define TEST_PID_OUT 0xe1u
#define TEST_PID_IN 0x69u
#define TEST_PID_SETUP 0x2du
#define TEST_LOG_LINES 1024u
#define TEST_LINE_SIZE 512u
static char log_lines[TEST_LOG_LINES][TEST_LINE_SIZE];
static unsigned log_count;
static bool logger_full, retry_each_line;
static unsigned retry_count, rejected_lines;
static char rejected_line[TEST_LINE_SIZE];
static uint8_t vendor_reply[96];
static bool supersede_in_callback;
static uint8_t handover_reply[DEVICES][83];
static unsigned handover_acks[DEVICES];
static bool supersede_status_in_callback;
static bool reject_status_in_callback;
static unsigned handover_data_callbacks[DEVICES];
static uint8_t approved_reply[DEVICES][2u*PACKET];
typedef struct {
uint8_t slot, stage;
tusb_control_request_t request;
} approved_callback_t;
static approved_callback_t approved_callbacks[64];
static unsigned approved_callback_count;
static bool reset_on_root_complete;
typedef struct {
usb_device_dpram_t dpram;
uint32_t stall;
} hardware_bank_t;
static bool clock_steps, commit_probe, commit_handover;
static uint8_t commit_address, commit_owner;
static uint32_t commit_cycle, commit_buffers[CHANNELS], commit_controls[4];
static uint32_t commit_root_control, commit_stall;
static hardware_bank_t commit_bank, commit_latched_bank;
static hardware_bank_t hardware_bank(void) {
hardware_bank_t bank;
memcpy(&bank.dpram,usb_dpram,sizeof(bank.dpram));
bank.stall = usb_hw->ep_stall_arm;
return bank;
}
static void expect_bank_unchanged(const hardware_bank_t* bank) {
assert(memcmp(&bank->dpram,usb_dpram,sizeof(bank->dpram)) == 0);
assert(usb_hw->ep_stall_arm == bank->stall);
}
static sio_hw_t* trace_test_sio(void) {
if (clock_steps) ++native_test_sio.mtime;
if (commit_probe && usb_hw->dev_addr_ctrl == commit_address) {
commit_probe = false;
commit_cycle = native_test_sio.mtime;
// Latch the bank at the first clock access after the address store.
// Later packet delivery uses this metadata, not a repaired return-time
// bank. This models the publication contract, NOT physical SIE timing,
// packet acceptance, or how hardware ACKs a mismatched DATA PID.
commit_latched_bank = hardware_bank();
assert(active_device == commit_owner);
if (commit_handover) {
const uint32_t* buffers = (const uint32_t*)&commit_latched_bank.dpram.ep_buf_ctrl[0];
const uint32_t* controls = (const uint32_t*)&commit_latched_bank.dpram.ep_ctrl[0];
for (unsigned channel = 0; channel < CHANNELS; ++channel) {
assert(!(buffers[channel] & USB_BUF_CTRL_AVAIL));
assert(buffers[channel] == commit_buffers[channel] &&
"address commit exposed missing or outgoing packet metadata");
}
for (unsigned i = 0; i < 4; ++i)
assert(controls[i] == commit_controls[i]);
assert(commit_latched_bank.dpram.ep_ctrl[14].in == commit_root_control);
assert(commit_latched_bank.stall == commit_stall);
// Root EP15 has independent storage; its buffer is not the child
// EP1 bank. Visibility follows the incoming root endpoint control.
assert(commit_latched_bank.dpram.ep_buf_ctrl[15].in ==
commit_bank.dpram.ep_buf_ctrl[15].in);
} else expect_bank_unchanged(&commit_bank);
}
return &native_test_sio;
}
static uint16_t handover_length(uint8_t slot) {
return slot == 0 ? 16 : slot % 2u ? PACKET : sizeof(handover_reply[slot]);
}
int probe_debug_printf(const char* format, ...) {
char line[TEST_LINE_SIZE];
va_list arguments;
va_start(arguments,format);
int length = vsnprintf(line,sizeof(line),format,arguments);
va_end(arguments);
assert(length >= 0 && (size_t)length < sizeof(line));
if (reset_on_root_complete && strstr(line,"[HUB_CTRL] complete ") == line) {
reset_on_root_complete = false;
native_test_bus_reset(false);
}
if (strncmp(line,"[HUB_FLIGHT",11) != 0) return length;
if (logger_full) return -1;
if (retry_each_line) {
if (!retry_count) memcpy(rejected_line,line,(size_t)length+1u);
else assert(strcmp(rejected_line,line) == 0 && "a rejected dump line was skipped or changed");
if (++retry_count <= 2u) { ++rejected_lines; return -1; }
retry_count = 0;
}
assert(log_count < TEST_LOG_LINES);
memcpy(log_lines[log_count++],line,(size_t)length+1u);
return length;
}
void reset_usb_boot(uint32_t gpio_mask, uint32_t disable_mask) {
(void)gpio_mask; (void)disable_mask;
assert(false && "transport failed closed during trace regression");
abort();
}
bool tud_vendor_control_xfer_cb(uint8_t slot, uint8_t stage, const tusb_control_request_t* request) {
if (request->bmRequestType == 0xc0 && request->bRequest == 0x5d) {
if (stage == CONTROL_STAGE_SETUP) {
if (!request->wValue || request->wValue > sizeof(approved_reply[slot]) ||
!request->wLength) return false;
return native_hub_control_xfer(slot,request,approved_reply[slot],request->wValue,true);
}
assert(stage == CONTROL_STAGE_DATA || stage == CONTROL_STAGE_ACK);
assert(approved_callback_count < sizeof(approved_callbacks)/sizeof(approved_callbacks[0]));
approved_callbacks[approved_callback_count++] = (approved_callback_t){
.slot = slot, .stage = stage, .request = *request,
};
return true; // SETUP validated the reply; DATA never rejects status.
}
if (request->bmRequestType == 0xc0 && request->bRequest == 0x5b) {
if (stage == CONTROL_STAGE_ACK) ++handover_acks[slot];
if (stage == CONTROL_STAGE_DATA) {
++handover_data_callbacks[slot];
if (reject_status_in_callback) return false;
}
if (stage == CONTROL_STAGE_DATA && supersede_status_in_callback) {
supersede_status_in_callback = false;
const tusb_control_request_t replacement = {
.bmRequestType = 0x80, .bRequest = TUSB_REQ_GET_DESCRIPTOR,
.wValue = TUSB_DESC_DEVICE << 8, .wLength = 18,
};
assert(native_test_setup(slot,&replacement,false));
}
return stage != CONTROL_STAGE_SETUP || native_hub_control_xfer(
slot,request,handover_reply[slot],handover_length(slot),false);
}
if (request->bmRequestType != 0xc0 || request->bRequest != 0x5a) return false;
if (stage != CONTROL_STAGE_SETUP) return true;
bool accepted = native_hub_control_xfer(slot,request,vendor_reply,sizeof(vendor_reply),false);
if (supersede_in_callback) {
supersede_in_callback = false;
const tusb_control_request_t replacement = {
.bmRequestType = 0x80, .bRequest = TUSB_REQ_GET_DESCRIPTOR,
.wValue = TUSB_DESC_DEVICE << 8, .wLength = 18,
};
native_test_time_us += 7u;
assert(native_test_setup(slot,&replacement,false));
}
return accepted;
}
static bool tagged(const char* line, const char* tag) {
return strncmp(line,tag,strlen(tag)) == 0;
}
static unsigned count_tag(const char* tag) {
unsigned count = 0;
for (unsigned i = 0; i < log_count; ++i) count += tagged(log_lines[i],tag);
return count;
}
static const char* snapshot_line(unsigned snapshot, const char* tag) {
unsigned current = 0;
for (unsigned i = 0; i < log_count; ++i) {
if (tagged(log_lines[i],"[HUB_FLIGHT_FREEZE]")) ++current;
if (current == snapshot+1u && tagged(log_lines[i],tag)) return log_lines[i];
}
assert(false && "required snapshot diagnostic was not emitted");
return NULL;
}
static uint32_t field(const char* line, const char* name, unsigned base) {
const char* value = strstr(line,name);
assert(value != NULL && "required diagnostic field is absent");
value += strlen(name);
char* end;
unsigned long result = strtoul(value,&end,(int)base);
assert(end != value && result <= UINT32_MAX);
return (uint32_t)result;
}
static const char* record_line(unsigned snapshot, unsigned index) {
unsigned current = 0, record = 0;
for (unsigned i = 0; i < log_count; ++i) {
const char* line = log_lines[i];
if (tagged(line,"[HUB_FLIGHT_FREEZE]")) ++current;
if (current == snapshot+1u && tagged(line,"[HUB_FLIGHT]") && record++ == index)
return line;
}
assert(false && "required retained token was not emitted");
return NULL;
}
static const char* expect_observation(unsigned snapshot, unsigned index, uint8_t address,
uint8_t owner, uint8_t pid, uint32_t cycle,
bool selected) {
const char* line = record_line(snapshot,index);
assert(field(line," cutoff=",16) == 0x70000000u+cycle);
assert(field(line," pid=",16) == pid);
assert(field(line," pre=",10) == 0 && field(line," ok=",10) == selected);
if (!selected) assert(field(line," commit=",16) == 0);
char expected[80];
snprintf(expected,sizeof(expected)," req=%02x/%u ",(unsigned)address,(unsigned)owner);
assert(strstr(line,expected));
assert(strstr(line," addr=00000000/") && strstr(line," owner=255/"));
snprintf(expected,sizeof(expected)," clock=00000000/%08"PRIx32" ",cycle);
assert(strstr(line,expected));
return line;
}
static void expect_lost(unsigned snapshots, uint32_t lost) {
for (unsigned i = 0; i < snapshots; ++i)
assert(field(snapshot_line(i,"[HUB_FLIGHT_END]")," lost=",10) == lost);
}
static void poll(unsigned ticks) {
for (unsigned i = 0; i < ticks; ++i) native_test_advance(TEST_LINE_US);
}
static void expect_dump_order(unsigned snapshots) {
unsigned line = 0;
const char* headers[] = {
"[HUB_FLIGHT_FREEZE]", "[HUB_FLIGHT_CONTEXT]", "[HUB_FLIGHT_CONTROL]",
"[HUB_FLIGHT_CONTROL_CLOCK]", "[HUB_FLIGHT_STATUS_OUT]",
};
for (unsigned snapshot = 0; snapshot < snapshots; ++snapshot) {
assert(line < log_count);
unsigned records = field(log_lines[line]," n=",10);
for (unsigned h = 0; h < sizeof(headers)/sizeof(headers[0]); ++h) {
assert(line < log_count && tagged(log_lines[line],headers[h]));
++line;
}
if (CHILDREN != 2u) {
for (unsigned slot = 1; slot <= CHILDREN; ++slot) {
assert(line < log_count && tagged(log_lines[line],"[HUB_FLIGHT_INPUT]"));
assert(field(log_lines[line++]," slot=",10) == slot);
}
}
for (unsigned record = 0; record < records; ++record) {
assert(line < log_count && tagged(log_lines[line],"[HUB_FLIGHT]"));
++line;
}
assert(line < log_count && tagged(log_lines[line],"[HUB_FLIGHT_END]"));
assert(field(log_lines[line++]," n=",10) == records);
}
}
static void dump_through(unsigned snapshots) {
unsigned attempts = 0;
while (count_tag("[HUB_FLIGHT_END]") < snapshots && attempts++ < 1000u)
native_test_advance(TEST_LINE_US);
assert(count_tag("[HUB_FLIGHT_END]") == snapshots && "snapshot dump failed to finish");
expect_dump_order(snapshots);
}
static void configure_child(uint8_t slot) {
const tusb_control_request_t request = {
.bRequest = TUSB_REQ_SET_CONFIGURATION, .wValue = 1,
};
uint8_t data[PACKET];
uint16_t length = UINT16_MAX;
assert(native_test_setup(slot,&request,true));
assert(native_test_in(slot,data,&length,true) && length == 0);
assert(native_hub_mounted(slot-1u));
}
static void input_completion(uint8_t slot) {
const uint8_t payload[] = {0x12,0x34,0x56};
uint8_t data[PACKET];
uint16_t length = 0;
assert(native_hub_hid_report(slot-1u,0x30,payload,sizeof(payload)));
assert(native_test_private_in(slot,0x81,data,&length));
assert(length == sizeof(payload)+1u && data[0] == 0x30);
assert(memcmp(data+1,payload,sizeof(payload)) == 0);
native_test_drain();
}
static void selections(uint32_t marker, unsigned count) {
for (unsigned i = 0; i < count; ++i) {
uint8_t slot = 1u + i % CHILDREN;
sio_hw->mtime = 1000u+i;
assert(native_hub_select_device(addresses[slot],slot,marker+i));
native_hub_note_selected_token(addresses[slot],slot,marker+i,TEST_PID_OUT);
}
}
static void expect_records(unsigned snapshot, uint32_t marker, unsigned count) {
unsigned current = 0, found = 0;
for (unsigned i = 0; i < log_count; ++i) {
const char* line = log_lines[i];
if (tagged(line,"[HUB_FLIGHT_FREEZE]")) ++current;
if (current != snapshot+1u || !tagged(line,"[HUB_FLIGHT]")) continue;
assert(found < count);
assert(field(line," cutoff=",16) == marker+found);
assert(field(line," pid=",16) == TEST_PID_OUT);
assert(field(line," ok=",10) == 1);
++found;
}
assert(found == count && "snapshot lost selections or read the overwritten live ring");
assert(field(snapshot_line(snapshot,"[HUB_FLIGHT_FREEZE]")," n=",10) == count);
assert(field(snapshot_line(snapshot,"[HUB_FLIGHT_END]")," n=",10) == count);
assert(field(snapshot_line(snapshot,"[HUB_FLIGHT_FREEZE]")," reason=",10) == 0);
}
static void root_read(void) {
const tusb_control_request_t request = {
.bmRequestType = 0x80, .bRequest = TUSB_REQ_GET_STATUS, .wLength = 2,
};
uint8_t data[PACKET];
uint16_t length = 0;
assert(native_test_setup(0,&request,true));
assert(native_test_in(0,data,&length,true) && length == 2);
assert(data[0] == 1 && data[1] == 0);
assert(native_test_out(0,NULL,0,true));
}
typedef struct {
uint32_t time_us, generation;
uint8_t slot;
} marker_receipt_t;
static tusb_control_request_t marker_request(uint16_t slot) {
const tusb_control_request_t request = {
.bmRequestType = 0xc0, .bRequest = 0x5e, .wValue = 0x5452,
.wIndex = slot, .wLength = 16,
};
return request;
}
static uint32_t reply_u32(const uint8_t* data) {
return (uint32_t)data[0] | (uint32_t)data[1] << 8 |
(uint32_t)data[2] << 16 | (uint32_t)data[3] << 24;
}
static marker_receipt_t capture_marker(uint8_t slot, uint8_t status) {
const tusb_control_request_t request = marker_request(slot);
uint8_t data[PACKET];
memset(data,0xa5,sizeof(data));
uint16_t length = UINT16_MAX;
assert(native_test_setup(0,&request,true));
assert(native_test_in(0,data,&length,true) && length == 16);
assert(memcmp(data,"NHTR",4) == 0 && data[4] == 1);
assert(data[5] == status && data[6] == slot && data[7] == 0);
const marker_receipt_t receipt = {
.time_us = reply_u32(data+8), .generation = reply_u32(data+12), .slot = data[6],
};
if (status == 1) assert(receipt.time_us == 0 && receipt.generation == 0);
assert(native_test_out(0,NULL,0,true));
return receipt;
}
static void expect_marker(unsigned snapshot, const marker_receipt_t* receipt) {
const char* header = snapshot_line(snapshot,"[HUB_FLIGHT_FREEZE]");
const char* control = snapshot_line(snapshot,"[HUB_FLIGHT_CONTROL]");
assert(field(header," reason=",10) == 3);
assert(field(header," us=",10) == receipt->time_us);
assert(field(control," slot=",10) == receipt->slot);
assert(field(control," gen=",10) == receipt->generation);
const char* clock = snapshot_line(snapshot,"[HUB_FLIGHT_CONTROL_CLOCK]");
assert(field(clock," slot=",10) == receipt->slot);
assert(field(clock," gen=",10) == receipt->generation);
const char* status = snapshot_line(snapshot,"[HUB_FLIGHT_STATUS_OUT]");
assert(field(status," slot=",10) == receipt->slot);
assert(field(status," gen=",10) == receipt->generation);
}
static void live_wrap(void) {
configure_child(CHILDREN);
selections(0x10000000u,TEST_RETAIN);
input_completion(CHILDREN);
native_test_advance(TEST_STALL_US);
assert(count_tag("[HUB_FLIGHT_FREEZE]") == 1);
assert(count_tag("[HUB_FLIGHT_END]") == 0);
// The first idle dump is still underway. New successful selections must
// remain observable in a second snapshot, not disappear until UART drains.
input_completion(CHILDREN);
selections(0x20000000u,TEST_RETAIN);
native_test_advance(TEST_STALL_US);
selections(0x30000000u,3u*TEST_RETAIN);
dump_through(2);
expect_records(0,0x10000000u,TEST_RETAIN);
expect_records(1,0x20000000u,TEST_RETAIN);
poll(100);
assert(count_tag("[HUB_FLIGHT_FREEZE]") == 2);
}
static void root_does_not_rearm(void) {
configure_child(CHILDREN);
selections(0x10000000u,4);
input_completion(CHILDREN);
native_test_advance(TEST_STALL_US);
for (unsigned i = 0; i < 40; ++i) {
root_read();
native_test_advance(TEST_LINE_US);
}
dump_through(1);
for (unsigned i = 0; i < 40; ++i) {
root_read();
native_test_advance(TEST_LINE_US);
}
assert(count_tag("[HUB_FLIGHT_FREEZE]") == 1 && "root control traffic rearmed input-idle capture");
input_completion(CHILDREN);
native_test_advance(TEST_STALL_US-1u);
assert(count_tag("[HUB_FLIGHT_FREEZE]") == 1);
native_test_advance(1);
dump_through(2);
assert(count_tag("[HUB_FLIGHT_FREEZE]") == 2);
}
static void queue_pressure(void) {
configure_child(CHILDREN);
selections(0x10000000u,TEST_RETAIN);
input_completion(CHILDREN);
native_test_advance(TEST_STALL_US);
assert(count_tag("[HUB_FLIGHT_FREEZE]") == 1);
logger_full = true;
for (unsigned request = 2; request <= 3; ++request) {
input_completion(CHILDREN);
selections(request*0x10000000u,TEST_RETAIN);
native_test_advance(TEST_STALL_US);
}
const tusb_control_request_t pending = {
.bmRequestType = 0x80, .bRequest = TUSB_REQ_GET_DESCRIPTOR,
.wValue = TUSB_DESC_DEVICE << 8, .wLength = 18,
};
assert(native_test_setup(CHILDREN,&pending,true));
native_test_advance(TEST_STALL_US); // One more drop, from pending control.
selections(0x40000000u,3u*TEST_RETAIN);
poll(40); // Queue pressure consumes both idle and pending one-shots.
logger_full = false;
dump_through(2);
poll(100); // No later trigger is available to reveal the lost request.
assert(count_tag("[HUB_FLIGHT_FREEZE]") == 2);
assert(count_tag("[HUB_FLIGHT_END]") == 2);
expect_records(0,0x10000000u,TEST_RETAIN);
expect_records(1,0x20000000u,TEST_RETAIN);
uint32_t lost = 0;
for (unsigned i = 0; i < log_count; ++i)
if (strstr(log_lines[i]," lost=")) {
uint32_t reported = field(log_lines[i]," lost=",10);
assert(reported <= 2u && "full queue repeatedly counted an unchanged trigger");
if (reported > lost) lost = reported;
}
assert(lost == 2u && "dropped snapshot requests were never reported");
}
static void backpressure(bool full) {
configure_child(CHILDREN);
selections(0x10000000u,TEST_RETAIN);
input_completion(CHILDREN);
retry_each_line = full;
native_test_advance(TEST_STALL_US);
dump_through(1);
assert(count_tag("[HUB_FLIGHT_FREEZE]") == 1);
assert(count_tag("[HUB_FLIGHT_CONTEXT]") == 1);
assert(count_tag("[HUB_FLIGHT_CONTROL]") == 1);
assert(count_tag("[HUB_FLIGHT_CONTROL_CLOCK]") == 1);
assert(count_tag("[HUB_FLIGHT_STATUS_OUT]") == 1);
expect_records(0,0x10000000u,TEST_RETAIN);
if (full) assert(rejected_lines == 2u*log_count && retry_count == 0);
for (unsigned i = 0; i < log_count; ++i) fputs(log_lines[i],stdout);
}
static tusb_control_request_t vendor_request(void) {
const tusb_control_request_t request = {
.bmRequestType = 0xc0, .bRequest = 0x5a, .wValue = 0x1122,
.wIndex = 0x3344, .wLength = sizeof(vendor_reply),
};
return request;
}
static tusb_control_request_t descriptor_request(void) {
const tusb_control_request_t request = {
.bmRequestType = 0x80, .bRequest = TUSB_REQ_GET_DESCRIPTOR,
.wValue = TUSB_DESC_DEVICE << 8, .wLength = 18,
};
return request;
}
static void expect_clock(unsigned snapshot, uint32_t setup, uint32_t arm,
uint32_t complete, uint32_t flags, uint16_t arm_length,
uint16_t complete_length) {
const char* clock = snapshot_line(snapshot,"[HUB_FLIGHT_CONTROL_CLOCK]");
const char* control = snapshot_line(snapshot,"[HUB_FLIGHT_CONTROL]");
assert(field(clock," slot=",10) == field(control," slot=",10));
assert(field(clock," gen=",10) == field(control," gen=",10));
assert(field(clock," setup=",16) == setup);
assert(field(clock," arm=",16) == arm);
assert(field(clock," complete=",16) == complete);
assert(field(clock," flags=",16) == flags);
assert(field(clock," pid=",10) == 1); // First EP0 publication is DATA1.
assert(field(clock," arm_len=",10) == arm_length);
assert(field(clock," len=",10) == complete_length);
}
static const char* expect_status_out(unsigned snapshot, uint32_t arm, uint32_t complete,
uint8_t flags, uint16_t length) {
const char* status = snapshot_line(snapshot,"[HUB_FLIGHT_STATUS_OUT]");
const char* control = snapshot_line(snapshot,"[HUB_FLIGHT_CONTROL]");
assert(field(status," slot=",10) == field(control," slot=",10));
assert(field(status," gen=",10) == field(control," gen=",10));
assert(field(status," arm=",16) == arm);
assert(field(status," complete=",16) == complete);
assert(field(status," flags=",16) == flags);
assert(field(status," len=",10) == length);
return status;
}
static void pending_one_shot(void) {
const tusb_control_request_t request = vendor_request();
assert(native_test_setup(CHILDREN,&request,true));
native_test_advance(TEST_STALL_US-1u);
assert(count_tag("[HUB_FLIGHT_FREEZE]") == 0);
native_test_advance(1);
dump_through(1);
poll(20);
assert(count_tag("[HUB_FLIGHT_FREEZE]") == 1);
uint8_t data[PACKET];
uint16_t length = 0;
assert(native_test_in(CHILDREN,data,&length,true) && length == PACKET);
assert(memcmp(data,vendor_reply,length) == 0);
native_test_advance(TEST_STALL_US);
dump_through(2);
poll(20);
assert(count_tag("[HUB_FLIGHT_FREEZE]") == 2);
assert(native_test_in(CHILDREN,data,&length,true) && length == sizeof(vendor_reply)-PACKET);
assert(memcmp(data,vendor_reply+PACKET,length) == 0);
native_test_advance(TEST_STALL_US);
dump_through(3);
poll(20);
assert(count_tag("[HUB_FLIGHT_FREEZE]") == 3);
const char* first = snapshot_line(0,"[HUB_FLIGHT_CONTROL]");
const char* second = snapshot_line(1,"[HUB_FLIGHT_CONTROL]");
const char* third = snapshot_line(2,"[HUB_FLIGHT_CONTROL]");
assert(strstr(first," pos=0/96 ") && strstr(second," pos=64/96 ") && strstr(third," pos=96/96 "));
assert(field(first," gen=",10) == field(second," gen=",10));
assert(field(second," gen=",10) == field(third," gen=",10));
assert(field(first," stage=",10) == field(second," stage=",10));
assert(field(second," stage=",10) != field(third," stage=",10));
for (unsigned i = 0; i < 3; ++i)
assert(field(snapshot_line(i,"[HUB_FLIGHT_FREEZE]")," reason=",10) == 1);
// A zero cycle is valid evidence, not a missing-sample sentinel. Later
// 32-byte completion must not replace the first 64-byte publication.
expect_clock(0,0,0,0,1,PACKET,0);
expect_clock(1,0,0,0,3,PACKET,PACKET);
expect_clock(2,0,0,0,3,PACKET,PACKET);
expect_status_out(0,0,0,0,0);
expect_status_out(1,0,0,0,0);
const char* status = expect_status_out(2,0,0,1,0);
assert((field(status," shadow_out=",16) &
(USB_BUF_CTRL_AVAIL | USB_BUF_CTRL_DATA1_PID | USB_BUF_CTRL_LEN_MASK)) ==
(USB_BUF_CTRL_AVAIL | USB_BUF_CTRL_DATA1_PID));
// Superseding a state already captured by the pending-control trigger must
// not queue the same generation/stage/position for a second time.
const tusb_control_request_t replacement = descriptor_request();
assert(native_test_setup(CHILDREN,&replacement,true));
assert(count_tag("[HUB_FLIGHT_FREEZE]") == 3);
assert(native_test_in(CHILDREN,data,&length,true) && length == 18);
assert(native_test_out(CHILDREN,NULL,0,true));
poll(100);
assert(count_tag("[HUB_FLIGHT_FREEZE]") == 3);
}
static void expect_control_tokens(unsigned snapshot, const uint32_t* cycles, unsigned count) {
unsigned current = 0, found = 0;
for (unsigned i = 0; i < log_count; ++i) {
const char* line = log_lines[i];
if (tagged(line,"[HUB_FLIGHT_FREEZE]")) ++current;
if (current != snapshot+1u || !tagged(line,"[HUB_FLIGHT]")) continue;
assert(found < count);
assert(field(line," cutoff=",16) == 0x70000000u+cycles[found]);
assert(field(line," pid=",16) == (found % 2u ? TEST_PID_IN : TEST_PID_SETUP));
assert(field(line," pre=",10) == 0 && field(line," ok=",10) == 1);
char clock[48];
snprintf(clock,sizeof(clock)," clock=00000000/%08"PRIx32" ",cycles[found]);
assert(strstr(line,clock));
++found;
}
assert(found == count && "accepted SETUP/first-IN token coverage is incomplete or duplicated");
}
static void control_token(uint8_t slot, uint8_t pid, uint32_t cycle) {
sio_hw->mtime = cycle;
assert(native_test_select(slot));
native_hub_note_selected_token(addresses[slot],slot,0x70000000u+cycle,pid);
}
static void superseded(void) {
const uint8_t slot = CHILDREN;
const tusb_control_request_t old_request = vendor_request();
const tusb_control_request_t new_request = descriptor_request();
control_token(slot,TEST_PID_SETUP,100);
sio_hw->mtime = 200;
assert(native_test_setup(slot,&old_request,false));
sio_hw->mtime = 300;
native_test_drain();
control_token(slot,TEST_PID_IN,400);
control_token(slot,TEST_PID_IN,450); // Only the first accepted IN is retained.
uint8_t data[PACKET];
uint16_t length = 0;
sio_hw->mtime = 500;
assert(native_test_in(slot,data,&length,false) && length == PACKET);
assert(memcmp(data,vendor_reply,length) == 0);
control_token(slot,TEST_PID_SETUP,600);
sio_hw->mtime = 700;
assert(native_test_setup(slot,&new_request,false));
// Both events are queued. Hardware completion time must survive delayed
// foreground processing and the newer device generation's protocol abort.
sio_hw->mtime = 800;
native_test_drain();
control_token(slot,TEST_PID_IN,900);
control_token(slot,TEST_PID_IN,950);
sio_hw->mtime = 1000;
assert(native_test_in(slot,data,&length,false) && length == 18);
sio_hw->mtime = 1100;
native_test_drain();
native_test_advance(TEST_STALL_US);
dump_through(2);
poll(100);
assert(count_tag("[HUB_FLIGHT_FREEZE]") == 2);
const char* old = snapshot_line(0,"[HUB_FLIGHT_CONTROL]");
const char* next = snapshot_line(1,"[HUB_FLIGHT_CONTROL]");
assert(field(old," slot=",10) == slot && field(next," slot=",10) == slot);
assert(field(next," gen=",10) == field(old," gen=",10)+1u);
assert(strstr(old," setup=c0/5a v=1122 i=3344 n=96 "));
assert(strstr(old," pos=0/96 "));
assert(strstr(next," setup=80/06 v=0100 i=0000 n=18 "));
assert(strstr(next," pos=18/18 "));
assert(field(snapshot_line(0,"[HUB_FLIGHT_FREEZE]")," reason=",10) == 2);
assert(field(snapshot_line(1,"[HUB_FLIGHT_FREEZE]")," reason=",10) == 1);
expect_clock(0,200,300,500,3,PACKET,PACKET);
expect_clock(1,700,800,1000,3,18,18);
expect_status_out(0,0,0,0,0);
expect_status_out(1,1000,0,1,0); // Standard reads arm at final-IN IRQ, not drain at 1100.
const uint32_t cycles[] = {100,400,600,900};
expect_control_tokens(0,cycles,3);
expect_control_tokens(1,cycles,4);
}
static void immediate_supersession(void) {
supersede_in_callback = true;
const tusb_control_request_t request = vendor_request();
assert(native_test_setup(CHILDREN,&request,true));
uint8_t data[PACKET];
uint16_t length = 0;
assert(native_test_in(CHILDREN,data,&length,true) && length == 18);
assert(native_test_out(CHILDREN,NULL,0,true));
dump_through(1);
const char* header = snapshot_line(0,"[HUB_FLIGHT_FREEZE]");
assert(field(header," reason=",10) == 2);
assert(field(header," quiet=",10) == 7u && "immediate supersession reported uptime instead of transfer age");
}
static bool routed_token(uint8_t address, uint8_t owner, uint8_t pid, uint32_t cycle) {
sio_hw->mtime = cycle;
const uint32_t cutoff = 0x70000000u+cycle;
const bool selected = native_hub_select_device(address,owner,cutoff);
if (selected) native_hub_note_selected_token(address,owner,cutoff,pid);
else native_hub_note_failed_select(address,owner,cutoff,pid);
return selected;
}
static const char* expect_publication(unsigned snapshot, unsigned index, uint8_t slot,
uint32_t cycle, uint32_t ticket) {
const char* line = expect_observation(snapshot,index,addresses[slot],slot,TEST_PID_IN,cycle,true);
const char* clock = snapshot_line(snapshot,"[HUB_FLIGHT_CONTROL_CLOCK]");
const char* control = snapshot_line(snapshot,"[HUB_FLIGHT_CONTROL]");
assert(field(line," why=",16) == 0x20 && field(line," pub=",16) == ticket);
assert(field(clock," flags=",16) & 1u);
assert(field(clock," slot=",10) == slot && field(control," slot=",10) == slot);
assert(field(clock," gen=",10) == field(control," gen=",10));
assert(field(clock," pub=",16) == ticket);
return line;
}
static void complete_descriptor(uint8_t slot) {
uint8_t data[PACKET];
uint16_t length = 0;
assert(native_test_in(slot,data,&length,true) && length == 18);
assert(memcmp(data,hub_device,length) == 0);
assert(native_test_out(slot,NULL,0,true));
}
static void delayed_publication(void) {
const uint8_t slot = CHILDREN;
const tusb_control_request_t request = vendor_request();
uint8_t data[PACKET];
uint16_t length = 0;
control_token(slot,TEST_PID_SETUP,100);
sio_hw->mtime = 110;
assert(native_test_setup(slot,&request,false));
control_token(slot,TEST_PID_IN,120);
assert(!native_test_in(slot,data,&length,false));
control_token(slot,TEST_PID_IN,130);
// Foreground reply preparation lags the first same-owner, unarmed IN.
// This is the old recorder's blind spot: SETUP's first-IN bit is gone.
native_test_time_us += 3000u;
sio_hw->mtime = 200;
native_test_drain();
// An address-only attempt exercises the lock guard without changing owner;
// the identical address/owner fast path deliberately bypasses that lock.
const uint8_t rejected_address = addresses[slot]+1u;
trace_test_lock_busy = true;
assert(!routed_token(rejected_address,slot,TEST_PID_IN,210));
trace_test_lock_busy = false;
control_token(slot,TEST_PID_OUT,220);
usb_hw->ep_rx_error = 2u; // Model a sticky EP0 sequence-error observation.
control_token(slot,TEST_PID_IN,230);
control_token(slot,TEST_PID_IN,240);
assert(usb_hw->ep_rx_error == 2u);
logger_full = true;
const marker_receipt_t receipt = capture_marker(slot,0);
assert(usb_hw->ep_rx_error == 2u);
// Snapshot bytes must survive later hardware changes and real completion.
usb_hw->ep_rx_error |= 8u;
assert(native_test_in(slot,data,&length,true) && length == PACKET);
assert(memcmp(data,vendor_reply,length) == 0);
assert(native_test_in(slot,data,&length,true) && length == sizeof(vendor_reply)-PACKET);
assert(memcmp(data,vendor_reply+PACKET,length) == 0);
assert(native_test_out(slot,NULL,0,true));
logger_full = false;
dump_through(1);
expect_marker(0,&receipt);
assert(field(snapshot_line(0,"[HUB_FLIGHT_FREEZE]")," n=",10) == 5);
expect_observation(0,0,addresses[slot],slot,TEST_PID_SETUP,100,true);
const char* early = expect_observation(0,1,addresses[slot],slot,TEST_PID_IN,120,true);
assert(!(field(early," in0=",16) & USB_BUF_CTRL_AVAIL));
assert(field(early," why=",16) == 0 && field(early," rxerr=",16) == 0);
const char* rejected = expect_observation(0,2,rejected_address,slot,TEST_PID_IN,210,false);
assert(field(rejected," why=",16) == 1 && field(rejected," pub=",16) == 0);
const char* out = expect_observation(0,3,addresses[slot],slot,TEST_PID_OUT,220,true);
assert(field(out," why=",16) == 0 && field(out," pub=",16) == 0);
const char* after = expect_publication(0,4,slot,230,1);
assert(field(after," in0=",16) & USB_BUF_CTRL_AVAIL);
assert(field(after," rxerr=",16) == 2u);
assert(field(snapshot_line(0,"[HUB_FLIGHT_CONTEXT]")," rxerr=",16) == 2u);
assert(usb_hw->ep_rx_error == 10u); // Recorder never clears a hardware error.
expect_clock(0,110,200,0,1,PACKET,0);
assert(strstr(snapshot_line(0,"[HUB_FLIGHT_CONTROL]")," pos=0/96 "));
// The posthook knows device address/PID, not endpoint number. Even the
// ticket-matched record does not prove an EP0 poll or SIE/host acceptance.
expect_lost(1,0);
}
static void publication_isolation(void) {
const tusb_control_request_t request = descriptor_request();
const uint8_t slots[] = {1,CHILDREN};
for (unsigned i = 0; i < sizeof(slots); ++i) {
control_token(slots[i],TEST_PID_SETUP,100u+100u*i);
assert(native_test_setup(slots[i],&request,false));
control_token(slots[i],TEST_PID_IN,120u+100u*i);
}
sio_hw->mtime = 300;
native_test_drain(); // Both children publish their independent first ticket.
__atomic_store_n(&out_trace_frozen,1u,__ATOMIC_SEQ_CST);
control_token(1,TEST_PID_IN,400);
__atomic_store_n(&out_trace_frozen,0u,__ATOMIC_SEQ_CST);
for (unsigned i = 0; i < TEST_RETAIN; ++i)
control_token(1,TEST_PID_IN,410u+i);
control_token(CHILDREN,TEST_PID_IN,500);
control_token(CHILDREN,TEST_PID_IN,510);
logger_full = true;
const marker_receipt_t first = capture_marker(1,0);
const marker_receipt_t second = capture_marker(CHILDREN,0);
complete_descriptor(1);
complete_descriptor(CHILDREN);
logger_full = false;
dump_through(2);
expect_marker(0,&first);
expect_marker(1,&second);
for (unsigned snapshot = 0; snapshot < 2; ++snapshot) {
assert(field(snapshot_line(snapshot,"[HUB_FLIGHT_FREEZE]")," n=",10) == 5);
for (unsigned i = 0; i < sizeof(slots); ++i) {
expect_observation(snapshot,2u*i,addresses[slots[i]],slots[i],TEST_PID_SETUP,100u+100u*i,true);
expect_observation(snapshot,2u*i+1u,addresses[slots[i]],slots[i],TEST_PID_IN,120u+100u*i,true);
}
expect_observation(snapshot,4,addresses[CHILDREN],CHILDREN,TEST_PID_IN,500,true);
}
assert(field(snapshot_line(0,"[HUB_FLIGHT_CONTROL_CLOCK]")," pub=",16) == 1);
expect_publication(1,4,CHILDREN,500,1);
// Consuming child 1's ticket while frozen neither consumes child 2's equal
// numeric ticket nor replays child 1's discarded observation after thaw.
// A later real publication for child 1 must still become observable.
control_token(1,TEST_PID_SETUP,700);
sio_hw->mtime = 710;
assert(native_test_setup(1,&request,true));
control_token(1,TEST_PID_IN,720);
control_token(1,TEST_PID_IN,730);
const marker_receipt_t next = capture_marker(1,0);
complete_descriptor(1);
dump_through(3);
expect_marker(2,&next);
assert(field(snapshot_line(2,"[HUB_FLIGHT_FREEZE]")," n=",10) == 7);
expect_observation(2,5,addresses[1],1,TEST_PID_SETUP,700,true);
expect_publication(2,6,1,720,2);
expect_lost(3,0);
}
static void publication_wrap_supersession(void) {
const uint8_t slot = CHILDREN;
const tusb_control_request_t request = descriptor_request();
// Seed only the counter boundary; all publications still come from real
// control requests, not fabricated watch fields or posthook forwarding.
__atomic_store_n(&out_trace_publication_sequence[slot-1u],UINT32_MAX-1u,__ATOMIC_RELEASE);
control_token(slot,TEST_PID_SETUP,100);
sio_hw->mtime = 110;
assert(native_test_setup(slot,&request,true)); // First ticket: ffffffff.
control_token(slot,TEST_PID_SETUP,200);
sio_hw->mtime = 210;
assert(native_test_setup(slot,&request,false));
// SETUP does not consume the old notification. Its replacement IRQ has
// revoked readiness, but Core0 has not yet superseded the old watch.
control_token(slot,TEST_PID_IN,220);
logger_full = true;
sio_hw->mtime = 300;
native_test_drain(); // Captures the old watch, then publishes ticket zero.
control_token(slot,TEST_PID_IN,310);
control_token(slot,TEST_PID_IN,320);
const marker_receipt_t receipt = capture_marker(slot,0);
complete_descriptor(slot);
logger_full = false;
dump_through(2);
const char* old_control = snapshot_line(0,"[HUB_FLIGHT_CONTROL]");
const char* old_clock = snapshot_line(0,"[HUB_FLIGHT_CONTROL_CLOCK]");
assert(field(snapshot_line(0,"[HUB_FLIGHT_FREEZE]")," reason=",10) == 2);
assert(field(snapshot_line(0,"[HUB_FLIGHT_FREEZE]")," n=",10) == 3);
assert(field(old_clock," flags=",16) == 1 && field(old_clock," pub=",16) == UINT32_MAX);
assert(field(old_clock," gen=",10) == field(old_control," gen=",10));
assert(field(snapshot_line(0,"[HUB_FLIGHT_STATUS_OUT]")," dgen=",10) > field(old_clock," gen=",10));
const char* stale = expect_observation(0,2,addresses[slot],slot,TEST_PID_IN,220,true);
assert(field(stale," why=",16) == 0x20 && field(stale," pub=",16) == UINT32_MAX);
assert(!(field(stale," in0=",16) & USB_BUF_CTRL_AVAIL));
expect_marker(1,&receipt);
assert(receipt.generation == field(old_clock," gen=",10)+1u);
assert(field(snapshot_line(1,"[HUB_FLIGHT_FREEZE]")," n=",10) == 4);
expect_publication(1,3,slot,310,0); // why bit validates zero; zero is not absent.
assert(field(record_line(1,2)," pub=",16) != field(snapshot_line(1,"[HUB_FLIGHT_CONTROL_CLOCK]")," pub=",16));
expect_clock(1,210,300,0,1,18,0);
expect_lost(2,0);
}
static void poll_retention(void) {
const uint8_t slot = CHILDREN;
const tusb_control_request_t request = vendor_request();
uint8_t data[PACKET];
uint16_t length = 0;
control_token(slot,TEST_PID_SETUP,100);
assert(native_test_setup(slot,&request,true));
control_token(slot,TEST_PID_IN,200);
assert(native_test_in(slot,data,&length,true) && length == PACKET);
assert(memcmp(data,vendor_reply,length) == 0);
control_token(slot,TEST_PID_IN,201);
assert(native_test_in(slot,data,&length,true) && length == sizeof(vendor_reply)-PACKET);
assert(memcmp(data,vendor_reply+PACKET,length) == 0);
control_token(slot,TEST_PID_OUT,300);
assert(native_test_out(slot,NULL,0,true));
for (unsigned i = 0; i < 3u*TEST_RETAIN; ++i)
assert(routed_token(addresses[slot],slot,i % 2u ? TEST_PID_IN : TEST_PID_OUT,400u+i));
assert(routed_token(addresses[0],0,TEST_PID_IN,1000));
for (unsigned i = 0; i < 3u*TEST_RETAIN; ++i) {
assert(routed_token(addresses[0],0,i % 2u ? TEST_PID_IN : TEST_PID_OUT,1100u+i));
root_read();
}
const marker_receipt_t first = capture_marker(slot,0);
dump_through(1);
expect_marker(0,&first);
assert(field(snapshot_line(0,"[HUB_FLIGHT_FREEZE]")," n=",10) == 4);
expect_observation(0,0,addresses[slot],slot,TEST_PID_SETUP,100,true);
expect_observation(0,1,addresses[slot],slot,TEST_PID_IN,200,true);
expect_observation(0,2,addresses[slot],slot,TEST_PID_OUT,300,true);
const char* root = expect_observation(0,3,addresses[0],0,TEST_PID_IN,1000,true);
assert(field(root," commit=",16) == 1000);
// Each child's SETUP rearms both directions independently. OUT observation
// before IN is also valid recorder evidence, not a physical-acceptance claim.
const tusb_control_request_t descriptor = descriptor_request();
control_token(1,TEST_PID_SETUP,2000);
assert(native_test_setup(1,&descriptor,true));
control_token(slot,TEST_PID_SETUP,2100);
assert(native_test_setup(slot,&descriptor,true));
control_token(1,TEST_PID_OUT,2200);
control_token(1,TEST_PID_OUT,2250);
control_token(1,TEST_PID_IN,2300);
assert(native_test_in(1,data,&length,true) && length == 18);
control_token(slot,TEST_PID_IN,2400);
assert(native_test_in(slot,data,&length,true) && length == 18);
control_token(slot,TEST_PID_OUT,2500);
assert(native_test_out(slot,NULL,0,true));
assert(native_test_out(1,NULL,0,true));
const marker_receipt_t second = capture_marker(slot,0);
dump_through(2);
expect_marker(1,&second);
assert(field(snapshot_line(1,"[HUB_FLIGHT_FREEZE]")," n=",10) == 10);
expect_observation(1,4,addresses[1],1,TEST_PID_SETUP,2000,true);
expect_observation(1,5,addresses[slot],slot,TEST_PID_SETUP,2100,true);
expect_observation(1,6,addresses[1],1,TEST_PID_OUT,2200,true);
expect_observation(1,7,addresses[1],1,TEST_PID_IN,2300,true);
expect_observation(1,8,addresses[slot],slot,TEST_PID_IN,2400,true);
expect_observation(1,9,addresses[slot],slot,TEST_PID_OUT,2500,true);
expect_lost(2,0);
}
static void selection_history(void) {
// Synthetic selector inputs separate address-only from owner-only changes.
// The selector does not claim to validate the router's address mapping.
assert(routed_token(5,0,TEST_PID_IN,100));
for (unsigned i = 0; i < 3u*TEST_RETAIN; ++i)
assert(routed_token(5,0,i % 2u ? TEST_PID_IN : TEST_PID_OUT,1000u+i));
assert(routed_token(5,CHILDREN,TEST_PID_OUT,200));
for (unsigned i = 0; i < 3u*TEST_RETAIN; ++i)
assert(routed_token(5,CHILDREN,i % 2u ? TEST_PID_IN : TEST_PID_OUT,2000u+i));
assert(routed_token(addresses[CHILDREN],CHILDREN,TEST_PID_IN,300));
const uint8_t pids[] = {TEST_PID_SETUP,TEST_PID_IN,TEST_PID_OUT};
for (unsigned i = 0; i < sizeof(pids); ++i)
assert(!routed_token(0,DEVICES,pids[i],400u+i));
usb_hw->buf_status = 2;
for (unsigned i = 0; i < sizeof(pids); ++i)
assert(!routed_token(addresses[0],0,pids[i],500u+i));
usb_hw->buf_status = 0;
root_read();
const marker_receipt_t receipt = capture_marker(CHILDREN,0);
dump_through(1);
expect_marker(0,&receipt);
assert(field(snapshot_line(0,"[HUB_FLIGHT_FREEZE]")," n=",10) == 9);
const char* line = expect_observation(0,0,5,0,TEST_PID_IN,100,true);
assert(strstr(line," addr=00000000/00000005 owner=255/0 "));
assert(field(line," commit=",16) == 100);
line = expect_observation(0,1,5,CHILDREN,TEST_PID_OUT,200,true);
char transition[80];
snprintf(transition,sizeof(transition)," addr=00000000/00000005 owner=255/%u ",(unsigned)CHILDREN);
assert(strstr(line,transition));
assert(field(line," commit=",16) == 200);
line = expect_observation(0,2,addresses[CHILDREN],CHILDREN,TEST_PID_IN,300,true);
snprintf(transition,sizeof(transition)," addr=00000000/%08x owner=255/%u ",
(unsigned)addresses[CHILDREN],(unsigned)CHILDREN);
assert(strstr(line,transition));
assert(field(line," commit=",16) == 300);
for (unsigned i = 0; i < sizeof(pids); ++i) {
line = expect_observation(0,3u+i,0,DEVICES,pids[i],400u+i,false);
assert(field(line," why=",16) == 0x10);
line = expect_observation(0,6u+i,addresses[0],0,pids[i],500u+i,false);
assert(field(line," why=",16) == 0x0a);
assert(strstr(line," block=00000002/00000000 "));
}
expect_lost(1,0);
}
static void frozen_selection_history(void) {
assert(routed_token(addresses[1],1,TEST_PID_SETUP,100));
// Deterministically interleave Core1 tokens with the recorder's Core0
// snapshot freeze. Assert the eventual log, not its internal cursor state.
__atomic_store_n(&out_trace_frozen,1u,__ATOMIC_SEQ_CST);
assert(routed_token(addresses[CHILDREN],CHILDREN,TEST_PID_SETUP,200));
assert(routed_token(addresses[CHILDREN],CHILDREN,TEST_PID_IN,210));
assert(routed_token(addresses[CHILDREN],CHILDREN,TEST_PID_OUT,220));
__atomic_store_n(&out_trace_frozen,0u,__ATOMIC_SEQ_CST);
for (unsigned i = 0; i < TEST_RETAIN; ++i)
assert(routed_token(addresses[CHILDREN],CHILDREN,
i % 2u ? TEST_PID_IN : TEST_PID_OUT,300u+i));
__atomic_store_n(&out_trace_frozen,1u,__ATOMIC_SEQ_CST);
assert(routed_token(addresses[0],0,TEST_PID_IN,500));
__atomic_store_n(&out_trace_frozen,0u,__ATOMIC_SEQ_CST);
for (unsigned i = 0; i < TEST_RETAIN; ++i)
assert(routed_token(addresses[0],0,i % 2u ? TEST_PID_IN : TEST_PID_OUT,600u+i));
// A frozen SETUP still rearms first-IN/OUT retention, but a first token
// already consumed during the earlier freeze must not reappear afterward.
__atomic_store_n(&out_trace_frozen,1u,__ATOMIC_SEQ_CST);
assert(routed_token(addresses[1],1,TEST_PID_SETUP,800));
__atomic_store_n(&out_trace_frozen,0u,__ATOMIC_SEQ_CST);
assert(routed_token(addresses[1],1,TEST_PID_IN,900));
assert(routed_token(addresses[1],1,TEST_PID_OUT,1000));
for (unsigned i = 0; i < TEST_RETAIN; ++i)
assert(routed_token(addresses[1],1,i % 2u ? TEST_PID_IN : TEST_PID_OUT,1100u+i));
const marker_receipt_t receipt = capture_marker(1,0);
dump_through(1);
expect_marker(0,&receipt);
assert(field(snapshot_line(0,"[HUB_FLIGHT_FREEZE]")," n=",10) == 3);
expect_observation(0,0,addresses[1],1,TEST_PID_SETUP,100,true);
expect_observation(0,1,addresses[1],1,TEST_PID_IN,900,true);
expect_observation(0,2,addresses[1],1,TEST_PID_OUT,1000,true);
expect_lost(1,0);
}
static void marker_priority(bool partial) {
const uint8_t slot = CHILDREN;
configure_child(slot);
selections(0x10000000u,TEST_RETAIN);
input_completion(slot);
logger_full = !partial;
native_test_advance(TEST_STALL_US);
if (partial) {
poll(6u+CHILDREN);
assert(count_tag("[HUB_FLIGHT]") > 0 && count_tag("[HUB_FLIGHT_END]") == 0);
} else assert(log_count == 0);
logger_full = true;
input_completion(slot);
selections(0x20000000u,TEST_RETAIN);
native_test_advance(TEST_STALL_US);
// Both automatic slots are occupied. The marker must replace the waiting
// automatic snapshot, never the head (even before its first UART line).
selections(0x30000000u,TEST_RETAIN-3u);
control_token(slot,TEST_PID_SETUP,100);
const tusb_control_request_t request = vendor_request();
sio_hw->mtime = 150;
assert(native_test_setup(slot,&request,true));
control_token(slot,TEST_PID_IN,200);
uint8_t data[PACKET];
uint16_t length = 0;
sio_hw->mtime = 250;
assert(native_test_in(slot,data,&length,true) && length == PACKET);
assert(memcmp(data,vendor_reply,length) == 0);
control_token(slot,TEST_PID_OUT,300);
native_test_time_us += 7u;
const uint32_t captured_time = native_test_time_us;
const uint32_t captured_generation = devices[slot].control.generation;
const marker_receipt_t receipt = capture_marker(slot,0);
assert(receipt.time_us == captured_time && receipt.generation == captured_generation);
root_read();
capture_marker(1,1); // A pending host snapshot cannot be replaced by another host.
// The root read must not disturb the child's remaining payload or status.
assert(native_test_in(slot,data,&length,true) && length == sizeof(vendor_reply)-PACKET);
assert(memcmp(data,vendor_reply+PACKET,length) == 0);
assert(native_test_out(slot,NULL,0,true));
const tusb_control_request_t replacement = descriptor_request();
assert(native_test_setup(slot,&replacement,true));
assert(devices[slot].control.generation != receipt.generation);
assert(native_test_in(slot,data,&length,true) && length == 18);
assert(memcmp(data,hub_device,length) == 0);
assert(native_test_out(slot,NULL,0,true));
for (unsigned i = 0; i < 3; ++i) {
input_completion(slot);
selections(0x40000000u+i*0x10000000u,TEST_RETAIN);
native_test_advance(TEST_STALL_US);
}
poll(40); // Unchanged automatic triggers must not keep increasing lost.
logger_full = false;
dump_through(2);
poll(100);
assert(count_tag("[HUB_FLIGHT_FREEZE]") == 2);
expect_records(0,0x10000000u,TEST_RETAIN);
expect_marker(1,&receipt);
const char* control = snapshot_line(1,"[HUB_FLIGHT_CONTROL]");
assert(strstr(control," setup=c0/5a v=1122 i=3344 n=96 "));
assert(strstr(control," pos=64/96 "));
expect_clock(1,150,150,250,3,PACKET,PACKET);
const char* status = expect_status_out(1,0,0,0,0);
assert(field(status," dgen=",10) == receipt.generation);
assert((field(status," shadow_in=",16) &
(USB_BUF_CTRL_AVAIL | USB_BUF_CTRL_FULL | USB_BUF_CTRL_LEN_MASK)) ==
(USB_BUF_CTRL_AVAIL | USB_BUF_CTRL_FULL | (sizeof(vendor_reply)-PACKET)));
assert(!(field(status," shadow_out=",16) & USB_BUF_CTRL_AVAIL));
assert(field(snapshot_line(1,"[HUB_FLIGHT_FREEZE]")," n=",10) == TEST_RETAIN);
for (unsigned i = 0; i < TEST_RETAIN-3u; ++i) {
const char* line = record_line(1,i);
assert(field(line," cutoff=",16) == 0x30000000u+i);
assert(field(line," pid=",16) == TEST_PID_OUT && field(line," ok=",10) == 1);
}
expect_observation(1,TEST_RETAIN-3u,addresses[slot],slot,TEST_PID_SETUP,100,true);
expect_observation(1,TEST_RETAIN-2u,addresses[slot],slot,TEST_PID_IN,200,true);
expect_observation(1,TEST_RETAIN-1u,addresses[slot],slot,TEST_PID_OUT,300,true);
expect_lost(2,5); // One replacement, one busy marker, three automatic drops.
}
static void marker_zero_and_capacity(void) {
configure_child(CHILDREN);
native_test_time_us = 0;
logger_full = true;
const marker_receipt_t receipt = capture_marker(1,0);
// Both receipt fields may legitimately be zero: this child has not yet
// received a SETUP. Only status distinguishes capture from a busy reply.
assert(receipt.time_us == 0 && receipt.generation == 0);
const marker_receipt_t second = capture_marker(CHILDREN,0);
assert(second.time_us == 0 && second.generation != 0);
capture_marker(1,1); // Only a full queue with a waiting host refuses a marker.
assert(log_count == 0);
logger_full = false;
native_test_advance(TEST_LINE_US);
assert(count_tag("[HUB_FLIGHT_FREEZE]") == 1 && count_tag("[HUB_FLIGHT_END]") == 0);
logger_full = true;
input_completion(CHILDREN);
selections(0x10000000u,TEST_RETAIN);
native_test_advance(TEST_STALL_US);
root_read();
for (unsigned i = 0; i < 2; ++i) {
input_completion(CHILDREN);
selections(0x20000000u+i*0x10000000u,TEST_RETAIN);
native_test_advance(TEST_STALL_US);
}
poll(40);
logger_full = false;
dump_through(2);
poll(100);
assert(count_tag("[HUB_FLIGHT_FREEZE]") == 2);
expect_marker(0,&receipt);
assert(field(snapshot_line(0,"[HUB_FLIGHT_FREEZE]")," n=",10) == 0);
expect_marker(1,&second);
assert(field(snapshot_line(1,"[HUB_FLIGHT_FREEZE]")," n=",10) == 0);
expect_lost(2,4); // One busy marker and three automatic drops.
}
static void marker_active_priority(void) {
configure_child(CHILDREN);
selections(0x10000000u,TEST_RETAIN);
const marker_receipt_t first = capture_marker(1,0);
assert(count_tag("[HUB_FLIGHT_FREEZE]") == 1 && count_tag("[HUB_FLIGHT_END]") == 0);
logger_full = true;
input_completion(CHILDREN);
selections(0x20000000u,TEST_RETAIN);
native_test_advance(TEST_STALL_US);
// A host head is protected, but does not prevent replacing the automatic
// snapshot waiting behind it with another independently protected host.
const marker_receipt_t second = capture_marker(CHILDREN,0);
capture_marker(1,1);
root_read();
for (unsigned i = 0; i < 2; ++i) {
input_completion(CHILDREN);
selections(0x30000000u+i*0x10000000u,TEST_RETAIN);
native_test_advance(TEST_STALL_US);
}
poll(40);
logger_full = false;
dump_through(2);
poll(100);
assert(count_tag("[HUB_FLIGHT_FREEZE]") == 2);
expect_marker(0,&first);
expect_marker(1,&second);
for (unsigned snapshot = 0; snapshot < 2; ++snapshot) {
assert(field(snapshot_line(snapshot,"[HUB_FLIGHT_FREEZE]")," n=",10) == TEST_RETAIN);
for (unsigned i = 0; i < TEST_RETAIN; ++i) {
const char* line = record_line(snapshot,i);
assert(field(line," cutoff=",16) == (snapshot+1u)*0x10000000u+i);
assert(field(line," pid=",16) == TEST_PID_OUT && field(line," ok=",10) == 1);
}
}
expect_lost(2,4); // One automatic replacement, one busy marker, two drops.
}
static void marker_rejected_requests(void) {
const tusb_control_request_t valid = marker_request(CHILDREN);
tusb_control_request_t invalid[11];
for (unsigned i = 0; i < sizeof(invalid)/sizeof(invalid[0]); ++i) invalid[i] = valid;
invalid[0].bmRequestType = 0x40; // OUT direction.
invalid[1].bmRequestType = 0xc1; // Interface recipient.
invalid[2].bmRequestType = 0xa0; // Class request.
invalid[3].bRequest = 0x7e;
invalid[4].wValue ^= 1u;
invalid[5].wIndex = 0;
invalid[6].wIndex = CHILDREN+1u;
invalid[7].wIndex = 0x100u+CHILDREN;
invalid[8].wLength = 0;
invalid[9].wLength = 15;
invalid[10].wLength = 17;
for (unsigned i = 0; i < sizeof(invalid)/sizeof(invalid[0]); ++i) {
assert(!native_test_setup(0,&invalid[i],true));
root_read();
poll(10);
assert(count_tag("[HUB_FLIGHT_FREEZE]") == 0);
}
for (uint8_t slot = 1; slot <= CHILDREN; ++slot) {
assert(!native_test_setup(slot,&valid,true));
root_read();
poll(10);
assert(count_tag("[HUB_FLIGHT_FREEZE]") == 0);
}
const marker_receipt_t receipt = capture_marker(CHILDREN,0);
dump_through(1);
expect_marker(0,&receipt);
expect_lost(1,0);
}
static void expect_handover_packet(uint8_t slot, uint16_t offset, uint16_t expected_length,
bool data1) {
uint8_t data[PACKET];
uint16_t length = UINT16_MAX;
assert(native_test_select(slot));
const uint32_t packet = buffer_regs()[0];
assert(((packet & USB_BUF_CTRL_DATA1_PID) != 0) == data1);
// Selection must expose the packet before the token, with no Core0 task.
assert(native_test_in(slot,data,&length,false));
assert(length == expected_length);
assert(memcmp(data,handover_reply[slot]+offset,length) == 0);
}
static tusb_control_request_t approved_request(uint16_t length, uint16_t host_length,
uint16_t tag) {
const tusb_control_request_t request = {
.bmRequestType = 0xc0, .bRequest = 0x5d, .wValue = length,
.wIndex = tag, .wLength = host_length,
};
return request;
}
static void expect_approved_packet(uint8_t slot, uint16_t offset, uint16_t expected_length,
bool data1) {
uint8_t data[PACKET];
uint16_t length = UINT16_MAX;
assert(native_test_select(slot));
assert(((buffer_regs()[0] & USB_BUF_CTRL_DATA1_PID) != 0) == data1);
assert(native_test_in(slot,data,&length,false) && length == expected_length);
assert(memcmp(data,approved_reply[slot]+offset,length) == 0);
}
static void complete_ready_status(uint8_t slot) {
assert(native_test_select(slot));
assert((buffer_regs()[1] & (USB_BUF_CTRL_AVAIL | USB_BUF_CTRL_DATA1_PID |
USB_BUF_CTRL_STALL | USB_BUF_CTRL_LEN_MASK)) ==
(USB_BUF_CTRL_AVAIL | USB_BUF_CTRL_DATA1_PID));
assert(native_test_out(slot,NULL,0,false));
assert(!native_test_out(slot,NULL,0,false));
}
static void expect_approved_callback(unsigned index, uint8_t slot, uint8_t stage,
const tusb_control_request_t* request) {
assert(index < approved_callback_count);
const approved_callback_t* callback = &approved_callbacks[index];
assert(callback->slot == slot && callback->stage == stage);
assert(memcmp(&callback->request,request,sizeof(*request)) == 0);
}
static void expect_no_control_packets(void) {
uint8_t data[PACKET];
uint16_t length = 0;
for (uint8_t slot = 0; slot < DEVICES; ++slot) {
assert(!native_test_in(slot,data,&length,false));
assert(!native_test_out(slot,NULL,0,false));
}
}
static void approved_status_handoff(void) {
unsigned callbacks = 0;
for (unsigned pass = 0; pass < 3; ++pass) {
tusb_control_request_t requests[DEVICES];
bool final_first[DEVICES];
for (uint8_t slot = 0; slot < DEVICES; ++slot) {
// Mixed short reply, terminating ZLP, and multi-packet tail;
// then exact one/two-packet replies with no padding ZLP.
const uint16_t length = pass == 0 ? (slot == 0 ? 1 : slot % 2u ? PACKET : 83) :
pass == 1 ? PACKET : 2u*PACKET;
requests[slot] = approved_request(length,pass == 0 ? 128 : length,
(uint16_t)(100u*pass+slot));
final_first[slot] = length < PACKET || (length == PACKET && requests[slot].wLength == PACKET);
assert(native_test_setup(slot,&requests[slot],true));
assert(!native_test_out(slot,NULL,0,false));
}
for (uint8_t slot = 0; slot < DEVICES; ++slot) {
const uint16_t length = requests[slot].wValue < PACKET ? requests[slot].wValue : PACKET;
expect_approved_packet(slot,0,length,true);
if (final_first[slot]) complete_ready_status(slot);
else assert(!native_test_out(slot,NULL,0,false));
}
assert(approved_callback_count == callbacks); // No callbacks in IRQ.
native_test_drain();
for (uint8_t slot = 0; slot < DEVICES; ++slot) {
if (!final_first[slot]) continue;
expect_approved_callback(callbacks++,slot,CONTROL_STAGE_DATA,&requests[slot]);
expect_approved_callback(callbacks++,slot,CONTROL_STAGE_ACK,&requests[slot]);
}
assert(approved_callback_count == callbacks);
for (uint8_t slot = 0; slot < DEVICES; ++slot) {
assert(!native_test_out(slot,NULL,0,false));
if (final_first[slot]) continue;
// A required zero-length IN must complete before OUT can be ready.
expect_approved_packet(slot,PACKET,requests[slot].wValue-PACKET,false);
complete_ready_status(slot);
}
assert(approved_callback_count == callbacks);
native_test_drain();
for (uint8_t slot = 0; slot < DEVICES; ++slot) {
if (final_first[slot]) continue;
expect_approved_callback(callbacks++,slot,CONTROL_STAGE_DATA,&requests[slot]);
expect_approved_callback(callbacks++,slot,CONTROL_STAGE_ACK,&requests[slot]);
}
assert(approved_callback_count == callbacks);
expect_no_control_packets(); // Draining final IN must not rearm consumed OUT.
native_test_drain();
assert(approved_callback_count == callbacks);
}
}
static void approved_status_superseded(void) {
tusb_control_request_t old[DEVICES], next[DEVICES];
for (uint8_t slot = 0; slot < DEVICES; ++slot) {
old[slot] = approved_request(1,1,100u+slot);
next[slot] = approved_request(7,7,200u+slot);
assert(native_test_setup(slot,&old[slot],true));
}
for (uint8_t slot = 0; slot < DEVICES; ++slot) {
expect_approved_packet(slot,0,1,true);
complete_ready_status(slot);
// SETUP revokes readiness but cannot erase the real, queued DATA/ACK.
assert(native_test_setup(slot,&next[slot],false));
assert(!native_test_out(slot,NULL,0,false));
}
assert(approved_callback_count == 0);
native_test_drain();
for (uint8_t slot = 0; slot < DEVICES; ++slot) {
expect_approved_callback(2u*slot,slot,CONTROL_STAGE_DATA,&old[slot]);
expect_approved_callback(2u*slot+1u,slot,CONTROL_STAGE_ACK,&old[slot]);
assert(!native_test_out(slot,NULL,0,false));
expect_approved_packet(slot,0,7,true);
complete_ready_status(slot);
}
assert(approved_callback_count == 2u*DEVICES);
native_test_drain();
for (uint8_t slot = 0; slot < DEVICES; ++slot) {
expect_approved_callback(2u*(DEVICES+slot),slot,CONTROL_STAGE_DATA,&next[slot]);
expect_approved_callback(2u*(DEVICES+slot)+1u,slot,CONTROL_STAGE_ACK,&next[slot]);
}
assert(approved_callback_count == 4u*DEVICES);
expect_no_control_packets();
// A replacement received before IN completion must revoke the pending
// eligibility, rather than publish the old status for the new SETUP.
const tusb_control_request_t descriptor = descriptor_request();
for (uint8_t slot = 0; slot < DEVICES; ++slot)
assert(native_test_setup(slot,&old[slot],true));
for (uint8_t slot = 0; slot < DEVICES; ++slot)
assert(native_test_setup(slot,&descriptor,false));
expect_no_control_packets();
native_test_drain();
for (uint8_t slot = 0; slot < DEVICES; ++slot) {
assert(!native_test_out(slot,NULL,0,false));
complete_descriptor(slot);
}
assert(approved_callback_count == 4u*DEVICES);
expect_no_control_packets();
}
static void approved_status_reset(void) {
const tusb_control_request_t request = approved_request(1,1,100);
for (uint8_t slot = 0; slot < DEVICES; ++slot)
assert(native_test_setup(slot,&request,true));
event_t final_in = {0}, status = {0};
for (uint8_t slot = 0; slot < DEVICES; ++slot) {
expect_approved_packet(slot,0,1,true);
if (slot == 0) final_in = events[event_tail];
if (slot % 2u == 0) {
const unsigned status_index = event_head;
complete_ready_status(slot);
if (slot == 0) status = events[status_index];
}
}
// Both already-completed statuses and still-ready inactive statuses lose
// ownership in reset IRQ, before the queued foreground completions run.
assert(approved_callback_count == 0);
native_test_bus_reset(false);
expect_no_control_packets();
native_test_drain();
expect_no_control_packets();
assert(approved_callback_count == 0);
// Restore the fixture's synthetic routes after deferred reset processing,
// just as native_test_bus_reset(true) does after its internal drain.
for (uint8_t slot = 1; slot < DEVICES; ++slot) addresses[slot] = slot*17u;
const tusb_control_request_t descriptor = descriptor_request();
for (uint8_t slot = 0; slot < DEVICES; ++slot)
assert(native_test_setup(slot,&descriptor,true));
transfer_complete(&final_in);
transfer_complete(&status);
for (uint8_t slot = 0; slot < DEVICES; ++slot) {
uint8_t data[PACKET];
uint16_t length = 0;
assert(!native_test_out(slot,NULL,0,false));
assert(native_test_in(slot,data,&length,false) && length == 18);
assert(memcmp(data,hub_device,length) == 0);
complete_ready_status(slot);
}
native_test_drain();
expect_no_control_packets();
assert(approved_callback_count == 0);
}
static void approved_status_reset_during_completion(void) {
const tusb_control_request_t request = approved_request(1,1,100);
assert(native_test_setup(0,&request,true));
expect_approved_packet(0,0,1,true);
assert(approved_callback_count == 0);
// Interrupt foreground after its first reset-generation check, while its
// diagnostic is emitted, but before it claims the queued final-IN event.
reset_on_root_complete = true;
native_test_drain();
assert(!reset_on_root_complete);
assert(approved_callback_count == 0);
expect_no_control_packets();
assert(native_test_setup(0,&request,true));
assert(!native_test_out(0,NULL,0,false));
expect_approved_packet(0,0,1,true);
complete_ready_status(0);
native_test_drain();
expect_approved_callback(0,0,CONTROL_STAGE_DATA,&request);
expect_approved_callback(1,0,CONTROL_STAGE_ACK,&request);
assert(approved_callback_count == 2);
expect_no_control_packets();
}
static bool reset_hook_seen, reset_hook_bus_reset;
static void service_during_child_reset(uint8_t instance) {
const uint8_t target = CHILDREN > 2 ? 3 : 2;
assert(instance == target-1u);
native_test_reset_hook = NULL;
reset_hook_seen = true;
if (reset_hook_bus_reset) {
native_test_bus_reset(false);
return;
}
// A sibling SETUP can arrive while reset bookkeeping reads stored state.
// Its IRQ must release SETUP_REC before the next root interrupt poll.
const tusb_control_request_t descriptor = descriptor_request();
assert(native_test_setup(1,&descriptor,false));
uint8_t packet[PACKET];
uint16_t length = 0;
assert(native_test_private_in(0,0x8f,packet,&length) &&
"child reset callback blocked the next hub status-change poll");
assert(length == 1 && packet[0] == (1u << target));
}
static void port_reset_interrupt_progress(bool bus_reset) {
const uint8_t target = CHILDREN > 2 ? 3 : 2;
uint8_t packet[PACKET];
uint16_t length = 0;
const tusb_control_request_t prepare[] = {
{.bRequest = TUSB_REQ_SET_ADDRESS, .wValue = 9},
{.bRequest = TUSB_REQ_SET_CONFIGURATION, .wValue = 1},
{.bmRequestType = 0x23, .bRequest = TUSB_REQ_SET_FEATURE,
.wValue = 8, .wIndex = target},
};
for (unsigned i = 0; i < sizeof(prepare)/sizeof(prepare[0]); ++i) {
assert(native_test_setup(0,&prepare[i],true));
assert(native_test_in(0,packet,&length,true) && length == 0);
}
const tusb_control_request_t reset = {
.bmRequestType = 0x23, .bRequest = TUSB_REQ_SET_FEATURE,
.wValue = 4, .wIndex = target,
};
assert(native_test_setup(0,&reset,true));
reset_hook_bus_reset = bus_reset;
native_test_reset_hook = service_during_child_reset;
assert(native_test_in(0,packet,&length,true) && length == 0);
assert(reset_hook_seen);
const tusb_control_request_t descriptor = descriptor_request();
if (bus_reset) {
assert(default_device == 0 && addresses[0] == 0 &&
devices[0].configuration == 0);
for (unsigned p = 0; p < CHILDREN; ++p)
assert(ports[p].status == 0 && ports[p].change == 0);
assert(native_test_setup(0,&descriptor,true));
assert(native_test_in(0,packet,&length,true) && length == 18);
assert(native_test_out(0,NULL,0,true));
return;
}
assert(native_test_in(1,packet,&length,true) && length == 18);
assert(memcmp(packet,hub_device,length) == 0);
assert(native_test_out(1,NULL,0,true));
native_test_advance(10000u);
assert(default_device == target && addresses[target] == 0);
assert(native_test_setup(target,&descriptor,true));
assert(native_test_in(target,packet,&length,true) && length == 18);
assert(native_test_out(target,NULL,0,true));
}
static void approved_status_port_reset(bool queued_status) {
const uint8_t target = CHILDREN;
uint8_t data[PACKET];
uint16_t length = 0;
const tusb_control_request_t preparation[] = {
{.bRequest = TUSB_REQ_SET_ADDRESS, .wValue = 9},
{.bmRequestType = 0x23, .bRequest = TUSB_REQ_SET_FEATURE,
.wValue = 8, .wIndex = target},
};
for (unsigned i = 0; i < sizeof(preparation)/sizeof(preparation[0]); ++i) {
assert(native_test_setup(0,&preparation[i],true));
assert(native_test_in(0,data,&length,true) && length == 0);
}
const tusb_control_request_t reset = {
.bmRequestType = 0x23, .bRequest = TUSB_REQ_SET_FEATURE,
.wValue = 4, .wIndex = target,
};
const tusb_control_request_t request = approved_request(1,1,100);
assert(native_test_setup(0,&reset,true));
for (uint8_t slot = 1; slot < DEVICES; ++slot)
assert(native_test_setup(slot,&request,true));
// Queue the real root status-IN action first. Its reset_device invalidates
// the target's later queued completions, without touching sibling owners.
assert(native_test_in(0,data,&length,false) && length == 0);
assert(!native_test_out(0,NULL,0,false)); // Writes never acquire status OUT.
for (uint8_t slot = 1; slot < DEVICES; ++slot) {
expect_approved_packet(slot,0,1,true);
if (queued_status) complete_ready_status(slot);
}
assert(approved_callback_count == 0);
native_test_drain();
native_test_advance(10000u);
assert(native_test_select(target)); // Reset has returned this child to address zero.
assert(!native_test_in(target,data,&length,false));
assert(!native_test_out(target,NULL,0,false));
unsigned callbacks = 0;
for (uint8_t slot = 1; slot < DEVICES; ++slot) {
if (slot == target) continue;
expect_approved_callback(callbacks++,slot,CONTROL_STAGE_DATA,&request);
if (queued_status)
expect_approved_callback(callbacks++,slot,CONTROL_STAGE_ACK,&request);
else complete_ready_status(slot);
}
assert(approved_callback_count == callbacks);
native_test_drain();
if (!queued_status) {
for (uint8_t slot = 1; slot < DEVICES; ++slot)
if (slot != target)
expect_approved_callback(callbacks++,slot,CONTROL_STAGE_ACK,&request);
}
assert(approved_callback_count == 2u*(CHILDREN-1u) && approved_callback_count == callbacks);
expect_no_control_packets();
}
static void approved_status_invalid_length(void) {
const tusb_control_request_t request = approved_request(16,16,100);
for (uint8_t slot = 0; slot < DEVICES; ++slot) {
assert(native_test_setup(slot,&request,true));
assert(native_test_select(slot));
// Model an actual completed length that disagrees with the armed IN.
const uint16_t actual = slot % 2u ? 17 : 15;
buffer_regs()[0] = (buffer_regs()[0] & ~USB_BUF_CTRL_LEN_MASK) | actual;
uint8_t data[PACKET];
uint16_t length = 0;
assert(native_test_in(slot,data,&length,false) && length == actual);
assert(!native_test_out(slot,NULL,0,false));
}
native_test_drain();
expect_no_control_packets();
assert(approved_callback_count == 0);
for (uint8_t slot = 0; slot < DEVICES; ++slot) {
assert(native_test_select(slot));
assert(buffer_regs()[0] & USB_BUF_CTRL_STALL);
assert(buffer_regs()[1] & USB_BUF_CTRL_STALL);
assert(native_test_setup(slot,&request,true));
assert(!native_test_out(slot,NULL,0,false));
expect_approved_packet(slot,0,16,true);
complete_ready_status(slot);
native_test_drain();
expect_approved_callback(2u*slot,slot,CONTROL_STAGE_DATA,&request);
expect_approved_callback(2u*slot+1u,slot,CONTROL_STAGE_ACK,&request);
}
assert(approved_callback_count == 2u*DEVICES);
expect_no_control_packets();
}
static void approved_status_watch(void) {
const uint8_t slot = CHILDREN;
const tusb_control_request_t request = approved_request(1,1,100);
sio_hw->mtime = 100;
assert(native_test_setup(slot,&request,true));
assert(!native_test_out(slot,NULL,0,false));
sio_hw->mtime = 200;
expect_approved_packet(slot,0,1,true);
assert(approved_callback_count == 0);
assert(native_test_select(1));
sio_hw->mtime = 300;
native_test_drain();
expect_approved_callback(0,slot,CONTROL_STAGE_DATA,&request);
assert(approved_callback_count == 1);
logger_full = true;
sio_hw->mtime = 400;
const marker_receipt_t armed = capture_marker(slot,0);
sio_hw->mtime = 500;
complete_ready_status(slot);
assert(approved_callback_count == 1);
sio_hw->mtime = 600;
native_test_drain();
expect_approved_callback(1,slot,CONTROL_STAGE_ACK,&request);
sio_hw->mtime = 700;
const marker_receipt_t completed = capture_marker(slot,0);
assert(completed.generation == armed.generation);
const tusb_control_request_t replacement = descriptor_request();
sio_hw->mtime = 800;
assert(native_test_setup(slot,&replacement,true));
sio_hw->mtime = 900;
complete_descriptor(slot);
logger_full = false;
dump_through(2);
expect_marker(0,&armed);
expect_marker(1,&completed);
expect_clock(0,100,100,200,3,1,1);
expect_clock(1,100,100,200,3,1,1);
const char* first = expect_status_out(0,200,0,1,0);
const char* second = expect_status_out(1,200,500,3,0);
assert(field(first," dgen=",10) == armed.generation);
assert(field(second," dgen=",10) == armed.generation);
assert((field(first," shadow_out=",16) & (USB_BUF_CTRL_AVAIL | USB_BUF_CTRL_DATA1_PID |
USB_BUF_CTRL_LEN_MASK)) == (USB_BUF_CTRL_AVAIL | USB_BUF_CTRL_DATA1_PID));
assert(field(second," shadow_out=",16) == 0);
assert(field(snapshot_line(0,"[HUB_FLIGHT_CONTROL]")," stage=",10) == STATUS_OUT);
assert(field(snapshot_line(1,"[HUB_FLIGHT_CONTROL]")," stage=",10) == IDLE);
assert(approved_callback_count == 2);
expect_no_control_packets();
}
static void prepare_status_read(uint8_t slot) {
const tusb_control_request_t request = {
.bmRequestType = 0xc0, .bRequest = 0x5b, .wLength = 16,
};
for (unsigned i = 0; i < sizeof(handover_reply[slot]); ++i)
handover_reply[slot][i] = (uint8_t)(slot * 0x31u + i * 3u);
sio_hw->mtime = 100;
assert(native_test_setup(slot,&request,true));
sio_hw->mtime = 200;
expect_handover_packet(slot,0,16,true);
// This nonpreapproved vendor still requires the foreground DATA callback.
assert(!(buffer_regs()[1] & USB_BUF_CTRL_AVAIL));
assert(!native_test_out(slot,NULL,0,false));
}
static void status_out_rejected_data(void) {
reject_status_in_callback = true;
for (uint8_t slot = 0; slot < DEVICES; ++slot) prepare_status_read(slot);
native_test_drain();
for (uint8_t slot = 0; slot < DEVICES; ++slot) {
assert(native_test_select(slot));
assert(buffer_regs()[1] & USB_BUF_CTRL_STALL);
assert(handover_data_callbacks[slot] == 1 && handover_acks[slot] == 0);
}
expect_no_control_packets();
native_test_drain();
for (uint8_t slot = 0; slot < DEVICES; ++slot)
assert(handover_data_callbacks[slot] == 1 && handover_acks[slot] == 0);
}
static void status_out_lifecycle(void) {
const uint8_t slot = CHILDREN;
prepare_status_read(slot);
assert(native_test_select(1));
sio_hw->mtime = 300;
native_test_drain(); // Publish to an inactive child's shadow, not root EP0.
logger_full = true;
sio_hw->mtime = 400;
const marker_receipt_t armed = capture_marker(slot,0);
assert(native_test_select(slot));
assert((buffer_regs()[1] & (USB_BUF_CTRL_AVAIL | USB_BUF_CTRL_DATA1_PID |
USB_BUF_CTRL_STALL | USB_BUF_CTRL_LEN_MASK)) ==
(USB_BUF_CTRL_AVAIL | USB_BUF_CTRL_DATA1_PID));
sio_hw->mtime = 500;
assert(native_test_out(slot,NULL,0,false));
assert(handover_acks[slot] == 0);
assert(native_test_select(1));
sio_hw->mtime = 600;
native_test_drain();
assert(handover_acks[slot] == 1);
sio_hw->mtime = 700;
const marker_receipt_t completed = capture_marker(slot,0);
assert(completed.generation == armed.generation);
// Replace the live control/watch and finish it before either host snapshot
// drains. Both headers must retain their own publication/completion image.
const tusb_control_request_t replacement = descriptor_request();
uint8_t data[PACKET];
uint16_t length = 0;
sio_hw->mtime = 800;
assert(native_test_setup(slot,&replacement,true));
sio_hw->mtime = 900;
assert(native_test_in(slot,data,&length,true) && length == 18);
sio_hw->mtime = 1000;
assert(native_test_out(slot,NULL,0,true));
logger_full = false;
dump_through(2);
expect_marker(0,&armed);
expect_marker(1,&completed);
expect_clock(0,100,100,200,3,16,16);
const char* first = expect_status_out(0,300,0,1,0);
const char* second = expect_status_out(1,300,500,3,0);
assert(field(first," dgen=",10) == armed.generation);
assert(field(second," dgen=",10) == armed.generation);
assert(field(first," shadow_in=",16) == 0 && field(second," shadow_in=",16) == 0);
assert((field(first," shadow_out=",16) &
(USB_BUF_CTRL_AVAIL | USB_BUF_CTRL_DATA1_PID | USB_BUF_CTRL_LEN_MASK)) ==
(USB_BUF_CTRL_AVAIL | USB_BUF_CTRL_DATA1_PID));
assert(field(second," shadow_out=",16) == 0);
assert(field(snapshot_line(0,"[HUB_FLIGHT_CONTROL]")," stage=",10) == STATUS_OUT);
assert(field(snapshot_line(1,"[HUB_FLIGHT_CONTROL]")," stage=",10) == IDLE);
}
static void status_out_superseded_arm(void) {
const uint8_t slot = CHILDREN;
prepare_status_read(slot);
supersede_status_in_callback = true;
logger_full = true;
sio_hw->mtime = 300;
native_test_drain();
// The DATA callback injected a replacement SETUP before the old status
// publication acquired its generation guard. STATUS_OUT alone is no arm.
sio_hw->mtime = 400;
const marker_receipt_t replacement = capture_marker(slot,0);
uint8_t data[PACKET];
uint16_t length = 0;
assert(native_test_in(slot,data,&length,true) && length == 18);
assert(memcmp(data,hub_device,length) == 0);
assert(native_test_out(slot,NULL,0,true));
assert(handover_acks[slot] == 0);
logger_full = false;
dump_through(2);
const char* old = expect_status_out(0,0,0,0,0);
assert(field(snapshot_line(0,"[HUB_FLIGHT_FREEZE]")," reason=",10) == 2);
assert(field(snapshot_line(0,"[HUB_FLIGHT_CONTROL]")," stage=",10) == STATUS_OUT);
assert(field(old," gen=",10)+1u == replacement.generation);
assert(field(old," dgen=",10) == replacement.generation);
assert(!(field(old," shadow_out=",16) & USB_BUF_CTRL_AVAIL));
expect_marker(1,&replacement);
const char* next = expect_status_out(1,0,0,0,0);
assert(field(next," dgen=",10) == replacement.generation);
assert((field(next," shadow_in=",16) & (USB_BUF_CTRL_AVAIL | USB_BUF_CTRL_LEN_MASK)) ==
(USB_BUF_CTRL_AVAIL | 18u));
}
static void status_out_stale_completion(bool reset) {
const uint8_t slot = CHILDREN;
prepare_status_read(slot);
sio_hw->mtime = 300;
native_test_drain();
sio_hw->mtime = 400;
assert(native_test_out(slot,NULL,0,false));
assert(event_tail != event_head);
const event_t completion = events[event_tail];
assert(completion.device == slot && completion.channel == 1 && completion.length == 0);
sio_hw->mtime = 500;
if (reset) native_test_bus_reset(true);
else native_test_drain();
// A queued status ACK is revoked by reset, not merely by delayed Core0.
assert(handover_acks[slot] == (reset ? 0u : 1u));
const tusb_control_request_t replacement = descriptor_request();
sio_hw->mtime = 600;
assert(native_test_setup(slot,&replacement,true));
// Replay a captured hardware event, not a fabricated watch update: a late
// completion of the prior control/reset generation cannot mark this read.
sio_hw->mtime = 700;
transfer_complete(&completion);
assert(handover_acks[slot] == (reset ? 0u : 1u));
assert(!native_test_out(slot,NULL,0,false));
logger_full = true;
const marker_receipt_t receipt = capture_marker(slot,0);
uint8_t data[PACKET];
uint16_t length = 0;
assert(native_test_in(slot,data,&length,true) && length == 18);
assert(memcmp(data,hub_device,length) == 0);
assert(native_test_out(slot,NULL,0,true));
logger_full = false;
dump_through(1);
expect_marker(0,&receipt);
const char* status = expect_status_out(0,0,0,0,0);
assert(field(status," dgen=",10) == receipt.generation);
assert((field(status," shadow_in=",16) & (USB_BUF_CTRL_AVAIL | USB_BUF_CTRL_LEN_MASK)) ==
(USB_BUF_CTRL_AVAIL | 18u));
}
static void status_out_reset_watch(bool port_reset) {
const uint8_t slot = CHILDREN;
uint8_t data[PACKET];
uint16_t length = 0;
if (port_reset) {
// Release address zero and power the port through ordinary root
// requests so the subsequent child reset follows its real guards.
const tusb_control_request_t requests[] = {
{.bRequest = TUSB_REQ_SET_ADDRESS, .wValue = 9},
{.bmRequestType = 0x23, .bRequest = TUSB_REQ_SET_FEATURE,
.wValue = 8, .wIndex = slot},
};
for (unsigned i = 0; i < sizeof(requests)/sizeof(requests[0]); ++i) {
assert(native_test_setup(0,&requests[i],true));
assert(native_test_in(0,data,&length,true) && length == 0);
}
}
prepare_status_read(slot);
sio_hw->mtime = 300;
native_test_drain();
sio_hw->mtime = 400;
assert(native_test_out(slot,NULL,0,true));
assert(handover_acks[slot] == 1);
logger_full = true;
sio_hw->mtime = 500;
const marker_receipt_t completed = capture_marker(slot,0);
assert(completed.generation != 0);
sio_hw->mtime = 600;
if (port_reset) {
const tusb_control_request_t request = {
.bmRequestType = 0x23, .bRequest = TUSB_REQ_SET_FEATURE,
.wValue = 4, .wIndex = slot,
};
assert(native_test_setup(0,&request,true));
assert(native_test_in(0,data,&length,true) && length == 0);
native_test_advance(10000u);
} else native_test_bus_reset(true);
// A released notification can outlive its control. Observe it after reset,
// before any replacement SETUP: its ticket must not make the empty watch valid.
control_token(slot,TEST_PID_IN,650);
control_token(slot,TEST_PID_IN,660);
assert(!native_test_in(slot,data,&length,false));
sio_hw->mtime = 700;
// No child SETUP may clear the old watch on behalf of reset. A root marker
// must already describe an empty control, while the earlier latch stays intact.
const marker_receipt_t reset = capture_marker(slot,0);
assert(reset.generation == 0);
logger_full = false;
dump_through(2);
expect_marker(0,&completed);
expect_marker(1,&reset);
expect_clock(0,100,100,200,3,16,16);
const char* before = expect_status_out(0,300,400,3,0);
const char* after = expect_status_out(1,0,0,0,0);
assert(field(before," dgen=",10) == completed.generation);
assert(field(after," dgen=",10) > field(before," dgen=",10));
assert(field(after," shadow_in=",16) == 0 && field(after," shadow_out=",16) == 0);
assert(field(snapshot_line(1,"[HUB_FLIGHT_CONTROL]")," stage=",10) == IDLE);
const char* clock = snapshot_line(1,"[HUB_FLIGHT_CONTROL_CLOCK]");
const char* cleared[] = {" setup="," arm="," complete="," flags="," pid="," arm_len="," len="," pub="};
for (unsigned i = 0; i < sizeof(cleared)/sizeof(cleared[0]); ++i)
assert(field(clock,cleared[i],16) == 0);
assert(handover_acks[slot] == 1);
const uint32_t prior_ticket = field(snapshot_line(0,"[HUB_FLIGHT_CONTROL_CLOCK]")," pub=",16);
assert(prior_ticket != 0);
assert(field(snapshot_line(1,"[HUB_FLIGHT_FREEZE]")," n=",10) == 1);
const char* stale = expect_observation(1,0,addresses[slot],slot,TEST_PID_IN,650,true);
assert(field(stale," why=",16) == 0x20 && field(stale," pub=",16) == prior_ticket);
assert(!(field(stale," in0=",16) & USB_BUF_CTRL_AVAIL));
const tusb_control_request_t replacement = descriptor_request();
sio_hw->mtime = 800;
assert(native_test_setup(slot,&replacement,true));
control_token(slot,TEST_PID_IN,820);
control_token(slot,TEST_PID_IN,830);
const marker_receipt_t next = capture_marker(slot,0);
complete_descriptor(slot);
dump_through(3);
expect_marker(2,&next);
assert(field(snapshot_line(2,"[HUB_FLIGHT_FREEZE]")," n=",10) == 2);
expect_publication(2,1,slot,820,prior_ticket+1u);
expect_lost(3,0);
}
static uint32_t observe_commit(uint8_t address, uint8_t owner, bool handover, uint32_t cutoff) {
assert(usb_hw->dev_addr_ctrl != address);
assert(handover == (active_device != owner));
commit_bank = hardware_bank();
commit_address = address;
commit_owner = owner;
commit_handover = handover;
if (handover) {
for (unsigned channel = 0; channel < CHANNELS; ++channel)
commit_buffers[channel] = owner == 0 && channel >= 2 ? 0 :
devices[owner].buffers[channel] & ~USB_BUF_CTRL_AVAIL;
for (unsigned i = 0; i < 4; ++i)
commit_controls[i] = devices[owner].endpoint_controls[i];
commit_root_control = owner == 0 ? hub_endpoint_control : 0;
commit_stall = ((commit_buffers[0] & USB_BUF_CTRL_STALL) ? 1u : 0u) |
((commit_buffers[1] & USB_BUF_CTRL_STALL) ? 2u : 0u);
}
commit_cycle = 0;
commit_probe = clock_steps = true;
assert(native_hub_select_device(address,owner,cutoff));
native_hub_note_selected_token(address,owner,cutoff,TEST_PID_OUT);
clock_steps = false;
assert(!commit_probe && commit_cycle != 0);
assert(usb_hw->dev_addr_ctrl == address && active_device == owner);
return commit_cycle;
}
static void reject_unchanged(uint8_t address, uint8_t owner, uint32_t cutoff) {
const hardware_bank_t bank = hardware_bank();
const uint32_t previous_address = usb_hw->dev_addr_ctrl;
const uint8_t previous_owner = active_device;
assert(!native_hub_select_device(address,owner,cutoff));
native_hub_note_failed_select(address,owner,cutoff,TEST_PID_OUT);
expect_bank_unchanged(&bank);
assert(usb_hw->dev_addr_ctrl == previous_address && active_device == previous_owner);
}
static void coherent_publication(void) {
const tusb_control_request_t configuration = {
.bRequest = TUSB_REQ_SET_CONFIGURATION, .wValue = 1,
};
const tusb_control_request_t request = {
.bmRequestType = 0xc0, .bRequest = 0x5b, .wLength = 128,
};
uint8_t data[PACKET];
uint16_t length = 0;
for (uint8_t slot = 1; slot < DEVICES; ++slot) configure_child(slot);
assert(native_test_setup(0,&configuration,true));
assert(native_test_in(0,data,&length,true) && length == 0);
ports[0].change = C_RESET;
native_test_drain();
for (uint8_t slot = 0; slot < DEVICES; ++slot) {
for (unsigned i = 0; i < sizeof(handover_reply[slot]); ++i)
handover_reply[slot][i] = (uint8_t)(slot * 0x31u + i * 3u);
assert(native_test_setup(slot,&request,true));
if (slot) {
const uint8_t payload[] = {slot,0xa5,0x5a};
assert(native_hub_hid_report(slot-1u,0x30,payload,sizeof(payload)));
assert(native_hub_vendor_write(slot-1u,payload,sizeof(payload)) == sizeof(payload));
assert(native_hub_vendor_write_flush(slot-1u) == sizeof(payload));
}
}
assert(native_test_select(0));
assert(buffer_regs()[0] & USB_BUF_CTRL_AVAIL);
assert(buffer_regs()[30] & USB_BUF_CTRL_AVAIL);
assert(usb_dpram->ep_ctrl[14].in & EP_CTRL_ENABLE_BITS);
native_test_sio.mtime = 1000;
const uint32_t first_commit = observe_commit(addresses[1],1,true,0x70000000u);
assert(buffer_regs()[0] & USB_BUF_CTRL_AVAIL);
for (unsigned channel = 2; channel < CHANNELS; ++channel)
assert(buffer_regs()[channel] & USB_BUF_CTRL_AVAIL);
// A posthook on the same owner refers to the prior selector write, not to
// its later observation clock. Polls must not manufacture fresh commits.
native_test_sio.mtime += 10u;
native_hub_note_selected_token(addresses[1],1,0x70000001u,TEST_PID_SETUP);
native_test_sio.mtime += 10u;
assert(native_test_select(1));
native_hub_note_selected_token(addresses[1],1,0x70000002u,TEST_PID_IN);
const uint8_t incoming = CHILDREN;
trace_test_lock_busy = true;
reject_unchanged(addresses[incoming],incoming,0x70000003u);
trace_test_lock_busy = false;
usb_hw->sie_status = USB_SIE_STATUS_SETUP_REC_BITS;
reject_unchanged(addresses[incoming],incoming,0x70000004u);
usb_hw->sie_status = 0;
const uint32_t pending[] = {1u,2u,4u,8u,16u,32u,1u << 30};
for (unsigned i = 0; i < sizeof(pending)/sizeof(pending[0]); ++i) {
usb_hw->buf_status = pending[i];
reject_unchanged(addresses[incoming],incoming,0x70000010u+i);
}
usb_hw->buf_status = 0;
reject_unchanged(addresses[incoming],incoming,native_test_sio.mtime);
reject_unchanged(addresses[incoming],DEVICES,0x70000020u);
spin_lock_t* lock = bank_lock;
bank_lock = NULL;
reject_unchanged(addresses[incoming],incoming,0x70000021u);
bank_lock = lock;
// The unchanged-owner/address fast path remains a no-op even when a bank
// handover would be rejected. An address-only write must not quiesce data.
const hardware_bank_t ready = hardware_bank();
trace_test_lock_busy = true;
usb_hw->sie_status = USB_SIE_STATUS_SETUP_REC_BITS;
usb_hw->buf_status = 1u << 30;
assert(native_hub_select_device(addresses[1],1,0));
expect_bank_unchanged(&ready);
trace_test_lock_busy = false;
usb_hw->sie_status = usb_hw->buf_status = 0;
observe_commit(5,1,false,0x70000022u);
assert(native_test_select(1));
expect_bank_unchanged(&ready);
observe_commit(addresses[incoming],incoming,true,0x70000023u);
observe_commit(addresses[0],0,true,0x70000024u);
// All replies/private banks were prepared before any of these tokens.
// No Core0 task may repair a handover between selection and consumption.
for (uint8_t slot = 0; slot < DEVICES; ++slot) {
expect_handover_packet(slot,0,slot ? PACKET : 16,true);
if (!slot) continue;
const uint8_t payload[] = {slot,0xa5,0x5a};
assert(!(buffer_regs()[2] & USB_BUF_CTRL_DATA1_PID));
assert(native_test_private_in(slot,0x81,data,&length));
assert(length == sizeof(payload)+1u && data[0] == 0x30);
assert(memcmp(data+1,payload,sizeof(payload)) == 0);
assert(!(buffer_regs()[4] & USB_BUF_CTRL_DATA1_PID));
assert(native_test_private_in(slot,0x82,data,&length));
assert(length == sizeof(payload) && memcmp(data,payload,length) == 0);
assert(!(buffer_regs()[3] & USB_BUF_CTRL_DATA1_PID));
assert(native_test_private_out(slot,0x01,payload,sizeof(payload),false));
assert(!(buffer_regs()[5] & USB_BUF_CTRL_DATA1_PID));
assert(native_test_private_out(slot,0x02,payload,sizeof(payload),false));
}
assert(native_test_private_in(0,0x8f,data,&length));
assert(length == 1 && data[0] == 2);
native_test_drain();
for (unsigned instance = 0; instance < CHILDREN; ++instance) {
assert(native_test_hid_completions[instance] == 1);
assert(native_test_bulk_completions[instance] == 1);
assert(native_test_received_count[instance][0] == 1);
assert(native_test_received_count[instance][1] == 1);
}
// Outgoing root visibility must be gone when a child address commits;
// selecting the root again must already expose its incoming STALL state.
tusb_control_request_t rejected = request;
rejected.bRequest = 0x5c;
assert(!native_test_setup(0,&rejected,true));
assert(usb_hw->ep_stall_arm == 3u);
assert(buffer_regs()[0] & USB_BUF_CTRL_STALL);
assert(usb_dpram->ep_ctrl[14].in & EP_CTRL_ENABLE_BITS);
observe_commit(addresses[1],1,true,0x70000025u);
expect_handover_packet(1,PACKET,0,false);
observe_commit(addresses[0],0,true,0x70000026u);
assert(usb_hw->ep_stall_arm == 3u);
assert(buffer_regs()[0] & USB_BUF_CTRL_STALL);
assert(buffer_regs()[1] & USB_BUF_CTRL_STALL);
assert(!native_test_in(0,data,&length,false));
const marker_receipt_t receipt = capture_marker(CHILDREN,0);
dump_through(1);
expect_marker(0,&receipt);
const char* first = record_line(0,0);
uint32_t before, after;
const char* clocks = strstr(first," clock=");
assert(clocks && sscanf(clocks," clock=%"SCNx32"/%"SCNx32,&before,&after) == 2);
assert(field(first," pre=",10) == 0 && before == 0 && first_commit < after);
assert(field(first," commit=",16) == first_commit);
for (unsigned index = 1; index <= 2; ++index) {
const char* line = record_line(0,index);
assert(field(line," pre=",10) == 0);
assert(field(line," commit=",16) == first_commit);
}
unsigned records = field(snapshot_line(0,"[HUB_FLIGHT_FREEZE]")," n=",10);
for (unsigned index = 0; index < records; ++index) {
const char* line = record_line(0,index);
if (!field(line," ok=",10)) assert(field(line," commit=",16) == 0);
}
}
static uint16_t latched_bulk_packet(uint8_t slot, bool in, bool host_data1,
uint8_t* data, uint16_t length) {
const unsigned channel = in ? 4u : 5u;
const uint32_t control = in ? commit_latched_bank.dpram.ep_ctrl[1].in :
commit_latched_bank.dpram.ep_ctrl[1].out;
const uint32_t packet = in ? commit_latched_bank.dpram.ep_buf_ctrl[2].in :
commit_latched_bank.dpram.ep_buf_ctrl[2].out;
assert(active_device == slot && commit_owner == slot);
assert(usb_hw->dev_addr_ctrl == addresses[slot]);
assert(control & EP_CTRL_ENABLE_BITS);
assert((control & 0xffffu) == data_offset(slot,channel));
assert(!(packet & (USB_BUF_CTRL_AVAIL | USB_BUF_CTRL_STALL)));
assert(((packet & USB_BUF_CTRL_DATA1_PID) != 0) == host_data1 &&
"commit-time DATA PID disagrees with independent host sequence");
assert(buffer_regs()[channel] == (packet | USB_BUF_CTRL_AVAIL));
uint8_t* payload = (uint8_t*)USBCTRL_DPRAM_BASE + (control & 0xffffu);
if (in) {
assert(packet & USB_BUF_CTRL_FULL);
assert((packet & USB_BUF_CTRL_LEN_MASK) <= length);
length = packet & USB_BUF_CTRL_LEN_MASK;
copy_from_usb(data,payload,length);
buffer_regs()[channel] = packet;
} else {
assert(!(packet & USB_BUF_CTRL_FULL));
assert(length <= (packet & USB_BUF_CTRL_LEN_MASK));
copy_to_usb(payload,data,length);
buffer_regs()[channel] = (packet & ~USB_BUF_CTRL_LEN_MASK) | length;
}
// Deliver only a correctly matched transaction. There is deliberately no
// model of hardware ACK/discard behavior for an incorrect DATA PID.
usb_hw->buf_status |= 1u << channel;
usb_hw->ints |= USB_INTS_BUFF_STATUS_BITS;
native_test_service_interrupt();
assert(!failed);
return length;
}
static void bulk_commit_pids(void) {
uint8_t commands[CHILDREN][PACKET], replies[CHILDREN][PACKET], received[PACKET];
uint16_t command_lengths[CHILDREN], reply_lengths[CHILDREN];
for (uint8_t slot = 1; slot <= CHILDREN; ++slot) configure_child(slot);
native_test_sio.mtime = 2000;
// Slots name virtual USB child ports, not controller profile identities.
// The host starts each EP2 direction at DATA0 after configuration, then
// expects DATA1 after one completion. Never infer this from ep.next_pid.
for (unsigned round = 0; round < 2; ++round) {
const bool host_data1 = round != 0;
for (uint8_t slot = 1; slot <= CHILDREN; ++slot) {
const unsigned instance = slot-1u;
command_lengths[instance] = round ? 16u+slot : 16u;
reply_lengths[instance] = 19u+slot+round*7u;
for (unsigned i = 0; i < PACKET; ++i) {
commands[instance][i] = (uint8_t)(slot*0x21u+round*0x43u+i);
replies[instance][i] = (uint8_t)(slot*0x31u+round*0x57u+i*3u);
}
assert(native_hub_vendor_write(instance,replies[instance],reply_lengths[instance]) ==
reply_lengths[instance]);
assert(native_hub_vendor_write_flush(instance) == reply_lengths[instance]);
}
assert(native_test_select(0));
for (uint8_t slot = 1; slot <= CHILDREN; ++slot) {
const unsigned instance = slot-1u;
observe_commit(addresses[slot],slot,true,0x70001000u+round*CHILDREN+slot);
assert(latched_bulk_packet(slot,false,host_data1,commands[instance],
command_lengths[instance]) == command_lengths[instance]);
const uint16_t length = latched_bulk_packet(slot,true,host_data1,received,sizeof(received));
assert(length == reply_lengths[instance]);
assert(memcmp(received,replies[instance],length) == 0);
// IRQs consumed the bank, but callbacks/rearming await Core0. No
// foreground pass may repair a bank before its modeled packets.
assert(native_test_received_count[instance][1] == round);
assert(native_test_bulk_completions[instance] == round);
uint16_t unarmed_length = UINT16_MAX;
assert(!native_test_private_in(slot,0x82,received,&unarmed_length));
assert(!native_test_private_out(slot,0x02,commands[instance],command_lengths[instance],false));
}
native_test_drain();
for (unsigned instance = 0; instance < CHILDREN; ++instance) {
assert(native_test_received_count[instance][1] == round+1u);
assert(native_test_received_length[instance][1] == command_lengths[instance]);
assert(memcmp(native_test_received_data[instance][1],commands[instance],
command_lengths[instance]) == 0);
assert(native_test_bulk_completions[instance] == round+1u);
assert(native_test_received_count[instance][0] == 0);
assert(native_test_hid_completions[instance] == 0);
}
// Draining and selecting every bank again must not replay a completion.
for (uint8_t slot = 1; slot <= CHILDREN; ++slot) {
uint16_t length = UINT16_MAX;
assert(!native_test_private_in(slot,0x82,received,&length));
}
native_test_drain();
for (unsigned instance = 0; instance < CHILDREN; ++instance) {
assert(native_test_received_count[instance][1] == round+1u);
assert(native_test_bulk_completions[instance] == round+1u);
}
}
}
static void ep0_handover(void) {
const tusb_control_request_t request = {
.bmRequestType = 0xc0, .bRequest = 0x5b, .wLength = 128,
};
uint8_t data[PACKET];
uint16_t length = 0;
const tusb_control_request_t configuration = {
.bRequest = TUSB_REQ_SET_CONFIGURATION, .wValue = 1,
};
for (uint8_t slot = 1; slot < DEVICES; ++slot)
assert(native_test_setup(slot,&configuration,true));
for (uint8_t slot = 1; slot < DEVICES; ++slot)
expect_handover_packet(slot,0,0,true);
native_test_drain();
const uint8_t input[] = {0xa5,0x6b,0xd2};
assert(native_hub_hid_report(0,0x30,input,sizeof(input)));
for (uint8_t slot = 0; slot < DEVICES; ++slot) {
for (unsigned i = 0; i < sizeof(handover_reply[slot]); ++i)
handover_reply[slot][i] = (uint8_t)(slot * 0x31u + i * 3u);
assert(native_test_setup(slot,&request,true));
}
// A private completion stays queued while root/child and child/child INs
// consume their already-prepared, distinct EP0 packets without foreground work.
assert(native_test_private_in(1,0x81,data,&length));
assert(length == sizeof(input)+1u && data[0] == 0x30);
assert(memcmp(data+1,input,sizeof(input)) == 0);
for (uint8_t slot = 0; slot < DEVICES; ++slot)
expect_handover_packet(slot,0,slot == 0 ? 16 : PACKET,true);
for (uint8_t slot = 0; slot < DEVICES; ++slot)
assert(!native_test_in(slot,data,&length,false));
native_test_drain();
assert(native_test_hid_completions[0] == 1 && native_hub_hid_ready(0));
// Full short replies need a DATA0 padding ZLP; longer replies need their
// exact DATA0 tail. Both are prepared while other owners hold the bank.
for (uint8_t slot = 1; slot < DEVICES; ++slot)
expect_handover_packet(slot,PACKET,handover_length(slot)-PACKET,false);
for (uint8_t slot = 0; slot < DEVICES; ++slot)
assert(!native_test_in(slot,data,&length,false));
native_test_drain();
for (uint8_t slot = 0; slot < DEVICES; ++slot) {
assert(handover_acks[slot] == 0);
assert(native_test_select(slot));
assert(buffer_regs()[1] & USB_BUF_CTRL_DATA1_PID);
assert(native_test_out(slot,NULL,0,false));
}
native_test_drain();
for (uint8_t slot = 0; slot < DEVICES; ++slot) {
assert(!native_test_in(slot,data,&length,false));
assert(!native_test_out(slot,NULL,0,false));
assert(handover_acks[slot] == 1);
}
native_test_drain();
assert(native_test_hid_completions[0] == 1);
assert(!native_test_private_in(1,0x81,data,&length));
// A rejected replacement SETUP must restore STALL, never the old ready
// root reply. Reset then revokes both queued and still-prepared child data.
for (uint8_t slot = 0; slot < DEVICES; ++slot)
assert(native_test_setup(slot,&request,true));
tusb_control_request_t rejected = request;
rejected.bRequest = 0x5c;
assert(!native_test_setup(0,&rejected,true));
expect_handover_packet(1,0,PACKET,true);
assert(!native_test_in(0,data,&length,false));
assert(buffer_regs()[0] & USB_BUF_CTRL_STALL);
assert(native_hub_hid_report(0,0x30,input,sizeof(input)));
native_test_bus_reset(false);
// The reset IRQ must revoke inactive readiness before Core0 consumes reset.
for (uint8_t slot = 0; slot < DEVICES; ++slot) {
assert(!native_test_in(slot,data,&length,false));
assert(!native_test_out(slot,NULL,0,false));
if (slot) {
assert(!native_test_private_in(slot,0x81,data,&length));
assert(!native_test_private_out(slot,0x01,input,sizeof(input),false));
}
}
native_test_drain();
for (uint8_t slot = 0; slot < DEVICES; ++slot) {
assert(!native_test_in(slot,data,&length,false));
assert(!native_test_out(slot,NULL,0,false));
assert(handover_acks[slot] == 1);
if (slot) assert(native_test_hid_completions[slot-1] == (slot == 1 ? 1u : 0u));
}
}
int main(int argc, char** argv) {
assert(argc >= 2);
native_test_initialize();
for (unsigned i = 0; i < sizeof(vendor_reply); ++i) vendor_reply[i] = (uint8_t)(i ^ 0x5a);
for (uint8_t slot = 0; slot < DEVICES; ++slot)
for (unsigned i = 0; i < sizeof(approved_reply[slot]); ++i)
approved_reply[slot][i] = (uint8_t)(slot * 0x31u + i * 3u);
if (strcmp(argv[1],"live-wrap") == 0) live_wrap();
else if (strcmp(argv[1],"root-idle") == 0) root_does_not_rearm();
else if (strcmp(argv[1],"queue-pressure") == 0) queue_pressure();
else if (strcmp(argv[1],"backpressure") == 0) {
assert(argc == 3);
backpressure(strcmp(argv[2],"full") == 0);
}
else if (strcmp(argv[1],"pending") == 0) pending_one_shot();
else if (strcmp(argv[1],"superseded") == 0) superseded();
else if (strcmp(argv[1],"immediate-supersession") == 0) immediate_supersession();
else if (strcmp(argv[1],"poll-retention") == 0) poll_retention();
else if (strcmp(argv[1],"selection-history") == 0) selection_history();
else if (strcmp(argv[1],"frozen-selection-history") == 0) frozen_selection_history();
else if (strcmp(argv[1],"delayed-publication") == 0) delayed_publication();
else if (strcmp(argv[1],"publication-isolation") == 0) publication_isolation();
else if (strcmp(argv[1],"publication-wrap-supersession") == 0) publication_wrap_supersession();
else if (strcmp(argv[1],"ep0-handover") == 0) ep0_handover();
else if (strcmp(argv[1],"coherent-publication") == 0) coherent_publication();
else if (strcmp(argv[1],"bulk-commit-pids") == 0) bulk_commit_pids();
else if (strcmp(argv[1],"approved-status-handoff") == 0) approved_status_handoff();
else if (strcmp(argv[1],"approved-status-superseded") == 0) approved_status_superseded();
else if (strcmp(argv[1],"approved-status-reset") == 0) approved_status_reset();
else if (strcmp(argv[1],"approved-status-reset-during-completion") == 0) approved_status_reset_during_completion();
else if (strcmp(argv[1],"approved-status-port-reset-ready") == 0) approved_status_port_reset(false);
else if (strcmp(argv[1],"approved-status-port-reset-queued") == 0) approved_status_port_reset(true);
else if (strcmp(argv[1],"port-reset-interrupt-progress") == 0) port_reset_interrupt_progress(false);
else if (strcmp(argv[1],"port-reset-interrupt-reset") == 0) port_reset_interrupt_progress(true);
else if (strcmp(argv[1],"approved-status-invalid-length") == 0) approved_status_invalid_length();
else if (strcmp(argv[1],"approved-status-watch") == 0) approved_status_watch();
else if (strcmp(argv[1],"status-out-rejected-data") == 0) status_out_rejected_data();
else if (strcmp(argv[1],"status-out") == 0) status_out_lifecycle();
else if (strcmp(argv[1],"status-out-superseded") == 0) status_out_superseded_arm();
else if (strcmp(argv[1],"status-out-stale") == 0) status_out_stale_completion(false);
else if (strcmp(argv[1],"status-out-reset") == 0) status_out_stale_completion(true);
else if (strcmp(argv[1],"status-out-reset-watch") == 0) status_out_reset_watch(false);
else if (strcmp(argv[1],"status-out-port-reset-watch") == 0) status_out_reset_watch(true);
else if (strcmp(argv[1],"marker-priority") == 0) {
assert(argc == 3);
marker_priority(strcmp(argv[2],"partial") == 0);
}
else if (strcmp(argv[1],"marker-zero") == 0) marker_zero_and_capacity();
else if (strcmp(argv[1],"marker-active") == 0) marker_active_priority();
else if (strcmp(argv[1],"marker-rejected") == 0) marker_rejected_requests();
else assert(false && "unknown trace scenario");
fprintf(stderr,"native hub trace scenario %s passed (%u children)\n",argv[1],(unsigned)CHILDREN);
return 0;
}