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.
2298 lines
104 KiB
C
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;
|
|
}
|