feat(native-usb): add 32-frame HD gameplay rumble and USB flight diagnostics
Preserve native frequency/amplitude timelines for DualSense PCM output, remove the unsupported 64-frame path, and retain compatibility rumble for other controllers. Use the validated 300 MHz sampling phase and restrict non-bondable Classic discovery autoconnect to previously paired peers. Buffer native-hub UART stdout and release USB IRQs around port-reset callbacks. Add observer liveness and pre-SETUP root-response observations without changing recovery behavior. Cover transport and haptics boundaries. Record the user-accepted 0.108 trial: controls remained responsive and rumble felt fine. Instrumentation changes timing; the disconnect root cause and long-term reliability remain unqualified. Validation: 624 tests, 11 affected firmware/probe builds, and on-device concurrent USB and HD-auto-start checks. Private captures, generated images, and unrelated working-tree files are intentionally excluded.
This commit is contained in:
parent
ae1ca51b9a
commit
80b788099b
46 changed files with 2873 additions and 412 deletions
|
|
@ -24,8 +24,6 @@ extern bool native_hub_select_device(uint8_t address, uint8_t owner, uint32_t cu
|
|||
// makes it a zero-wait-state cycle counter next to the GPIO inputs, avoiding
|
||||
// SysTick's PPB accesses and 24-bit down-counter arithmetic in every sample.
|
||||
// Deadlines use modular 32-bit arithmetic for intervals below 2^31 cycles.
|
||||
#define FS_CLOCK_HZ 240000000u
|
||||
#define FS_BIT_CYCLES 20u
|
||||
#define LINE_SE0 0u
|
||||
#define LINE_J 1u
|
||||
#define LINE_K 2u
|
||||
|
|
@ -85,6 +83,10 @@ static uint32_t fatal_fault;
|
|||
static bool valid_clock;
|
||||
static uint8_t address_decoder[2][256];
|
||||
static bool address_decoder_ready;
|
||||
#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB && defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
enum { ROOT_OBSERVE_IDLE, ROOT_OBSERVE_HEADER, ROOT_OBSERVE_EOP };
|
||||
static uint8_t root_observation_stage; // Core1 only after initialization.
|
||||
#endif
|
||||
|
||||
static __force_inline uint32_t atomic_read(const uint32_t* value) {
|
||||
return __atomic_load_n(value, __ATOMIC_RELAXED);
|
||||
|
|
@ -195,6 +197,9 @@ void probe_router_init(uint32_t system_clock_hz) {
|
|||
memset(addresses, PROBE_ROUTER_UNASSIGNED, sizeof(addresses));
|
||||
addresses[0] = 0u;
|
||||
memset(&counters, 0, sizeof(counters));
|
||||
#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB && defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
root_observation_stage = ROOT_OBSERVE_IDLE;
|
||||
#endif
|
||||
published_generation = 0u;
|
||||
reader_index = NO_READER;
|
||||
enabled = 0u;
|
||||
|
|
@ -202,7 +207,7 @@ void probe_router_init(uint32_t system_clock_hz) {
|
|||
setup_publication = SETUP_INVALID;
|
||||
fatal_fault = 0u;
|
||||
valid_clock = system_clock_hz == FS_CLOCK_HZ;
|
||||
counters.cycles_per_bit = system_clock_hz / 12000000u;
|
||||
counters.cycles_per_bit = valid_clock ? FS_BIT_CYCLES : 0u;
|
||||
build_table(&tables[0], addresses, 0u);
|
||||
}
|
||||
|
||||
|
|
@ -230,12 +235,12 @@ void probe_router_publish(const uint8_t addresses[PROBE_ROUTER_SLOTS], uint8_t d
|
|||
void probe_router_enable(bool enable) {
|
||||
// ARM qualification (observed hub tokens/SETUPs) belongs to the control
|
||||
// request handler. This additionally prevents enabling a failed observer.
|
||||
__atomic_store_n(&enabled, enable && atomic_read(&counters.ready) != 0u &&
|
||||
__atomic_store_n(&enabled, enable && valid_clock && atomic_read(&counters.ready) != 0u &&
|
||||
atomic_read(&fatal_fault) == 0u, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
bool probe_router_set_phase(uint32_t cycles) {
|
||||
if (cycles >= atomic_read(&counters.cycles_per_bit) || atomic_read(&enabled) != 0u)
|
||||
if (!valid_clock || cycles >= FS_BIT_CYCLES || atomic_read(&enabled) != 0u)
|
||||
return false;
|
||||
__atomic_store_n(&phase_cycles, cycles, __ATOMIC_RELEASE);
|
||||
return true;
|
||||
|
|
@ -261,7 +266,24 @@ void probe_router_snapshot(probe_router_stats* out) {
|
|||
SNAPSHOT(last_raw_count);
|
||||
SNAPSHOT(last_raw_eop);
|
||||
SNAPSHOT(last_raw_late);
|
||||
SNAPSHOT(capture_returns);
|
||||
SNAPSHOT(discarded_headers);
|
||||
SNAPSHOT(last_discarded_header);
|
||||
SNAPSHOT(root_in_count);
|
||||
SNAPSHOT(root_in_cutoff);
|
||||
SNAPSHOT(root_header);
|
||||
SNAPSHOT(root_header_cycle);
|
||||
SNAPSHOT(root_eop_cycle);
|
||||
SNAPSHOT(before_setup_in_count);
|
||||
SNAPSHOT(before_setup_in_cutoff);
|
||||
SNAPSHOT(before_setup_header);
|
||||
SNAPSHOT(before_setup_header_cycle);
|
||||
SNAPSHOT(before_setup_eop_cycle);
|
||||
#undef SNAPSHOT
|
||||
out->enabled = atomic_read(&enabled);
|
||||
out->fatal_fault = atomic_read(&fatal_fault);
|
||||
out->published_generation = atomic_read(&published_generation);
|
||||
out->reader_index = atomic_read(&reader_index);
|
||||
const uint32_t setup = __atomic_load_n(&setup_publication, __ATOMIC_ACQUIRE);
|
||||
out->last_setup_sequence = setup & SETUP_SEQUENCE_MASK;
|
||||
const uint32_t slot = setup >> SETUP_SLOT_SHIFT;
|
||||
|
|
@ -308,20 +330,82 @@ static __force_inline bool sample_line(uint32_t* deadline, uint32_t* line) {
|
|||
return true;
|
||||
}
|
||||
|
||||
#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB && defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
static __force_inline void root_observe_token(uint8_t owner, uint32_t signature,
|
||||
uint32_t cutoff, bool selected) {
|
||||
if (selected && owner == 0 && signature == TOKEN_IN_SIGNATURE) {
|
||||
// Early-prefix and full-address decisions share a cutoff. Do not reset
|
||||
// an observation twice for the same token.
|
||||
if (root_observation_stage == ROOT_OBSERVE_HEADER &&
|
||||
atomic_read(&counters.root_in_cutoff) == cutoff) return;
|
||||
count_one(&counters.root_in_count);
|
||||
atomic_write(&counters.root_in_cutoff,cutoff);
|
||||
atomic_write(&counters.root_header,0);
|
||||
atomic_write(&counters.root_header_cycle,0);
|
||||
atomic_write(&counters.root_eop_cycle,0);
|
||||
root_observation_stage = ROOT_OBSERVE_HEADER;
|
||||
return;
|
||||
}
|
||||
// A new token ends attribution to the preceding root IN, including when
|
||||
// its selection fails. A later child's response must not become the root's.
|
||||
root_observation_stage = ROOT_OBSERVE_IDLE;
|
||||
if (selected && owner == 0 && signature == TOKEN_SETUP_SIGNATURE) {
|
||||
atomic_write(&counters.before_setup_in_count,atomic_read(&counters.root_in_count));
|
||||
atomic_write(&counters.before_setup_in_cutoff,atomic_read(&counters.root_in_cutoff));
|
||||
atomic_write(&counters.before_setup_header,atomic_read(&counters.root_header));
|
||||
atomic_write(&counters.before_setup_header_cycle,atomic_read(&counters.root_header_cycle));
|
||||
atomic_write(&counters.before_setup_eop_cycle,atomic_read(&counters.root_eop_cycle));
|
||||
}
|
||||
}
|
||||
|
||||
static __force_inline void observe_discarded_header(uint32_t header) {
|
||||
atomic_write(&counters.last_discarded_header,header);
|
||||
count_one(&counters.discarded_headers);
|
||||
if (root_observation_stage == ROOT_OBSERVE_HEADER) {
|
||||
atomic_write(&counters.root_header,header);
|
||||
atomic_write(&counters.root_header_cycle,cycles_now());
|
||||
root_observation_stage = ROOT_OBSERVE_EOP;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static __force_inline void root_observe_eop(uint32_t cycle, bool qualified) {
|
||||
#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB && defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
if (root_observation_stage == ROOT_OBSERVE_EOP) {
|
||||
// This is the drain's observed SE0-to-J qualification, not a measured
|
||||
// two-bit EOP width. Idle/timeout must not attribute a later EOP here.
|
||||
if (qualified) atomic_write(&counters.root_eop_cycle,cycle);
|
||||
root_observation_stage = ROOT_OBSERVE_IDLE;
|
||||
}
|
||||
#else
|
||||
(void)cycle;
|
||||
(void)qualified;
|
||||
#endif
|
||||
}
|
||||
|
||||
static __force_inline void route_header(const routing_table* table, uint32_t address,
|
||||
uint32_t signature, uint32_t initial_address,
|
||||
uint32_t cutoff, raw_packet* packet) {
|
||||
// TinyUSB clears SETUP_REC only AFTER copying the hardware-validated SETUP
|
||||
// into its event callback. Until then, preserve both address and owner.
|
||||
if (usb_hw->sie_status & USB_SIE_STATUS_SETUP_REC_BITS)
|
||||
if (usb_hw->sie_status & USB_SIE_STATUS_SETUP_REC_BITS) {
|
||||
#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB && defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
root_observe_token(PROBE_ROUTER_UNASSIGNED,signature,cutoff,false);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
invalidate_setup();
|
||||
if (address >= 128u || table->owner[address] >= PROBE_ROUTER_SLOTS)
|
||||
if (address >= 128u || table->owner[address] >= PROBE_ROUTER_SLOTS) {
|
||||
#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB && defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
root_observe_token(PROBE_ROUTER_UNASSIGNED,signature,cutoff,false);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB
|
||||
if (atomic_read(&enabled) != 0u) {
|
||||
const bool selected = native_hub_select_device((uint8_t)address, table->owner[address], cutoff);
|
||||
#if defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
root_observe_token(table->owner[address],signature,cutoff,selected);
|
||||
// Keep diagnostic PID classification behind the address-critical call.
|
||||
__asm volatile ("" : "+r"(signature) : : "memory");
|
||||
const uint8_t pid = signature == TOKEN_OUT_SIGNATURE ? PID_OUT :
|
||||
|
|
@ -338,6 +422,11 @@ static __force_inline void route_header(const routing_table* table, uint32_t add
|
|||
#endif
|
||||
if (initial_address != address) ++packet->retargets;
|
||||
}
|
||||
#if defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
else {
|
||||
root_observe_token(PROBE_ROUTER_UNASSIGNED,signature,cutoff,false);
|
||||
}
|
||||
#endif
|
||||
#else
|
||||
if (initial_address != address && atomic_read(&enabled) != 0u) {
|
||||
if (cycles_after(cycles_now(), cutoff) >= 0) {
|
||||
|
|
@ -390,15 +479,25 @@ drain_prepared_capture:;
|
|||
uint32_t se0_since = 0;
|
||||
for (;;) {
|
||||
uint32_t line = receive_line(), now = cycles_now();
|
||||
if (cycles_after(now,stop) >= 0) { result.resync = true; return result; }
|
||||
if (cycles_after(now,stop) >= 0) {
|
||||
root_observe_eop(now,false);
|
||||
result.resync = true;
|
||||
return result;
|
||||
}
|
||||
if (line == LINE_SE0) {
|
||||
if (!saw_se0) se0_since = now;
|
||||
saw_se0 = true;
|
||||
} else {
|
||||
// Half a bit rejects pad skew while allowing late ACK EOP entry.
|
||||
if (line == LINE_J && saw_se0 &&
|
||||
cycles_after(now,se0_since) >= (int32_t)(FS_BIT_CYCLES / 2u)) break;
|
||||
if (line == LINE_J && observe_idle_j()) break;
|
||||
cycles_after(now,se0_since) >= (int32_t)FS_HALF_BIT_CYCLES) {
|
||||
root_observe_eop(now,true);
|
||||
break;
|
||||
}
|
||||
if (line == LINE_J && observe_idle_j()) {
|
||||
root_observe_eop(now,false);
|
||||
break;
|
||||
}
|
||||
saw_se0 = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -448,7 +547,7 @@ edge:
|
|||
const uint32_t sop_time = cycles_now();
|
||||
// This timestamp follows the PHY read and edge-detection instructions.
|
||||
// Captures showed an extra full-bit delay skipped SYNC's second symbol.
|
||||
// Sweep the next sample relative to read completion, then keep 20-cycle
|
||||
// Sweep the next sample relative to read completion, then keep one-bit
|
||||
// spacing; every stored line symbol is still physically observed.
|
||||
uint32_t deadline = sop_time + phase;
|
||||
result.sop = true;
|
||||
|
|
@ -458,7 +557,13 @@ edge:
|
|||
// 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.
|
||||
#if defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
#define NOTE_DISCARDED_HEADER() observe_discarded_header(word0)
|
||||
#else
|
||||
#define NOTE_DISCARDED_HEADER() ((void)0)
|
||||
#endif
|
||||
#define DISCARD_NON_TOKEN() do { \
|
||||
NOTE_DISCARDED_HEADER(); \
|
||||
result.sop = false; word0 = LINE_K; draining = true; \
|
||||
goto drain_prepared_capture; \
|
||||
} while (0)
|
||||
|
|
@ -533,6 +638,7 @@ edge:
|
|||
#undef ROUTE_EARLY
|
||||
#undef SET_EARLY_DECODER
|
||||
#undef DISCARD_NON_TOKEN
|
||||
#undef NOTE_DISCARDED_HEADER
|
||||
result.count = RAW_BITS;
|
||||
goto done;
|
||||
eop:
|
||||
|
|
@ -560,11 +666,11 @@ static bool __not_in_flash_func(observe_idle_j)(void) {
|
|||
// Stuffing prohibits eight consecutive J bit times inside a packet.
|
||||
// Use tight PHY polling, not sparse timer-paced reads that could miss K.
|
||||
const uint32_t start = cycles_now();
|
||||
for (uint32_t i = 0; i < 64u; ++i) {
|
||||
for (uint32_t i = 0; i < FS_IDLE_POLLS; ++i) {
|
||||
if (receive_line() != LINE_J)
|
||||
return false;
|
||||
}
|
||||
return cycles_after(cycles_now(), start) >= (int32_t)(8u * FS_BIT_CYCLES);
|
||||
return cycles_after(cycles_now(), start) >= (int32_t)FS_IDLE_CYCLES;
|
||||
}
|
||||
|
||||
#if !defined(SWITCH2_PROBE_HUB) || !SWITCH2_PROBE_HUB
|
||||
|
|
@ -734,8 +840,8 @@ void __not_in_flash_func(probe_router_core1)(void) {
|
|||
break;
|
||||
}
|
||||
// Phase is relative to the observed J->K edge, not a promised physical
|
||||
// edge timestamp. The host sweeps 0..19 cycles and correlates sampled
|
||||
// headers with the native DCD's CRC-accepted SETUP interrupts. A successful
|
||||
// edge timestamp. The host sweeps the compiled bit period and correlates
|
||||
// sampled headers with the native DCD's CRC-accepted SETUP interrupts. A successful
|
||||
// passive phase still does NOT prove when the SIE latches its address.
|
||||
// Calibrate the real routing instruction path, not a lighter sampler
|
||||
// whose phase/register allocation changes when routing is enabled.
|
||||
|
|
@ -745,6 +851,9 @@ void __not_in_flash_func(probe_router_core1)(void) {
|
|||
#else
|
||||
const raw_packet packet = capture_packet(phase, table);
|
||||
#endif
|
||||
#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB && defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
count_one(&counters.capture_returns);
|
||||
#endif
|
||||
if (!packet.sop) {
|
||||
if (packet.resync) {
|
||||
draining = true;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,28 @@
|
|||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// The standalone RAM probe remains at 240 MHz. Only the integrated hub
|
||||
// consumes the firmware clock; every receiver deadline is compiled for it.
|
||||
#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB && defined(SWITCH_PICO_SYS_CLOCK_MHZ)
|
||||
#define FS_CLOCK_MHZ SWITCH_PICO_SYS_CLOCK_MHZ
|
||||
#else
|
||||
#define FS_CLOCK_MHZ 240u
|
||||
#endif
|
||||
#if FS_CLOCK_MHZ != 240 && FS_CLOCK_MHZ != 300
|
||||
#error "Native SIO receiver requires a compiled 240 or 300 MHz clock"
|
||||
#endif
|
||||
#define FS_CLOCK_HZ (FS_CLOCK_MHZ * 1000000u)
|
||||
#define FS_BIT_CYCLES (FS_CLOCK_HZ / 12000000u)
|
||||
#define FS_HALF_BIT_CYCLES ((FS_BIT_CYCLES + 1u) / 2u)
|
||||
#define FS_IDLE_CYCLES (8u * FS_BIT_CYCLES)
|
||||
// Preserve the baseline tight-poll duration: 64 polls at 240, 80 at 300 MHz.
|
||||
#define FS_IDLE_POLLS (FS_CLOCK_MHZ * 64u / 240u)
|
||||
// Relative to the software SOP timestamp, after edge detection—not a fraction
|
||||
// of the USB bit period. Fixed instruction latency needs a separate offset at
|
||||
// 300 MHz. Phase 12 passed loaded child-control tests where scaled phase 5 failed.
|
||||
// Preserve the established 240 MHz setting; Switch recognition is a separate check.
|
||||
#define PROBE_ROUTER_DEFAULT_PHASE (FS_CLOCK_MHZ == 300u ? 12u : 4u)
|
||||
|
||||
#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB && defined(PROBE_CONTROLLER_COUNT)
|
||||
#define PROBE_ROUTER_SLOTS (PROBE_CONTROLLER_COUNT + 1u)
|
||||
#else
|
||||
|
|
@ -29,6 +51,17 @@ typedef struct {
|
|||
uint32_t last_raw_count;
|
||||
uint32_t last_raw_eop;
|
||||
uint32_t last_raw_late;
|
||||
// Trace-enabled hub diagnostics, updated outside token sampling deadlines.
|
||||
uint32_t capture_returns;
|
||||
uint32_t discarded_headers;
|
||||
uint32_t last_discarded_header;
|
||||
uint32_t enabled, fatal_fault, published_generation, reader_index;
|
||||
// First non-token header following a selected root-IN candidate. These are
|
||||
// sampler observations, not endpoint decoding or full packet validation.
|
||||
// The pre-SETUP copy survives the recovery control's own IN/status response.
|
||||
uint32_t root_in_count, root_in_cutoff, root_header, root_header_cycle, root_eop_cycle;
|
||||
uint32_t before_setup_in_count, before_setup_in_cutoff;
|
||||
uint32_t before_setup_header, before_setup_header_cycle, before_setup_eop_cycle;
|
||||
} probe_router_stats;
|
||||
|
||||
// Core 0 initializes before launching Core 1. The native SIE drives USB;
|
||||
|
|
|
|||
|
|
@ -491,9 +491,9 @@ extern "C" void probe_controller_input_set_full_stick_calibration(
|
|||
}
|
||||
|
||||
extern "C" bool probe_controller_input_submit_rumble(
|
||||
uint8_t instance, const uint8_t* magnitudes, uint8_t count) {
|
||||
uint8_t instance, const NativeHapticsActuatorFrame* frame) {
|
||||
return g_flash_ready && instance < PROBE_CONTROLLER_COUNT &&
|
||||
bluepad32_input_backend_native_rumble_submit(instance, magnitudes, count);
|
||||
bluepad32_input_backend_native_rumble_submit(instance, frame);
|
||||
}
|
||||
|
||||
extern "C" void probe_controller_input_cancel_rumble(uint8_t instance) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include "core/native_haptics.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
|
|
@ -51,10 +52,10 @@ void probe_controller_input_set_native_features(uint8_t features);
|
|||
// Supply each child's advertised, validated nine-byte stick record. Native
|
||||
// output stays unavailable until that child's calibration has been supplied.
|
||||
void probe_controller_input_set_full_stick_calibration(uint8_t instance, const uint8_t calibration[9]);
|
||||
// Native single-actuator gameplay envelope for this virtual half. Backend owns
|
||||
// binding/generation checks, profile gain, finite playback and stale-stream stop.
|
||||
bool probe_controller_input_submit_rumble(uint8_t instance, const uint8_t* magnitudes,
|
||||
uint8_t count);
|
||||
// Lossless native waveform for this virtual half. Backend owns source binding,
|
||||
// profile gain, selected HD output and bounded conventional fallback.
|
||||
bool probe_controller_input_submit_rumble(uint8_t instance,
|
||||
const NativeHapticsActuatorFrame* frame);
|
||||
void probe_controller_input_cancel_rumble(uint8_t instance);
|
||||
#endif
|
||||
// Core0 native07/08 output. Disable discards queued/prepared data; repeated
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@
|
|||
#include "pico/stdlib.h"
|
||||
#include "hardware/sync.h"
|
||||
#include "hardware/uart.h"
|
||||
#if SWITCH2_PROBE_HUB
|
||||
#include "pico/stdio/driver.h"
|
||||
#include "pico/stdio_uart.h"
|
||||
#endif
|
||||
#include "tusb.h"
|
||||
#include "descriptors.h"
|
||||
#include "protocol.h"
|
||||
|
|
@ -127,6 +131,40 @@ static void gate_join_shoulders(uint8_t instance, uint8_t report_id,
|
|||
}
|
||||
#endif
|
||||
|
||||
static void count_dropped_log_bytes(uint32_t count) {
|
||||
if (!count) return;
|
||||
#if SWITCH2_PROBE_HUB
|
||||
__atomic_add_fetch(&log_dropped, count, __ATOMIC_RELAXED);
|
||||
#else
|
||||
log_dropped += count;
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool queue_log_bytes(const char* message, size_t size, uint32_t requested) {
|
||||
#if !SWITCH2_PROBE_HUB
|
||||
const uint32_t interrupts = save_and_disable_interrupts();
|
||||
#elif defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
const uint32_t trace_parent = native_hub_trace_phase(NATIVE_HUB_TRACE_PHASE_LOG_COPY);
|
||||
#endif
|
||||
const bool queued = LOG_CAPACITY - (log_written - log_read) >= size;
|
||||
if (queued) {
|
||||
const size_t offset = log_written % LOG_CAPACITY;
|
||||
const size_t first = size < LOG_CAPACITY - offset ? size : LOG_CAPACITY - offset;
|
||||
memcpy(log_bytes + offset, message, first);
|
||||
memcpy(log_bytes, message + first, size - first);
|
||||
log_written += (uint32_t)size;
|
||||
count_dropped_log_bytes(requested - (uint32_t)size);
|
||||
} else {
|
||||
count_dropped_log_bytes(requested);
|
||||
}
|
||||
#if !SWITCH2_PROBE_HUB
|
||||
restore_interrupts(interrupts);
|
||||
#elif defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
native_hub_trace_phase(trace_parent);
|
||||
#endif
|
||||
return queued;
|
||||
}
|
||||
|
||||
int probe_debug_printf(const char* format, ...) {
|
||||
#if SWITCH2_PROBE_HUB
|
||||
// Native-hub producers and the UART consumer all run on Core0 foreground.
|
||||
|
|
@ -142,30 +180,30 @@ int probe_debug_printf(const char* format, ...) {
|
|||
va_end(args);
|
||||
if (result <= 0) return result;
|
||||
const size_t size = (size_t)result < sizeof(message) ? (size_t)result : sizeof(message) - 1;
|
||||
#if !SWITCH2_PROBE_HUB
|
||||
const uint32_t interrupts = save_and_disable_interrupts();
|
||||
#elif defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
const uint32_t trace_parent = native_hub_trace_phase(NATIVE_HUB_TRACE_PHASE_LOG_COPY);
|
||||
#endif
|
||||
const bool queued = LOG_CAPACITY - (log_written - log_read) >= size;
|
||||
if (queued) {
|
||||
const size_t offset = log_written % LOG_CAPACITY;
|
||||
const size_t first = size < LOG_CAPACITY - offset ? size : LOG_CAPACITY - offset;
|
||||
memcpy(log_bytes + offset, message, first);
|
||||
memcpy(log_bytes, message + first, size - first);
|
||||
log_written += (uint32_t)size;
|
||||
log_dropped += (uint32_t)result - (uint32_t)size;
|
||||
} else {
|
||||
log_dropped += (uint32_t)result;
|
||||
}
|
||||
#if !SWITCH2_PROBE_HUB
|
||||
restore_interrupts(interrupts);
|
||||
#elif defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
native_hub_trace_phase(trace_parent);
|
||||
#endif
|
||||
return queued ? result : -1;
|
||||
return queue_log_bytes(message, size, (uint32_t)result) ? result : -1;
|
||||
}
|
||||
|
||||
#if SWITCH2_PROBE_HUB
|
||||
static void buffered_stdio_out(const char* bytes, int length) {
|
||||
if (length <= 0) return;
|
||||
// Stdio can also be called by panic/IRQ paths. Never recursively assert,
|
||||
// race the foreground ring, or wait for UART from those contexts.
|
||||
if (get_core_num() != 0 || __get_current_exception() != 0) {
|
||||
count_dropped_log_bytes((uint32_t)length);
|
||||
return;
|
||||
}
|
||||
queue_log_bytes(bytes, (size_t)length, (uint32_t)length);
|
||||
}
|
||||
|
||||
static void buffer_uart_stdio(void) {
|
||||
// Install before Bluetooth starts. Bluepad32's vfprintf/printf output
|
||||
// must share the ordered queue rather than blocking RADIO_POLL on UART.
|
||||
// Keep the SDK's UART initialization, stdin and availability callbacks.
|
||||
stdio_uart.out_chars = buffered_stdio_out;
|
||||
stdio_uart.out_flush = NULL; // Foreground drain_log owns physical output.
|
||||
}
|
||||
#endif
|
||||
|
||||
static void drain_log(void) {
|
||||
while (uart_is_writable(uart0)) {
|
||||
#if !SWITCH2_PROBE_HUB
|
||||
|
|
@ -305,12 +343,11 @@ void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id,
|
|||
if (instance < PROBE_CONTROLLER_COUNT && report_type == HID_REPORT_TYPE_OUTPUT &&
|
||||
controllers[instance].protocol.initialized &&
|
||||
probe_transport_mounted(instance) && !probe_transport_suspended(instance)) {
|
||||
probe_rumble_frame frame;
|
||||
NativeHapticsActuatorFrame frame;
|
||||
if (probe_protocol_decode_rumble(report_id, buffer, length, &frame)) {
|
||||
// Count-zero HOLD leaves both motor state and watchdog untouched.
|
||||
// Valid gameplay traffic must not fill the slow UART log ring.
|
||||
if (frame.count && probe_controller_input_submit_rumble(
|
||||
instance, frame.magnitude, frame.count))
|
||||
if (frame.sample_count && probe_controller_input_submit_rumble(instance, &frame))
|
||||
controllers[instance].gameplay_rumble_seen = true;
|
||||
return;
|
||||
}
|
||||
|
|
@ -836,6 +873,9 @@ int main(void) {
|
|||
system_clock_initialize();
|
||||
#endif
|
||||
stdio_init_all();
|
||||
#if SWITCH2_PROBE_HUB
|
||||
buffer_uart_stdio();
|
||||
#endif
|
||||
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
probe_debug_printf("\n[PROBE] " PROBE_JOYCON_PRODUCT " Bluetooth-to-USB controller/native mouse bridge\n");
|
||||
#elif SWITCH2_PROBE_NEUTRAL_INPUT
|
||||
|
|
@ -994,7 +1034,7 @@ int main(void) {
|
|||
" log_dropped_bytes=%" PRIu32 "\n",
|
||||
now, probe_transport_mounted(0), bulk_packets, hid_packets,
|
||||
identity_requests, version_requests, setup_completions,
|
||||
input_reports, command_drops, log_dropped);
|
||||
input_reports, command_drops, __atomic_load_n(&log_dropped, __ATOMIC_RELAXED));
|
||||
#ifdef SWITCH2_PROBE_USB_INIT
|
||||
for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance) {
|
||||
probe_usb_controller* controller = &controllers[instance];
|
||||
|
|
|
|||
|
|
@ -417,27 +417,27 @@ function(switch2_usb_probe_configure target)
|
|||
endif()
|
||||
if(SWITCH2_PROBE_NEUTRAL_INPUT)
|
||||
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
pico_set_program_version(${target} "0.102-neutral-hub-${SWITCH2_PROBE_PAIR_COUNT}pair-trace")
|
||||
pico_set_program_version(${target} "0.108-neutral-hub-${SWITCH2_PROBE_PAIR_COUNT}pair-trace")
|
||||
else()
|
||||
pico_set_program_version(${target} "0.102-neutral-hub-${SWITCH2_PROBE_PAIR_COUNT}pair")
|
||||
pico_set_program_version(${target} "0.108-neutral-hub-${SWITCH2_PROBE_PAIR_COUNT}pair")
|
||||
endif()
|
||||
elseif(SWITCH2_PROBE_HUB AND SWITCH2_BRIDGE_FULL_INPUT)
|
||||
if(probe_controller_count GREATER 2)
|
||||
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
pico_set_program_version(${target} "0.102-live-two-pair-trace")
|
||||
pico_set_program_version(${target} "0.108-live-two-pair-trace")
|
||||
else()
|
||||
pico_set_program_version(${target} "0.102-live-two-pair")
|
||||
pico_set_program_version(${target} "0.108-live-two-pair")
|
||||
endif()
|
||||
elseif(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
pico_set_program_version(${target} "0.102-native-digital-stick-trace")
|
||||
pico_set_program_version(${target} "0.108-native-digital-stick-trace")
|
||||
else()
|
||||
pico_set_program_version(${target} "0.102-native-digital-stick")
|
||||
pico_set_program_version(${target} "0.108-native-digital-stick")
|
||||
endif()
|
||||
elseif(SWITCH2_PROBE_HUB)
|
||||
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
pico_set_program_version(${target} "0.102-native-hub-profiles-trace")
|
||||
pico_set_program_version(${target} "0.108-native-hub-profiles-trace")
|
||||
else()
|
||||
pico_set_program_version(${target} "0.102-native-hub-profiles")
|
||||
pico_set_program_version(${target} "0.108-native-hub-profiles")
|
||||
endif()
|
||||
elseif(SWITCH2_PROBE_JOIN_CHORD_GATE)
|
||||
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
|
|
|
|||
|
|
@ -409,7 +409,7 @@ void probe_protocol_gate_native_report(const probe_protocol_state* state,
|
|||
}
|
||||
|
||||
bool probe_protocol_decode_rumble(uint8_t report_id, const uint8_t* data,
|
||||
size_t length, probe_rumble_frame* output) {
|
||||
size_t length, NativeHapticsActuatorFrame* output) {
|
||||
if (!data || !output) return false;
|
||||
if (report_id == 0) {
|
||||
if (length < 17 || length > 64 || data[0] != 0x01) return false;
|
||||
|
|
@ -424,16 +424,14 @@ bool probe_protocol_decode_rumble(uint8_t report_id, const uint8_t* data,
|
|||
// packs frequency/amplitude/frequency/amplitude as four 10-bit LE fields.
|
||||
// Header: format 01 [7:6], sample count [5:4], sequence [3:0].
|
||||
// Sequence is informational; unused sample bytes and USB padding may be stale.
|
||||
probe_rumble_frame decoded = {.count = (data[0] >> 4) & 3u};
|
||||
for (unsigned i = 0; i < decoded.count; ++i) {
|
||||
NativeHapticsActuatorFrame decoded = {.sample_count = (data[0] >> 4) & 3u};
|
||||
for (unsigned i = 0; i < decoded.sample_count; ++i) {
|
||||
const uint8_t* sample = data + 1u + 5u * i;
|
||||
const unsigned first = (sample[1] >> 2) | ((sample[2] & 0x0fu) << 6);
|
||||
const unsigned second = (sample[3] >> 6) | ((unsigned)sample[4] << 2);
|
||||
const unsigned amplitude = first > second ? first : second;
|
||||
// ERM compatibility, not HD waveform reproduction: ignore frequencies
|
||||
// and round max(amplitudes) across the full 10-bit range to 0..255.
|
||||
// SDL's conservative outbound clamp is not an inbound validity limit.
|
||||
decoded.magnitude[i] = (uint8_t)((amplitude * 255u + 511u) / 1023u);
|
||||
NativeHapticsSample* out = &decoded.samples[i];
|
||||
out->low_frequency_code = sample[0] | ((uint16_t)(sample[1] & 3u) << 8);
|
||||
out->low_amplitude = (sample[1] >> 2) | ((uint16_t)(sample[2] & 15u) << 6);
|
||||
out->high_frequency_code = (sample[2] >> 4) | ((uint16_t)(sample[3] & 63u) << 4);
|
||||
out->high_amplitude = (sample[3] >> 6) | ((uint16_t)sample[4] << 2);
|
||||
}
|
||||
*output = decoded;
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "model.h"
|
||||
#include "core/native_haptics.h"
|
||||
|
||||
#define PROBE_COMMAND_MAX_SIZE 263u
|
||||
#define PROBE_REPLY_MAX_SIZE 96u
|
||||
|
|
@ -50,17 +51,14 @@ typedef struct {
|
|||
uint32_t report_counter;
|
||||
} probe_protocol_state;
|
||||
|
||||
typedef struct {
|
||||
uint8_t count;
|
||||
uint8_t magnitude[3];
|
||||
} probe_rumble_frame;
|
||||
|
||||
// Stateless Output 01 compatibility rumble: report_id 0 includes the leading
|
||||
// Stateless Output 01 waveform decoding: report_id 0 includes the leading
|
||||
// wire ID (17..64 bytes); report_id 1 omits it (16..63 bytes).
|
||||
// Count zero means HOLD/no update, not stop or watchdog refresh. A nonempty
|
||||
// zero-amplitude sample is stop. Malformed input leaves output unchanged.
|
||||
// Both 10-bit frequency and amplitude fields survive decoding; each physical
|
||||
// output backend chooses its own supported frequency range and rendering.
|
||||
bool probe_protocol_decode_rumble(uint8_t report_id, const uint8_t* data,
|
||||
size_t length, probe_rumble_frame* output);
|
||||
size_t length, NativeHapticsActuatorFrame* output);
|
||||
|
||||
void probe_protocol_reset(probe_protocol_state* state, bool is_left);
|
||||
// Blob: own address[6], count[1], zero-padded host addresses[42][6], AES key[16].
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue