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

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

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

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

154 lines
6.1 KiB
C

#include <assert.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "hardware_stub.h"
#include "tusb_config.h"
static unsigned test_core, test_exception;
#define get_core_num() test_core
#define __get_current_exception() test_exception
#define hard_assert(value) assert(value)
#define HID_REPORT_TYPE_INPUT 1
static uint64_t get_absolute_time(void) { return 0; }
static uint32_t to_ms_since_boot(uint64_t value) { return (uint32_t)value; }
static void sleep_us(uint32_t microseconds) { (void)microseconds; }
static void panic(const char* text) { (void)text; abort(); }
#define main unused_probe_firmware_main
#include "../tools/switch2_usb_probe/main.c"
#undef main
uint32_t native_test_interrupt_mask;
static bool pending_completion, inject_completion;
static unsigned completions, missed_tokens;
static uint32_t phase;
static char serial_bytes[2 * LOG_CAPACITY];
static size_t serial_size;
static bool uart_backpressure;
void native_test_service_interrupt(void) {
if (pending_completion && !native_test_interrupt_mask) {
pending_completion = false;
++completions;
}
}
uint32_t native_hub_trace_phase(uint32_t next) {
const uint32_t previous = phase;
phase = next;
if (inject_completion && next == NATIVE_HUB_TRACE_PHASE_LOG_COPY) {
// A completed child packet must be serviced before the next owner's
// token can be selected. This is the same blocking condition checked
// by native_hub_select_device; the real logger runs between both.
pending_completion = true;
native_test_service_interrupt();
if (pending_completion) ++missed_tokens;
}
return previous;
}
bool uart_is_writable(void* uart) { (void)uart; return !uart_backpressure && serial_size < sizeof(serial_bytes); }
void uart_putc_raw(void* uart, char value) { (void)uart; serial_bytes[serial_size++] = value; }
static void blocking_stdio_out(const char* bytes, int length) {
assert(!uart_backpressure && "stdio waited for UART instead of queuing");
for (int i = 0; i < length; ++i) uart_putc_raw(uart0,bytes[i]);
}
static void blocking_stdio_flush(void) {
assert(!uart_backpressure && "stdio flush waited for UART");
}
stdio_driver_t stdio_uart = {
.out_chars = blocking_stdio_out,
.out_flush = blocking_stdio_flush,
};
int main(int argc, char** argv) {
if (argc == 2) {
if (strcmp(argv[1], "core") == 0) test_core = 1;
else if (strcmp(argv[1], "irq") == 0) test_exception = 16;
else return 2;
probe_debug_printf("unsafe caller\n");
return 0;
}
// Exercise a wrapped, full-length diagnostic message, not just empty logs.
char message[480];
memset(message, 'x', sizeof(message) - 1);
message[sizeof(message) - 1] = 0;
log_read = log_written = LOG_CAPACITY - 13;
inject_completion = true;
assert(probe_debug_printf("%s", message) == (int)strlen(message));
assert(missed_tokens == 0 && "logging blocked a USB completion and the next device's token");
assert(completions == 1 && !pending_completion);
drain_log();
assert(serial_size == strlen(message));
assert(memcmp(serial_bytes, message, serial_size) == 0);
// Full-ring overflow drops a complete message without corrupting queued data.
inject_completion = false;
serial_size = 0;
log_read = 0; log_written = LOG_CAPACITY;
memset(log_bytes, 'q', sizeof(log_bytes));
const uint32_t drops_before = log_dropped;
assert(probe_debug_printf("discard me") < 0);
drain_log();
assert(serial_size == LOG_CAPACITY);
for (size_t i = 0; i < serial_size; ++i) assert(serial_bytes[i] == 'q');
assert(log_dropped == drops_before + 10);
serial_size = 0;
assert(probe_debug_printf("discard me") == 10);
drain_log();
assert(serial_size == 10 && memcmp(serial_bytes, "discard me", 10) == 0);
// Respect a caller's existing critical section; logging cannot enable IRQs.
serial_size = 0;
inject_completion = true;
native_test_interrupt_mask = 1;
const unsigned completed_before = completions;
probe_debug_printf("caller owns mask");
assert(native_test_interrupt_mask == 1 && completions == completed_before);
restore_interrupts(0);
assert(completions == completed_before + 1);
drain_log();
assert(serial_size == strlen("caller owns mask"));
assert(memcmp(serial_bytes, "caller owns mask", serial_size) == 0);
// Pico libc/Bluepad32 output must share the existing ordered queue. UART
// backpressure cannot stall radio polling or prevent USB IRQ completion.
buffer_uart_stdio();
serial_size = 0;
log_read = log_written = LOG_CAPACITY - 5;
missed_tokens = 0;
uart_backpressure = true;
stdio_uart.out_chars("radio first\n",12);
assert(probe_debug_printf("probe\n") == 6);
stdio_uart.out_chars("radio last\n",11);
if (stdio_uart.out_flush) stdio_uart.out_flush();
drain_log();
assert(serial_size == 0 && !pending_completion && missed_tokens == 0);
uart_backpressure = false;
drain_log();
const char expected[] = "radio first\nprobe\nradio last\n";
assert(serial_size == sizeof(expected)-1);
assert(memcmp(serial_bytes,expected,serial_size) == 0);
// Reject a whole stdout chunk when full; queued bytes remain intact.
inject_completion = false;
serial_size = 0; log_read = 0; log_written = LOG_CAPACITY;
memset(log_bytes,'s',sizeof(log_bytes));
uint32_t dropped = log_dropped;
stdio_uart.out_chars("overflow",8);
drain_log();
assert(serial_size == LOG_CAPACITY && log_dropped == dropped+8);
for (size_t i = 0; i < serial_size; ++i) assert(serial_bytes[i] == 's');
// Panic/IRQ stdio must not recurse into assertions or corrupt this
// foreground-only ring. Count those discarded bytes instead.
serial_size = 0; dropped = log_dropped;
test_core = 1; stdio_uart.out_chars("core",4); test_core = 0;
test_exception = 16; stdio_uart.out_chars("irq",3); test_exception = 0;
drain_log();
assert(serial_size == 0 && log_dropped == dropped+7);
puts("native logging preserved USB progress, message order and caller IRQ state");
return 0;
}