diff --git a/README.md b/README.md index 01875ea..44bb4c5 100644 --- a/README.md +++ b/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 diff --git a/patches/bluepad32-sdl3-imu.patch b/patches/bluepad32-sdl3-imu.patch index 8bfbb46..101c1dd 100644 --- a/patches/bluepad32-sdl3-imu.patch +++ b/patches/bluepad32-sdl3-imu.patch @@ -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)); } diff --git a/src/firmware/usb/native_hub/native_hub.c b/src/firmware/usb/native_hub/native_hub.c index 0318326..e2bae45 100644 --- a/src/firmware/usb/native_hub/native_hub.c +++ b/src/firmware/usb/native_hub/native_hub.c @@ -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. diff --git a/src/firmware/usb/usb_configuration_management.cpp b/src/firmware/usb/usb_configuration_management.cpp index 5a3c70c..5baf453 100644 --- a/src/firmware/usb/usb_configuration_management.cpp +++ b/src/firmware/usb/usb_configuration_management.cpp @@ -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, diff --git a/src/switch_pico_bridge/config_manager.py b/src/switch_pico_bridge/config_manager.py index 1dad6e4..4993581 100755 --- a/src/switch_pico_bridge/config_manager.py +++ b/src/switch_pico_bridge/config_manager.py @@ -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 diff --git a/tests/native_hub_management_test.cpp b/tests/native_hub_management_test.cpp index b7c7a66..be4946f 100644 --- a/tests/native_hub_management_test.cpp +++ b/tests/native_hub_management_test.cpp @@ -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 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(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"; } diff --git a/tests/native_hub_stubs/hardware_stub.h b/tests/native_hub_stubs/hardware_stub.h index d611092..62325f1 100644 --- a/tests/native_hub_stubs/hardware_stub.h +++ b/tests/native_hub_stubs/hardware_stub.h @@ -5,14 +5,28 @@ #include #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) diff --git a/tests/native_hub_transport_fixture.c b/tests/native_hub_transport_fixture.c index 49a5864..147755a 100644 --- a/tests/native_hub_transport_fixture.c +++ b/tests/native_hub_transport_fixture.c @@ -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; diff --git a/tests/switch2_native_gamepad_bridge_test.cpp b/tests/switch2_native_gamepad_bridge_test.cpp index 2e361e2..01bd512 100644 --- a/tests/switch2_native_gamepad_bridge_test.cpp +++ b/tests/switch2_native_gamepad_bridge_test.cpp @@ -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(ControllerProfileLogicalButton::kSouth)] = static_cast(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(); diff --git a/tests/switch2_wii_bridge_test.cpp b/tests/switch2_wii_bridge_test.cpp index 0cd3917..22f3ee7 100644 --- a/tests/switch2_wii_bridge_test.cpp +++ b/tests/switch2_wii_bridge_test.cpp @@ -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)); diff --git a/tests/test_config_manager.py b/tests/test_config_manager.py index 86048d6..c5ed3e5 100644 --- a/tests/test_config_manager.py +++ b/tests/test_config_manager.py @@ -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) diff --git a/tests/wii_parser_native_test.c b/tests/wii_parser_native_test.c index 564c2ca..60bd66b 100644 --- a/tests/wii_parser_native_test.c +++ b/tests/wii_parser_native_test.c @@ -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); diff --git a/tools/pico_usb_address_probe/router.c b/tools/pico_usb_address_probe/router.c index debfce4..a80a3a1 100644 --- a/tools/pico_usb_address_probe/router.c +++ b/tools/pico_usb_address_probe/router.c @@ -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; } \ diff --git a/tools/switch2_usb_probe/controller_input.cpp b/tools/switch2_usb_probe/controller_input.cpp index 6b0856b..490739b 100644 --- a/tools/switch2_usb_probe/controller_input.cpp +++ b/tools/switch2_usb_probe/controller_input.cpp @@ -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; diff --git a/tools/switch2_usb_probe/native_gamepad_input.cpp b/tools/switch2_usb_probe/native_gamepad_input.cpp index a50ef09..f23660e 100644 --- a/tools/switch2_usb_probe/native_gamepad_input.cpp +++ b/tools/switch2_usb_probe/native_gamepad_input.cpp @@ -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(g_source.battery) * 9u + 127u) / 255u; child.pending_report[1] = static_cast((battery_level << 2) | 0x01u); memcpy(child.pending_report + 2, child.input.buttons, sizeof(child.input.buttons)); diff --git a/tools/switch2_usb_probe/probe_build.cmake b/tools/switch2_usb_probe/probe_build.cmake index 5bec805..c7365a0 100644 --- a/tools/switch2_usb_probe/probe_build.cmake +++ b/tools/switch2_usb_probe/probe_build.cmake @@ -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")