fix(native-usb): prevent transmit-bank starvation and retain battery telemetry
This commit is contained in:
parent
a407e84a39
commit
a699a6920b
16 changed files with 342 additions and 82 deletions
51
README.md
51
README.md
|
|
@ -1002,10 +1002,53 @@ the old standalone Wii nominal-charge fallback is removed.
|
|||
Power-field regressions fail before the correction and pass afterward. Eight
|
||||
focused regressions and both firmware builds pass; a PC capture of real
|
||||
motion-bearing input verified Power Info `0x01` on both halves. The
|
||||
`0.72-native-usb-power` trial is installed with persistent storage verified
|
||||
unchanged. A sustained Switch/idle test is still required before attributing
|
||||
the timeout to this flag or claiming recovery. Additional trace-only logs
|
||||
include raw USB interrupts, frame count and device-watchdog state.
|
||||
`0.72-native-usb-power` trial was installed with persistent storage verified
|
||||
unchanged. Later `0.76` passed a ten-minute PC mixed input/control/bulk soak but
|
||||
still lost input on Switch. Captured failures end after a completed root hub
|
||||
endpoint-halt clear, with addressed-token and root NAK counters stopped while
|
||||
SOF continues. This also occurs without USB receive-error flags; the disconnect
|
||||
cause is not established. Trace-only logs include raw USB interrupts, frame
|
||||
count and device-watchdog state.
|
||||
|
||||
**Wii battery telemetry (0.77):** the Wii parser previously ignored the battery
|
||||
byte in status report `0x20` and cleared the battery field on every input report.
|
||||
It now retains the latest measurement between reports, rejects truncated status
|
||||
updates and clears the old measurement on parser setup. The voltage-derived
|
||||
capacity uses the same 5/20/70/100-percent bands as
|
||||
[SDL's Wii driver](https://github.com/libsdl-org/SDL/blob/main/src/joystick/hidapi/SDL_hidapi_wii.c).
|
||||
The USB bridge forwards that measured level with external power set and charging
|
||||
clear, rather than forcing full battery. Updates occur when a status report
|
||||
arrives; this change adds no periodic status requests. Parser regressions cover
|
||||
retention, low-battery updates and reconnection.
|
||||
The installed `0.77` PC smoke test read source battery `179/255` (the 70-percent
|
||||
band) and Power Info `0x19` from both native children, with live IMU and mixed
|
||||
control/bulk checks passing. Saved flash data was verified unchanged. The
|
||||
connected-controller JSON percentage also uses the full `1..255` battery range,
|
||||
so full charge is 100 percent rather than 102. The subsequent Switch trial still
|
||||
disconnected around 240 seconds while delivering Power Info `0x19`; correcting
|
||||
false-empty battery reporting alone did not resolve the failure.
|
||||
|
||||
**Private transmit-bank publication (0.78):** the earlier
|
||||
PC checker submitted R/L reads sequentially. USBmon confirmed zero overlapping
|
||||
child reads. With one reader per child and 500-ms application timeouts, `0.77`
|
||||
stopped delivering fresh input within about three seconds. Short 10-ms timeouts
|
||||
had repeatedly cancelled those pending reads and obscured this starvation.
|
||||
|
||||
Bank selection had disabled every IN buffer until Core 0 restored the bank,
|
||||
including HID and bulk payloads already stored in private DPRAM. `0.78` publishes
|
||||
those prepared private buffers during selection; only the shared EP0 IN image
|
||||
still waits for Core 0 copying. A regression polls alternating child HID/bulk
|
||||
endpoints without a foreground task and checks payload isolation, completion,
|
||||
and newly queued data while EP0 restoration is pending. It fails before the fix
|
||||
and passes afterward; 14 focused tests and both native firmware builds pass.
|
||||
The image is installed with persistent storage verified unchanged. With the Wii
|
||||
reconnected, the same concurrent 500-ms-read test passed 600 seconds: 145,746 R
|
||||
packets and 149,567 L packets, including 113,316 and 114,276 fresh IMU packets.
|
||||
USBmon measured overlapping child reads during 94.4% of the steady-state window
|
||||
with no failed read completions there and no capture drops. A subsequent
|
||||
15-second mixed control/bulk/input check passed 57 rounds with no errors.
|
||||
This confirms the PC starvation reproduction is corrected. The user's subsequent
|
||||
Switch gameplay trial also stopped reproducing the L+R disconnect.
|
||||
|
||||
For sensorless hardware, the checker supports `--input-only`: press real buttons
|
||||
and keep changing controls on both halves during the run. Neutral fallback
|
||||
|
|
|
|||
|
|
@ -2037,7 +2037,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
|
||||
// Constants
|
||||
static const char* wii_devtype_names[] = {
|
||||
@@ -217,124 +297,450 @@
|
||||
@@ -217,124 +297,456 @@
|
||||
|
||||
// process_ functions
|
||||
|
||||
|
|
@ -2409,6 +2409,12 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
+ if (len < 7)
|
||||
+ return;
|
||||
+ wii_instance_t* ins = get_wii_instance(d);
|
||||
+ // Wii Remote status contains a voltage-derived level; use SDL's capacity bands.
|
||||
+ if (ins->dev_type != WII_DEVTYPE_PRO_CONTROLLER) {
|
||||
+ const unsigned raw = report[6];
|
||||
+ const unsigned percent = raw > 178 ? 100 : raw > 51 ? 70 : raw > 13 ? 20 : 5;
|
||||
+ d->controller.battery = (uint8_t)((percent * 255u + 50u) / 100u);
|
||||
+ }
|
||||
+ bool connected = (report[3] & 2) != 0;
|
||||
if (ins->state == WII_FSM_DID_REQ_STATUS) {
|
||||
- if (d->product_id == 0x0306) {
|
||||
|
|
@ -2598,7 +2604,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
}
|
||||
|
||||
static void process_req_data_read_calibration_data(uni_hid_device_t* d, const uint8_t* report, uint16_t len) {
|
||||
@@ -444,17 +850,90 @@
|
||||
@@ -444,17 +856,90 @@
|
||||
|
||||
// Defined here: http://wiibrew.org/wiki/Wiimote#0x21:_Read_Memory_Data
|
||||
static void process_req_data(uni_hid_device_t* d, const uint8_t* report, uint16_t len) {
|
||||
|
|
@ -2696,7 +2702,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
case WII_FSM_BALANCE_BOARD_DID_READ_CALIBRATION:
|
||||
process_req_data_read_calibration_data(d, report, len);
|
||||
break;
|
||||
@@ -465,7 +944,6 @@
|
||||
@@ -465,7 +950,6 @@
|
||||
process_req_data_dump_eeprom(d, report, len);
|
||||
break;
|
||||
default:
|
||||
|
|
@ -2704,7 +2710,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
break;
|
||||
}
|
||||
}
|
||||
@@ -473,29 +951,44 @@
|
||||
@@ -473,29 +957,44 @@
|
||||
// Defined here:
|
||||
// http://wiibrew.org/wiki/Wiimote#0x22:_Acknowledge_output_report.2C_return_function_result
|
||||
static void process_req_return(uni_hid_device_t* d, const uint8_t* report, uint16_t len) {
|
||||
|
|
@ -2772,7 +2778,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
}
|
||||
}
|
||||
|
||||
@@ -514,15 +1007,26 @@
|
||||
@@ -514,15 +1013,26 @@
|
||||
const uint8_t* data = &report[1];
|
||||
wii_instance_t* ins = get_wii_instance(d);
|
||||
|
||||
|
|
@ -2808,7 +2814,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
}
|
||||
// Process misc buttons
|
||||
ctl->gamepad.misc_buttons |= (data[1] & 0x80) ? MISC_BUTTON_SYSTEM : 0; // Button "home"
|
||||
@@ -563,46 +1067,168 @@
|
||||
@@ -563,46 +1073,168 @@
|
||||
// Used for WiiMote in Accelerometer Mode. Defined here:
|
||||
// http://wiibrew.org/wiki/Wiimote#0x31:_Core_Buttons_and_Accelerometer
|
||||
static void process_drm_ka(uni_hid_device_t* d, const uint8_t* report, uint16_t len) {
|
||||
|
|
@ -3017,7 +3023,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
}
|
||||
|
||||
// Used in WiiMote + Nunchuk Mode
|
||||
@@ -617,31 +1243,17 @@
|
||||
@@ -617,31 +1249,17 @@
|
||||
}
|
||||
|
||||
wii_instance_t* ins = get_wii_instance(d);
|
||||
|
|
@ -3057,7 +3063,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
// Better if uDraw reports its own "type", but for the moment
|
||||
// it gets reported and the 2nd half of a gamepad.
|
||||
|
||||
@@ -668,59 +1280,59 @@
|
||||
@@ -668,59 +1286,59 @@
|
||||
ctl->gamepad.buttons |= n.button_lower ? BUTTON_Y : 0;
|
||||
}
|
||||
|
||||
|
|
@ -3165,7 +3171,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
|
||||
static udraw_tablet_t process_udraw_tablet(const uint8_t* e, uint16_t len) {
|
||||
// uDraw Tablet format here:
|
||||
@@ -1002,52 +1614,18 @@
|
||||
@@ -1002,52 +1620,18 @@
|
||||
logi("fsm: req_status\n");
|
||||
wii_instance_t* ins = get_wii_instance(d);
|
||||
ins->state = WII_FSM_DID_REQ_STATUS;
|
||||
|
|
@ -3223,7 +3229,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
}
|
||||
|
||||
static void wii_fsm_ext_read_register(uni_hid_device_t* d) {
|
||||
@@ -1055,8 +1633,7 @@
|
||||
@@ -1055,8 +1639,7 @@
|
||||
wii_instance_t* ins = get_wii_instance(d);
|
||||
ins->state = WII_FSM_EXT_DID_READ_REGISTER;
|
||||
|
||||
|
|
@ -3233,7 +3239,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
uint16_t bytes_to_read = 6;
|
||||
wii_read_mem(d, WII_READ_FROM_REGISTERS, offset, bytes_to_read);
|
||||
}
|
||||
@@ -1066,8 +1643,7 @@
|
||||
@@ -1066,8 +1649,7 @@
|
||||
wii_instance_t* ins = get_wii_instance(d);
|
||||
ins->state = WII_FSM_BALANCE_BOARD_DID_READ_CALIBRATION;
|
||||
|
||||
|
|
@ -3243,7 +3249,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
uint16_t bytes_to_read = 16;
|
||||
wii_read_mem(d, WII_READ_FROM_REGISTERS, offset, bytes_to_read);
|
||||
}
|
||||
@@ -1077,8 +1653,7 @@
|
||||
@@ -1077,8 +1659,7 @@
|
||||
wii_instance_t* ins = get_wii_instance(d);
|
||||
ins->state = WII_FSM_BALANCE_BOARD_DID_READ_CALIBRATION2;
|
||||
|
||||
|
|
@ -3253,7 +3259,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
uint16_t bytes_to_read = 8;
|
||||
wii_read_mem(d, WII_READ_FROM_REGISTERS, offset, bytes_to_read);
|
||||
}
|
||||
@@ -1086,6 +1661,10 @@
|
||||
@@ -1086,6 +1667,10 @@
|
||||
static void wii_fsm_assign_device(uni_hid_device_t* d) {
|
||||
logi("fsm: assign_device\n");
|
||||
wii_instance_t* ins = get_wii_instance(d);
|
||||
|
|
@ -3264,7 +3270,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
uint8_t dev = ins->dev_type;
|
||||
switch (dev) {
|
||||
case WII_DEVTYPE_UNK:
|
||||
@@ -1100,18 +1679,8 @@
|
||||
@@ -1100,18 +1685,8 @@
|
||||
}
|
||||
uint8_t report_type = 0xff;
|
||||
if (ins->ext_type == WII_EXT_NUNCHUK) {
|
||||
|
|
@ -3285,7 +3291,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
} else if (ins->ext_type == WII_EXT_CLASSIC_CONTROLLER) {
|
||||
logi("Wii: requesting E (Classic Controller)\n");
|
||||
d->controller_subtype = CONTROLLER_SUBTYPE_WII_CLASSIC;
|
||||
@@ -1125,21 +1694,17 @@
|
||||
@@ -1125,21 +1700,17 @@
|
||||
d->controller_subtype = CONTROLLER_SUBTYPE_WIIMOTE_UDRAW_TABLET;
|
||||
report_type = WII_REPORT_TYPE_KE;
|
||||
} else {
|
||||
|
|
@ -3317,7 +3323,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
uni_hid_parser_wii_request_report_type(d, report_type);
|
||||
break;
|
||||
}
|
||||
@@ -1164,7 +1729,10 @@
|
||||
@@ -1164,7 +1735,10 @@
|
||||
ins->state = WII_FSM_LED_UPDATED;
|
||||
wii_process_fsm(d);
|
||||
|
||||
|
|
@ -3329,7 +3335,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
}
|
||||
|
||||
static void wii_fsm_dump_eeprom(struct uni_hid_device_s* d) {
|
||||
@@ -1218,7 +1786,15 @@
|
||||
@@ -1218,7 +1792,15 @@
|
||||
// Do nothing
|
||||
break;
|
||||
case WII_FSM_DEV_GUESSED:
|
||||
|
|
@ -3346,7 +3352,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
break;
|
||||
case WII_FSM_BALANCE_BOARD_READ_CALIBRATION:
|
||||
wii_fsm_balance_board_read_calibration(d);
|
||||
@@ -1234,6 +1810,19 @@
|
||||
@@ -1234,6 +1816,19 @@
|
||||
wii_fsm_update_led(d);
|
||||
break;
|
||||
case WII_FSM_LED_UPDATED:
|
||||
|
|
@ -3366,10 +3372,11 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
break;
|
||||
default:
|
||||
loge("Wii: wii_process_fsm() unexpected state: %d\n", ins->state);
|
||||
@@ -1249,10 +1838,10 @@
|
||||
@@ -1249,10 +1844,11 @@
|
||||
|
||||
ins->mode = WII_MODE_HORIZONTAL;
|
||||
ins->state = WII_FSM_SETUP;
|
||||
+ d->controller.battery = UNI_CONTROLLER_BATTERY_NOT_AVAILABLE;
|
||||
-
|
||||
- // Start with 0xa40000 (all Wii devices, except for the Wii Remote Plus)
|
||||
- // If it fails it will use 0xa60000
|
||||
|
|
@ -3381,7 +3388,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
|
||||
// Dump EEPROM
|
||||
#if ENABLE_EEPROM_DUMP
|
||||
@@ -1266,14 +1855,49 @@
|
||||
@@ -1266,14 +1862,51 @@
|
||||
wii_process_fsm(d);
|
||||
}
|
||||
|
||||
|
|
@ -3418,8 +3425,10 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
void uni_hid_parser_wii_init_report(uni_hid_device_t* d) {
|
||||
- // Reset old state. Each report contains a full-state.
|
||||
+ wii_instance_t* ins = get_wii_instance(d);
|
||||
+ const uint8_t battery = d->controller.battery;
|
||||
memset(&d->controller, 0, sizeof(d->controller));
|
||||
d->controller.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||
+ d->controller.battery = battery;
|
||||
+ // Status/ack and Nunchuk interleaves do not contain new motion samples.
|
||||
+ for (int i = 0; i < 3; i++) {
|
||||
+ d->controller.gamepad.accel[i] = ins->accel[i];
|
||||
|
|
@ -3433,7 +3442,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
return;
|
||||
switch (report[0]) {
|
||||
case WIIPROTO_REQ_STATUS:
|
||||
@@ -1291,6 +1915,14 @@
|
||||
@@ -1291,6 +1924,14 @@
|
||||
case WII_REPORT_TYPE_KAE:
|
||||
process_drm_kae(d, report, len);
|
||||
break;
|
||||
|
|
@ -3448,7 +3457,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
case WII_REPORT_TYPE_KEE:
|
||||
process_drm_kee(d, report, len);
|
||||
break;
|
||||
@@ -1307,6 +1939,9 @@
|
||||
@@ -1307,6 +1948,9 @@
|
||||
logi("Wii parser: unknown report type: 0x%02x\n", report[0]);
|
||||
printf_hexdump(report, len);
|
||||
}
|
||||
|
|
@ -3458,7 +3467,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
}
|
||||
|
||||
void uni_hid_parser_wii_set_player_leds(uni_hid_device_t* d, uint8_t leds) {
|
||||
@@ -1371,29 +2006,25 @@
|
||||
@@ -1371,29 +2015,25 @@
|
||||
|
||||
void uni_hid_parser_wii_set_mode(uni_hid_device_t* d, wii_mode_t mode) {
|
||||
wii_instance_t* ins = get_wii_instance(d);
|
||||
|
|
@ -3503,7 +3512,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
uni_hid_device_send_intr_report(d, report, sizeof(report));
|
||||
}
|
||||
|
||||
@@ -1480,10 +2111,13 @@
|
||||
@@ -1480,10 +2120,13 @@
|
||||
|
||||
static void wii_read_mem(uni_hid_device_t* d, wii_read_type_t t, uint32_t offset, uint16_t size) {
|
||||
logi("****** read_mem: offset=0x%04x, size=%d from=%d\n", offset, size, t);
|
||||
|
|
@ -3518,7 +3527,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
|
|||
(offset & 0xff0000) >> 16, (offset & 0xff00) >> 8, (offset & 0xff), // Offset
|
||||
(size & 0xff00) >> 8, (size & 0xff), // Size in bytes
|
||||
// clang-format on
|
||||
@@ -1491,6 +2125,15 @@
|
||||
@@ -1491,6 +2134,15 @@
|
||||
uni_hid_device_send_intr_report(d, report, sizeof(report));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ static __force_inline void buffer_settle(void) {
|
|||
static __force_inline void set_buffer(uint8_t device, uint8_t channel, uint32_t value) {
|
||||
devices[device].buffers[channel] = value;
|
||||
__dmb();
|
||||
if ((active_device == device && (!bank_restore_pending || (channel & 1u))) ||
|
||||
if ((active_device == device && (!bank_restore_pending || channel != 0)) ||
|
||||
(device == 0 && channel == 2)) {
|
||||
unsigned physical = physical_channel(device,channel);
|
||||
buffer_regs()[physical] = value & ~USB_BUF_CTRL_AVAIL;
|
||||
|
|
@ -191,8 +191,8 @@ bool __not_in_flash_func(native_hub_select_device)(uint8_t address, uint8_t owne
|
|||
const device_t* restrict incoming = &devices[owner];
|
||||
volatile uint32_t* buffers = (volatile uint32_t*)&usb_dpram->ep_buf_ctrl[0];
|
||||
volatile uint32_t* controls = (volatile uint32_t*)&usb_dpram->ep_ctrl[0];
|
||||
// Receive PID/availability must be selected before accepting an OUT token.
|
||||
// Transmit buffers can safely NAK until Core0 publishes their contents.
|
||||
// Select metadata before accepting a token. Only EP0 IN needs a later
|
||||
// shared-buffer copy; other endpoints already have private DPRAM data.
|
||||
for (unsigned i = 0; i < CHANNELS; ++i) {
|
||||
uint32_t value = owner == 0 && i >= 2 ? 0 : incoming->buffers[i];
|
||||
buffers[i] = value & ~USB_BUF_CTRL_AVAIL;
|
||||
|
|
@ -200,7 +200,7 @@ bool __not_in_flash_func(native_hub_select_device)(uint8_t address, uint8_t owne
|
|||
usb_dpram->ep_ctrl[14].in = owner == 0 ? hub_endpoint_control : 0;
|
||||
for (unsigned i = 0; i < 4; ++i) controls[i] = incoming->endpoint_controls[i];
|
||||
buffer_settle();
|
||||
for (unsigned i = 1; i < CHANNELS; i += 2)
|
||||
for (unsigned i = 1; i < CHANNELS; ++i)
|
||||
buffers[i] = owner == 0 && i >= 2 ? 0 : incoming->buffers[i];
|
||||
__dmb();
|
||||
usb_hw->dev_addr_ctrl = address;
|
||||
|
|
@ -220,7 +220,9 @@ bool __not_in_flash_func(native_hub_select_device)(uint8_t address, uint8_t owne
|
|||
spin_unlock_unsafe(bank_lock);
|
||||
return true;
|
||||
}
|
||||
static void __not_in_flash_func(restore_selected_bank)(void) {
|
||||
// Section placement alone permits inlining into the flash-backed task. Keep
|
||||
// bank-lock ownership independent of XIP instruction-cache refill latency.
|
||||
static void __no_inline_not_in_flash_func(restore_selected_bank)(void) {
|
||||
if (!bank_restore_pending) return;
|
||||
uint32_t flags = spin_lock_blocking(bank_lock);
|
||||
if (!bank_restore_pending || (usb_hw->sie_status & USB_SIE_STATUS_SETUP_REC_BITS) ||
|
||||
|
|
@ -238,15 +240,11 @@ static void __not_in_flash_func(restore_selected_bank)(void) {
|
|||
unsigned words = ((incoming->buffers[0] & USB_BUF_CTRL_LEN_MASK) + 3u) / 4u;
|
||||
for (unsigned i = 0; i < words; ++i) to[i] = from[i];
|
||||
}
|
||||
for (unsigned i = 0; i < CHANNELS; i += 2) {
|
||||
uint32_t value = owner == 0 && i >= 2 ? 0 : incoming->buffers[i];
|
||||
buffers[i] = value & ~USB_BUF_CTRL_AVAIL;
|
||||
}
|
||||
buffers[0] = incoming->buffers[0] & ~USB_BUF_CTRL_AVAIL;
|
||||
usb_hw->ep_stall_arm = ((incoming->buffers[0] & USB_BUF_CTRL_STALL) ? 1u : 0u) |
|
||||
((incoming->buffers[1] & USB_BUF_CTRL_STALL) ? 2u : 0u);
|
||||
buffer_settle();
|
||||
for (unsigned i = 0; i < CHANNELS; i += 2)
|
||||
buffers[i] = owner == 0 && i >= 2 ? 0 : incoming->buffers[i];
|
||||
buffers[0] = incoming->buffers[0];
|
||||
bank_restore_pending = false;
|
||||
spin_unlock(bank_lock,flags);
|
||||
}
|
||||
|
|
@ -334,10 +332,24 @@ static void __not_in_flash_func(usb_interrupt)(void) {
|
|||
uint8_t actual_owner = hardware_owner();
|
||||
uint8_t setup[8];
|
||||
copy_from_usb(setup, usb_dpram->setup_packet, sizeof(setup));
|
||||
// A new SETUP revokes the preceding control transfer. Reclaim both
|
||||
// physical EP0 buffers through the controller's ownership handshake,
|
||||
// as the SDK DCD does, before changing their metadata or PID.
|
||||
hw_set_bits(&usb_hw->abort, 3u);
|
||||
unsigned abort_wait = 4096u;
|
||||
while ((usb_hw->abort_done & 3u) != 3u && --abort_wait) {}
|
||||
if ((usb_hw->abort_done & 3u) != 3u) {
|
||||
// Never acknowledge new work over a buffer the SIE still owns.
|
||||
failed = true;
|
||||
usb_hw->inte = 0;
|
||||
spin_unlock(bank_lock, flags);
|
||||
return;
|
||||
}
|
||||
if (actual_owner < DEVICES && actual_owner == owner) {
|
||||
++devices[owner].generation;
|
||||
devices[owner].buffers[0] = devices[owner].buffers[1] = 0;
|
||||
buffer_regs()[0] = buffer_regs()[1] = 0;
|
||||
devices[owner].buffers[0] = devices[owner].buffers[1] =
|
||||
USB_BUF_CTRL_DATA1_PID | USB_BUF_CTRL_SEL;
|
||||
buffer_regs()[0] = buffer_regs()[1] = USB_BUF_CTRL_DATA1_PID | USB_BUF_CTRL_SEL;
|
||||
devices[owner].ep[0].next_pid = devices[owner].ep[1].next_pid = 1;
|
||||
push_event(owner,0,1,sizeof(setup),setup);
|
||||
} else {
|
||||
|
|
@ -345,6 +357,8 @@ static void __not_in_flash_func(usb_interrupt)(void) {
|
|||
hw_set_bits(&usb_hw->ep_stall_arm,3u);
|
||||
buffer_regs()[0] = buffer_regs()[1] = USB_BUF_CTRL_STALL;
|
||||
}
|
||||
hw_clear_bits(&usb_hw->abort_done, 3u);
|
||||
hw_clear_bits(&usb_hw->abort, 3u);
|
||||
hw_clear_bits(&usb_hw->sie_status,USB_SIE_STATUS_SETUP_REC_BITS);
|
||||
}
|
||||
if (status & USB_INTS_DEV_SUSPEND_BITS) {
|
||||
|
|
@ -368,7 +382,7 @@ static void stall(uint8_t slot) {
|
|||
set_buffer(slot,0,USB_BUF_CTRL_STALL); set_buffer(slot,1,USB_BUF_CTRL_STALL);
|
||||
spin_unlock(bank_lock, flags);
|
||||
}
|
||||
static void __not_in_flash_func(arm_packet)(uint8_t slot, uint8_t channel, const uint8_t* data, uint16_t length) {
|
||||
static void __no_inline_not_in_flash_func(arm_packet)(uint8_t slot, uint8_t channel, const uint8_t* data, uint16_t length) {
|
||||
endpoint_t* ep = &devices[slot].ep[channel];
|
||||
uint32_t generation = channel < 2 ? devices[slot].control.generation :
|
||||
devices[slot].endpoint_generation[channel];
|
||||
|
|
@ -726,7 +740,7 @@ static void transmit_next(uint8_t slot, uint8_t channel) {
|
|||
uint8_t packet[64]; if (size) memcpy(packet,ep->data+ep->sent,size);
|
||||
arm_packet(slot,channel,packet,size);
|
||||
}
|
||||
static void transfer_complete(const event_t* event) {
|
||||
static void __no_inline_not_in_flash_func(transfer_complete)(const event_t* event) {
|
||||
uint8_t slot = event->device, channel = event->channel;
|
||||
device_t* d = &devices[slot];
|
||||
if (event->reset_generation != d->reset_generation) return;
|
||||
|
|
@ -742,6 +756,16 @@ static void transfer_complete(const event_t* event) {
|
|||
if (event->generation != c->generation) return;
|
||||
if ((c->stage == STATUS_IN && channel == 0) || (c->stage == STATUS_OUT && channel == 1)) {
|
||||
if (event->length) stall(slot);
|
||||
else if (c->stage == STATUS_OUT && (c->request.bmRequestType & 0x80u)) {
|
||||
// Claim the completed read before a reset can revoke it, but
|
||||
// let USB IRQs run while its read-only ACK callback executes.
|
||||
uint32_t flags = save_and_disable_interrupts();
|
||||
bool acknowledged = event->reset_generation == d->reset_generation;
|
||||
if (acknowledged) c->stage = IDLE;
|
||||
restore_interrupts(flags);
|
||||
if (acknowledged && c->vendor)
|
||||
tud_vendor_control_xfer_cb(slot,CONTROL_STAGE_ACK,&c->request);
|
||||
}
|
||||
else {
|
||||
// Do not let a reset IRQ revoke ownership between checking it
|
||||
// and publishing the acknowledged service transaction.
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ size_t encode_transaction(uint8_t* output, size_t output_size) {
|
|||
size_t encode_info(uint8_t* output, size_t output_size) {
|
||||
uint8_t payload[8] = {
|
||||
#if SWITCH2_PROBE_HUB
|
||||
0, 72, 0, 2,
|
||||
0, 78, 0, 2,
|
||||
kNativeHubActiveMode,
|
||||
USB_OUTPUT_CAPABILITY_INPUT | USB_OUTPUT_CAPABILITY_RUMBLE |
|
||||
USB_OUTPUT_CAPABILITY_MOTION,
|
||||
|
|
|
|||
|
|
@ -1108,7 +1108,7 @@ class ProfilePlaytest:
|
|||
"right": self.triggers[1],
|
||||
},
|
||||
"battery": (
|
||||
round((self.battery - 1) / 250 * 100) if self.battery != 0 else None
|
||||
round((self.battery - 1) / 254 * 100) if self.battery != 0 else None
|
||||
),
|
||||
"capabilities": [
|
||||
name
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ bool native_test_setup(uint8_t, const tusb_control_request_t*, bool);
|
|||
bool native_test_out(uint8_t, const uint8_t*, uint16_t, bool);
|
||||
bool native_test_in(uint8_t, uint8_t*, uint16_t*, bool);
|
||||
void native_test_bus_reset(bool);
|
||||
void native_test_hold_abort(bool);
|
||||
bool native_test_select(uint8_t);
|
||||
bool native_test_private_in(uint8_t, uint8_t, uint8_t*, uint16_t*);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
|
@ -26,6 +29,7 @@ uint32_t programs = 0;
|
|||
uint32_t erases = 0;
|
||||
uint32_t bootsel_calls = 0;
|
||||
std::array<uint8_t, 64> child_identity[2];
|
||||
bool interleave_identity_ack = false;
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) { std::cerr << message << '\n'; std::exit(1); }
|
||||
|
|
@ -155,9 +159,9 @@ void test_profile_transport() {
|
|||
const auto original = encoded_profile(0);
|
||||
const auto edited = encoded_profile(4);
|
||||
auto info = read_operation(Operation::kInfo);
|
||||
require(info[kResponseHeaderSize] == 0 && info[kResponseHeaderSize + 1] == 72 &&
|
||||
info[kResponseHeaderSize + 2] == 0 && info[kResponseHeaderSize + 4] == 5 &&
|
||||
info[kResponseHeaderSize + 5] == 7, "native INFO does not describe the fixed image");
|
||||
require(info.size() == kResponseHeaderSize + 8 &&
|
||||
info[kResponseHeaderSize + 4] == 5 && info[kResponseHeaderSize + 5] == 7,
|
||||
"native INFO does not describe the fixed output and its capabilities");
|
||||
auto list = read_operation(Operation::kProfileList);
|
||||
require(list[kResponseHeaderSize] == 1 && list.size() > 64, "root catalog omitted the global profile owner");
|
||||
auto playtest = read_operation(Operation::kProfilePlaytest);
|
||||
|
|
@ -261,6 +265,74 @@ void test_interrupted_transactions() {
|
|||
require(!native_test_in(0, packet, &length, true), "completed request retained a second status ACK");
|
||||
}
|
||||
|
||||
void test_pending_control_buffer_ownership() {
|
||||
const auto info = request(Operation::kInfo, true, kMaximumResponseSize);
|
||||
require(native_test_setup(0, &info, true), "pending INFO setup failed");
|
||||
const unsigned programs_before = programs, erases_before = erases;
|
||||
native_test_hold_abort(true);
|
||||
const auto replacement = request(Operation::kProfileSelect, false, kRequestHeaderSize + 15);
|
||||
require(!native_test_setup(0, &replacement, false),
|
||||
"new SETUP replaced an EP0 buffer before the controller released ownership");
|
||||
uint8_t packet[64]; uint16_t length;
|
||||
require(!native_test_in(0, packet, &length, false),
|
||||
"an unquiesced control endpoint acknowledged replacement work");
|
||||
profile_service_task_on_storage_core(4000);
|
||||
require(programs == programs_before && erases == erases_before,
|
||||
"unquiesced replacement changed saved profiles");
|
||||
native_test_initialize();
|
||||
}
|
||||
|
||||
void test_read_ack_allows_usb_progress() {
|
||||
interleave_identity_ack = true;
|
||||
read_child(1);
|
||||
const auto next = receive(2);
|
||||
require(next == std::vector<uint8_t>(child_identity[1].begin(), child_identity[1].end()),
|
||||
"SETUP received during read ACK did not retain the next child's response");
|
||||
}
|
||||
|
||||
void test_private_transmit_survives_round_robin_tokens() {
|
||||
native_test_initialize();
|
||||
tusb_control_request_t configuration{};
|
||||
configuration.bRequest = TUSB_REQ_SET_CONFIGURATION;
|
||||
configuration.wValue = 1;
|
||||
for (uint8_t slot : {1, 2}) {
|
||||
require(native_test_setup(slot, &configuration, true), "child configuration failed");
|
||||
acknowledge(slot);
|
||||
}
|
||||
const uint8_t payloads[2][3] = {{0x11, 0x22, 0x33}, {0x44, 0x55, 0x66}};
|
||||
for (uint8_t instance : {0, 1}) {
|
||||
require(native_hub_hid_report(instance, instance ? 7 : 8, payloads[instance], 3),
|
||||
"could not queue HID packet");
|
||||
require(native_hub_vendor_write(instance, payloads[instance], 3) == 3 &&
|
||||
native_hub_vendor_write_flush(instance) == 3, "could not queue bulk packet");
|
||||
}
|
||||
uint8_t packet[64];
|
||||
uint16_t length = 0;
|
||||
for (uint8_t endpoint : {0x81, 0x82}) {
|
||||
for (uint8_t slot : {1, 2}) {
|
||||
require(native_test_private_in(slot, endpoint, packet, &length),
|
||||
"queued private IN packet required foreground work after bank selection");
|
||||
const unsigned prefix = endpoint == 0x81 ? 1 : 0;
|
||||
require(length == 3 + prefix &&
|
||||
(!prefix || packet[0] == (slot == 1 ? 8 : 7)) &&
|
||||
std::memcmp(packet + prefix, payloads[slot - 1], 3) == 0,
|
||||
"round-robin IN token received another endpoint's payload");
|
||||
}
|
||||
}
|
||||
native_test_drain();
|
||||
require(native_hub_hid_ready(0) && native_hub_hid_ready(1),
|
||||
"acknowledged HID packets did not release their queues");
|
||||
require(!native_test_private_in(1, 0x81, packet, &length),
|
||||
"acknowledged HID packet was retransmitted");
|
||||
// The idle poll selected R without restoring its shared EP0 image.
|
||||
require(native_hub_hid_report(0, 8, payloads[0], 3), "could not queue the next HID packet");
|
||||
require(native_test_private_in(1, 0x81, packet, &length) && length == 4 &&
|
||||
std::memcmp(packet + 1, payloads[0], 3) == 0,
|
||||
"pending shared EP0 restoration blocked a newly queued private IN packet");
|
||||
native_test_drain();
|
||||
native_test_initialize();
|
||||
}
|
||||
|
||||
void test_private_bootsel() {
|
||||
const auto bytes = envelope(Operation::kBootselReboot, {});
|
||||
const auto setup = request(Operation::kBootselReboot, false, bytes.size());
|
||||
|
|
@ -336,6 +408,14 @@ extern "C" bool tud_vendor_control_xfer_cb(uint8_t slot, uint8_t stage, const tu
|
|||
if (probe_management_vendor_control(slot, stage, setup)) return true;
|
||||
if (slot < 1 || slot > 2 || setup->bmRequestType != 0xc0 ||
|
||||
setup->bRequest != 3 || setup->wValue || setup->wIndex) return false;
|
||||
if (stage == CONTROL_STAGE_ACK && slot == 1 && interleave_identity_ack) {
|
||||
interleave_identity_ack = false;
|
||||
const auto next = *setup;
|
||||
require(native_test_setup(2, &next, false), "next child's SETUP was rejected during read ACK");
|
||||
// SETUP must be serviced before another token can change the bank.
|
||||
// This observes IRQ progress, rather than inspecting the CPU mask.
|
||||
require(native_test_select(0), "read ACK callback blocked servicing the next USB SETUP");
|
||||
}
|
||||
return stage != CONTROL_STAGE_SETUP || native_hub_control_xfer(slot, setup,
|
||||
child_identity[slot - 1].data(), child_identity[slot - 1].size());
|
||||
}
|
||||
|
|
@ -347,6 +427,9 @@ int main() {
|
|||
native_test_initialize();
|
||||
test_profile_transport();
|
||||
test_interrupted_transactions();
|
||||
test_pending_control_buffer_ownership();
|
||||
test_read_ack_allows_usb_progress();
|
||||
test_private_transmit_survives_round_robin_tokens();
|
||||
test_private_bootsel();
|
||||
std::cout << "native root management packet and persistence regressions passed\n";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,14 +5,28 @@
|
|||
#include <string.h>
|
||||
|
||||
#define __not_in_flash_func(name) name
|
||||
#define __no_inline_not_in_flash_func(name) __attribute__((noinline)) name
|
||||
#define __force_inline inline __attribute__((always_inline))
|
||||
#define __dmb() ((void)0)
|
||||
|
||||
typedef struct { unsigned unused; } spin_lock_t;
|
||||
static inline uint32_t save_and_disable_interrupts(void) { return 0; }
|
||||
static inline void restore_interrupts(uint32_t flags) { (void)flags; }
|
||||
static inline uint32_t spin_lock_blocking(spin_lock_t* lock) { (void)lock; return 0; }
|
||||
static inline void spin_unlock(spin_lock_t* lock, uint32_t flags) { (void)lock; (void)flags; }
|
||||
extern uint32_t native_test_interrupt_mask;
|
||||
void native_test_service_interrupt(void);
|
||||
static inline uint32_t save_and_disable_interrupts(void) {
|
||||
uint32_t flags = native_test_interrupt_mask;
|
||||
native_test_interrupt_mask = 1;
|
||||
return flags;
|
||||
}
|
||||
static inline void restore_interrupts(uint32_t flags) {
|
||||
native_test_interrupt_mask = flags;
|
||||
native_test_service_interrupt();
|
||||
}
|
||||
static inline uint32_t spin_lock_blocking(spin_lock_t* lock) {
|
||||
(void)lock; return save_and_disable_interrupts();
|
||||
}
|
||||
static inline void spin_unlock(spin_lock_t* lock, uint32_t flags) {
|
||||
(void)lock; restore_interrupts(flags);
|
||||
}
|
||||
static inline bool spin_try_lock_unsafe(spin_lock_t* lock) { (void)lock; return true; }
|
||||
static inline void spin_unlock_unsafe(spin_lock_t* lock) { (void)lock; }
|
||||
static inline int spin_lock_claim_unused(bool required) { (void)required; return 0; }
|
||||
|
|
@ -20,13 +34,13 @@ static inline spin_lock_t* spin_lock_instance(unsigned index) {
|
|||
static spin_lock_t lock; (void)index; return &lock;
|
||||
}
|
||||
static inline void hw_clear_bits(volatile uint32_t* address, uint32_t bits) { *address &= ~bits; }
|
||||
static inline void hw_set_bits(volatile uint32_t* address, uint32_t bits) { *address |= bits; }
|
||||
|
||||
typedef struct {
|
||||
volatile uint32_t ints, sie_status, buf_status, dev_addr_ctrl, inte;
|
||||
volatile uint32_t ep_stall_arm, muxing, phy_direct, phy_direct_override;
|
||||
volatile uint32_t pwr, main_ctrl, sie_ctrl, ep_nak_stall_status;
|
||||
volatile uint32_t ep_tx_error, ep_rx_error;
|
||||
volatile uint32_t abort, abort_done;
|
||||
} usb_hw_t;
|
||||
typedef struct { volatile uint32_t in, out; } usb_pair_t;
|
||||
typedef struct {
|
||||
|
|
@ -46,6 +60,13 @@ extern sio_hw_t native_test_sio;
|
|||
#define USBCTRL_DPRAM_BASE ((uintptr_t)usb_dpram)
|
||||
#define USB_DPRAM_SIZE sizeof(*usb_dpram)
|
||||
|
||||
extern bool native_test_abort_stuck;
|
||||
static inline void hw_set_bits(volatile uint32_t* address, uint32_t bits) {
|
||||
*address |= bits;
|
||||
if (address == &usb_hw->abort && !native_test_abort_stuck)
|
||||
usb_hw->abort_done |= bits;
|
||||
}
|
||||
|
||||
#define USB_BUF_CTRL_LEN_MASK 0x3ffu
|
||||
#define USB_BUF_CTRL_AVAIL (1u << 10)
|
||||
#define USB_BUF_CTRL_STALL (1u << 11)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,17 @@
|
|||
usb_hw_t native_test_usb;
|
||||
usb_device_dpram_t native_test_dpram;
|
||||
sio_hw_t native_test_sio;
|
||||
bool native_test_abort_stuck;
|
||||
uint32_t native_test_interrupt_mask;
|
||||
static bool servicing_interrupt;
|
||||
|
||||
void native_test_service_interrupt(void) {
|
||||
if (native_test_interrupt_mask || servicing_interrupt || !usb_hw->ints) return;
|
||||
servicing_interrupt = true;
|
||||
usb_interrupt();
|
||||
usb_hw->ints = 0;
|
||||
servicing_interrupt = false;
|
||||
}
|
||||
|
||||
void probe_router_init(uint32_t hz) { (void)hz; }
|
||||
void probe_router_core1(void) {}
|
||||
|
|
@ -36,6 +47,9 @@ void native_test_initialize(void) {
|
|||
memset(usb_hw,0,sizeof(*usb_hw));
|
||||
memset(usb_dpram,0,sizeof(*usb_dpram));
|
||||
event_head = event_tail = 0;
|
||||
native_test_abort_stuck = false;
|
||||
native_test_interrupt_mask = 0;
|
||||
servicing_interrupt = false;
|
||||
failed = bus_suspended = bank_restore_pending = false;
|
||||
bank_lock = spin_lock_instance(0);
|
||||
active_device = default_device = 0;
|
||||
|
|
@ -49,6 +63,8 @@ static bool select_slot(uint8_t slot) {
|
|||
return true;
|
||||
}
|
||||
|
||||
bool native_test_select(uint8_t slot) { return select_slot(slot); }
|
||||
|
||||
void native_test_drain(void) { native_hub_task(); }
|
||||
|
||||
bool native_test_setup(uint8_t slot, const tusb_control_request_t* request, bool drain) {
|
||||
|
|
@ -56,14 +72,16 @@ bool native_test_setup(uint8_t slot, const tusb_control_request_t* request, bool
|
|||
memcpy(usb_dpram->setup_packet,request,sizeof(*request));
|
||||
usb_hw->sie_status = USB_SIE_STATUS_SETUP_REC_BITS;
|
||||
usb_hw->ints = USB_INTS_SETUP_REQ_BITS;
|
||||
usb_interrupt();
|
||||
usb_hw->ints = 0;
|
||||
native_test_service_interrupt();
|
||||
if (drain) native_hub_task();
|
||||
return !failed && devices[slot].control.stage != STALLED;
|
||||
}
|
||||
|
||||
void native_test_hold_abort(bool hold) { native_test_abort_stuck = hold; }
|
||||
|
||||
bool native_test_out(uint8_t slot, const uint8_t* data, uint16_t length, bool drain) {
|
||||
if (!select_slot(slot)) return false;
|
||||
if (usb_hw->abort & 2u) return false;
|
||||
uint32_t value = buffer_regs()[1];
|
||||
if (!(value & USB_BUF_CTRL_AVAIL) || (value & USB_BUF_CTRL_STALL) ||
|
||||
length > (value & USB_BUF_CTRL_LEN_MASK)) return false;
|
||||
|
|
@ -71,14 +89,14 @@ bool native_test_out(uint8_t slot, const uint8_t* data, uint16_t length, bool dr
|
|||
buffer_regs()[1] = (value & ~(USB_BUF_CTRL_AVAIL | USB_BUF_CTRL_LEN_MASK)) | length;
|
||||
usb_hw->buf_status = 2;
|
||||
usb_hw->ints = USB_INTS_BUFF_STATUS_BITS;
|
||||
usb_interrupt();
|
||||
usb_hw->ints = 0;
|
||||
native_test_service_interrupt();
|
||||
if (drain) native_hub_task();
|
||||
return !failed && devices[slot].control.stage != STALLED;
|
||||
}
|
||||
|
||||
bool native_test_in(uint8_t slot, uint8_t* data, uint16_t* length, bool drain) {
|
||||
if (!select_slot(slot)) return false;
|
||||
if (usb_hw->abort & 1u) return false;
|
||||
uint32_t value = buffer_regs()[0];
|
||||
if (!(value & USB_BUF_CTRL_AVAIL) || !(value & USB_BUF_CTRL_FULL) ||
|
||||
(value & USB_BUF_CTRL_STALL)) return false;
|
||||
|
|
@ -87,17 +105,35 @@ bool native_test_in(uint8_t slot, uint8_t* data, uint16_t* length, bool drain) {
|
|||
buffer_regs()[0] = value & ~USB_BUF_CTRL_AVAIL;
|
||||
usb_hw->buf_status = 1;
|
||||
usb_hw->ints = USB_INTS_BUFF_STATUS_BITS;
|
||||
usb_interrupt();
|
||||
usb_hw->ints = 0;
|
||||
native_test_service_interrupt();
|
||||
if (drain) native_hub_task();
|
||||
return !failed && devices[slot].control.stage != STALLED;
|
||||
}
|
||||
|
||||
bool native_test_private_in(uint8_t slot, uint8_t endpoint, uint8_t* data, uint16_t* length) {
|
||||
if (slot < 1 || slot > 2 || (endpoint != 0x81 && endpoint != 0x82)) return false;
|
||||
// A host token selects the bank, but cannot wait for a foreground task.
|
||||
if (!native_hub_select_device(addresses[slot],slot,UINT32_MAX / 2)) return false;
|
||||
unsigned channel = (endpoint & 15u) * 2u;
|
||||
uint32_t control = endpoint_regs()[channel - 2u];
|
||||
uint32_t value = buffer_regs()[channel];
|
||||
if (!(control & EP_CTRL_ENABLE_BITS) || !(value & USB_BUF_CTRL_AVAIL) ||
|
||||
!(value & USB_BUF_CTRL_FULL) || (value & USB_BUF_CTRL_STALL)) return false;
|
||||
*length = value & USB_BUF_CTRL_LEN_MASK;
|
||||
if (*length > PACKET) return false;
|
||||
if (*length) copy_from_usb(data,
|
||||
(const volatile uint8_t*)USBCTRL_DPRAM_BASE + (control & 0xffffu), *length);
|
||||
buffer_regs()[channel] = value & ~USB_BUF_CTRL_AVAIL;
|
||||
usb_hw->buf_status |= 1u << channel;
|
||||
usb_hw->ints |= USB_INTS_BUFF_STATUS_BITS;
|
||||
native_test_service_interrupt();
|
||||
return !failed;
|
||||
}
|
||||
|
||||
void native_test_bus_reset(bool drain) {
|
||||
usb_hw->sie_status = USB_SIE_STATUS_BUS_RESET_BITS;
|
||||
usb_hw->ints = USB_INTS_BUS_RESET_BITS;
|
||||
usb_interrupt();
|
||||
usb_hw->ints = 0;
|
||||
native_test_service_interrupt();
|
||||
if (drain) native_hub_task();
|
||||
// Assign fixture addresses after reset, independently of EP0 state.
|
||||
addresses[0] = 0; addresses[1] = 1; addresses[2] = 2;
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ void mapped_halves_and_calibration() {
|
|||
source.controller.active = true;
|
||||
source.controller.connection_generation = 7;
|
||||
source.controller.identity = controller_identity_global();
|
||||
source.battery = 255;
|
||||
source.battery = 128;
|
||||
publish();
|
||||
// Neither an absent source nor an uncalibrated child masquerades as active.
|
||||
assert(!peek(0) && !controls[0].active);
|
||||
|
|
@ -132,7 +132,7 @@ void mapped_halves_and_calibration() {
|
|||
pair();
|
||||
assert(stick_x(0) == 2000 && stick_y(0) == 2100);
|
||||
assert(stick_x(1) == 1800 && stick_y(1) == 1900);
|
||||
assert(reports[0][1] == 0x25 && reports[1][1] == 0x25); // Known full level, USB powered, not charging.
|
||||
assert(reports[0][1] == 0x15 && reports[1][1] == 0x15); // Measured half battery, USB powered, not charging.
|
||||
// Actual profile transforms can move controls across native children.
|
||||
profile.button_map[static_cast<unsigned>(ControllerProfileLogicalButton::kSouth)] =
|
||||
static_cast<uint8_t>(ControllerProfileLogicalButton::kDpadRight);
|
||||
|
|
@ -364,7 +364,7 @@ void nunchuk_buttons_map_to_native_left_shoulders() {
|
|||
++source.controller.connection_generation;
|
||||
source.controller.state = {};
|
||||
source.accel_valid = source.gyro_valid = false;
|
||||
source.battery = 0; // Unknown source battery must not invent a full charge.
|
||||
source.battery = 0;
|
||||
profile = controller_profile_default(controller_identity_global(), 0);
|
||||
// The real Wii parser maps Nunchuk C to west and Z to north. These are
|
||||
// ordinary profile inputs, not the unrelated Switch2 extra "C" control.
|
||||
|
|
@ -375,7 +375,7 @@ void nunchuk_buttons_map_to_native_left_shoulders() {
|
|||
source.controller.state.button_west = true; // C -> ZL.
|
||||
publish(false); pair();
|
||||
assert(reports[0][2] == 0 && reports[1][2] == 0x20);
|
||||
assert(reports[0][1] == 0x01 && reports[1][1] == 0x01); // USB power remains real even without battery telemetry.
|
||||
assert(reports[0][1] == 0x01 && reports[1][1] == 0x01);
|
||||
source.controller.state.button_west = false;
|
||||
source.controller.state.button_north = true; // Z -> L.
|
||||
publish(false); pair();
|
||||
|
|
|
|||
|
|
@ -143,9 +143,10 @@ int main() {
|
|||
source.accel_valid = source.gyro_valid = true;
|
||||
source.accel_q13[1] = 8192; // SDL up -> virtual native rail-down +X.
|
||||
memcpy(source.gyro_q10, bias_q10, sizeof(bias_q10));
|
||||
source.battery = 128;
|
||||
assert(poll());
|
||||
assert(controls.active && packet[15] == 30); // Wii IMU starts before background bias learning.
|
||||
assert(packet[1] == 0x01); // USB power, no fabricated charge level or charging state.
|
||||
assert(packet[1] == 0x15); // Measured half battery, USB powered, not charging.
|
||||
for (unsigned i = 1; i < 450; ++i) assert(poll());
|
||||
assert(controls.active && packet[15] == 30 && packet[19] == 0x0c);
|
||||
assert(signed32(packet+32) == (1 << 28));
|
||||
|
|
|
|||
|
|
@ -2801,6 +2801,8 @@ def test_profile_playtest_decodes_raw_controller_state() -> None:
|
|||
"dpad_up",
|
||||
"dpad_right",
|
||||
]
|
||||
assert replace(playtest, battery=255).to_json_object()["battery"] == 100
|
||||
assert replace(playtest, battery=1).to_json_object()["battery"] == 0
|
||||
|
||||
device.playtest_connected = False
|
||||
disconnected = config_manager.read_profile_playtest(device)
|
||||
|
|
@ -2818,6 +2820,7 @@ def test_profile_playtest_decodes_raw_controller_state() -> None:
|
|||
capabilities=0,
|
||||
motion=None,
|
||||
)
|
||||
assert disconnected.to_json_object()["battery"] is None
|
||||
device.playtest_connected = True
|
||||
payload, flags = device._profile_playtest_payload()
|
||||
malformed = bytearray(payload)
|
||||
|
|
|
|||
|
|
@ -1232,6 +1232,33 @@ static void nunchuk_accel_detach_and_replacement_require_fresh_calibration(void)
|
|||
}
|
||||
}
|
||||
|
||||
static void battery_status_survives_input_reports(void) {
|
||||
reset_fixture(0x0306, false, EXT_NONE);
|
||||
assert(f.device.controller.battery == UNI_CONTROLLER_BATTERY_NOT_AVAILABLE);
|
||||
uni_hid_parser_wii_setup(&f.device);
|
||||
finish_setup();
|
||||
assert(f.device.controller.battery == UNI_CONTROLLER_BATTERY_FULL);
|
||||
send_core_and_accel();
|
||||
assert(f.device.controller.battery == UNI_CONTROLLER_BATTERY_FULL);
|
||||
|
||||
uint8_t status[] = {0x20, 0, 0, 0, 0, 0, 52};
|
||||
feed(status, sizeof(status));
|
||||
finish_setup();
|
||||
assert(f.device.controller.battery == 179); // Measured 70% band, not a full-charge placeholder.
|
||||
send_core_and_accel();
|
||||
assert(f.device.controller.battery == 179);
|
||||
status[6] = 0;
|
||||
feed(status, sizeof(status) - 1);
|
||||
assert(f.device.controller.battery == 179); // Truncation cannot erase the last measurement.
|
||||
feed(status, sizeof(status));
|
||||
finish_setup();
|
||||
assert(f.device.controller.battery == 13); // Measured low band remains distinct from unknown.
|
||||
send_core_and_accel();
|
||||
assert(f.device.controller.battery == 13);
|
||||
uni_hid_parser_wii_setup(&f.device);
|
||||
assert(f.device.controller.battery == UNI_CONTROLLER_BATTERY_NOT_AVAILABLE);
|
||||
}
|
||||
|
||||
static void run_case(const char* name, void (*test)(void)) {
|
||||
printf("Wii parser: %s\n", name);
|
||||
fflush(stdout);
|
||||
|
|
@ -1239,6 +1266,7 @@ static void run_case(const char* name, void (*test)(void)) {
|
|||
}
|
||||
|
||||
int main(void) {
|
||||
run_case("real battery retained between status and input reports", battery_status_survives_input_reports);
|
||||
run_case("fresh calibrated accelerometer snapshots without MotionPlus", accelerometer_snapshot_requires_fresh_calibrated_reports);
|
||||
run_case("independent fresh calibrated MotionPlus gyro snapshots", gyro_snapshot_advances_only_on_calibrated_motionplus_packets);
|
||||
run_case("gyro validity through hotplug, replacement and teardown", gyro_snapshot_invalidates_on_topology_and_teardown);
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ _Static_assert(PROBE_ROUTER_SLOTS == 3u, "Packed setup owner has three slots");
|
|||
typedef struct {
|
||||
uint8_t owner[128];
|
||||
#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB
|
||||
uint8_t early_address[2][16];
|
||||
uint8_t early_address[2][256];
|
||||
#endif
|
||||
} routing_table;
|
||||
|
||||
|
|
@ -149,6 +149,9 @@ static void build_table(routing_table* table,
|
|||
#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB
|
||||
// A unique observed prefix can preselect the SIE sooner. It still compares
|
||||
// the complete hardware address and CRC before accepting the transaction.
|
||||
// Index the four captured line pairs directly. Packing their D+ bits in
|
||||
// the sampling window delays bit 20 even when the prefix is ambiguous.
|
||||
memset(table->early_address, PROBE_ROUTER_UNASSIGNED, sizeof(table->early_address));
|
||||
for (unsigned kind = 0; kind < 2; ++kind) {
|
||||
for (unsigned prefix = 0; prefix < 16; ++prefix) {
|
||||
uint8_t candidate = PROBE_ROUTER_UNASSIGNED;
|
||||
|
|
@ -161,7 +164,10 @@ static void build_table(routing_table* table,
|
|||
}
|
||||
candidate = address;
|
||||
}
|
||||
table->early_address[kind][prefix] = candidate;
|
||||
unsigned raw_prefix = 0u;
|
||||
for (unsigned bit = 0; bit < 4; ++bit)
|
||||
raw_prefix |= ((prefix >> bit) & 1u ? LINE_J : LINE_K) << (2u * bit);
|
||||
table->early_address[kind][raw_prefix] = candidate;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
@ -331,14 +337,12 @@ static bool observe_idle_j(void);
|
|||
#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB
|
||||
static raw_packet __no_inline_not_in_flash_func(capture_packet)(
|
||||
uint32_t phase, const routing_table* table, bool draining) {
|
||||
prepare_capture:;
|
||||
#else
|
||||
static raw_packet __no_inline_not_in_flash_func(capture_packet)(
|
||||
uint32_t phase, const routing_table* table) {
|
||||
#endif
|
||||
raw_packet result = {0};
|
||||
uint32_t word0 = LINE_K, word1 = 0u, word2 = 0u;
|
||||
uint32_t address_wire = 0u;
|
||||
const uint8_t* decoder = NULL;
|
||||
uint32_t expected_word = 0u;
|
||||
const uint32_t initial_address = usb_hw->dev_addr_ctrl;
|
||||
|
|
@ -351,6 +355,7 @@ static raw_packet __no_inline_not_in_flash_func(capture_packet)(
|
|||
// forcing them into live registers adds spills and unnecessary ORs.
|
||||
__asm volatile ("" : "+r"(word0), "+m"(result) : : "memory");
|
||||
#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB
|
||||
drain_prepared_capture:;
|
||||
if (draining) {
|
||||
const uint32_t stop = cycles_now() + FS_CLOCK_HZ / 10000u;
|
||||
bool saw_se0 = false;
|
||||
|
|
@ -422,10 +427,16 @@ edge:
|
|||
// The first stored K is the observed SOP above, not an invented SYNC bit.
|
||||
#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB
|
||||
#define SET_EARLY_DECODER(kind) (early_decoder = table->early_address[kind])
|
||||
#define DISCARD_NON_TOKEN() do { draining = true; goto prepare_capture; } while (0)
|
||||
// PID rejection happens before any address/body samples. Reuse the prepared
|
||||
// frame: rebuilding its stack image here can miss the end of a short ACK/NAK
|
||||
// and the following token. All other result/address accumulators are still zero.
|
||||
#define DISCARD_NON_TOKEN() do { \
|
||||
result.sop = false; word0 = LINE_K; draining = true; \
|
||||
goto drain_prepared_capture; \
|
||||
} while (0)
|
||||
#define ROUTE_EARLY(bit, base) do { \
|
||||
if ((base) + (bit) == 19u && decoder != NULL) { \
|
||||
uint8_t candidate = early_decoder[address_wire]; \
|
||||
uint8_t candidate = early_decoder[word1 & 0xffu]; \
|
||||
if (candidate < 128u) \
|
||||
route_header(table, candidate, word0 == 0x9a56a666u, initial_address, \
|
||||
deadline + 11u * FS_BIT_CYCLES, &result); \
|
||||
|
|
@ -447,10 +458,13 @@ edge:
|
|||
decoder = NULL; \
|
||||
DISCARD_NON_TOKEN(); \
|
||||
} \
|
||||
if ((base) + (bit) >= 16u && (base) + (bit) <= 23u) \
|
||||
address_wire |= (line & 1u) << (bit); \
|
||||
ROUTE_EARLY(bit, base); \
|
||||
if ((base) + (bit) == 23u && decoder != NULL) { \
|
||||
/* Compact observed D+ bits only after all eight symbols exist. */ \
|
||||
uint32_t address_wire = word1 & 0x5555u; \
|
||||
address_wire = (address_wire | (address_wire >> 1u)) & 0x3333u; \
|
||||
address_wire = (address_wire | (address_wire >> 2u)) & 0x0f0fu; \
|
||||
address_wire = (address_wire | (address_wire >> 4u)) & 0xffu; \
|
||||
route_header(table, decoder[address_wire], word0 == 0x9a56a666u, \
|
||||
initial_address, deadline + 7u * FS_BIT_CYCLES, &result); \
|
||||
if (result.late) { result.count = (base) + (bit) + 1u; goto done; } \
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ int g_sensor_status = -1;
|
|||
uint16_t g_stick_center[2]{2048, 2048};
|
||||
uint16_t g_stick_positive[2]{2047, 2047};
|
||||
uint16_t g_stick_negative[2]{2048, 2048};
|
||||
uint8_t g_power_info = 0x01; // USB powered; no invented source charge or charging state.
|
||||
uint8_t g_power_info = 0x01;
|
||||
uint8_t g_report_counter;
|
||||
uint32_t g_report_serial;
|
||||
uint32_t g_pending_serial;
|
||||
|
|
|
|||
|
|
@ -290,9 +290,7 @@ uint32_t probe_native_gamepad_input_peek_native_report(uint8_t instance, uint32_
|
|||
if (g_report_token == UINT32_MAX) return 0; // Boot-unique, including across children/resets.
|
||||
memset(child.pending_report, 0, sizeof(child.pending_report));
|
||||
child.pending_report[0] = child.counter;
|
||||
// Battery telemetry belongs to the source; external power belongs to
|
||||
// these USB-powered virtual controllers. Unknown is not a full charge,
|
||||
// and the Pico does not charge the wireless source.
|
||||
// Source battery level and the virtual controller's USB power are separate.
|
||||
const unsigned battery_level = (static_cast<unsigned>(g_source.battery) * 9u + 127u) / 255u;
|
||||
child.pending_report[1] = static_cast<uint8_t>((battery_level << 2) | 0x01u);
|
||||
memcpy(child.pending_report + 2, child.input.buttons, sizeof(child.input.buttons));
|
||||
|
|
|
|||
|
|
@ -369,12 +369,12 @@ function(switch2_usb_probe_configure target)
|
|||
endif()
|
||||
if(SWITCH2_PROBE_HUB AND SWITCH2_BRIDGE_FULL_INPUT)
|
||||
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
pico_set_program_version(${target} "0.72-native-usb-power")
|
||||
pico_set_program_version(${target} "0.78-native-tx-bank-trace")
|
||||
else()
|
||||
pico_set_program_version(${target} "0.72-native-gamepad")
|
||||
pico_set_program_version(${target} "0.78-native-gamepad")
|
||||
endif()
|
||||
elseif(SWITCH2_PROBE_HUB)
|
||||
pico_set_program_version(${target} "0.72-native-hub-editor")
|
||||
pico_set_program_version(${target} "0.78-native-hub-tx-bank")
|
||||
elseif(SWITCH2_PROBE_JOIN_CHORD_GATE)
|
||||
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
pico_set_program_version(${target} "0.37-pair-chord-trace")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue