Add stock-USB native Joy-Con R/L hub bridge

This commit is contained in:
Joey Yakimowich-Payne 2026-09-12 15:28:58 -06:00
commit 1748910316
41 changed files with 7101 additions and 909 deletions

1062
tools/native_joycon_hub_check.py Executable file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,45 @@
cmake_minimum_required(VERSION 3.13)
set(PICO_BOARD pico2_w CACHE STRING "Target board")
include(${CMAKE_CURRENT_LIST_DIR}/../../pico_sdk_import.cmake)
project(pico_usb_address_probe C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
pico_sdk_init()
if(NOT PICO_PLATFORM STREQUAL "rp2350-arm-s")
message(FATAL_ERROR "The native PHY timing probe requires the RP2350 ARM platform")
endif()
option(PROBE_USB_GPIO_MODE "Enable native-pad SIO observation with SIO outputs disabled" ON)
set(TINYUSB_DIR ${PICO_SDK_PATH}/lib/tinyusb/src)
set(RP_USB_DIR ${TINYUSB_DIR}/portable/raspberrypi/rp2040)
add_executable(native_usb_address_probe
main.c
router.c
usb_probe.c
${RP_USB_DIR}/dcd_rp2040.c
${RP_USB_DIR}/rp2040_usb.c
${TINYUSB_DIR}/common/tusb_fifo.c
)
target_include_directories(native_usb_address_probe PRIVATE
${CMAKE_CURRENT_LIST_DIR}
${TINYUSB_DIR}
${RP_USB_DIR}
)
target_compile_definitions(native_usb_address_probe PRIVATE
CFG_TUSB_CONFIG_FILE="${CMAKE_CURRENT_LIST_DIR}/tusb_config.h"
CFG_TUSB_MCU=OPT_MCU_RP2040
RP2040_USB_DEVICE_MODE=1
PROBE_USB_GPIO_MODE=$<BOOL:${PROBE_USB_GPIO_MODE}>
)
target_compile_options(native_usb_address_probe PRIVATE -O3 -Wall -Wextra)
target_link_libraries(native_usb_address_probe PRIVATE
pico_stdlib pico_multicore hardware_structs hardware_irq hardware_resets
hardware_sync hardware_timer hardware_clocks hardware_vreg hardware_watchdog
)
pico_set_binary_type(native_usb_address_probe no_flash)
pico_enable_stdio_usb(native_usb_address_probe 0)
pico_enable_stdio_uart(native_usb_address_probe 1)
pico_set_program_name(native_usb_address_probe "RAM-only native USB address capability probe")
pico_set_program_version(native_usb_address_probe "0.5-native-sio-hub-probe")
pico_add_extra_outputs(native_usb_address_probe)

View file

@ -0,0 +1,288 @@
#!/usr/bin/env python3
"""Exercise the RAM-only native USB address probe; never flash firmware."""
from __future__ import annotations
import argparse
import json
import os
import struct
import subprocess
import time
from collections.abc import Iterable
from pathlib import Path
from typing import Any, cast
import usb.core
import usb.util
VID = 0x1209
HUB_PID = 0x0001
CHILD_PIDS = (0x0002, 0x0003)
FIELDS: tuple[str, ...] = (
"magic",
"version",
"system_hz",
"routing_enabled",
"hub_address",
"child1_address",
"child2_address",
"default_slot",
"hub_setups",
"child1_setups",
"child2_setups",
"bad_setup_owner",
"observer_ready",
"sops",
"sync_ok",
"valid_tokens",
"valid_setups",
"crc_errors",
"late_samples",
"retargets",
"hub_tokens",
"child1_tokens",
"child2_tokens",
"cycles_per_bit",
"raw0",
"raw1",
"raw2",
"raw_count",
"raw_eop",
"raw_late",
"live_phy",
"correlated_setups",
)
def probe_devices(product_id: int) -> list[usb.core.Device]:
found = usb.core.find(find_all=True, idVendor=VID, idProduct=product_id)
return list(cast(Iterable[usb.core.Device], found)) if found is not None else []
def device_location(device: usb.core.Device) -> tuple[int, int]:
bus, address = device.bus, device.address
if not isinstance(bus, int) or not isinstance(address, int):
raise TypeError("USB device has no usable bus/address")
return bus, address
def grant_access(device: usb.core.Device) -> None:
bus, address = device_location(device)
node = f"/dev/bus/usb/{bus:03d}/{address:03d}"
subprocess.run(
["sudo", "-n", "setfacl", "-m", f"u:{os.getuid()}:rw", node],
check=True,
capture_output=True,
text=True,
timeout=3,
)
def stats(device: usb.core.Device) -> dict[str, int]:
packet = bytes(device.ctrl_transfer(0xC0, 0x5A, 0, 0, 128, timeout=400))
if len(packet) != 128:
raise RuntimeError(f"statistics length {len(packet)} != 128")
result = {
key: int(value)
for key, value in zip(FIELDS, struct.unpack("<32I", packet), strict=True)
}
if result["magic"] != 0x42554850 or result["version"] != 3:
raise RuntimeError("device did not return the address-probe signature")
return result
def descriptor(device: usb.core.Device, expected_pid: int) -> dict[str, int]:
data = bytes(device.ctrl_transfer(0x80, 6, 0x0100, 0, 18, timeout=400))
if len(data) != 18 or data[0:2] != b"\x12\x01":
raise RuntimeError("invalid device descriptor")
vendor, product = struct.unpack_from("<HH", data, 8)
if (vendor, product) != (VID, expected_pid):
raise RuntimeError(
f"address {device.address} returned wrong identity {vendor:04x}:{product:04x}"
)
bus, address = device_location(device)
return {"bus": bus, "address": address, "vid": int(vendor), "pid": int(product)}
def delta(after: dict[str, int], before: dict[str, int], field: str) -> int:
return (after[field] - before[field]) & 0xFFFFFFFF
def measure_phase(device: usb.core.Device, phase: int) -> dict[str, Any]:
device.ctrl_transfer(0x40, 0x5D, phase, 0, b"", timeout=400)
before = stats(device)
after = before
for _ in range(24):
after = stats(device)
time.sleep(0.002)
hardware = delta(after, before, "hub_setups")
confirmed = delta(after, before, "correlated_setups")
hits = delta(after, before, "hub_tokens")
# Qualify observed headers against real, CRC-accepted hardware SETUP IRQs.
# Full software CRC capture can overrun after routing work and is diagnostic.
credible = hardware >= 20 and hardware * 0.8 <= confirmed <= hardware * 1.5
return {
"phase": phase,
"hardware_setups": hardware,
"correlated_setups": confirmed,
"crc_verified_setups": delta(after, before, "valid_setups"),
"matched_hub_tokens": hits,
"credible": credible,
"crc_errors": delta(after, before, "crc_errors"),
"late_samples": delta(after, before, "late_samples"),
"before": before,
"after": after,
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--wait-seconds", type=float, default=30)
parser.add_argument(
"--arm",
action="store_true",
help="attempt address routing only after credible passive capture",
)
args = parser.parse_args()
if args.output.exists():
parser.error("output already exists; choose a new capture filename")
if not 0 < args.wait_seconds <= 120:
parser.error("wait-seconds must be in (0,120]")
result: dict[str, Any] = {
"success": False,
"armed": False,
"phases": [],
"return_request_sent": False,
}
hub: usb.core.Device | None = None
print("[HOSTPROBE] waiting for RAM hub probe", flush=True)
try:
deadline = time.monotonic() + args.wait_seconds
while time.monotonic() < deadline:
devices = probe_devices(HUB_PID)
if len(devices) > 1:
raise RuntimeError(
"multiple matching hub probes; refusing ambiguous target"
)
if devices:
hub = devices[0]
break
time.sleep(0.05)
if hub is None:
raise RuntimeError("RAM hub did not enumerate before timeout")
grant_access(hub)
result["hub"] = descriptor(hub, HUB_PID)
result["initial_stats"] = stats(hub)
print(
f"[HOSTPROBE] hub address={hub.address}, observer={result['initial_stats']['observer_ready']}",
flush=True,
)
if result["initial_stats"]["observer_ready"] != 1:
result["failure"] = (
"cycle-timed observer did not initialize; no address routing attempted"
)
return 2
phases = result["initial_stats"]["cycles_per_bit"]
if not 1 <= phases <= 64:
raise RuntimeError(f"invalid cycles-per-bit {phases}")
for phase in range(phases):
measured = measure_phase(hub, phase)
result["phases"].append(measured)
print(
f"[HOSTPROBE] phase={phase} confirmed={measured['correlated_setups']}/{measured['hardware_setups']} hits={measured['matched_hub_tokens']} late={measured['late_samples']} raw={measured['after']['raw0']:08x}/{measured['after']['raw1']:08x} n={measured['after']['raw_count']} eop={measured['after']['raw_eop']}",
flush=True,
)
candidates = [item for item in result["phases"] if item["credible"]]
if not candidates:
result["failure"] = (
"no sampling phase reliably observed native USB SETUP tokens; retargeting was not armed"
)
return 2
best = min(
candidates,
key=lambda item: (
abs(item["correlated_setups"] - item["hardware_setups"]),
item["crc_errors"],
item["late_samples"],
),
)
hub.ctrl_transfer(0x40, 0x5D, best["phase"], 0, b"", timeout=400)
result["selected_phase"] = best["phase"]
if not args.arm:
result["passive_capture_verified"] = True
return 0
hub.ctrl_transfer(0x40, 0x5B, 1, 0, b"", timeout=400)
result["armed"] = True
print(
"[HOSTPROBE] address retargeting armed; waiting for real downstream enumeration",
flush=True,
)
children = {}
deadline = time.monotonic() + 5
# Let the kernel finish downstream enumeration without injecting root
# control transfers into the probe's still-shared physical EP0 context.
# Five seconds is below the ACK-fed watchdog's eight-second deadline.
while time.monotonic() < deadline and len(children) != 2:
for port, pid in enumerate(CHILD_PIDS, 1):
found = probe_devices(pid)
if (
len(found) == 1
and found[0].bus == hub.bus
and hub.port_numbers is not None
and found[0].port_numbers == (*hub.port_numbers, port)
):
children[pid] = found[0]
time.sleep(0.05)
if len(children) != 2:
result["failure"] = (
"hub did not enumerate both separately addressed children"
)
result["children_seen"] = [hex(pid) for pid in children]
return 2
ordered = [hub, children[CHILD_PIDS[0]], children[CHILD_PIDS[1]]]
if len({device.address for device in ordered}) != 3:
raise RuntimeError("host did not assign three distinct USB addresses")
for child in ordered[1:]:
grant_access(child)
result["devices"] = []
for _ in range(20):
for device, pid in zip(ordered, (HUB_PID, *CHILD_PIDS), strict=True):
identity = descriptor(device, pid)
result["devices"].append(identity)
result["latest_stats"] = stats(hub)
result["success"] = True
print(
"[HOSTPROBE] PASS: three actual addresses, each repeatedly returned its own descriptor",
flush=True,
)
return 0
except (
usb.core.USBError,
RuntimeError,
TypeError,
subprocess.SubprocessError,
OSError,
) as error:
result["failure"] = str(error)
print(f"[HOSTPROBE] failure: {error}", flush=True)
return 2
finally:
if hub is not None:
try:
hub.ctrl_transfer(0x40, 0x5C, 0, 0, b"", timeout=400)
result["return_request_sent"] = True
except usb.core.USBError as error:
result["return_request_error"] = str(error)
usb.util.dispose_resources(hub)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(result, indent=2) + "\n")
print(
f"[HOSTPROBE] saved {args.output}; RAM probe has an 8-second watchdog fallback",
flush=True,
)
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,62 @@
#include <inttypes.h>
#include <stdio.h>
#include "hardware/clocks.h"
#include "hardware/structs/sio.h"
#include "hardware/structs/usb.h"
#include "hardware/vreg.h"
#include "hardware/watchdog.h"
#include "pico/multicore.h"
#include "pico/stdlib.h"
#include "router.h"
#include "usb_probe.h"
#if !PICO_NO_FLASH
#error "The native USB address probe must run from RAM, never replace flash firmware"
#endif
#if !PICO_RP2350
#error "The native USB address probe requires RP2350"
#endif
int main(void) {
// A failed USB experiment must not strand the board in this RAM program.
// Explicit host GET_STATS requests are the only keepalive after startup.
watchdog_enable(8000, false);
vreg_set_voltage(VREG_VOLTAGE_1_30);
sleep_ms(10);
set_sys_clock_khz(240000, true);
stdio_init_all();
printf("\n[HUBPROBE] RAM-only built-in USB address experiment, clock=%" PRIu32 " Hz\n",
clock_get_hz(clk_sys));
printf("[HUBPROBE] No GPIO data wiring, Bluetooth, or flash writes; watchdog returns to stored firmware\n");
probe_router_init(clock_get_hz(clk_sys));
multicore_launch_core1(probe_router_core1);
const uint32_t start = time_us_32();
probe_router_stats observer = {0};
do {
probe_router_snapshot(&observer);
if (observer.ready) break;
sleep_us(10);
} while ((uint32_t)(time_us_32() - start) < 100000);
printf("[HUBPROBE] Observer ready=%" PRIu32 " cycles/bit=%" PRIu32 "\n",
observer.ready, observer.cycles_per_bit);
probe_hub_init();
#if PROBE_USB_GPIO_MODE
// With TO_PHY retained and SIO outputs disabled, hardware measurements
// showed both live SIO inputs and successful native-controller enumeration.
// Keep only the internal full-speed attachment resistor; do not drive data.
sio_hw->gpio_hi_oe_clr = SIO_GPIO_HI_IN_USB_DP_BITS | SIO_GPIO_HI_IN_USB_DM_BITS;
hw_set_bits(&usb_hw->phy_direct, USB_USBPHY_DIRECT_DP_PULLUP_EN_BITS);
hw_set_bits(&usb_hw->phy_direct_override,
USB_USBPHY_DIRECT_OVERRIDE_DP_PULLUP_EN_OVERRIDE_EN_BITS);
hw_set_bits(&usb_hw->muxing, USB_USB_MUXING_USBPHY_AS_GPIO_BITS);
printf("[HUBPROBE] USBPHY_AS_GPIO plus TO_PHY; SIO outputs disabled, internal pull-up retained\n");
#endif
printf("[HUBPROBE] RX=SIO GPIO_HI_IN[25:24], mux=%08" PRIx32 "; SIO=%08" PRIx32
" PHY=%08" PRIx32 "\n", usb_hw->muxing, sio_hw->gpio_hi_in, usb_hw->phy_direct);
while (true) {
probe_hub_task();
sleep_us(100);
}
}

View file

@ -0,0 +1,738 @@
#include "router.h"
#include <string.h>
#include "pico.h"
#include "hardware/structs/sio.h"
#include "hardware/structs/usb.h"
#include "hardware/sync.h"
#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB
extern bool native_hub_select_device(uint8_t address, uint8_t owner, uint32_t cutoff);
#endif
#if !PICO_RP2350 || defined(__riscv)
#error "The native PHY observer requires an RP2350 Arm core"
#endif
// This sampler does not drive USB data. Main enables the native-pad input
// mux and attachment pull-up; the native SIE remains the USB transmitter.
// This isolated probe owns SIO MTIME, usable by a Secure Arm core. FULLSPEED
// 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
#define LINE_SE1 3u
#define PID_OUT 0xe1u
#define PID_IN 0x69u
#define PID_SETUP 0x2du
#define NO_READER 2u
#define SETUP_SEQUENCE_MASK 0x3fffffffu
#define SETUP_SLOT_SHIFT 30u
#define SETUP_INVALID (3u << SETUP_SLOT_SHIFT)
#define RAW_BITS 40u
_Static_assert(SIO_GPIO_HI_IN_USB_DP_BITS == (1u << 24), "SIO USB DP layout");
_Static_assert(SIO_GPIO_HI_IN_USB_DM_BITS == (1u << 25), "SIO USB DM layout");
_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];
#endif
} routing_table;
// Complete physical NRZI SYNC+PID signatures. The PID's two distinguishing
// symbols index this table, but the entire signature must match.
static uint32_t token_words[16];
typedef struct {
uint32_t words[3];
uint32_t count;
uint32_t retargets;
bool eop;
bool late;
bool sop;
bool resync;
} raw_packet;
static routing_table tables[2];
static probe_router_stats counters;
static uint32_t published_generation;
static uint32_t reader_index;
static uint32_t enabled;
static uint32_t phase_cycles;
static uint32_t setup_publication;
static uint32_t fatal_fault;
static bool valid_clock;
static uint8_t address_decoder[2][256];
static bool address_decoder_ready;
static __force_inline uint32_t atomic_read(const uint32_t* value) {
return __atomic_load_n(value, __ATOMIC_RELAXED);
}
static __force_inline void atomic_write(uint32_t* value, uint32_t next) {
__atomic_store_n(value, next, __ATOMIC_RELAXED);
}
// These counters have one writer (Core 1); only loads/stores, not exclusive
// read-modify-write loops, are needed. They are updated outside sample windows.
static __force_inline void count_one(uint32_t* counter) {
atomic_write(counter, atomic_read(counter) + 1u);
}
static __force_inline void invalidate_setup(void) {
const uint32_t previous = atomic_read(&setup_publication);
__atomic_store_n(&setup_publication,
(previous & SETUP_SEQUENCE_MASK) | SETUP_INVALID,
__ATOMIC_RELEASE);
}
static __force_inline void publish_setup(uint8_t slot) {
const uint32_t sequence = (atomic_read(&setup_publication) + 1u) & SETUP_SEQUENCE_MASK;
const uint32_t owner = slot < PROBE_ROUTER_SLOTS ? slot : 3u;
__atomic_store_n(&setup_publication, sequence | (owner << SETUP_SLOT_SHIFT),
__ATOMIC_RELEASE);
}
static void build_address_decoder(void) {
if (address_decoder_ready) return;
// C0 initializes once. Eight observed D+ symbols cover all seven address
// bits plus at most one stuffed bit. All three token PIDs end in K.
for (unsigned kind = 0; kind < 2; ++kind) {
for (unsigned wire = 0; wire < 256; ++wire) {
unsigned previous = 0, ones = kind ? 3u : 0u, bits = 0, address = 0;
bool valid = true;
for (unsigned n = 0; n < 8 && bits < 7; ++n) {
const unsigned line = (wire >> n) & 1u;
const unsigned bit = line == previous;
previous = line;
if (ones == 6u) {
if (bit != 0u) valid = false;
ones = 0;
continue;
}
address |= bit << bits++;
ones = bit ? ones + 1u : 0u;
}
address_decoder[kind][wire] = valid && bits == 7 ?
(uint8_t)address : PROBE_ROUTER_UNASSIGNED;
}
}
address_decoder_ready = true;
}
static void build_table(routing_table* table,
const uint8_t addresses[PROBE_ROUTER_SLOTS], uint8_t default_slot) {
build_address_decoder();
memset(table->owner, PROBE_ROUTER_UNASSIGNED, sizeof(table->owner));
if (default_slot < PROBE_ROUTER_SLOTS)
table->owner[0] = default_slot;
for (uint8_t slot = 0; slot < PROBE_ROUTER_SLOTS; ++slot) {
const uint8_t address = addresses[slot];
if (address == 0 || address >= 128) continue;
bool unique = true;
for (uint8_t other = 0; other < PROBE_ROUTER_SLOTS; ++other) {
if (other != slot && addresses[other] == address)
unique = false;
}
if (unique)
table->owner[address] = slot;
}
#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.
for (unsigned kind = 0; kind < 2; ++kind) {
for (unsigned prefix = 0; prefix < 16; ++prefix) {
uint8_t candidate = PROBE_ROUTER_UNASSIGNED;
for (unsigned suffix = 0; suffix < 16; ++suffix) {
uint8_t address = address_decoder[kind][prefix | (suffix << 4)];
if (address >= 128 || table->owner[address] >= PROBE_ROUTER_SLOTS) continue;
if (candidate != PROBE_ROUTER_UNASSIGNED && candidate != address) {
candidate = PROBE_ROUTER_UNASSIGNED;
break;
}
candidate = address;
}
table->early_address[kind][prefix] = candidate;
}
}
#endif
}
void probe_router_init(uint32_t system_clock_hz) {
// Explicit SRAM data: Core1 must never fetch flash during durable saves.
token_words[6] = 0xaa66a666u;
token_words[10] = 0x95a6a666u;
token_words[5] = 0x9a56a666u;
const uint8_t addresses[PROBE_ROUTER_SLOTS] = {0u, PROBE_ROUTER_UNASSIGNED,
PROBE_ROUTER_UNASSIGNED};
memset(&counters, 0, sizeof(counters));
published_generation = 0u;
reader_index = NO_READER;
enabled = 0u;
phase_cycles = 0u;
setup_publication = SETUP_INVALID;
fatal_fault = 0u;
valid_clock = system_clock_hz == FS_CLOCK_HZ;
counters.cycles_per_bit = system_clock_hz / 12000000u;
build_table(&tables[0], addresses, 0u);
}
void probe_router_publish(const uint8_t addresses[PROBE_ROUTER_SLOTS], uint8_t default_slot) {
const uint32_t generation = atomic_read(&published_generation);
const uint32_t next_index = (generation + 1u) & 1u;
// A pointer swap alone is NOT safe double buffering: a second publication
// could overwrite the table still in use by a packet. The reader's hazard
// index protects that table until decoding finishes. Core 1 never waits.
// Bound the writer's wait as well: an unexpectedly stopped observer must
// not trap Core 0 or prevent the watchdog/reboot control path from running.
uint32_t remaining = 1000000u;
while (__atomic_load_n(&reader_index, __ATOMIC_SEQ_CST) == next_index) {
if (--remaining == 0u) {
atomic_write(&enabled, 0u);
atomic_write(&fatal_fault, 1u);
atomic_write(&counters.ready, 0u);
return;
}
}
build_table(&tables[next_index], addresses, default_slot);
__atomic_store_n(&published_generation, generation + 1u, __ATOMIC_SEQ_CST);
}
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_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)
return false;
__atomic_store_n(&phase_cycles, cycles, __ATOMIC_RELEASE);
return true;
}
void probe_router_snapshot(probe_router_stats* out) {
#define SNAPSHOT(member) out->member = atomic_read(&counters.member)
SNAPSHOT(ready);
SNAPSHOT(sops);
SNAPSHOT(sync_ok);
SNAPSHOT(valid_tokens);
SNAPSHOT(valid_setups);
SNAPSHOT(crc_errors);
SNAPSHOT(late_samples);
SNAPSHOT(retargets);
for (uint32_t slot = 0u; slot < PROBE_ROUTER_SLOTS; ++slot)
out->address_hits[slot] = atomic_read(&counters.address_hits[slot]);
SNAPSHOT(cycles_per_bit);
SNAPSHOT(last_pid);
SNAPSHOT(last_address);
for (uint32_t i = 0; i < 3; ++i)
out->last_raw[i] = atomic_read(&counters.last_raw[i]);
SNAPSHOT(last_raw_count);
SNAPSHOT(last_raw_eop);
SNAPSHOT(last_raw_late);
#undef SNAPSHOT
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;
out->last_setup_slot = slot < PROBE_ROUTER_SLOTS ? slot : PROBE_ROUTER_UNASSIGNED;
}
uint8_t probe_router_setup_slot(uint32_t* sequence) {
const uint32_t setup = __atomic_load_n(&setup_publication, __ATOMIC_ACQUIRE);
*sequence = setup & SETUP_SEQUENCE_MASK;
const uint32_t slot = setup >> SETUP_SLOT_SHIFT;
return slot < PROBE_ROUTER_SLOTS ? (uint8_t)slot : PROBE_ROUTER_UNASSIGNED;
}
static __force_inline uint32_t cycles_now(void) {
return sio_hw->mtime;
}
static __force_inline int32_t cycles_after(uint32_t now, uint32_t deadline) {
return (int32_t)(now - deadline);
}
static __force_inline uint32_t receive_line(void) {
// Native-mode measurements returned zero here while PHY_DIRECT saw traffic.
// Main can select USBPHY_AS_GPIO to test the separate native-pad SIO path.
return (sio_hw->gpio_hi_in >> 24) & 3u;
}
static __force_inline bool sample_line(uint32_t* deadline, uint32_t* line) {
uint32_t now;
do {
now = cycles_now();
} while (cycles_after(now, *deadline) < 0);
// Reuse the wait-loop timestamp instead of a second timer access per bit.
// A full-bit overrun is definitely a missed sample. Edge-poll timing still
// needs calibration: the host correlates sampled headers with actual
// hardware-accepted SETUP requests before enabling address writes.
if (cycles_after(now, *deadline) >= (int32_t)FS_BIT_CYCLES)
return false;
*line = receive_line();
*deadline += FS_BIT_CYCLES;
// Keep one rolling deadline. GCC's unrolled affine expansion otherwise
// retains SOP/phase and spills/rebuilds per-bit deadlines in the hot path.
__asm volatile ("" : "+r"(*deadline));
return true;
}
static __force_inline void route_header(const routing_table* table, uint32_t address,
bool setup, 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)
return;
invalidate_setup();
if (address >= 128u || table->owner[address] >= PROBE_ROUTER_SLOTS)
return;
#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB
if (atomic_read(&enabled) != 0u) {
if (!native_hub_select_device((uint8_t)address, table->owner[address], cutoff))
return;
if (initial_address != address) ++packet->retargets;
}
#else
if (initial_address != address && atomic_read(&enabled) != 0u) {
if (cycles_after(cycles_now(), cutoff) >= 0) {
packet->late = true;
return;
}
__dmb();
usb_hw->dev_addr_ctrl = address;
++packet->retargets;
}
#endif
// Candidate observations qualify calibration only. Runtime ownership
// comes from the hardware address frozen by SETUP_REC. A missed software
// candidate must not reject a correctly addressed, hardware-accepted SETUP.
if (setup)
publish_setup(table->owner[address]);
}
// The timing-critical path samples the complete address before selecting the
// native SIE. Hardware SETUP acceptance qualifies the candidate; opportunistic
// full-token CRC decoding below is diagnostic, not an ownership authority.
static bool observe_idle_j(void);
// Prepare before waiting for EOP: an ACK can be followed immediately by a poll.
#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;
#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB
const uint8_t* early_decoder = NULL;
#endif
// Complete capture preparation before looking for the edge. The first
// hardware traces showed that preparing this state after SOP lost bit 1.
// Later zero-valued accumulators must remain constants until first use;
// 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
if (draining) {
const uint32_t stop = cycles_now() + FS_CLOCK_HZ / 10000u;
bool saw_se0 = false;
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 (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;
saw_se0 = false;
}
}
}
#endif
uint32_t line = receive_line();
if (line != LINE_J) {
result.resync = true;
return result;
}
// A falling D+ leaves full-speed idle. Inspect the complete captured pair
// before accepting K; defer normalization until after the polling loop.
// Eight straight polls amortize loop bookkeeping and reduce edge jitter.
uint32_t pins;
#define POLL_IDLE() do { \
pins = sio_hw->gpio_hi_in; \
if ((pins & SIO_GPIO_HI_IN_USB_DP_BITS) == 0u) goto edge; \
} while (0)
#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB
for (;;) {
POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE();
POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE();
POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE();
POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE();
POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE();
POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE();
POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE();
POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE();
// Keep the prepared frame while idle; leave only for a table update/fault.
if ((atomic_read(&published_generation) & 1u) != atomic_read(&reader_index) ||
atomic_read(&fatal_fault) != 0u) return result;
}
#else
for (unsigned poll = 0; poll < 512u; ++poll) {
POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE();
POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE();
}
#endif
#undef POLL_IDLE
return result;
edge:
line = (pins >> 24) & 3u;
if (line != LINE_K) {
result.resync = true;
return result;
}
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
// spacing; every stored line symbol is still physically observed.
uint32_t deadline = sop_time + phase;
result.sop = true;
// 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)
#define ROUTE_EARLY(bit, base) do { \
if ((base) + (bit) == 19u && decoder != NULL) { \
uint8_t candidate = early_decoder[address_wire]; \
if (candidate < 128u) \
route_header(table, candidate, word0 == 0x9a56a666u, initial_address, \
deadline + 11u * FS_BIT_CYCLES, &result); \
} \
} while (0)
#else
#define SET_EARLY_DECODER(kind) ((void)0)
#define DISCARD_NON_TOKEN() ((void)0)
#define ROUTE_EARLY(bit, base) ((void)0)
#endif
#define ROUTE_BITS(word, bit, base) do { \
if ((base) + (bit) == 11u) { \
const uint32_t index = (word0 >> 20) & 15u; \
expected_word = token_words[index]; \
decoder = address_decoder[index == 6u]; \
SET_EARLY_DECODER(index == 6u); \
} \
if ((base) + (bit) == 15u && word0 != expected_word) { \
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) { \
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; } \
} \
} while (0)
#define CAPTURE(word, bit, base) do { \
if (!sample_line(&deadline, &line)) { \
result.count = (base) + (bit); goto late; \
} \
if (line == LINE_SE0) { result.count = (base) + (bit); goto eop; } \
(word) |= line << (2u * (bit)); \
ROUTE_BITS(word, bit, base); \
} while (0)
#define CAPTURE_16(word, base) \
CAPTURE(word, 0u, base); CAPTURE(word, 1u, base); \
CAPTURE(word, 2u, base); CAPTURE(word, 3u, base); \
CAPTURE(word, 4u, base); CAPTURE(word, 5u, base); \
CAPTURE(word, 6u, base); CAPTURE(word, 7u, base); \
CAPTURE(word, 8u, base); CAPTURE(word, 9u, base); \
CAPTURE(word, 10u, base); CAPTURE(word, 11u, base); \
CAPTURE(word, 12u, base); CAPTURE(word, 13u, base); \
CAPTURE(word, 14u, base); CAPTURE(word, 15u, base)
CAPTURE(word0, 1u, 0u); CAPTURE(word0, 2u, 0u);
CAPTURE(word0, 3u, 0u); CAPTURE(word0, 4u, 0u);
CAPTURE(word0, 5u, 0u); CAPTURE(word0, 6u, 0u);
CAPTURE(word0, 7u, 0u); CAPTURE(word0, 8u, 0u);
CAPTURE(word0, 9u, 0u); CAPTURE(word0, 10u, 0u);
CAPTURE(word0, 11u, 0u); CAPTURE(word0, 12u, 0u);
CAPTURE(word0, 13u, 0u); CAPTURE(word0, 14u, 0u);
CAPTURE(word0, 15u, 0u);
CAPTURE_16(word1, 16u);
CAPTURE(word2, 0u, 32u); CAPTURE(word2, 1u, 32u);
CAPTURE(word2, 2u, 32u); CAPTURE(word2, 3u, 32u);
CAPTURE(word2, 4u, 32u); CAPTURE(word2, 5u, 32u);
CAPTURE(word2, 6u, 32u); CAPTURE(word2, 7u, 32u);
#undef CAPTURE_16
#undef CAPTURE
#undef ROUTE_EARLY
#undef SET_EARLY_DECODER
#undef DISCARD_NON_TOKEN
result.count = RAW_BITS;
goto done;
eop:
// Full-speed EOP is two bit times of SE0 followed by one J bit. A reset,
// truncated packet, or SE1 is not a token. Check all three samples.
if (!sample_line(&deadline, &line))
goto late;
if (line != LINE_SE0)
goto done;
if (!sample_line(&deadline, &line))
goto late;
result.eop = line == LINE_J;
goto done;
late:
result.late = true;
done:
result.words[0] = word0;
result.words[1] = word1;
result.words[2] = word2;
return result;
}
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) {
if (receive_line() != LINE_J)
return false;
}
return cycles_after(cycles_now(), start) >= (int32_t)(8u * FS_BIT_CYCLES);
}
#if !defined(SWITCH2_PROBE_HUB) || !SWITCH2_PROBE_HUB
static __force_inline uint32_t raw_line(const raw_packet* packet, uint32_t bit) {
return (packet->words[bit >> 4] >> ((bit & 15u) * 2u)) & 3u;
}
static void __not_in_flash_func(decode_packet)(const raw_packet* packet, const routing_table* table) {
// With LSB-first two-bit line samples, K J K J K J K K is 0xa666.
if (packet->count < 8u || (packet->words[0] & 0xffffu) != 0xa666u) {
return;
}
count_one(&counters.sync_ok);
uint32_t previous = LINE_K;
uint32_t ones = 1u; // Final decoded SYNC bit is one.
uint32_t decoded = 0u;
uint32_t value = 0u;
uint32_t pid = 0u;
bool token = false;
for (uint32_t wire_bit = 8u; wire_bit < packet->count; ++wire_bit) {
const uint32_t line = raw_line(packet, wire_bit);
if (line != LINE_J && line != LINE_K) {
return;
}
const uint32_t bit = line == previous;
previous = line;
if (ones == 6u) {
if (bit != 0u) {
return;
}
ones = 0u;
continue;
}
ones = bit != 0u ? ones + 1u : 0u;
if (decoded < 8u) {
pid |= bit << decoded;
++decoded;
if (decoded == 8u) {
if ((((pid >> 4) ^ pid) & 15u) != 15u) {
return;
}
atomic_write(&counters.last_pid, pid);
token = pid == PID_IN || pid == PID_OUT || pid == PID_SETUP;
if (!token)
return; // Do not parse device data, SOFs, or handshakes.
}
} else {
if (decoded == 24u) {
return;
}
value |= bit << (decoded - 8u);
++decoded;
}
}
if (!token || decoded != 24u || ones == 6u || !packet->eop || packet->late) {
return;
}
uint32_t crc = 0x1fu;
for (uint32_t bit = 0u; bit < 11u; ++bit) {
const uint32_t feedback = (crc ^ (value >> bit)) & 1u;
crc >>= 1;
if (feedback != 0u)
crc ^= 0x14u; // Reflected x^5 + x^2 + 1.
}
if (((crc ^ 0x1fu) & 0x1fu) != (value >> 11)) {
count_one(&counters.crc_errors);
return;
}
const uint32_t address = value & 0x7fu;
const uint8_t owner = table->owner[address];
atomic_write(&counters.last_address, address);
count_one(&counters.valid_tokens);
if (owner < PROBE_ROUTER_SLOTS)
count_one(&counters.address_hits[owner]);
if (pid == PID_SETUP) {
count_one(&counters.valid_setups);
}
}
#endif
static const routing_table* __not_in_flash_func(acquire_table)(uint32_t* generation) {
for (;;) {
const uint32_t selected = __atomic_load_n(&published_generation, __ATOMIC_SEQ_CST);
__atomic_store_n(&reader_index, selected & 1u, __ATOMIC_SEQ_CST);
if (__atomic_load_n(&published_generation, __ATOMIC_SEQ_CST) == selected) {
*generation = selected;
return &tables[selected & 1u];
}
}
}
static void __not_in_flash_func(observer_failed)(void) {
atomic_write(&enabled, 0u);
atomic_write(&counters.ready, 0u);
invalidate_setup();
__atomic_store_n(&reader_index, NO_READER, __ATOMIC_SEQ_CST);
for (;;)
__wfe();
}
void __not_in_flash_func(probe_router_core1)(void) {
(void)save_and_disable_interrupts();
if (!valid_clock || atomic_read(&fatal_fault) != 0u)
observer_failed();
sio_hw->mtime_ctrl = 0u;
sio_hw->mtimecmp = UINT32_MAX;
sio_hw->mtimecmph = UINT32_MAX;
sio_hw->mtime = 0u;
sio_hw->mtimeh = 0u;
sio_hw->mtime_ctrl = SIO_MTIME_CTRL_EN_BITS | SIO_MTIME_CTRL_FULLSPEED_BITS;
__dsb();
__isb();
bool timer_running = false;
uint32_t previous_timer = cycles_now();
for (uint32_t attempt = 0u; attempt < 256u; ++attempt) {
const uint32_t now = cycles_now();
const int32_t elapsed = cycles_after(now, previous_timer);
if (elapsed > 0 && elapsed < 1024) {
timer_running = true;
break;
}
previous_timer = now;
}
if (!timer_running)
observer_failed();
atomic_write(&counters.ready, 1u);
uint32_t generation;
const routing_table* table = acquire_table(&generation);
// Resynchronize at qualified EOP or a long idle J, never an arbitrary
// data transition. An idle gap must not cost the next control's SETUP.
bool draining = true;
#if !defined(SWITCH2_PROBE_HUB) || !SWITCH2_PROBE_HUB
bool saw_se0 = false;
uint32_t se0_since = 0u;
#endif
for (;;) {
if (atomic_read(&fatal_fault) != 0u)
observer_failed();
const uint32_t phase = atomic_read(&phase_cycles);
for (;;) {
if (atomic_read(&published_generation) != generation)
table = acquire_table(&generation);
#if !defined(SWITCH2_PROBE_HUB) || !SWITCH2_PROBE_HUB
if (draining) {
const uint32_t line = receive_line();
const uint32_t now = cycles_now();
if (line == LINE_SE0) {
if (!saw_se0) se0_since = now;
saw_se0 = true;
} else {
// Reject momentary pad skew as EOP. A real SE0 persists
// across at least one complete bit before returning to J.
if (line == LINE_J && saw_se0 &&
cycles_after(now, se0_since) >= (int32_t)FS_BIT_CYCLES)
draining = false;
else if (line == LINE_J && observe_idle_j())
draining = false;
saw_se0 = false;
}
if (draining) continue;
break;
}
#endif
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
// 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.
// The independent enabled flag still forbids every dry-run USB write.
#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB
const raw_packet packet = capture_packet(phase, table, draining);
#else
const raw_packet packet = capture_packet(phase, table);
#endif
if (!packet.sop) {
if (packet.resync) {
draining = true;
#if !defined(SWITCH2_PROBE_HUB) || !SWITCH2_PROBE_HUB
saw_se0 = false;
#endif
}
continue;
}
#if !defined(SWITCH2_PROBE_HUB) || !SWITCH2_PROBE_HUB
for (uint32_t i = 0; i < 3; ++i)
atomic_write(&counters.last_raw[i], packet.words[i]);
atomic_write(&counters.last_raw_count, packet.count);
atomic_write(&counters.last_raw_eop, packet.eop);
atomic_write(&counters.last_raw_late, packet.late);
count_one(&counters.sops);
atomic_write(&counters.retargets, atomic_read(&counters.retargets) + packet.retargets);
if (packet.late)
count_one(&counters.late_samples);
decode_packet(&packet, table);
// Decoding can outlast the minimum interpacket gap. Qualify another
// EOP or a long idle J before accepting a new SOP; an arbitrary J->K
// inside a packet is not a start. Missing traffic is preferable to
// manufacturing a SETUP owner from a payload transition.
draining = true;
saw_se0 = false;
#else
// A response may start during the return/preparation path even if the
// preceding EOP was sampled. Requalify from the prepared capture frame.
draining = true;
#endif
}
}

View file

@ -0,0 +1,48 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#define PROBE_ROUTER_SLOTS 3u
#define PROBE_ROUTER_UNASSIGNED 0xffu
typedef struct {
uint32_t ready;
uint32_t sops;
uint32_t sync_ok;
uint32_t valid_tokens;
uint32_t valid_setups;
uint32_t crc_errors;
uint32_t late_samples;
uint32_t retargets;
uint32_t address_hits[PROBE_ROUTER_SLOTS];
uint32_t cycles_per_bit;
uint32_t last_pid;
uint32_t last_address;
uint32_t last_setup_sequence;
uint32_t last_setup_slot;
uint32_t last_raw[3];
uint32_t last_raw_count;
uint32_t last_raw_eop;
uint32_t last_raw_late;
} probe_router_stats;
// Core 0 initializes before launching Core 1. The native SIE drives USB;
// Core 1 observes the existing socket and selects known device addresses.
void probe_router_init(uint32_t system_clock_hz);
void probe_router_core1(void);
// Core 0 publishes assigned addresses (0xff means unassigned). Address zero
// belongs only to default_slot, or nobody when default_slot is 0xff.
void probe_router_publish(const uint8_t addresses[PROBE_ROUTER_SLOTS], uint8_t default_slot);
void probe_router_enable(bool enabled);
// Sampling phase within one USB bit, for passive timing calibration only.
// Reject out-of-range values and changes after address retargeting is enabled.
bool probe_router_set_phase(uint32_t cycles);
// Diagnostic snapshots. Counters are individually atomic, not a transaction.
void probe_router_snapshot(probe_router_stats* out);
// Last sampled SETUP-header candidate, used for calibration correlation only.
// Runtime control ownership comes from the SIE address at its SETUP interrupt.
uint8_t probe_router_setup_slot(uint32_t* sequence);

View file

@ -0,0 +1,13 @@
#pragma once
#define CFG_TUSB_RHPORT0_MODE (OPT_MODE_DEVICE | OPT_MODE_FULL_SPEED)
#ifndef CFG_TUSB_OS
#define CFG_TUSB_OS OPT_OS_NONE
#endif
#define CFG_TUSB_DEBUG 0
#define CFG_TUD_ENDPOINT0_SIZE 64
#define CFG_TUD_HID 0
#define CFG_TUD_CDC 0
#define CFG_TUD_MSC 0
#define CFG_TUD_MIDI 0
#define CFG_TUD_VENDOR 0

View file

@ -0,0 +1,839 @@
// RAM-only native-SIE address-retargeting experiment. These are vendor test
// devices, not controllers. No usbd/tud global-device state is linked here.
#include "usb_probe.h"
#include "router.h"
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include "device/dcd.h"
#include "hardware/clocks.h"
#include "hardware/structs/usb.h"
#include "hardware/sync.h"
#include "hardware/uart.h"
#include "hardware/watchdog.h"
#include "pico/stdlib.h"
#define RHPORT 0u
#define EP0_OUT 0x00u
#define EP0_IN 0x80u
#define HUB_EP 0x81u
#define EP0_SIZE 64u
#define EVENT_CAPACITY 32u
#define PORT_COUNT 2u
#define PORT_CONNECTION 0x0001u
#define PORT_ENABLE 0x0002u
#define PORT_SUSPEND 0x0004u
#define PORT_RESET 0x0010u
#define PORT_POWER 0x0100u
#define C_CONNECTION 0x0001u
#define C_ENABLE 0x0002u
#define C_SUSPEND 0x0004u
#define C_RESET 0x0010u
enum {
FEATURE_PORT_ENABLE = 1,
FEATURE_PORT_SUSPEND = 2,
FEATURE_PORT_RESET = 4,
FEATURE_PORT_POWER = 8,
FEATURE_C_CONNECTION = 16,
FEATURE_C_ENABLE = 17,
FEATURE_C_SUSPEND = 18,
FEATURE_C_OVERCURRENT = 19,
FEATURE_C_RESET = 20,
};
typedef enum {
CTRL_IDLE,
CTRL_DATA_IN,
CTRL_STATUS_IN,
CTRL_STATUS_OUT,
CTRL_STALLED,
} control_stage;
typedef enum {
ACTION_NONE,
ACTION_ADDRESS,
ACTION_CONFIGURATION,
ACTION_INTERFACE,
ACTION_HALT,
ACTION_CLEAR_HALT,
ACTION_PORT_SET,
ACTION_PORT_CLEAR,
ACTION_KEEPALIVE,
ACTION_ARM,
ACTION_REBOOT,
} control_action;
typedef struct {
uint16_t status;
uint16_t change;
uint32_t reset_deadline;
uint32_t resume_deadline;
bool resetting;
bool resuming;
} hub_port;
typedef struct {
dcd_event_t event;
uint32_t generation;
uint32_t endpoint_epoch;
uint8_t setup_slot;
} queued_event;
typedef struct {
tusb_control_request_t request;
uint32_t generation;
uint16_t length;
uint16_t sent;
uint16_t packet_length;
uint8_t owner;
control_stage stage;
control_action action;
bool need_zlp;
} control_transfer;
static uint8_t addresses[PROBE_ROUTER_SLOTS];
static uint8_t configurations[PROBE_ROUTER_SLOTS];
static uint8_t default_slot;
static bool routing_enabled;
static hub_port ports[PORT_COUNT];
static control_transfer control;
static uint8_t control_data[128] TU_ATTR_ALIGNED(4);
// Even a malformed nonempty status OUT cannot make the DCD copy into NULL.
static uint8_t control_out[EP0_SIZE] TU_ATTR_ALIGNED(4);
static uint32_t setup_count[PROBE_ROUTER_SLOTS];
static uint32_t bad_setup_owner;
static uint32_t correlated_setups;
static uint32_t system_clock_hz;
static bool interrupt_open;
static bool interrupt_pending;
static bool interrupt_halted;
static uint8_t interrupt_bitmap;
static uint32_t endpoint_epoch;
static bool reboot_pending;
static bool failed;
// Only the DCD IRQ produces; only Core 0's task consumes. All task-side DCD
// operations run with USB IRQ disabled. The IRQ never rearms a transfer: this
// SDK resets its transfer state *after* invoking dcd_event_handler().
static queued_event events[EVENT_CAPACITY];
static volatile uint32_t event_head;
static volatile uint32_t event_tail;
static volatile uint32_t event_generation;
static volatile bool event_overflow;
static uint32_t observed_setup_sequence;
static const uint8_t hub_configuration[] = {
9, 2, 25, 0, 1, 1, 0, 0x80, 50,
9, 4, 0, 0, 1, 9, 0, 0, 0,
7, 5, HUB_EP, 3, 1, 0, 12,
};
static const uint8_t child_configuration[] = {
9, 2, 18, 0, 1, 1, 0, 0x80, 0,
9, 4, 0, 0, 0, 0xff, 0, 0, 0,
};
static const uint8_t hub_descriptor[] = {
// Individual logical port power, no overcurrent sensing, 10ms power-good.
// Both embedded vendor children are non-removable; USB 1.1 full-speed hub.
9, 0x29, PORT_COUNT, 0x11, 0, 5, 100, 0x06, 0xff,
};
static const tusb_desc_endpoint_t hub_endpoint = {
.bLength = 7,
.bDescriptorType = TUSB_DESC_ENDPOINT,
.bEndpointAddress = HUB_EP,
.bmAttributes = { .xfer = TUSB_XFER_INTERRUPT },
.wMaxPacketSize = 1,
.bInterval = 12,
};
static void put16(uint8_t* out, uint16_t value) {
out[0] = (uint8_t)value;
out[1] = (uint8_t)(value >> 8);
}
static void put32(uint8_t* out, uint32_t value) {
put16(out, (uint16_t)value);
put16(out + 2, (uint16_t)(value >> 16));
}
static void publish_addresses(void) {
probe_router_publish(addresses, default_slot);
}
static void stall_control(void) {
control.stage = CTRL_STALLED;
control.action = ACTION_NONE;
dcd_edpt_stall(RHPORT, EP0_OUT);
dcd_edpt_stall(RHPORT, EP0_IN);
}
static bool queue_control(uint8_t endpoint, uint8_t* data, uint16_t length) {
if (dcd_edpt_xfer(RHPORT, endpoint, data, length)) return true;
stall_control();
return false;
}
static void status_in(control_action action) {
control.action = action;
control.stage = CTRL_STATUS_IN;
queue_control(EP0_IN, control_out, 0);
}
static void next_control_packet(void) {
uint16_t remaining = (uint16_t)(control.length - control.sent);
control.packet_length = remaining > EP0_SIZE ? EP0_SIZE : remaining;
if (remaining == 0) control.need_zlp = false;
control.stage = CTRL_DATA_IN;
queue_control(EP0_IN, control_data + control.sent, control.packet_length);
}
static void reply_data(uint16_t length) {
control.length = length < control.request.wLength ? length : control.request.wLength;
control.sent = 0;
control.need_zlp = length < control.request.wLength && (length % EP0_SIZE) == 0;
if (control.request.wLength == 0) {
control.stage = CTRL_STATUS_OUT;
queue_control(EP0_OUT, control_out, 0);
} else {
next_control_packet();
}
}
static void reply_copy(const uint8_t* data, uint16_t length) {
memcpy(control_data, data, length);
reply_data(length);
}
static void reply_word(uint16_t value, uint16_t length) {
put16(control_data, value);
reply_data(length);
}
static uint8_t changed_ports(void) {
uint8_t bitmap = 0;
for (unsigned i = 0; i < PORT_COUNT; ++i) {
if (ports[i].change) bitmap |= (uint8_t)(1u << (i + 1));
}
return bitmap;
}
static void arm_interrupt(void) {
if (!interrupt_open || interrupt_pending || interrupt_halted) return;
interrupt_bitmap = changed_ports();
if (!interrupt_bitmap) return; // NAK until a hub/port change exists.
interrupt_pending = dcd_edpt_xfer(RHPORT, HUB_EP, &interrupt_bitmap, 1);
if (!interrupt_pending) failed = true;
}
static void close_interrupt(void) {
++endpoint_epoch;
dcd_edpt_close_all(RHPORT);
interrupt_open = false;
interrupt_pending = false;
interrupt_halted = false;
}
static void open_interrupt(void) {
close_interrupt();
interrupt_open = dcd_edpt_open(RHPORT, &hub_endpoint);
if (!interrupt_open) failed = true;
}
static void forget_child(unsigned port) {
uint8_t slot = (uint8_t)(port + 1);
addresses[slot] = PROBE_ROUTER_UNASSIGNED;
configurations[slot] = 0;
if (default_slot == slot) default_slot = PROBE_ROUTER_UNASSIGNED;
}
static void reset_bus_state(void) {
probe_router_enable(false);
routing_enabled = false;
correlated_setups = 0;
addresses[0] = 0;
addresses[1] = PROBE_ROUTER_UNASSIGNED;
addresses[2] = PROBE_ROUTER_UNASSIGNED;
default_slot = 0;
memset(configurations, 0, sizeof(configurations));
memset(ports, 0, sizeof(ports));
memset(&control, 0, sizeof(control));
interrupt_open = false;
interrupt_pending = false;
interrupt_halted = false;
++endpoint_epoch;
publish_addresses();
usb_hw->dev_addr_ctrl = 0;
}
static void fill_stats(void) {
probe_router_stats router;
probe_router_snapshot(&router);
const uint32_t words[32] = {
0x42554850u, 3u, system_clock_hz, routing_enabled,
addresses[0], addresses[1], addresses[2], default_slot,
setup_count[0], setup_count[1], setup_count[2], bad_setup_owner,
router.ready, router.sops, router.sync_ok, router.valid_tokens,
router.valid_setups, router.crc_errors, router.late_samples,
router.retargets, router.address_hits[0], router.address_hits[1],
router.address_hits[2], router.cycles_per_bit,
router.last_raw[0], router.last_raw[1], router.last_raw[2],
router.last_raw_count, router.last_raw_eop, router.last_raw_late,
usb_hw->phy_direct, correlated_setups,
};
for (unsigned i = 0; i < 32; ++i) put32(control_data + 4 * i, words[i]);
}
static bool get_descriptor(void) {
const tusb_control_request_t* request = &control.request;
uint8_t type = (uint8_t)(request->wValue >> 8);
uint8_t index = (uint8_t)request->wValue;
if (type == TUSB_DESC_DEVICE && index == 0 && request->wIndex == 0) {
uint8_t descriptor[] = {
18, 1, 0x10, 0x01, 0, 0, 0, EP0_SIZE,
0x09, 0x12, 0, 0, 0x00, 0x01, 1, 2, 3, 1,
};
descriptor[4] = control.owner == 0 ? 9 : 0;
descriptor[10] = (uint8_t)(control.owner + 1);
reply_copy(descriptor, sizeof(descriptor));
return true;
}
if (type == TUSB_DESC_CONFIGURATION && index == 0 && request->wIndex == 0) {
if (control.owner == 0) reply_copy(hub_configuration, sizeof(hub_configuration));
else reply_copy(child_configuration, sizeof(child_configuration));
return true;
}
if (type != TUSB_DESC_STRING) return false;
if (index == 0 && request->wIndex == 0) {
static const uint8_t languages[] = {4, 3, 0x09, 0x04};
reply_copy(languages, sizeof(languages));
return true;
}
if (request->wIndex != 0x0409) return false;
const char* text;
if (index == 1) text = "Native USB capability probe";
else if (index == 2) {
static const char* const products[] = {
"RP2350 native hub probe",
"RP2350 vendor probe child 1",
"RP2350 vendor probe child 2",
};
text = products[control.owner];
} else if (index == 3) {
static const char* const serials[] = {"PHUB-ROOT", "PHUB-CHILD1", "PHUB-CHILD2"};
text = serials[control.owner];
} else return false;
uint16_t length = (uint16_t)strlen(text);
control_data[0] = (uint8_t)(2 + 2 * length);
control_data[1] = TUSB_DESC_STRING;
for (uint16_t i = 0; i < length; ++i) put16(control_data + 2 + 2 * i, (uint8_t)text[i]);
reply_data((uint16_t)(2 + 2 * length));
return true;
}
static bool endpoint_exists(uint16_t index) {
return index == EP0_OUT || index == EP0_IN ||
(index == HUB_EP && control.owner == 0 && configurations[0] == 1);
}
static bool standard_request(void) {
const tusb_control_request_t* request = &control.request;
uint8_t slot = control.owner;
switch (request->bRequest) {
case TUSB_REQ_GET_DESCRIPTOR:
return request->bmRequestType == 0x80 && get_descriptor();
case TUSB_REQ_SET_ADDRESS:
if (request->bmRequestType != 0 || request->wValue > 127 ||
request->wIndex || request->wLength || configurations[slot]) return false;
if (request->wValue == 0 && default_slot != PROBE_ROUTER_UNASSIGNED && default_slot != slot)
return false;
for (unsigned i = 0; i < PROBE_ROUTER_SLOTS; ++i) {
if (i != slot && addresses[i] == request->wValue) return false;
}
status_in(ACTION_ADDRESS);
return true;
case TUSB_REQ_GET_CONFIGURATION:
if (request->bmRequestType != 0x80 || request->wValue || request->wIndex || request->wLength != 1)
return false;
reply_word(configurations[slot], 1);
return true;
case TUSB_REQ_SET_CONFIGURATION:
if (request->bmRequestType != 0 || request->wValue > 1 || request->wIndex || request->wLength ||
addresses[slot] == 0 || addresses[slot] == PROBE_ROUTER_UNASSIGNED) return false;
status_in(ACTION_CONFIGURATION);
return true;
case TUSB_REQ_GET_STATUS:
if (request->wValue || request->wLength != 2) return false;
if (request->bmRequestType == 0x80 && request->wIndex == 0) {
reply_word(0, 2); // Bus powered; no remote wakeup capability.
return true;
}
if (request->bmRequestType == 0x81 && request->wIndex == 0 && configurations[slot]) {
reply_word(0, 2);
return true;
}
if (request->bmRequestType == 0x82 && endpoint_exists(request->wIndex)) {
reply_word(request->wIndex == HUB_EP && interrupt_halted ? 1 : 0, 2);
return true;
}
return false;
case TUSB_REQ_CLEAR_FEATURE:
case TUSB_REQ_SET_FEATURE:
if (request->bmRequestType != 0x02 || request->wValue != 0 || request->wIndex != HUB_EP ||
request->wLength || slot != 0 || !configurations[0]) return false;
status_in(request->bRequest == TUSB_REQ_SET_FEATURE ? ACTION_HALT : ACTION_CLEAR_HALT);
return true;
case TUSB_REQ_GET_INTERFACE:
if (request->bmRequestType != 0x81 || request->wValue || request->wIndex ||
request->wLength != 1 || !configurations[slot]) return false;
reply_word(0, 1);
return true;
case TUSB_REQ_SET_INTERFACE:
if (request->bmRequestType != 0x01 || request->wValue || request->wIndex ||
request->wLength || !configurations[slot]) return false;
status_in(ACTION_INTERFACE);
return true;
default:
return false;
}
}
static bool hub_request(void) {
const tusb_control_request_t* request = &control.request;
if (control.owner != 0) return false;
if (request->bmRequestType == 0xa0 && request->bRequest == TUSB_REQ_GET_DESCRIPTOR &&
request->wValue == 0x2900 && request->wIndex == 0) {
reply_copy(hub_descriptor, sizeof(hub_descriptor));
return true;
}
if (!configurations[0]) return false;
if (request->bmRequestType == 0xa0 && request->bRequest == TUSB_REQ_GET_STATUS &&
request->wValue == 0 && request->wIndex == 0 && request->wLength == 4) {
put32(control_data, 0); // No local-power loss or overcurrent changes.
reply_data(4);
return true;
}
if (request->bmRequestType == 0x20 && request->bRequest == TUSB_REQ_CLEAR_FEATURE &&
request->wValue <= 1 && request->wIndex == 0 && request->wLength == 0) {
status_in(ACTION_NONE); // Both supported hub change flags are already clear.
return true;
}
if (request->wIndex < 1 || request->wIndex > PORT_COUNT) return false;
hub_port* port = &ports[request->wIndex - 1];
if (request->bmRequestType == 0xa3 && request->bRequest == TUSB_REQ_GET_STATUS &&
request->wValue == 0 && request->wLength == 4) {
put16(control_data, port->status);
put16(control_data + 2, port->change);
reply_data(4);
return true;
}
if (request->bmRequestType != 0x23 || request->wLength) return false;
if (request->bRequest == TUSB_REQ_SET_FEATURE) {
switch (request->wValue) {
case FEATURE_PORT_POWER:
break;
case FEATURE_PORT_RESET:
if (!routing_enabled || (port->status & (PORT_CONNECTION | PORT_POWER)) !=
(PORT_CONNECTION | PORT_POWER)) return false;
// The one physical SIE cannot own two simultaneous default addresses.
if (default_slot != PROBE_ROUTER_UNASSIGNED && default_slot != request->wIndex) return false;
break;
case FEATURE_PORT_SUSPEND:
if ((port->status & (PORT_CONNECTION | PORT_ENABLE | PORT_POWER | PORT_RESET)) !=
(PORT_CONNECTION | PORT_ENABLE | PORT_POWER)) return false;
break;
default:
return false;
}
status_in(ACTION_PORT_SET);
return true;
}
if (request->bRequest == TUSB_REQ_CLEAR_FEATURE) {
switch (request->wValue) {
case FEATURE_PORT_POWER:
case FEATURE_PORT_ENABLE:
case FEATURE_PORT_SUSPEND:
case FEATURE_C_CONNECTION:
case FEATURE_C_ENABLE:
case FEATURE_C_SUSPEND:
case FEATURE_C_OVERCURRENT:
case FEATURE_C_RESET:
status_in(ACTION_PORT_CLEAR);
return true;
default:
return false;
}
}
return false;
}
static bool vendor_request(void) {
const tusb_control_request_t* request = &control.request;
if (request->wIndex) return false;
if (request->bmRequestType == 0xc0 && request->bRequest == 0x5a &&
request->wValue == 0 && request->wLength == 128) {
fill_stats();
control.action = ACTION_KEEPALIVE;
reply_data(128);
return true;
}
if (request->bmRequestType != 0x40 || request->wLength) return false;
if (request->bRequest == 0x5b && request->wValue == 1 && control.owner == 0) {
probe_router_stats router;
probe_router_snapshot(&router);
if (!router.ready || correlated_setups < 20) return false;
status_in(ACTION_ARM);
return true;
}
if (request->bRequest == 0x5c && request->wValue == 0) {
status_in(ACTION_REBOOT);
return true;
}
if (request->bRequest == 0x5d && !routing_enabled &&
probe_router_set_phase(request->wValue)) {
status_in(ACTION_NONE);
return true;
}
return false;
}
static void handle_setup(const queued_event* queued) {
// A newer SETUP has already aborted this one's hardware transfer.
if (queued->generation != event_generation) return;
memset(&control, 0, sizeof(control));
control.request = queued->event.setup_received;
control.generation = queued->generation;
control.owner = routing_enabled ? queued->setup_slot : 0;
if (routing_enabled && (control.owner >= PROBE_ROUTER_SLOTS ||
(addresses[control.owner] == PROBE_ROUTER_UNASSIGNED && default_slot != control.owner))) {
++bad_setup_owner;
stall_control();
return;
}
++setup_count[control.owner];
uint8_t type = control.request.bmRequestType & 0x60;
bool supported = type == 0 ? standard_request() :
type == 0x20 ? hub_request() : type == 0x40 ? vendor_request() : false;
if (!supported) stall_control();
}
static void apply_port_feature(bool set) {
unsigned index = control.request.wIndex - 1;
hub_port* port = &ports[index];
uint16_t feature = control.request.wValue;
uint32_t now = time_us_32();
if (set) {
if (feature == FEATURE_PORT_POWER) {
port->status |= PORT_POWER;
if (routing_enabled && !(port->status & PORT_CONNECTION)) {
port->status |= PORT_CONNECTION;
port->change |= C_CONNECTION;
}
} else if (feature == FEATURE_PORT_RESET) {
forget_child(index);
port->status = (uint16_t)((port->status | PORT_RESET) & ~(PORT_ENABLE | PORT_SUSPEND));
port->resetting = true;
port->resuming = false;
port->reset_deadline = now + 10000u;
publish_addresses();
} else if (feature == FEATURE_PORT_SUSPEND) {
port->status |= PORT_SUSPEND;
port->resuming = false;
}
return;
}
if (feature >= FEATURE_C_CONNECTION && feature <= FEATURE_C_RESET) {
port->change &= (uint16_t)~(1u << (feature - FEATURE_C_CONNECTION));
} else if (feature == FEATURE_PORT_ENABLE) {
port->status &= (uint16_t)~(PORT_ENABLE | PORT_SUSPEND | PORT_RESET);
port->resetting = false;
port->resuming = false;
forget_child(index);
publish_addresses();
} else if (feature == FEATURE_PORT_POWER) {
if (port->status & PORT_CONNECTION) port->change |= C_CONNECTION;
port->status = 0;
port->resetting = false;
port->resuming = false;
forget_child(index);
publish_addresses();
} else if (feature == FEATURE_PORT_SUSPEND && (port->status & PORT_SUSPEND)) {
port->resuming = true;
port->resume_deadline = now + 20000u;
}
}
static void complete_control(void) {
uint8_t owner = control.owner;
control_action action = control.action;
control.stage = CTRL_IDLE;
control.action = ACTION_NONE;
switch (action) {
case ACTION_ADDRESS:
addresses[owner] = (uint8_t)control.request.wValue;
if (addresses[owner] == 0) default_slot = owner;
else if (default_slot == owner) default_slot = PROBE_ROUTER_UNASSIGNED;
publish_addresses();
// Once routing is active, C1 is the sole address-register writer.
// It selects the logical address from each token, after this ACK.
// This prevents a C0 SET_ADDRESS completion changing the register
// between another token's acceptance and its SETUP interrupt.
if (!routing_enabled)
dcd_edpt0_status_complete(RHPORT, &control.request);
break;
case ACTION_CONFIGURATION:
configurations[owner] = (uint8_t)control.request.wValue;
if (owner == 0) {
if (configurations[0]) open_interrupt();
else {
close_interrupt();
probe_router_enable(false);
routing_enabled = false;
memset(ports, 0, sizeof(ports));
forget_child(0);
forget_child(1);
publish_addresses();
usb_hw->dev_addr_ctrl = addresses[0];
}
}
break;
case ACTION_INTERFACE:
if (owner == 0) open_interrupt();
break;
case ACTION_HALT:
++endpoint_epoch;
interrupt_halted = true;
interrupt_pending = false;
dcd_edpt_stall(RHPORT, HUB_EP);
break;
case ACTION_CLEAR_HALT:
++endpoint_epoch;
interrupt_pending = false;
interrupt_halted = false;
// Reopening also cancels a previously queued interrupt safely and
// resets DATA0; no child has a noncontrol endpoint to disturb.
open_interrupt();
break;
case ACTION_PORT_SET:
apply_port_feature(true);
break;
case ACTION_PORT_CLEAR:
apply_port_feature(false);
break;
case ACTION_KEEPALIVE:
watchdog_update();
break;
case ACTION_ARM:
if (!routing_enabled) {
routing_enabled = true;
publish_addresses();
probe_router_enable(true);
for (unsigned i = 0; i < PORT_COUNT; ++i) {
ports[i].status |= PORT_CONNECTION;
ports[i].change |= C_CONNECTION;
}
}
break;
case ACTION_REBOOT:
reboot_pending = true;
break;
case ACTION_NONE:
break;
}
}
static void handle_transfer(const queued_event* queued) {
const dcd_event_t* event = &queued->event;
uint8_t endpoint = event->xfer_complete.ep_addr;
if (endpoint == HUB_EP) {
if (queued->endpoint_epoch != endpoint_epoch) return;
interrupt_pending = false;
if (event->xfer_complete.result != XFER_RESULT_SUCCESS || event->xfer_complete.len != 1) failed = true;
return;
}
if ((endpoint != EP0_IN && endpoint != EP0_OUT) || queued->generation != control.generation ||
control.stage == CTRL_IDLE || control.stage == CTRL_STALLED) return;
if (event->xfer_complete.result != XFER_RESULT_SUCCESS) {
stall_control();
return;
}
if ((control.stage == CTRL_STATUS_IN && endpoint == EP0_IN) ||
(control.stage == CTRL_STATUS_OUT && endpoint == EP0_OUT)) {
if (event->xfer_complete.len == 0) complete_control();
else stall_control();
return;
}
if (queued->generation != event_generation) return;
if (control.stage != CTRL_DATA_IN || endpoint != EP0_IN ||
event->xfer_complete.len != control.packet_length) {
stall_control();
return;
}
control.sent = (uint16_t)(control.sent + control.packet_length);
if (control.sent < control.length || control.need_zlp) next_control_packet();
else {
control.stage = CTRL_STATUS_OUT;
queue_control(EP0_OUT, control_out, 0);
}
}
void dcd_event_handler(dcd_event_t const* event, bool in_isr) {
(void)in_isr;
if (event->rhport != RHPORT) return;
if (event->event_id != DCD_EVENT_SETUP_RECEIVED && event->event_id != DCD_EVENT_XFER_COMPLETE &&
event->event_id != DCD_EVENT_BUS_RESET && event->event_id != DCD_EVENT_UNPLUGGED) return;
if (event->event_id == DCD_EVENT_SETUP_RECEIVED || event->event_id == DCD_EVENT_BUS_RESET ||
event->event_id == DCD_EVENT_UNPLUGGED) ++event_generation;
uint32_t head = event_head;
uint32_t next = (head + 1u) % EVENT_CAPACITY;
if (next == event_tail) {
event_overflow = true;
return;
}
queued_event* queued = &events[head];
queued->event = *event;
queued->generation = event_generation;
queued->endpoint_epoch = endpoint_epoch;
queued->setup_slot = PROBE_ROUTER_UNASSIGNED;
if (event->event_id == DCD_EVENT_SETUP_RECEIVED) {
// C1 does not change the address while SETUP_REC is pending; C0 does
// not write it in routed mode. This is the hardware-accepted address,
// not a fallback inferred from whichever header we last sampled.
const uint8_t hw_address = usb_hw->dev_addr_ctrl & 0x7fu;
if (hw_address == 0) {
queued->setup_slot = default_slot;
} else {
for (uint8_t slot = 0; slot < PROBE_ROUTER_SLOTS; ++slot) {
if (addresses[slot] == hw_address) {
queued->setup_slot = slot;
break;
}
}
}
uint32_t sequence;
const uint8_t candidate = probe_router_setup_slot(&sequence);
if (candidate < PROBE_ROUTER_SLOTS && candidate == queued->setup_slot &&
sequence != observed_setup_sequence) ++correlated_setups;
observed_setup_sequence = sequence;
}
__dmb();
event_head = next;
}
static void port_task(uint32_t now) {
for (unsigned i = 0; i < PORT_COUNT; ++i) {
hub_port* port = &ports[i];
if (port->resetting && (int32_t)(now - port->reset_deadline) >= 0) {
port->resetting = false;
port->status &= (uint16_t)~PORT_RESET;
if (default_slot != PROBE_ROUTER_UNASSIGNED && default_slot != i + 1) {
// Concurrent default-address resets cannot be represented honestly.
failed = true;
return;
}
port->status |= PORT_ENABLE;
port->change |= C_RESET;
addresses[i + 1] = 0;
default_slot = (uint8_t)(i + 1);
publish_addresses();
}
if (port->resuming && (int32_t)(now - port->resume_deadline) >= 0) {
port->resuming = false;
port->status &= (uint16_t)~PORT_SUSPEND;
port->change |= C_SUSPEND;
}
}
}
static void diagnostic_task(uint32_t now) {
static uint32_t last_report;
static char line[384];
static uint16_t length;
static uint16_t sent;
if ((uint32_t)(now - last_report) >= 1000000u && sent == length) {
last_report = now;
probe_router_stats router;
probe_router_snapshot(&router);
int count = snprintf(line, sizeof(line),
"[PHUB] route=%u addr=%u,%u,%u default=%u setup=%" PRIu32 ",%" PRIu32 ",%" PRIu32
" bad=%" PRIu32 " ready=%" PRIu32 " sop=%" PRIu32 " sync=%" PRIu32
" token=%" PRIu32 " crc=%" PRIu32 " late=%" PRIu32 " retarget=%" PRIu32
" hits=%" PRIu32 ",%" PRIu32 ",%" PRIu32 " overflow=%u failed=%u"
" raw=%08" PRIx32 "/%08" PRIx32 " n=%" PRIu32 " eop=%" PRIu32 "\r\n",
routing_enabled, addresses[0], addresses[1], addresses[2], default_slot,
setup_count[0], setup_count[1], setup_count[2], bad_setup_owner,
router.ready, router.sops, router.sync_ok, router.valid_tokens, router.crc_errors,
router.late_samples, router.retargets, router.address_hits[0], router.address_hits[1],
router.address_hits[2], event_overflow, failed,
router.last_raw[0], router.last_raw[1], router.last_raw_count, router.last_raw_eop);
length = count < 0 ? 0 : (uint16_t)((unsigned)count < sizeof(line) ? (unsigned)count : sizeof(line) - 1);
sent = 0;
}
// No blocking stdio writes: fill only available UART FIFO positions. USB
// event service continues while the 115200-baud diagnostic line drains.
for (unsigned budget = 0; sent < length && budget < 32 && uart_is_writable(uart_default); ++budget)
uart_get_hw(uart_default)->dr = (uint8_t)line[sent++];
}
void probe_hub_init(void) {
system_clock_hz = clock_get_hz(clk_sys);
reset_bus_state();
// Enabling, bus resets, ordinary enumeration, and UART never feed this.
watchdog_enable(8000, false);
const tusb_rhport_init_t init = { .role = TUSB_ROLE_DEVICE, .speed = TUSB_SPEED_FULL };
if (!dcd_init(RHPORT, &init)) failed = true;
dcd_int_enable(RHPORT);
}
void probe_hub_task(void) {
if (!failed) {
for (unsigned count = 0; count < EVENT_CAPACITY; ++count) {
dcd_int_disable(RHPORT);
if (event_overflow) failed = true;
if (failed || event_tail == event_head) {
dcd_int_enable(RHPORT);
break;
}
__dmb();
queued_event queued = events[event_tail];
event_tail = (event_tail + 1u) % EVENT_CAPACITY;
switch (queued.event.event_id) {
case DCD_EVENT_BUS_RESET:
case DCD_EVENT_UNPLUGGED:
reset_bus_state();
break;
case DCD_EVENT_SETUP_RECEIVED:
handle_setup(&queued);
break;
case DCD_EVENT_XFER_COMPLETE:
handle_transfer(&queued);
break;
default:
break;
}
dcd_int_enable(RHPORT);
if (failed || reboot_pending) break;
}
}
uint32_t now = time_us_32();
dcd_int_disable(RHPORT);
if (!failed && !reboot_pending) {
port_task(now);
if (!failed) arm_interrupt();
}
if (failed) {
probe_router_enable(false);
routing_enabled = false;
dcd_disconnect(RHPORT);
}
dcd_int_enable(RHPORT);
if (reboot_pending) {
// This is reached only after the REBOOT request's status IN was ACKed.
watchdog_reboot(0, 0, 10);
reboot_pending = false;
failed = true;
}
diagnostic_task(now);
}

View file

@ -0,0 +1,5 @@
#pragma once
// Core 0 only. Main initializes the router/Core 1 before attaching USB here.
void probe_hub_init(void);
void probe_hub_task(void);

View file

@ -1,21 +1,71 @@
#include "bootsel.h"
#include "model.h"
#if SWITCH2_PROBE_HUB
#include <string.h>
#include "pico/bootrom.h"
#include "usb/native_hub/native_hub.h"
#else
#include "adapter/adapter_mode_controller.h"
#endif
#include "usb/usb_configuration_management.h"
namespace {
bool bootsel_accepted;
#if SWITCH2_PROBE_HUB
constexpr uint32_t kBootselRebootDelayMs = 50;
struct BootselTransfer {
uint8_t envelope[UsbConfigurationManagement::kRequestHeaderSize];
bool pending;
bool validated;
};
// Control state is independent even when the two children enumerate together.
BootselTransfer bootsel_transfers[PROBE_CONTROLLER_COUNT + 1];
bool bootsel_delay_started;
uint32_t bootsel_deadline_ms;
#endif
}
bool probe_bootsel_vendor_control(uint8_t rhport, uint8_t stage,
const tusb_control_request_t* request) {
using namespace UsbConfigurationManagement;
#if SWITCH2_PROBE_HUB
if (rhport > PROBE_CONTROLLER_COUNT) return false;
BootselTransfer& transfer = bootsel_transfers[rhport];
if (stage == CONTROL_STAGE_SETUP) {
transfer.pending = false;
transfer.validated = false;
}
#endif
if (request == nullptr || request->bmRequestType != 0x40 ||
request->bRequest != static_cast<uint8_t>(Operation::kBootselReboot) ||
request->wValue != kRequestValue || request->wIndex != kRequestIndex ||
request->wLength != kRequestHeaderSize) {
return false;
}
#if SWITCH2_PROBE_HUB
if (stage == CONTROL_STAGE_SETUP) {
// Any short OUT leaves nonzero reserved/CRC bytes and fails decoding.
memset(transfer.envelope, 0xff, sizeof(transfer.envelope));
transfer.pending = native_hub_control_xfer(
rhport, request, transfer.envelope, sizeof(transfer.envelope));
return transfer.pending;
}
if (stage == CONTROL_STAGE_DATA) {
DecodedRequest decoded{};
transfer.validated = transfer.pending &&
decode_request(Operation::kBootselReboot, transfer.envelope,
sizeof(transfer.envelope), &decoded) &&
decoded.payload_size == 0;
return transfer.validated;
}
if (stage == CONTROL_STAGE_ACK && transfer.pending && transfer.validated) {
transfer.pending = false;
bootsel_accepted = true;
return true;
}
return false;
#else
// The shared handler receives the envelope at SETUP and validates and
// dispatches it only at ACK, after the host's control transfer completes.
const bool accepted =
@ -24,12 +74,24 @@ bool probe_bootsel_vendor_control(uint8_t rhport, uint8_t stage,
bootsel_accepted = true;
}
return accepted;
#endif
}
void probe_bootsel_task(uint32_t now_ms) {
#if SWITCH2_PROBE_HUB
if (!bootsel_accepted) return;
if (!bootsel_delay_started) {
bootsel_delay_started = true;
bootsel_deadline_ms = now_ms + kBootselRebootDelayMs;
} else if (static_cast<int32_t>(now_ms - bootsel_deadline_ms) >= 0) {
bootsel_accepted = false;
reset_usb_boot(0, 0);
}
#else
// The native bridge does not initialize ordinary adapter-mode selection.
// A successful BOOTSEL dispatch guarantees the task takes its reboot path.
if (bootsel_accepted) {
adapter_mode_controller_task(now_ms);
}
#endif
}

View file

@ -1,4 +1,5 @@
#include "controller_input.h"
#include "model.h"
#include <string.h>
@ -8,6 +9,10 @@
#include "platform/pico/system_clock.h"
#include "profile/controller_profile_runtime.h"
#include "pico/stdlib.h"
#if SWITCH2_PROBE_HUB
#include <inttypes.h>
extern "C" int probe_debug_printf(const char* format, ...);
#endif
#if SWITCH2_BRIDGE_WII_INPUT
#include <math.h>
#include "input/wii_ir_pointer.h"
@ -25,17 +30,25 @@ extern "C" int probe_debug_printf(const char* format, ...);
namespace {
constexpr uint8_t kSourceAddress[] = {SWITCH2_BRIDGE_SOURCE_ADDRESS_BYTES};
static_assert(sizeof(kSourceAddress) == 6, "Select one physical Bluetooth address");
#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB
constexpr uint8_t kSecondSourceAddress[] = {SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS_BYTES};
static_assert(sizeof(kSecondSourceAddress) == 6, "Select the second physical Bluetooth address");
#endif
constexpr uint32_t kInputDeadlineMs = 500;
#if !SWITCH2_PROBE_HUB
constexpr uint32_t kFlashCoordinationTimeoutMs = 1000;
// The backend publishes stage 2 only after Core 1's flash-safe registration;
// reaching Core 1 already required successful Core 0 registration in start().
#endif
// Stage 2 publishes flash safety: both cores registered in dedicated-radio
// modes, or Core 0 registered with an SRAM-only/IRQ-disabled Core 1 in hub mode.
constexpr uint32_t kFlashCoordinationStage = 2;
bool g_initialized;
bool g_start_attempted;
bool g_flash_ready;
#if SWITCH2_BRIDGE_WII_INPUT
probe_controller_input g_input;
#if !SWITCH2_BRIDGE_WII_INPUT
uint32_t g_received_ms;
#else
probe_controller_input g_inputs[PROBE_CONTROLLER_COUNT];
uint32_t g_received_times[PROBE_CONTROLLER_COUNT];
#endif
#if SWITCH2_BRIDGE_WII_INPUT
#ifndef SWITCH2_WII_IR_SCREEN_CONFIG
@ -368,8 +381,13 @@ extern "C" void probe_controller_input_init(void) {
if (!g_screen_configured) probe_debug_printf("[PROBE] Invalid native IR viewport configuration\n");
wii_ir_mouse_set_output_enabled(false);
#else
static_assert(SWITCH2_MOUSE_CAPTURE_SOURCE_COUNT == PROBE_CONTROLLER_COUNT,
"Each native controller requires an independent capture channel");
switch2_mouse_capture_init();
switch2_mouse_capture_select_input(kSourceAddress);
switch2_mouse_capture_select_input(0, kSourceAddress, probe_model_pid(0));
#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB
switch2_mouse_capture_select_input(1, kSecondSourceAddress, probe_model_pid(1));
#endif
bluepad32_input_backend_init();
#endif
controller_profile_runtime_reset();
@ -384,6 +402,14 @@ extern "C" bool probe_controller_input_start(void) {
#endif
g_start_attempted = true;
bluepad32_input_backend_start();
#if SWITCH2_PROBE_HUB
// Initialization is synchronous on Core 0; there is no radio Core 1 to
// wait for. The SDK async context advances radio startup in task().
Bluepad32BackendDiagnostics diagnostics;
bluepad32_input_backend_diagnostics(&diagnostics);
g_flash_ready = diagnostics.initialization_stage >= kFlashCoordinationStage;
return g_flash_ready;
#else
const absolute_time_t deadline = make_timeout_time_ms(kFlashCoordinationTimeoutMs);
do {
Bluepad32BackendDiagnostics diagnostics;
@ -397,6 +423,25 @@ extern "C" bool probe_controller_input_start(void) {
// Do not reset Core 1 or retry a partially launched backend. It may still
// be running; a false return keeps USB and its flash writes fail-closed.
return false;
#endif
}
extern "C" void probe_controller_input_task(void) {
#if SWITCH2_PROBE_HUB
if (!g_flash_ready) return;
bluepad32_input_backend_poll();
static uint32_t last_diagnostics;
const uint32_t now = to_ms_since_boot(get_absolute_time());
if ((uint32_t)(now - last_diagnostics) >= 1000u) {
last_diagnostics = now;
Bluepad32BackendDiagnostics diagnostics;
bluepad32_input_backend_diagnostics(&diagnostics);
probe_debug_printf("[HUB_RADIO] stage=%" PRIu32 " timers=%" PRIu32 "/%" PRIu32
" reports=%" PRIu32 "\n", diagnostics.initialization_stage,
diagnostics.rumble_timer_ticks, diagnostics.configuration_timer_ticks,
diagnostics.controller_reports);
}
#endif
}
extern "C" bool probe_controller_input_pairing_task(void) {
@ -426,30 +471,31 @@ extern "C" void probe_controller_input_set_native_features(uint8_t features) {
}
#endif
extern "C" void probe_controller_input_set_native_stream(bool enabled) {
extern "C" void probe_controller_input_set_native_stream(uint8_t instance, bool enabled) {
if (instance >= PROBE_CONTROLLER_COUNT) return;
#if SWITCH2_BRIDGE_WII_INPUT
enabled = enabled && g_flash_ready;
if (g_native_stream != enabled || !enabled) discard_wii_output();
g_native_stream = enabled;
update_wii_ir_gate(time_us_32());
#else
switch2_mouse_capture_set_native_stream(g_flash_ready && enabled);
switch2_mouse_capture_set_native_stream(instance, g_flash_ready && enabled);
#endif
}
extern "C" uint32_t probe_controller_input_peek_native_report(
uint32_t now_ms, uint8_t report[63]) {
if (!g_flash_ready) return 0;
uint8_t instance, uint32_t now_ms, uint8_t report[63]) {
if (instance >= PROBE_CONTROLLER_COUNT || !g_flash_ready) return 0;
#if SWITCH2_BRIDGE_WII_INPUT
(void)now_ms;
return prepare_wii_report(report);
#else
return switch2_mouse_capture_peek_native_report(now_ms, report);
return switch2_mouse_capture_peek_native_report(instance, now_ms, report);
#endif
}
extern "C" bool probe_controller_input_commit_native_report(uint32_t serial) {
if (!g_flash_ready) return false;
extern "C" bool probe_controller_input_commit_native_report(uint8_t instance, uint32_t serial) {
if (instance >= PROBE_CONTROLLER_COUNT || !g_flash_ready) return false;
#if SWITCH2_BRIDGE_WII_INPUT
if (!g_native_stream || !serial || serial != g_pending_serial ||
g_pending_generation != g_wii_generation || !g_wii_active) return false;
@ -462,12 +508,12 @@ extern "C" bool probe_controller_input_commit_native_report(uint32_t serial) {
++g_report_counter;
return true;
#else
return switch2_mouse_capture_commit_native_report(serial);
return switch2_mouse_capture_commit_native_report(instance, serial);
#endif
}
extern "C" bool probe_controller_input_play_sample(uint8_t sample_id, uint64_t* token) {
if (!g_flash_ready) {
extern "C" bool probe_controller_input_play_sample(uint8_t instance, uint8_t sample_id, uint64_t* token) {
if (instance >= PROBE_CONTROLLER_COUNT || !g_flash_ready) {
if (token != nullptr) *token = 0;
return false;
}
@ -475,40 +521,43 @@ extern "C" bool probe_controller_input_play_sample(uint8_t sample_id, uint64_t*
return bluepad32_input_backend_wii_sample_request(sample_id, token);
#else
return switch2_mouse_capture_request_sample(
sample_id, to_ms_since_boot(get_absolute_time()), token);
instance, sample_id, to_ms_since_boot(get_absolute_time()), token);
#endif
}
extern "C" int probe_controller_input_sample_result(uint64_t token, uint32_t now_ms) {
if (!g_flash_ready) return -1;
extern "C" int probe_controller_input_sample_result(uint8_t instance, uint64_t token, uint32_t now_ms) {
if (instance >= PROBE_CONTROLLER_COUNT || !g_flash_ready) return -1;
#if SWITCH2_BRIDGE_WII_INPUT
(void)now_ms;
return bluepad32_input_backend_wii_sample_result(token);
#else
return switch2_mouse_capture_sample_result(token, now_ms);
return switch2_mouse_capture_sample_result(instance, token, now_ms);
#endif
}
extern "C" void probe_controller_input_cancel_sample(void) {
extern "C" void probe_controller_input_cancel_sample(uint8_t instance) {
if (instance >= PROBE_CONTROLLER_COUNT) return;
#if SWITCH2_BRIDGE_WII_INPUT
bluepad32_input_backend_wii_sample_cancel();
#else
switch2_mouse_capture_cancel_sample();
switch2_mouse_capture_cancel_sample(instance);
#endif
}
extern "C" void probe_controller_input_poll(uint32_t now_ms,
extern "C" void probe_controller_input_poll(uint8_t instance, uint32_t now_ms,
probe_controller_input* out) {
if (out == nullptr) return;
if (!g_flash_ready) {
if (instance >= PROBE_CONTROLLER_COUNT || !g_flash_ready) {
*out = {};
return;
}
#if SWITCH2_BRIDGE_WII_INPUT
poll_wii_source(now_ms);
#else
probe_controller_input& g_input = g_inputs[instance];
uint32_t& g_received_ms = g_received_times[instance];
Switch2MouseCaptureInput sample;
if (switch2_mouse_capture_latest_input(g_input.serial, &sample)) {
if (switch2_mouse_capture_latest_input(instance, g_input.serial, &sample)) {
g_input.serial = sample.serial;
g_input.active = sample.active;
g_received_ms = sample.received_ms;

View file

@ -12,7 +12,7 @@ typedef struct {
uint32_t serial;
uint8_t buttons[2];
uint8_t stick[3];
// Latest opaque native 08 byte 8.
// Latest opaque native 07/08 byte 8.
uint8_t native_status;
// Cumulative signed relative totals within mouse_epoch, not per-poll
// deltas. Cached polls repeat these totals without consuming motion.
@ -20,7 +20,7 @@ typedef struct {
uint32_t mouse_epoch;
int64_t mouse_total_x;
int64_t mouse_total_y;
// Latest opaque native 08 byte 13.
// Latest opaque native 07/08 byte 13.
uint8_t mouse_surface;
} probe_controller_input;
@ -28,10 +28,15 @@ typedef struct {
void probe_controller_input_clock_init(void);
// Core 0, after stdio and before protocol reset or USB startup.
void probe_controller_input_init(void);
// True means both cores are registered for flash coordination, not that the
// radio is ready or a controller is connected. Failure is latched: keep USB
// and flash-writing protocol operations disabled rather than retrying startup.
// True means flash coordination is ready, not that the radio is ready or a
// controller is connected. Hub mode initializes on Core 0 with Core 1 reserved
// for SRAM-only USB; other modes register both cores and launch the radio there.
// Failure is latched: keep USB and flash-writing protocol operations disabled.
bool probe_controller_input_start(void);
// Core 0 main loop before USB tasks, outside IRQs and application state locks.
// Hub mode cooperatively services CYW43/BTstack, including storage and haptics;
// a no-op before successful start and in dedicated-radio modes.
void probe_controller_input_task(void);
// Core 0 after start(): polls the existing two-second BOOTSEL hold gesture.
// True means a Bluetooth pairing-window request was queued. Long holds NEVER
// clear pairings in this bridge, and this does not inject USB controller input.
@ -42,29 +47,30 @@ void probe_controller_input_set_stick_calibration(const uint8_t calibration[9]);
// Native feature changes are output barriers, not Bluetooth/IMU resets.
void probe_controller_input_set_native_features(uint8_t features);
#endif
// Core0 native08 output. Disable discards queued/prepared data; repeated enable
// preserves it. Joy-Con mode relays its bounded FIFO; Wii mode synthesizes from
// fresh calibrated sensors and the selected IR pointer. No pairing changes.
void probe_controller_input_set_native_stream(bool enabled);
// Core0 native07/08 output. Disable discards queued/prepared data; repeated
// enable preserves it. Joy-Con mode relays its bounded FIFO; right-only Wii
// mode synthesizes fresh calibrated sensors and the selected IR pointer.
// No pairing changes.
void probe_controller_input_set_native_stream(uint8_t instance, bool enabled);
// Copy one63-byte payload without report ID. Returns a boot-unique token, or0
// without changing output. Nondestructive until successful HID submission and
// commit. now_ms uses the Pico boot-ms clock; unavailable/stale input is rejected.
uint32_t probe_controller_input_peek_native_report(uint32_t now_ms, uint8_t report[63]);
uint32_t probe_controller_input_peek_native_report(uint8_t instance, uint32_t now_ms, uint8_t report[63]);
// Remove only the exact current head once. A stale/replaced token cannot pop a
// new stream's packet. Before flash-ready startup peek/commit return 0/false.
bool probe_controller_input_commit_native_report(uint32_t serial);
bool probe_controller_input_commit_native_report(uint8_t instance, uint32_t serial);
// Built-in vibration samples only; raw HD-rumble output is not forwarded.
// A nonzero token means queued, not completed. Result:0 pending,1 completion,
// -1 failed/stale. Joy-Con completion is its application ACK; Wii completion is
// actual bounded rumble-driver dispatch (not an HD-waveform fidelity claim).
// Reset cancels the request, never stored pairing.
bool probe_controller_input_play_sample(uint8_t sample_id, uint64_t* token);
int probe_controller_input_sample_result(uint64_t token, uint32_t now_ms);
void probe_controller_input_cancel_sample(void);
bool probe_controller_input_play_sample(uint8_t instance, uint8_t sample_id, uint64_t* token);
int probe_controller_input_sample_result(uint8_t instance, uint64_t token, uint32_t now_ms);
void probe_controller_input_cancel_sample(uint8_t instance);
// Core0 at250Hz; now_ms uses Pico boot milliseconds. Supplies current mapped
// controls for diagnostic reports; the native sender owns motion consumption.
// Inactive controls are zero except serial; USB supplies its calibrated center.
void probe_controller_input_poll(uint32_t now_ms, probe_controller_input* out);
void probe_controller_input_poll(uint8_t instance, uint32_t now_ms, probe_controller_input* out);
#ifdef __cplusplus
}

View file

@ -1,33 +1,66 @@
#pragma once
#include <stdint.h>
#include "model.h"
// Published Joy-Con 2 (R) USB descriptors, reproduced for enumeration capture.
// Published Joy-Con 2 USB descriptors, reproduced for the selected model.
// https://github.com/ndeadly/switch2_controller_research/blob/master/descriptors.md
static const uint8_t probe_device_descriptor[] = {
0x12, 0x01, 0x00, 0x02, 0xef, 0x02, 0x01, 0x40, 0x7e, 0x05, 0x66, 0x20,
0x00, 0x01, 0x01, 0x02, 0x03, 0x01,
};
// Composite retains the primary right PID (0x2066): USB has one device identity,
// not a separate device PID for each left/right function.
#define PROBE_DEVICE_DESCRIPTOR(pid) { \
0x12, 0x01, 0x00, 0x02, 0xef, 0x02, 0x01, 0x40, 0x7e, 0x05, \
(pid) & 0xff, (pid) >> 8, \
0x00, 0x01, 0x01, 0x02, 0x03, 0x01, \
}
static const uint8_t probe_device_descriptor[] = PROBE_DEVICE_DESCRIPTOR(PROBE_JOYCON_PID);
#if SWITCH2_PROBE_HUB
static const uint8_t probe_left_device_descriptor[] = PROBE_DEVICE_DESCRIPTOR(0x2067u);
#endif
#undef PROBE_DEVICE_DESCRIPTOR
static const uint8_t probe_configuration_descriptor[] = {
0x09, 0x02, 0x50, 0x00, 0x02, 0x01, 0x04, 0xc0, 0xfa, 0x08, 0x0b, 0x00,
#if SWITCH2_PROBE_COMPOSITE
0x09, 0x02, 0x97, 0x00, 0x04, 0x01, 0x04, 0xc0, 0xfa,
#else
0x09, 0x02, 0x50, 0x00, 0x02, 0x01, 0x04, 0xc0, 0xfa,
#endif
0x08, 0x0b, 0x00,
0x01, 0x03, 0x00, 0x00, 0x00, 0x09, 0x04, 0x00, 0x00, 0x02, 0x03, 0x00,
0x00, 0x05, 0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x64, 0x00, 0x07,
0x05, 0x81, 0x03, 0x40, 0x00, 0x04, 0x07, 0x05, 0x01, 0x03, 0x40, 0x00,
0x04, 0x08, 0x0b, 0x01, 0x01, 0xff, 0x00, 0x00, 0x00, 0x09, 0x04, 0x01,
0x00, 0x02, 0xff, 0x00, 0x00, 0x06, 0x07, 0x05, 0x02, 0x02, 0x40, 0x00,
0x00, 0x07, 0x05, 0x82, 0x02, 0x40, 0x00, 0x00,
#if SWITCH2_PROBE_COMPOSITE
0x08, 0x0b, 0x02, 0x01, 0x03, 0x00, 0x00, 0x00,
0x09, 0x04, 0x02, 0x00, 0x02, 0x03, 0x00, 0x00, 0x07,
0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x64, 0x00,
0x07, 0x05, 0x83, 0x03, 0x40, 0x00, 0x04,
0x07, 0x05, 0x03, 0x03, 0x40, 0x00, 0x04,
0x08, 0x0b, 0x03, 0x01, 0xff, 0x00, 0x00, 0x00,
0x09, 0x04, 0x03, 0x00, 0x02, 0xff, 0x00, 0x00, 0x08,
0x07, 0x05, 0x04, 0x02, 0x40, 0x00, 0x00,
0x07, 0x05, 0x84, 0x02, 0x40, 0x00, 0x00,
#endif
};
static const uint8_t probe_hid_report_descriptor[] = {
0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x85, 0x05, 0x05, 0xff, 0x09, 0x01,
0x15, 0x00, 0x26, 0xff, 0x00, 0x95, 0x3f, 0x75, 0x08, 0x81, 0x02, 0x85,
0x08, 0x09, 0x01, 0x95, 0x02, 0x81, 0x02, 0x05, 0x09, 0x19, 0x01, 0x29,
0x10, 0x25, 0x01, 0x95, 0x10, 0x75, 0x01, 0x81, 0x02, 0x05, 0xff, 0x09,
0x01, 0x26, 0xff, 0x00, 0x95, 0x01, 0x75, 0x08, 0x81, 0x02, 0x05, 0x01,
0x09, 0x01, 0xa1, 0x00, 0x09, 0x30, 0x09, 0x31, 0x26, 0xff, 0x0f, 0x95,
0x02, 0x75, 0x0c, 0x81, 0x02, 0xc0, 0x05, 0xff, 0x09, 0x02, 0x26, 0xff,
0x00, 0x95, 0x37, 0x75, 0x08, 0x81, 0x02, 0x85, 0x01, 0x09, 0x01, 0x95,
0x3f, 0x91, 0x02, 0xc0,
#define PROBE_HID_DESCRIPTOR(report_id) { \
0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x85, 0x05, 0x05, 0xff, 0x09, 0x01, \
0x15, 0x00, 0x26, 0xff, 0x00, 0x95, 0x3f, 0x75, 0x08, 0x81, 0x02, 0x85, \
report_id, 0x09, 0x01, 0x95, 0x02, 0x81, 0x02, 0x05, 0x09, 0x19, 0x01, 0x29, \
0x10, 0x25, 0x01, 0x95, 0x10, 0x75, 0x01, 0x81, 0x02, 0x05, 0xff, 0x09, \
0x01, 0x26, 0xff, 0x00, 0x95, 0x01, 0x75, 0x08, 0x81, 0x02, 0x05, 0x01, \
0x09, 0x01, 0xa1, 0x00, 0x09, 0x30, 0x09, 0x31, 0x26, 0xff, 0x0f, 0x95, \
0x02, 0x75, 0x0c, 0x81, 0x02, 0xc0, 0x05, 0xff, 0x09, 0x02, 0x26, 0xff, \
0x00, 0x95, 0x37, 0x75, 0x08, 0x81, 0x02, 0x85, 0x01, 0x09, 0x01, 0x95, \
0x3f, 0x91, 0x02, 0xc0, \
}
static const uint8_t probe_hid_report_descriptors[PROBE_CONTROLLER_COUNT][100] = {
PROBE_HID_DESCRIPTOR(PROBE_NATIVE_REPORT_ID),
#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB
PROBE_HID_DESCRIPTOR(0x07u),
#endif
};
#undef PROBE_HID_DESCRIPTOR

File diff suppressed because it is too large Load diff

View file

@ -2,21 +2,23 @@
#include "probe_memory_data.h"
#include <string.h>
_Static_assert(sizeof(probe_factory_memory) == 8192, "factory capture size");
_Static_assert(sizeof(probe_user_calibration) == 4096, "user calibration capture size");
_Static_assert(sizeof(probe_factory_memories) == PROBE_CONTROLLER_COUNT * 8192u,
"factory capture sizes");
_Static_assert(sizeof(probe_user_calibrations) == PROBE_CONTROLLER_COUNT * 4096u,
"user calibration capture sizes");
bool probe_memory_read(uint32_t address, uint8_t* output, size_t length) {
if (!output) return false;
bool probe_memory_read(uint8_t instance, uint32_t address, uint8_t* output, size_t length) {
if (instance >= PROBE_CONTROLLER_COUNT || !output) return false;
const uint8_t* source;
size_t offset, available;
if (address >= 0x13000 && address < 0x15000) {
offset = address - 0x13000;
source = probe_factory_memory;
available = sizeof(probe_factory_memory) - offset;
source = probe_factory_memories[instance];
available = sizeof(probe_factory_memories[instance]) - offset;
} else if (address >= 0x1fc000 && address < 0x1fd000) {
offset = address - 0x1fc000;
source = probe_user_calibration;
available = sizeof(probe_user_calibration) - offset;
source = probe_user_calibrations[instance];
available = sizeof(probe_user_calibrations[instance]) - offset;
} else {
return false; // No fabricated erased bytes, pairing keys, or firmware reads.
}
@ -43,12 +45,12 @@ static bool valid_calibration(const uint8_t* data) {
return true;
}
bool probe_memory_right_stick_calibration(uint8_t output[9]) {
if (!output) return false;
// A solo Joy-Con uses the primary calibration record, even for the right
// controller. User magic precedes its 9-byte record; factory has no magic.
const uint8_t* selected = probe_factory_memory + 0xa8;
const uint8_t* user = probe_user_calibration + 0x40;
bool probe_memory_stick_calibration(uint8_t instance, uint8_t output[9]) {
if (instance >= PROBE_CONTROLLER_COUNT || !output) return false;
// Each Joy-Con's own capture uses the primary calibration record.
// User magic precedes its 9-byte record; factory has no magic.
const uint8_t* selected = probe_factory_memories[instance] + 0xa8;
const uint8_t* user = probe_user_calibrations[instance] + 0x40;
if (user[0] == 0xb2 && user[1] == 0xa1 && valid_calibration(user + 2))
selected = user + 2;
if (!valid_calibration(selected)) return false;

View file

@ -3,6 +3,7 @@
#include <stddef.h>
#include <stdint.h>
bool probe_memory_read(uint32_t address, uint8_t* output, size_t length);
// Invalid instances and unavailable ranges leave output unchanged.
bool probe_memory_read(uint8_t instance, uint32_t address, uint8_t* output, size_t length);
// Packed center, positive travel, negative travel (two12-bit axes each).
bool probe_memory_right_stick_calibration(uint8_t output[9]);
bool probe_memory_stick_calibration(uint8_t instance, uint8_t output[9]);

View file

@ -0,0 +1,94 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#ifndef SWITCH2_PROBE_HUB
#define SWITCH2_PROBE_HUB 0
#endif
#if SWITCH2_PROBE_HUB != 0 && SWITCH2_PROBE_HUB != 1
#error "SWITCH2_PROBE_HUB must be 0 or 1"
#endif
#ifndef SWITCH2_PROBE_COMPOSITE
#define SWITCH2_PROBE_COMPOSITE 0
#endif
#if SWITCH2_PROBE_COMPOSITE != 0 && SWITCH2_PROBE_COMPOSITE != 1
#error "SWITCH2_PROBE_COMPOSITE must be 0 or 1"
#endif
#if SWITCH2_PROBE_HUB && SWITCH2_PROBE_COMPOSITE
#error "Native hub and composite USB backends are mutually exclusive"
#endif
#ifndef SWITCH2_PROBE_JOYCON_LEFT
#define SWITCH2_PROBE_JOYCON_LEFT 0
#endif
#if SWITCH2_PROBE_JOYCON_LEFT != 0 && SWITCH2_PROBE_JOYCON_LEFT != 1
#error "SWITCH2_PROBE_JOYCON_LEFT must be 0 or 1"
#endif
#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB
#if SWITCH2_PROBE_JOYCON_LEFT
#error "Dual-controller primary must be Joy-Con 2 (R)"
#endif
#ifndef PROBE_CONTROLLER_COUNT
#define PROBE_CONTROLLER_COUNT 2
#endif
#if PROBE_CONTROLLER_COUNT != 2
#error "Dual-controller output requires two controller instances"
#endif
#else
#ifndef PROBE_CONTROLLER_COUNT
#define PROBE_CONTROLLER_COUNT 1
#endif
#if PROBE_CONTROLLER_COUNT != 1
#error "Standalone requires one controller instance"
#endif
#endif
#if SWITCH2_PROBE_JOYCON_LEFT
#define PROBE_JOYCON_PID 0x2067u
#define PROBE_JOYCON_PRODUCT "Joy-Con 2 (L)"
#define PROBE_JOYCON_SIDE "left"
#define PROBE_NATIVE_REPORT_ID 0x07u
#define PROBE_IMU_LENGTH_OFFSET 14u
#define PROBE_IMU_DATA_OFFSET 15u
#else
#define PROBE_JOYCON_PID 0x2066u
#define PROBE_JOYCON_PRODUCT "Joy-Con 2 (R)"
#define PROBE_JOYCON_SIDE "right"
#define PROBE_NATIVE_REPORT_ID 0x08u
#define PROBE_IMU_LENGTH_OFFSET 15u
#define PROBE_IMU_DATA_OFFSET 16u
#endif
// Instance zero is the standalone model or the dual-controller right function.
// In composite and native hub modes, instance one is the independent left side.
static inline bool probe_model_is_left(uint8_t instance) {
#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB
return instance == 1;
#else
(void)instance;
return SWITCH2_PROBE_JOYCON_LEFT != 0;
#endif
}
static inline uint16_t probe_model_pid(uint8_t instance) {
return probe_model_is_left(instance) ? 0x2067u : 0x2066u;
}
static inline uint8_t probe_model_report_id(uint8_t instance) {
return probe_model_is_left(instance) ? 0x07u : 0x08u;
}
static inline uint8_t probe_model_imu_length_offset(uint8_t instance) {
return probe_model_is_left(instance) ? 14u : 15u;
}
static inline uint8_t probe_model_imu_data_offset(uint8_t instance) {
return probe_model_is_left(instance) ? 15u : 16u;
}

View file

@ -3,6 +3,78 @@
set(SWITCH2_USB_PROBE_DIR "${CMAKE_CURRENT_LIST_DIR}")
set(PICO_MBEDTLS_CONFIG_FILE "${SWITCH2_USB_PROBE_DIR}/mbedtls_config.h")
option(SWITCH2_PROBE_COMPOSITE
"Experiment: independent right and left Joy-Con 2 functions on one USB port" OFF)
option(SWITCH2_PROBE_HUB "Native R/L devices on the built-in SIO USB hub" OFF)
if(SWITCH2_PROBE_HUB AND SWITCH2_PROBE_COMPOSITE)
message(FATAL_ERROR "Select native hub or composite, not both")
endif()
option(SWITCH2_PROBE_JOIN_CHORD_GATE
"Experiment: pass L/R shoulder presses only while both physical halves hold them" OFF)
set(SWITCH2_PROBE_SIDE "RIGHT" CACHE STRING "Primary Joy-Con 2 model: LEFT or RIGHT")
set_property(CACHE SWITCH2_PROBE_SIDE PROPERTY STRINGS LEFT RIGHT)
if(SWITCH2_PROBE_SIDE STREQUAL "LEFT")
if(SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB OR SWITCH2_BRIDGE_WII_INPUT)
message(FATAL_ERROR "SWITCH2_PROBE_SIDE=LEFT cannot be combined with composite or Wii input; select RIGHT")
endif()
set(probe_joycon_left 1)
elseif(SWITCH2_PROBE_SIDE STREQUAL "RIGHT")
set(probe_joycon_left 0)
else()
message(FATAL_ERROR "SWITCH2_PROBE_SIDE must be LEFT or RIGHT")
endif()
if(SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB)
if(NOT SWITCH_PICO_SWITCH2_USB_BRIDGE OR SWITCH2_BRIDGE_WII_INPUT
OR NOT SWITCH2_BRIDGE_INPUT STREQUAL "JOYCON2")
message(FATAL_ERROR "Composite Joy-Con 2 requires SWITCH_PICO_SWITCH2_USB_BRIDGE=ON and SWITCH2_BRIDGE_INPUT=JOYCON2")
endif()
set(probe_composite 0)
if(SWITCH2_PROBE_COMPOSITE)
set(probe_composite 1)
endif()
set(probe_controller_count 2)
else()
set(probe_composite 0)
set(probe_controller_count 1)
endif()
if(SWITCH2_PROBE_JOIN_CHORD_GATE AND NOT SWITCH2_PROBE_COMPOSITE)
message(FATAL_ERROR "The L+R shoulder gate requires the composite Joy-Con bridge")
endif()
# Capture, the Bluetooth backend, and TinyUSB must agree before their targets exist.
add_compile_definitions(
SWITCH2_PROBE_JOYCON_LEFT=${probe_joycon_left}
SWITCH2_PROBE_COMPOSITE=${probe_composite}
SWITCH2_PROBE_HUB=$<BOOL:${SWITCH2_PROBE_HUB}>
PROBE_CONTROLLER_COUNT=${probe_controller_count})
set(SWITCH2_BRIDGE_SOURCE_ADDRESS "" CACHE STRING
"Primary physical Bluetooth source address (xx:xx:xx:xx:xx:xx)")
set(SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS "" CACHE STRING
"Secondary left physical Bluetooth source address (xx:xx:xx:xx:xx:xx)")
if(SWITCH_PICO_SWITCH2_USB_BRIDGE OR SWITCH2_PROBE_COMPOSITE)
set(probe_source_fields SWITCH2_BRIDGE_SOURCE_ADDRESS)
if(SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB)
list(APPEND probe_source_fields SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS)
endif()
set(probe_source_addresses "")
foreach(field IN LISTS probe_source_fields)
string(TOLOWER "${${field}}" source_address)
string(LENGTH "${source_address}" source_address_length)
if(NOT source_address_length EQUAL 17
OR NOT source_address MATCHES "^([0-9a-f][0-9a-f]:)+[0-9a-f][0-9a-f]$"
OR source_address STREQUAL "00:00:00:00:00:00"
OR source_address STREQUAL "ff:ff:ff:ff:ff")
message(FATAL_ERROR "Provide ${field} as a physical six-byte Bluetooth address")
endif()
if(source_address IN_LIST probe_source_addresses)
message(FATAL_ERROR "Composite physical source addresses must be distinct")
endif()
list(APPEND probe_source_addresses "${source_address}")
string(REPLACE ":" ",0x" ${field}_BYTES "${source_address}")
string(PREPEND ${field}_BYTES "0x")
endforeach()
endif()
function(switch2_usb_probe_configure target)
set(probe_sources
${SWITCH2_USB_PROBE_DIR}/main.c
@ -10,6 +82,9 @@ function(switch2_usb_probe_configure target)
${SWITCH2_USB_PROBE_DIR}/storage.cpp
${SWITCH2_USB_PROBE_DIR}/button_test.c)
target_compile_features(${target} PRIVATE c_std_11 cxx_std_17)
if(SWITCH2_PROBE_JOIN_CHORD_GATE)
target_compile_definitions(${target} PRIVATE SWITCH2_PROBE_JOIN_CHORD_GATE=1)
endif()
target_include_directories(${target} PRIVATE
${SWITCH2_USB_PROBE_DIR}
${SWITCH2_USB_PROBE_DIR}/../../src/firmware
@ -81,64 +156,13 @@ function(switch2_usb_probe_configure target)
target_compile_definitions(${target} PRIVATE SWITCH2_PROBE_ZERO_NATIVE_IMU_PAYLOAD=1)
endif()
set(SWITCH2_PROBE_IDENTITY_FILE "" CACHE FILEPATH "64-byte Joy-Con 2 (R) factory-format identity block")
if(SWITCH2_PROBE_IDENTITY_FILE)
file(READ "${SWITCH2_PROBE_IDENTITY_FILE}" identity_hex LIMIT 65 HEX)
string(LENGTH "${identity_hex}" identity_length)
if(NOT identity_length EQUAL 128)
message(FATAL_ERROR "Identity capture must contain exactly 64 bytes")
endif()
string(TOLOWER "${identity_hex}" identity_hex)
string(SUBSTRING "${identity_hex}" 36 8 identity_vid_pid)
if(NOT identity_vid_pid STREQUAL "7e056620")
message(FATAL_ERROR "Identity capture must match Joy-Con 2 (R), 057e:2066")
endif()
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," identity_bytes "${identity_hex}")
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/probe_identity.h"
"// Generated from a private, read-only controller capture; do not commit.\n#include <stdint.h>\nstatic const uint8_t probe_identity_reply[64] = {${identity_bytes}};\n")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${SWITCH2_PROBE_IDENTITY_FILE}")
target_compile_definitions(${target} PRIVATE SWITCH2_PROBE_IDENTITY_REPLY=1)
endif()
set(SWITCH2_PROBE_VERSION_FILE "" CACHE FILEPATH "Captured 12-byte Joy-Con 2 (R) firmware-version reply")
set(SWITCH2_PROBE_CONTROLLER_ADDRESS "" CACHE STRING "Advertised controller address (captured or distinct virtual identity)")
if(SWITCH2_PROBE_VERSION_FILE)
if(NOT SWITCH2_PROBE_IDENTITY_FILE)
message(FATAL_ERROR "Version response requires the matching identity capture")
endif()
file(READ "${SWITCH2_PROBE_VERSION_FILE}" version_hex LIMIT 13 HEX)
string(LENGTH "${version_hex}" version_length)
if(NOT version_length EQUAL 24)
message(FATAL_ERROR "Firmware version capture must contain exactly 12 bytes")
endif()
string(TOLOWER "${version_hex}" version_hex)
string(SUBSTRING "${version_hex}" 6 2 firmware_type)
if(NOT firmware_type STREQUAL "01")
message(FATAL_ERROR "Firmware version capture must describe Joy-Con 2 (R)")
endif()
string(REPLACE ":" "" address_hex "${SWITCH2_PROBE_CONTROLLER_ADDRESS}")
string(TOLOWER "${address_hex}" address_hex)
string(LENGTH "${address_hex}" address_length)
if(NOT address_length EQUAL 12 OR NOT address_hex MATCHES "^[0-9a-f]+$")
message(FATAL_ERROR "Provide a six-byte advertised controller Bluetooth address")
endif()
set(address_reversed "")
foreach(byte RANGE 0 5)
math(EXPR position "10 - 2 * ${byte}")
string(SUBSTRING "${address_hex}" ${position} 2 octet)
string(APPEND address_reversed "${octet}")
endforeach()
string(SUBSTRING "${version_hex}" 0 6 main_version)
string(SUBSTRING "${version_hex}" 8 6 bluetooth_version)
# Layout corroborated against two genuine USB vendor-02 responses and their
# matching command-10 version and command-15 address responses.
set(status_hex "${main_version}000000${bluetooth_version}00${address_reversed}")
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," status_bytes "${status_hex}")
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," firmware_bytes "${version_hex}")
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/probe_version.h"
"// Generated from private controller captures; do not commit.\n#include <stdint.h>\nstatic const uint8_t probe_version_reply[16] = {${status_bytes}};\nstatic const uint8_t probe_firmware_version[12] = {${firmware_bytes}};\n")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${SWITCH2_PROBE_VERSION_FILE}")
target_compile_definitions(${target} PRIVATE SWITCH2_PROBE_VERSION_REPLY=1)
endif()
foreach(prefix IN ITEMS SWITCH2_PROBE SWITCH2_PROBE_SECOND)
set(${prefix}_IDENTITY_FILE "" CACHE FILEPATH "64-byte matching Joy-Con 2 factory-format identity block")
set(${prefix}_VERSION_FILE "" CACHE FILEPATH "Captured 12-byte matching Joy-Con 2 firmware-version reply")
set(${prefix}_CONTROLLER_ADDRESS "" CACHE STRING "Advertised controller address (captured or distinct virtual identity)")
set(${prefix}_FACTORY_FILE "" CACHE FILEPATH "8192-byte captured factory region with configured virtual identity")
set(${prefix}_USER_CALIBRATION_FILE "" CACHE FILEPATH "4096-byte captured user calibration region")
endforeach()
option(SWITCH2_PROBE_ACK_SETUP04 "Acknowledge the observed vendor-04 setup transaction" OFF)
if(SWITCH2_PROBE_ACK_SETUP04)
if(NOT SWITCH2_PROBE_IDENTITY_FILE OR NOT SWITCH2_PROBE_VERSION_FILE)
@ -153,29 +177,130 @@ function(switch2_usb_probe_configure target)
endif()
target_compile_definitions(${target} PRIVATE SWITCH2_PROBE_USB_INIT=1)
endif()
set(SWITCH2_PROBE_FACTORY_FILE "" CACHE FILEPATH "8192-byte captured factory region with configured virtual identity")
set(SWITCH2_PROBE_USER_CALIBRATION_FILE "" CACHE FILEPATH "4096-byte captured user calibration region")
if(SWITCH2_PROBE_FACTORY_FILE OR SWITCH2_PROBE_USER_CALIBRATION_FILE)
if(NOT SWITCH2_PROBE_USB_INIT OR NOT SWITCH2_PROBE_FACTORY_FILE OR NOT SWITCH2_PROBE_USER_CALIBRATION_FILE)
message(FATAL_ERROR "Memory replies require initialized USB and both calibration captures")
set(probe_capture_prefixes SWITCH2_PROBE)
if(SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB)
list(APPEND probe_capture_prefixes SWITCH2_PROBE_SECOND)
endif()
set(identity_rows "")
set(status_rows "")
set(firmware_rows "")
set(factory_rows "")
set(user_calibration_rows "")
set(controller_addresses "")
foreach(prefix IN LISTS probe_capture_prefixes)
if(probe_joycon_left OR prefix STREQUAL "SWITCH2_PROBE_SECOND")
set(probe_model "Joy-Con 2 (L)")
set(probe_vid_pid "7e056720")
set(probe_firmware_type "00")
else()
set(probe_model "Joy-Con 2 (R)")
set(probe_vid_pid "7e056620")
set(probe_firmware_type "01")
endif()
file(READ "${SWITCH2_PROBE_FACTORY_FILE}" factory_hex LIMIT 8193 HEX)
file(READ "${SWITCH2_PROBE_USER_CALIBRATION_FILE}" user_calibration_hex LIMIT 4097 HEX)
string(LENGTH "${factory_hex}" factory_length)
string(LENGTH "${user_calibration_hex}" user_calibration_length)
if(NOT factory_length EQUAL 16384 OR NOT user_calibration_length EQUAL 8192)
message(FATAL_ERROR "Factory/user captures must contain exactly 8192/4096 bytes")
if(SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB)
foreach(field IDENTITY_FILE VERSION_FILE FACTORY_FILE USER_CALIBRATION_FILE CONTROLLER_ADDRESS)
if(NOT ${prefix}_${field})
message(FATAL_ERROR "Composite ${probe_model} requires ${prefix}_${field}")
endif()
endforeach()
endif()
string(SUBSTRING "${factory_hex}" 0 128 factory_identity_hex)
if(NOT factory_identity_hex STREQUAL identity_hex)
message(FATAL_ERROR "Factory memory identity must match the vendor-control identity")
if(${prefix}_IDENTITY_FILE)
file(READ "${${prefix}_IDENTITY_FILE}" identity_hex LIMIT 65 HEX)
string(LENGTH "${identity_hex}" identity_length)
if(NOT identity_length EQUAL 128)
message(FATAL_ERROR "${prefix}_IDENTITY_FILE must contain exactly 64 bytes")
endif()
string(TOLOWER "${identity_hex}" identity_hex)
string(SUBSTRING "${identity_hex}" 36 8 identity_vid_pid)
if(NOT identity_vid_pid STREQUAL probe_vid_pid)
message(FATAL_ERROR "${prefix}_IDENTITY_FILE must match selected model ${probe_model}")
endif()
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," identity_bytes "${identity_hex}")
string(APPEND identity_rows " {${identity_bytes}},\n")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${${prefix}_IDENTITY_FILE}")
endif()
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," factory_bytes "${factory_hex}")
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," user_calibration_bytes "${user_calibration_hex}")
if(${prefix}_VERSION_FILE)
if(NOT ${prefix}_IDENTITY_FILE)
message(FATAL_ERROR "${prefix}_VERSION_FILE requires the matching identity capture")
endif()
file(READ "${${prefix}_VERSION_FILE}" version_hex LIMIT 13 HEX)
string(LENGTH "${version_hex}" version_length)
if(NOT version_length EQUAL 24)
message(FATAL_ERROR "${prefix}_VERSION_FILE must contain exactly 12 bytes")
endif()
string(TOLOWER "${version_hex}" version_hex)
string(SUBSTRING "${version_hex}" 6 2 firmware_type)
if(NOT firmware_type STREQUAL probe_firmware_type)
message(FATAL_ERROR "${prefix}_VERSION_FILE must describe selected model ${probe_model}")
endif()
string(REPLACE ":" "" address_hex "${${prefix}_CONTROLLER_ADDRESS}")
string(TOLOWER "${address_hex}" address_hex)
string(LENGTH "${address_hex}" address_length)
if(NOT address_length EQUAL 12 OR NOT address_hex MATCHES "^[0-9a-f]+$"
OR address_hex STREQUAL "000000000000" OR address_hex STREQUAL "ffffffffffff")
message(FATAL_ERROR "Provide ${prefix}_CONTROLLER_ADDRESS as a six-byte advertised Bluetooth address")
endif()
if(address_hex IN_LIST controller_addresses)
message(FATAL_ERROR "Composite advertised controller addresses must be distinct")
endif()
list(APPEND controller_addresses "${address_hex}")
set(address_reversed "")
foreach(byte RANGE 0 5)
math(EXPR position "10 - 2 * ${byte}")
string(SUBSTRING "${address_hex}" ${position} 2 octet)
string(APPEND address_reversed "${octet}")
endforeach()
string(SUBSTRING "${version_hex}" 0 6 main_version)
string(SUBSTRING "${version_hex}" 8 6 bluetooth_version)
# Layout corroborated against two genuine USB vendor-02 responses and their
# matching command-10 version and command-15 address responses.
set(status_hex "${main_version}000000${bluetooth_version}00${address_reversed}")
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," status_bytes "${status_hex}")
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," firmware_bytes "${version_hex}")
string(APPEND status_rows " {${status_bytes}},\n")
string(APPEND firmware_rows " {${firmware_bytes}},\n")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${${prefix}_VERSION_FILE}")
endif()
if(${prefix}_FACTORY_FILE OR ${prefix}_USER_CALIBRATION_FILE)
if(NOT SWITCH2_PROBE_USB_INIT OR NOT ${prefix}_FACTORY_FILE OR NOT ${prefix}_USER_CALIBRATION_FILE)
message(FATAL_ERROR "${prefix} memory replies require initialized USB and both calibration captures")
endif()
file(READ "${${prefix}_FACTORY_FILE}" factory_hex LIMIT 8193 HEX)
file(READ "${${prefix}_USER_CALIBRATION_FILE}" user_calibration_hex LIMIT 4097 HEX)
string(LENGTH "${factory_hex}" factory_length)
string(LENGTH "${user_calibration_hex}" user_calibration_length)
if(NOT factory_length EQUAL 16384 OR NOT user_calibration_length EQUAL 8192)
message(FATAL_ERROR "${prefix} factory/user captures must contain exactly 8192/4096 bytes")
endif()
string(TOLOWER "${factory_hex}" factory_hex)
string(TOLOWER "${user_calibration_hex}" user_calibration_hex)
string(SUBSTRING "${factory_hex}" 0 128 factory_identity_hex)
if(NOT factory_identity_hex STREQUAL identity_hex)
message(FATAL_ERROR "${prefix} factory memory identity must match the vendor-control identity")
endif()
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," factory_bytes "${factory_hex}")
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," user_calibration_bytes "${user_calibration_hex}")
string(APPEND factory_rows " {${factory_bytes}},\n")
string(APPEND user_calibration_rows " {${user_calibration_bytes}},\n")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
"${${prefix}_FACTORY_FILE}" "${${prefix}_USER_CALIBRATION_FILE}")
endif()
endforeach()
set(capture_header "// Generated from private, read-only controller captures; do not commit.\n#include <stdint.h>\n#include \"model.h\"\n")
if(SWITCH2_PROBE_IDENTITY_FILE)
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/probe_identity.h"
"${capture_header}static const uint8_t probe_identity_replies[PROBE_CONTROLLER_COUNT][64] = {\n${identity_rows}};\n")
target_compile_definitions(${target} PRIVATE SWITCH2_PROBE_IDENTITY_REPLY=1)
endif()
if(SWITCH2_PROBE_VERSION_FILE)
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/probe_version.h"
"${capture_header}static const uint8_t probe_version_replies[PROBE_CONTROLLER_COUNT][16] = {\n${status_rows}};\nstatic const uint8_t probe_firmware_versions[PROBE_CONTROLLER_COUNT][12] = {\n${firmware_rows}};\n")
target_compile_definitions(${target} PRIVATE SWITCH2_PROBE_VERSION_REPLY=1)
endif()
if(SWITCH2_PROBE_FACTORY_FILE)
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/probe_memory_data.h"
"// Generated from private calibration captures; do not commit.\n#include <stdint.h>\nstatic const uint8_t probe_factory_memory[8192] = {${factory_bytes}};\nstatic const uint8_t probe_user_calibration[4096] = {${user_calibration_bytes}};\n")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
"${SWITCH2_PROBE_FACTORY_FILE}" "${SWITCH2_PROBE_USER_CALIBRATION_FILE}")
"${capture_header}static const uint8_t probe_factory_memories[PROBE_CONTROLLER_COUNT][8192] = {\n${factory_rows}};\nstatic const uint8_t probe_user_calibrations[PROBE_CONTROLLER_COUNT][4096] = {\n${user_calibration_rows}};\n")
list(APPEND probe_sources ${SWITCH2_USB_PROBE_DIR}/memory.c)
target_compile_definitions(${target} PRIVATE SWITCH2_PROBE_MEMORY=1)
endif()
@ -189,17 +314,69 @@ function(switch2_usb_probe_configure target)
target_compile_options(${target} PRIVATE ${probe_compile_options})
endif()
target_link_libraries(${target} PRIVATE pico_stdlib hardware_uart hardware_sync
hardware_flash pico_flash pico_mbedtls_crypto pico_mbedtls_headers tinyusb_device)
hardware_flash pico_flash pico_mbedtls_crypto pico_mbedtls_headers)
if(SWITCH2_PROBE_HUB)
target_sources(${target} PRIVATE
${SWITCH2_USB_PROBE_DIR}/../pico_usb_address_probe/router.c
${SWITCH2_USB_PROBE_DIR}/../../src/firmware/usb/native_hub/native_hub.c)
target_include_directories(${target} PRIVATE
${SWITCH2_USB_PROBE_DIR}/../pico_usb_address_probe
${PICO_SDK_PATH}/lib/tinyusb/src)
target_compile_definitions(${target} PRIVATE CFG_TUSB_MCU=OPT_MCU_RP2040)
target_link_libraries(${target} PRIVATE pico_multicore pico_unique_id hardware_irq hardware_resets)
set_source_files_properties(
${SWITCH2_USB_PROBE_DIR}/../pico_usb_address_probe/router.c
${SWITCH2_USB_PROBE_DIR}/../../src/firmware/usb/native_hub/native_hub.c
PROPERTIES COMPILE_OPTIONS "-O3;-fno-jump-tables;-Wall;-Wextra;-Werror")
# Core0 now owns the Bluetooth call stack. Reserve16KiB from main
# SRAM instead of overflowing the SDK's4KiB scratch stack region.
set(default_linker "${PICO_SDK_PATH}/src/rp2_common/pico_crt0/rp2350/memmap_default.ld")
file(READ "${default_linker}" hub_linker)
string(REPLACE "RAM(rwx) : ORIGIN = 0x20000000, LENGTH = 512k"
"RAM(rwx) : ORIGIN = 0x20000000, LENGTH = 496k\n MAIN_STACK(rwx) : ORIGIN = 0x2007c000, LENGTH = 16k"
hub_linker "${hub_linker}")
string(REPLACE "KEEP(*(.stack*))\n } > SCRATCH_Y"
"KEEP(*(.stack*))\n } > MAIN_STACK" hub_linker "${hub_linker}")
string(REPLACE "__StackTop = ORIGIN(SCRATCH_Y) + LENGTH(SCRATCH_Y);"
"__StackTop = ORIGIN(MAIN_STACK) + LENGTH(MAIN_STACK);" hub_linker "${hub_linker}")
if(NOT hub_linker MATCHES "MAIN_STACK")
message(FATAL_ERROR "SDK linker stack layout changed")
endif()
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/native_hub_stack.ld" "${hub_linker}")
pico_set_linker_script(${target} "${CMAKE_CURRENT_BINARY_DIR}/native_hub_stack.ld")
else()
target_link_libraries(${target} PRIVATE tinyusb_device)
endif()
pico_enable_stdio_usb(${target} 0)
pico_enable_stdio_uart(${target} 1)
if(SWITCH2_BRIDGE_WII_INPUT)
if(SWITCH2_PROBE_HUB)
pico_set_program_name(${target} "Native Joy-Con 2 R and L stock USB hub bridge")
elseif(SWITCH2_PROBE_COMPOSITE)
pico_set_program_name(${target} "Switch 2 right and left Joy-Con composite bridge")
elseif(SWITCH2_BRIDGE_WII_INPUT)
pico_set_program_name(${target} "Switch 2 Wii IR and native motion bridge")
elseif(SWITCH_PICO_SWITCH2_USB_BRIDGE AND probe_joycon_left)
pico_set_program_name(${target} "Switch 2 left Joy-Con Bluetooth bridge")
elseif(SWITCH_PICO_SWITCH2_USB_BRIDGE)
pico_set_program_name(${target} "Switch 2 right Joy-Con Bluetooth bridge")
else()
pico_set_program_name(${target} "Switch 2 USB initialization capture")
endif()
if(SWITCH2_PROBE_OMIT_NATIVE_IMU)
if(SWITCH2_PROBE_HUB)
pico_set_program_version(${target} "0.66-native-hub-input")
elseif(SWITCH2_PROBE_JOIN_CHORD_GATE)
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
pico_set_program_version(${target} "0.37-pair-chord-trace")
else()
pico_set_program_version(${target} "0.37-pair-chord")
endif()
elseif(SWITCH2_PROBE_COMPOSITE)
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
pico_set_program_version(${target} "0.35-pair-trace")
else()
pico_set_program_version(${target} "0.35-pair")
endif()
elseif(SWITCH2_PROBE_OMIT_NATIVE_IMU)
pico_set_program_version(${target} "0.24-no-imu")
elseif(SWITCH2_PROBE_ZERO_NATIVE_IMU_PAYLOAD)
pico_set_program_version(${target} "0.24-zero-imu-payload")
@ -209,6 +386,12 @@ function(switch2_usb_probe_configure target)
else()
pico_set_program_version(${target} "0.33-wii")
endif()
elseif(SWITCH_PICO_SWITCH2_USB_BRIDGE AND probe_joycon_left)
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
pico_set_program_version(${target} "0.34-left-trace")
else()
pico_set_program_version(${target} "0.34-left")
endif()
elseif(SWITCH_PICO_SWITCH2_USB_BRIDGE)
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
pico_set_program_version(${target} "0.25-trace")

View file

@ -3,7 +3,7 @@
#include <string.h>
// Wire contracts: ndeadly/switch2_controller_research commands.md (03/0D,
// 03/0A, 07/01, 09/01-08, 16/01, 15/01-04) and hid_reports.md (05/08). USB reply headers
// 03/0A, 07/01, 09/01-08, 16/01, 15/01-04) and hid_reports.md (05/07/08). USB reply headers
// and status payloads match captures/usb/rumble-procon-gccon.pcapng.gz.
// This public component is not a pairing key. The host supplies the other half.
static const uint8_t device_key_component[16] = {
@ -71,7 +71,7 @@ static bool finalize_pairing(probe_protocol_state* state, const uint8_t* key) {
memcpy(blob + 7, state->pending_host_addresses, 6u * state->pending_host_count);
memcpy(blob + sizeof(blob) - 16u, key, 16);
// Preserve both the old committed key and pending retry on any save failure.
if (!state->save_pairing(blob, sizeof(blob))) return false;
if (!state->save_pairing(state->context, blob, sizeof(blob))) return false;
state->committed_host_count = blob[6];
memcpy(state->committed_host_addresses, blob + 7, sizeof(state->committed_host_addresses));
memcpy(state->committed_key, blob + sizeof(blob) - 16u, sizeof(state->committed_key));
@ -81,11 +81,12 @@ static bool finalize_pairing(probe_protocol_state* state, const uint8_t* key) {
return true;
}
void probe_protocol_reset(probe_protocol_state* state) {
void probe_protocol_reset(probe_protocol_state* state, bool is_left) {
memset(state, 0, sizeof(*state));
state->report_id = 0x08;
state->right_stick_center[1] = 0x08;
state->right_stick_center[2] = 0x80;
state->is_left = is_left;
state->report_id = is_left ? 0x07 : 0x08;
state->stick_center[1] = 0x08;
state->stick_center[2] = 0x80;
}
bool probe_protocol_restore_pairing(probe_protocol_state* state,
@ -226,14 +227,14 @@ size_t probe_protocol_command(probe_protocol_state* state, const uint8_t* comman
if (capacity < reply_length) return 0;
if (vibration_sample) {
uint64_t token = 0;
if (!state->play_sample(command[8], &token) || !token) return 0;
if (!state->play_sample(state->context, command[8], &token) || !token) return 0;
*deferred_token = token;
}
uint8_t encrypted_challenge[16];
if (confirm_key && !challenge_response(pairing_key, command + 9, encrypted_challenge)) return 0;
if (finalize && !finalize_pairing(state, pairing_key)) return 0;
if (memory_read &&
!state->read_memory(memory_address, reply + 16, memory_length)) return 0;
!state->read_memory(state->context, memory_address, reply + 16, memory_length)) return 0;
const uint8_t header[] = {command[0], 0x01, 0, command[3], 0, 0xf8, 0, 0};
memcpy(reply, header, sizeof(header));
if (info11_03) {
@ -255,7 +256,8 @@ size_t probe_protocol_command(probe_protocol_state* state, const uint8_t* comman
reply[8] = 1;
} else if (select_report) {
// The real controller acknowledges but ignores unsupported report IDs.
if (command[8] == 0x05 || command[8] == 0x08) state->report_id = command[8];
if (command[8] == 0x05 || command[8] == (state->is_left ? 0x07 : 0x08))
state->report_id = command[8];
} else if (exchange_addresses) {
if (length != 8) {
clear_pending_pairing(state);
@ -337,34 +339,48 @@ size_t probe_protocol_command(probe_protocol_state* state, const uint8_t* comman
size_t probe_protocol_report(const probe_protocol_state* state, uint8_t report_id,
uint8_t* output, size_t capacity) {
if (!state || !state->initialized || !output || capacity < PROBE_INPUT_SIZE ||
(report_id != 0x05 && report_id != 0x08)) return 0;
(report_id != 0x05 && report_id != (state->is_left ? 0x07 : 0x08))) return 0;
memset(output, 0, PROBE_INPUT_SIZE);
const bool buttons_enabled = (state->enabled_features & 1) != 0;
const uint8_t buttons0 = state->controller_active && buttons_enabled ? state->controller_buttons[0] : 0;
const uint8_t buttons1 = state->controller_active && buttons_enabled ? state->controller_buttons[1] & 0xd1 : 0;
const uint8_t buttons1 = state->controller_active && buttons_enabled ?
state->controller_buttons[1] & (state->is_left ? 0xc1 : 0xd1) : 0;
const uint8_t* stick = state->controller_active && (state->enabled_features & 2) ?
state->controller_stick : state->right_stick_center;
if (report_id == 0x08) {
state->controller_stick : state->stick_center;
if (report_id != 0x05) {
output[0] = (uint8_t)state->report_counter;
output[1] = 0x25; // Virtual full battery, external USB power.
output[2] = buttons0;
output[3] = buttons1;
if (state->test_rail_buttons && (state->enabled_features & 1))
output[3] |= 0xc0; // Joy-Con R native SL + SR.
output[3] |= 0xc0; // Both models' native SL + SR.
output[4] = 0x07;
memcpy(output + 5, stick, 3);
// Diagnostic snapshot only; complete live native packets bypass this generator.
} else {
for (unsigned i = 0; i < 4; ++i) output[i] = (uint8_t)(state->report_counter >> (8 * i));
output[4] = (uint8_t)(((buttons0 & 0x03) << 2) | ((buttons0 & 0x0c) >> 2) |
((buttons0 & 0x30) << 2) | ((buttons1 & 0xc0) >> 2));
output[5] = (uint8_t)(((buttons0 & 0xc0) >> 5) | ((buttons1 & 0x01) << 4) |
((buttons1 & 0x10) << 2));
if (state->test_rail_buttons && (state->enabled_features & 1))
output[4] |= 0x30; // Common report: right SL + SR.
output[11] = 0x08;
output[12] = 0x80;
memcpy(output + 13, stick, 3);
if (state->is_left) {
output[5] = (uint8_t)(((buttons0 & 0x40) >> 6) | ((buttons0 & 0x80) >> 4) |
((buttons1 & 0x01) << 5));
output[6] = (uint8_t)((buttons0 & 0x01) | ((buttons0 & 0x06) << 1) |
((buttons0 & 0x08) >> 2) | ((buttons0 & 0x30) << 2) |
((buttons1 & 0xc0) >> 2));
if (state->test_rail_buttons && buttons_enabled)
output[6] |= 0x30; // Common report: left SL + SR.
memcpy(output + 10, stick, 3);
output[14] = 0x08;
output[15] = 0x80;
} else {
output[4] = (uint8_t)(((buttons0 & 0x03) << 2) | ((buttons0 & 0x0c) >> 2) |
((buttons0 & 0x30) << 2) | ((buttons1 & 0xc0) >> 2));
output[5] = (uint8_t)(((buttons0 & 0xc0) >> 5) | ((buttons1 & 0x01) << 4) |
((buttons1 & 0x10) << 2));
if (state->test_rail_buttons && buttons_enabled)
output[4] |= 0x30; // Common report: right SL + SR.
output[11] = 0x08;
output[12] = 0x80;
memcpy(output + 13, stick, 3);
}
output[31] = 0xa0;
output[32] = 0x0f; // Virtual battery voltage 4000mV.
output[33] = 0x20;
@ -372,3 +388,22 @@ size_t probe_protocol_report(const probe_protocol_state* state, uint8_t report_i
}
return PROBE_INPUT_SIZE;
}
void probe_protocol_gate_native_report(const probe_protocol_state* state,
uint8_t input[PROBE_INPUT_SIZE]) {
const uint8_t imu_length_offset = state->is_left ? 14u : 15u;
if (!(state->enabled_features & 1)) memset(input + 2, 0, 2);
if (!(state->enabled_features & 2))
memcpy(input + 5, state->stick_center, sizeof(state->stick_center));
if (!(state->enabled_features & 0x10)) memset(input + 9, 0, 5);
#ifdef SWITCH2_PROBE_OMIT_NATIVE_IMU
// Deliberate A/B fault injection: leave every other field and feature bit intact.
memset(input + imu_length_offset, 0, 41);
#elif defined(SWITCH2_PROBE_ZERO_NATIVE_IMU_PAYLOAD)
if (!(state->enabled_features & 4)) input[imu_length_offset] = 0;
memset(input + imu_length_offset + 1u, 0, 40); // Preserve enabled genuine length.
#else
if (!(state->enabled_features & 4))
memset(input + imu_length_offset, 0, 41);
#endif
}

View file

@ -2,6 +2,7 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "model.h"
#define PROBE_COMMAND_MAX_SIZE 263u
#define PROBE_REPLY_MAX_SIZE 96u
@ -10,13 +11,15 @@
#define PROBE_INPUT_SIZE 63u
typedef struct {
bool is_left;
void* context; // Caller-owned context shared by this state's callbacks.
bool initialized;
uint8_t report_id;
bool test_rail_buttons;
bool runtime03_0c; // Observed USB toggle; full semantics remain unknown.
uint8_t right_stick_center[3];
uint8_t stick_center[3];
bool controller_active;
uint8_t controller_buttons[2]; // Native right Joy-Con button ordering.
uint8_t controller_buttons[2]; // Selected model's native Joy-Con button ordering.
uint8_t controller_stick[3]; // Raw packed 12-bit axes from the selected donor.
uint8_t player_leds; // Virtual four-LED mask, exposed through UART diagnostics.
bool player_leds_flashing;
@ -39,15 +42,15 @@ typedef struct {
uint8_t committed_host_addresses[PROBE_HOST_MAX_ADDRESSES][6];
uint8_t committed_key[16]; // Standard AES byte order.
// Synchronous durable save; NULL disables successful finalization.
bool (*save_pairing)(const uint8_t* blob, size_t length);
bool (*read_memory)(uint32_t address, uint8_t* output, size_t length);
bool (*save_pairing)(void* context, const uint8_t* blob, size_t length);
bool (*read_memory)(void* context, uint32_t address, uint8_t* output, size_t length);
// Queue a physical sample, returning true only with a nonzero completion token.
// Acceptance is not a Bluetooth application ACK.
bool (*play_sample)(uint8_t sample_id, uint64_t* token);
bool (*play_sample)(void* context, uint8_t sample_id, uint64_t* token);
uint32_t report_counter;
} probe_protocol_state;
void probe_protocol_reset(probe_protocol_state* state);
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].
// Rejects other identities, invalid counts/padding/lengths without mutation.
// A successful restore replaces the committed record and clears pending state.
@ -66,3 +69,7 @@ size_t probe_protocol_command(probe_protocol_state* state, const uint8_t* comman
// Button/stick snapshot without relative mouse events; safe for GET_REPORT.
size_t probe_protocol_report(const probe_protocol_state* state, uint8_t report_id,
uint8_t* output, size_t capacity);
// Apply virtual feature gates to one complete native payload in place.
// Enabled mouse/motion and all opaque bytes remain unchanged.
void probe_protocol_gate_native_report(const probe_protocol_state* state,
uint8_t input[PROBE_INPUT_SIZE]);

View file

@ -1,4 +1,5 @@
#include "storage.h"
#include "model.h"
#include <string.h>
@ -16,13 +17,16 @@ namespace {
constexpr size_t kSlotCount = 2;
constexpr size_t kMaximumPayloadSize = 512;
constexpr size_t kStorageSize = kSlotCount * FLASH_SECTOR_SIZE;
constexpr size_t kReservedStorageSize = 2 * kStorageSize;
constexpr size_t kConfigurationStorageSize =
CONFIGURATION_STORAGE_COPY_COUNT * FLASH_SECTOR_SIZE;
constexpr size_t kConfigurationStorageOffset =
PICO_FLASH_BANK_STORAGE_OFFSET - kConfigurationStorageSize;
constexpr size_t kProfileStorageOffset =
kConfigurationStorageOffset - PROFILE_STORAGE_TOTAL_SIZE;
constexpr uint32_t kStorageOffset = kProfileStorageOffset - kStorageSize;
// Keep the original right bank adjacent to profiles; reserve the left bank below.
constexpr uint32_t kRightStorageOffset = kProfileStorageOffset - kStorageSize;
constexpr uint32_t kLeftStorageOffset = kRightStorageOffset - kStorageSize;
constexpr uint32_t kFlashSafeTimeoutMs = 5000;
constexpr uint32_t kFormatVersion = 1;
@ -66,9 +70,11 @@ static_assert(PROFILE_STORAGE_TOTAL_SIZE % FLASH_SECTOR_SIZE == 0);
static_assert(PICO_FLASH_BANK_STORAGE_OFFSET % FLASH_SECTOR_SIZE == 0);
static_assert(PICO_FLASH_BANK_STORAGE_OFFSET >=
kConfigurationStorageSize + PROFILE_STORAGE_TOTAL_SIZE +
kStorageSize,
kReservedStorageSize,
"pairing storage offset underflows flash");
static_assert(kStorageOffset + kStorageSize == kProfileStorageOffset);
static_assert(kLeftStorageOffset + kStorageSize == kRightStorageOffset);
static_assert(kRightStorageOffset + kStorageSize == kProfileStorageOffset);
static_assert(kLeftStorageOffset + kReservedStorageSize == kProfileStorageOffset);
static_assert(kProfileStorageOffset + PROFILE_STORAGE_TOTAL_SIZE ==
kConfigurationStorageOffset);
static_assert(kConfigurationStorageOffset + kConfigurationStorageSize ==
@ -119,22 +125,23 @@ bool is_erased(const uint8_t *bytes, size_t size) {
return true;
}
bool storage_region_available() {
bool storage_region_available(uint32_t storage_offset) {
const uintptr_t binary_end = reinterpret_cast<uintptr_t>(&__flash_binary_end);
return binary_end >= XIP_BASE &&
binary_end - XIP_BASE <= kStorageOffset &&
kStorageOffset % FLASH_SECTOR_SIZE == 0 &&
kStorageOffset <= PICO_FLASH_SIZE_BYTES &&
kStorageSize <= PICO_FLASH_SIZE_BYTES - kStorageOffset &&
kStorageOffset + kStorageSize == kProfileStorageOffset;
binary_end - XIP_BASE <= kLeftStorageOffset &&
storage_offset % FLASH_SECTOR_SIZE == 0 &&
storage_offset <= PICO_FLASH_SIZE_BYTES &&
kStorageSize <= PICO_FLASH_SIZE_BYTES - storage_offset &&
storage_offset >= kLeftStorageOffset &&
storage_offset + kStorageSize <= kProfileStorageOffset;
}
uint32_t slot_offset(size_t slot) {
return static_cast<uint32_t>(kStorageOffset + slot * FLASH_SECTOR_SIZE);
uint32_t slot_offset(uint32_t storage_offset, size_t slot) {
return static_cast<uint32_t>(storage_offset + slot * FLASH_SECTOR_SIZE);
}
const uint8_t *slot_bytes(size_t slot) {
return reinterpret_cast<const uint8_t *>(XIP_BASE + slot_offset(slot));
const uint8_t *slot_bytes(uint32_t storage_offset, size_t slot) {
return reinterpret_cast<const uint8_t *>(XIP_BASE + slot_offset(storage_offset, slot));
}
bool owner_valid(const uint8_t *bytes, uint32_t offset) {
@ -180,10 +187,10 @@ bool commit_valid(const uint8_t *bytes, uint32_t offset) {
FLASH_PAGE_SIZE - kDescriptorSize);
}
Slot inspect_slot(size_t index) {
const uint8_t *bytes = slot_bytes(index);
Slot inspect_slot(uint32_t storage_offset, size_t index) {
const uint8_t *bytes = slot_bytes(storage_offset, index);
Slot slot{SlotKind::Unknown, bytes, 0, 0};
if (!owner_valid(bytes, slot_offset(index))) {
if (!owner_valid(bytes, slot_offset(storage_offset, index))) {
if (is_erased(bytes, FLASH_SECTOR_SIZE)) {
slot.kind = SlotKind::Erased;
}
@ -198,7 +205,7 @@ Slot inspect_slot(size_t index) {
// A complete ownership page plus an erased tail proves ownership of the
// bounded body/commit area, even if either subsequent write was interrupted.
slot.kind = SlotKind::OwnedIncomplete;
if (body_valid(bytes) && commit_valid(bytes, slot_offset(index))) {
if (body_valid(bytes) && commit_valid(bytes, slot_offset(storage_offset, index))) {
slot.kind = SlotKind::Committed;
slot.generation = read_u32(bytes + kBodyOffset + 8);
slot.size = read_u32(bytes + kBodyOffset + 16);
@ -245,37 +252,37 @@ void perform_flash_mutation(void *context) {
// The caller has classified BOTH sectors before permitting any erase. Only
// the inactive, explicitly owned sector is passed here; the active one survives.
bool erase_slot(size_t index) {
if (index >= kSlotCount || !storage_region_available()) {
bool erase_slot(uint32_t storage_offset, size_t index) {
if (index >= kSlotCount || !storage_region_available(storage_offset)) {
return false;
}
FlashMutation mutation{slot_offset(index), nullptr};
FlashMutation mutation{slot_offset(storage_offset, index), nullptr};
return flash_safe_execute(perform_flash_mutation, &mutation,
kFlashSafeTimeoutMs) == PICO_OK &&
is_erased(slot_bytes(index), FLASH_SECTOR_SIZE);
is_erased(slot_bytes(storage_offset, index), FLASH_SECTOR_SIZE);
}
bool program_page(size_t index, size_t offset, const uint8_t *page) {
bool program_page(uint32_t storage_offset, size_t index, size_t offset, const uint8_t *page) {
if (index >= kSlotCount || offset % FLASH_PAGE_SIZE != 0 ||
offset > kRecordFootprint - FLASH_PAGE_SIZE ||
!storage_region_available() ||
!is_erased(slot_bytes(index) + offset, FLASH_PAGE_SIZE)) {
!storage_region_available(storage_offset) ||
!is_erased(slot_bytes(storage_offset, index) + offset, FLASH_PAGE_SIZE)) {
return false;
}
FlashMutation mutation{
static_cast<uint32_t>(slot_offset(index) + offset), page,
static_cast<uint32_t>(slot_offset(storage_offset, index) + offset), page,
};
return flash_safe_execute(perform_flash_mutation, &mutation,
kFlashSafeTimeoutMs) == PICO_OK &&
memcmp(slot_bytes(index) + offset, page, FLASH_PAGE_SIZE) == 0;
memcmp(slot_bytes(storage_offset, index) + offset, page, FLASH_PAGE_SIZE) == 0;
}
void prepare_record(size_t target, uint32_t generation,
void prepare_record(uint32_t storage_offset, size_t target, uint32_t generation,
const uint8_t *data, size_t size) {
memset(staging, 0xff, sizeof(staging));
memcpy(staging, kOwnerMagic, sizeof(kOwnerMagic));
write_u32(staging + 16, kFormatVersion);
write_u32(staging + 20, slot_offset(target));
write_u32(staging + 20, slot_offset(storage_offset, target));
write_u32(staging + 24, kMaximumPayloadSize);
write_u32(staging + 28, FLASH_PAGE_SIZE);
write_u32(staging + 32, FLASH_SECTOR_SIZE);
@ -300,19 +307,23 @@ void prepare_record(size_t target, uint32_t generation,
write_u32(commit + 20, header_crc);
write_u32(commit + 24, payload_crc);
write_u32(commit + 28, static_cast<uint32_t>(size));
write_u32(commit + 32, slot_offset(target));
write_u32(commit + 32, slot_offset(storage_offset, target));
write_u32(commit + kDescriptorCrcOffset,
configuration_crc32(commit, kDescriptorCrcOffset));
}
} // namespace
bool probe_storage_load(uint8_t *output, size_t size) {
bool probe_storage_load(uint8_t instance, uint8_t *output, size_t size) {
if (instance >= PROBE_CONTROLLER_COUNT) return false;
const uint32_t storage_offset = probe_storage_offset(instance);
if (output == nullptr || size == 0 || size > kMaximumPayloadSize ||
!storage_region_available()) {
!storage_region_available(storage_offset)) {
return false;
}
const Slot slots[kSlotCount] = {inspect_slot(0), inspect_slot(1)};
const Slot slots[kSlotCount] = {
inspect_slot(storage_offset, 0), inspect_slot(storage_offset, 1),
};
int active;
if (!newest_slot(slots, &active) || active < 0 || slots[active].size != size) {
return false;
@ -321,12 +332,16 @@ bool probe_storage_load(uint8_t *output, size_t size) {
return true;
}
bool probe_storage_save(const uint8_t *data, size_t size) {
bool probe_storage_save(uint8_t instance, const uint8_t *data, size_t size) {
if (instance >= PROBE_CONTROLLER_COUNT) return false;
const uint32_t storage_offset = probe_storage_offset(instance);
if (data == nullptr || size == 0 || size > kMaximumPayloadSize ||
!storage_region_available()) {
!storage_region_available(storage_offset)) {
return false;
}
const Slot slots[kSlotCount] = {inspect_slot(0), inspect_slot(1)};
const Slot slots[kSlotCount] = {
inspect_slot(storage_offset, 0), inspect_slot(storage_offset, 1),
};
if (slots[0].kind == SlotKind::Unknown || slots[1].kind == SlotKind::Unknown) {
return false; // Never erase through an unrecognized region.
}
@ -342,32 +357,33 @@ bool probe_storage_save(const uint8_t *data, size_t size) {
? static_cast<size_t>(active) ^ 1u
: (slots[0].kind == SlotKind::Erased ? 0u : 1u);
const uint32_t generation = active >= 0 ? slots[active].generation + 1u : 1u;
prepare_record(target, generation, data, size);
prepare_record(storage_offset, target, generation, data, size);
if (slots[target].kind != SlotKind::Erased && !erase_slot(target)) {
if (slots[target].kind != SlotKind::Erased && !erase_slot(storage_offset, target)) {
return false;
}
if (!program_page(target, 0, staging)) {
if (!program_page(storage_offset, target, 0, staging)) {
return false;
}
for (size_t offset = kBodyOffset; offset < kCommitOffset;
offset += FLASH_PAGE_SIZE) {
if (!is_erased(staging + offset, FLASH_PAGE_SIZE) &&
!program_page(target, offset, staging + offset)) {
!program_page(storage_offset, target, offset, staging + offset)) {
return false;
}
}
if (!body_valid(slot_bytes(target)) ||
!program_page(target, kCommitOffset, staging + kCommitOffset)) {
if (!body_valid(slot_bytes(storage_offset, target)) ||
!program_page(storage_offset, target, kCommitOffset, staging + kCommitOffset)) {
return false;
}
const Slot committed = inspect_slot(target);
const Slot committed = inspect_slot(storage_offset, target);
return committed.kind == SlotKind::Committed &&
committed.generation == generation && committed.size == size &&
memcmp(committed.bytes + kPayloadOffset,
staging + kPayloadOffset, size) == 0;
}
uint32_t probe_storage_offset(void) {
return kStorageOffset;
uint32_t probe_storage_offset(uint8_t instance) {
if (instance >= PROBE_CONTROLLER_COUNT) return UINT32_MAX;
return probe_model_is_left(instance) ? kLeftStorageOffset : kRightStorageOffset;
}

View file

@ -11,14 +11,18 @@ extern "C" {
// Synchronous, main-loop-only API for the single-core probe. Serialize calls.
// Blobs are opaque, nonempty, and at most 512 bytes. Load requires an exact
// length match and leaves output unchanged on failure; it never writes flash.
bool probe_storage_load(uint8_t *output, size_t size);
// Invalid instances fail before reading a bank or writing output.
bool probe_storage_load(uint8_t instance, uint8_t *output, size_t size);
// Success means an identical blob was already committed, or a replacement was
// committed and read back. Failure never authorizes a protocol acknowledgement.
bool probe_storage_save(const uint8_t *data, size_t size);
bool probe_storage_save(uint8_t instance, const uint8_t *data, size_t size);
// Flash-relative offset of the two sectors immediately below profile storage.
uint32_t probe_storage_offset(void);
// Flash-relative offset of the instance's two-sector pairing bank, or UINT32_MAX
// for an invalid instance. The right bank remains immediately below profile
// storage; the left bank occupies the preceding two sectors. Both are reserved
// in every build, and load/save inspect and mutate only the selected bank.
uint32_t probe_storage_offset(uint8_t instance);
#ifdef __cplusplus
}

View file

@ -0,0 +1,101 @@
#pragma once
#include "model.h"
#include "tusb.h"
#if SWITCH2_PROBE_HUB
#include "usb/native_hub/native_hub.h"
#endif
// Application instances are controllers, never native hub device slots.
// Only control transfers retain the transport's rhport/device-slot argument.
static inline bool probe_transport_mounted(uint8_t instance) {
#if SWITCH2_PROBE_HUB
return native_hub_mounted(instance);
#else
(void)instance;
return tud_mounted();
#endif
}
static inline bool probe_transport_suspended(uint8_t instance) {
#if SWITCH2_PROBE_HUB
return native_hub_suspended(instance);
#else
(void)instance;
return tud_suspended();
#endif
}
static inline bool probe_transport_hid_ready(uint8_t instance) {
#if SWITCH2_PROBE_HUB
return native_hub_hid_ready(instance);
#else
return tud_hid_n_ready(instance);
#endif
}
static inline bool probe_transport_hid_report(uint8_t instance, uint8_t report_id,
const void* data, uint16_t length) {
#if SWITCH2_PROBE_HUB
return native_hub_hid_report(instance, report_id, data, length);
#else
return tud_hid_n_report(instance, report_id, data, length);
#endif
}
static inline uint32_t probe_transport_vendor_write_available(uint8_t instance) {
#if SWITCH2_PROBE_HUB
return native_hub_vendor_write_available(instance);
#else
return tud_vendor_n_write_available(instance);
#endif
}
static inline uint32_t probe_transport_vendor_write(uint8_t instance,
const void* data, uint32_t length) {
#if SWITCH2_PROBE_HUB
return native_hub_vendor_write(instance, data, length);
#else
return tud_vendor_n_write(instance, data, length);
#endif
}
static inline uint32_t probe_transport_vendor_write_flush(uint8_t instance) {
#if SWITCH2_PROBE_HUB
return native_hub_vendor_write_flush(instance);
#else
return tud_vendor_n_write_flush(instance);
#endif
}
static inline void probe_transport_vendor_discard_received(uint8_t instance) {
#if SWITCH2_PROBE_HUB
// Native RX supplies the actual packet once, with no second receive FIFO.
(void)instance;
#else
// The application consumes the raw callback packet, not this duplicate.
uint8_t discarded[64];
while (tud_vendor_n_available(instance)) {
if (!tud_vendor_n_read(instance, discarded, sizeof(discarded))) break;
}
#endif
}
static inline bool probe_transport_control_xfer(uint8_t rhport,
const tusb_control_request_t* request,
void* buffer, uint16_t length) {
#if SWITCH2_PROBE_HUB
return native_hub_control_xfer(rhport, request, buffer, length);
#else
return tud_control_xfer(rhport, request, buffer, length);
#endif
}
static inline bool probe_transport_control_status(uint8_t rhport,
const tusb_control_request_t* request) {
#if SWITCH2_PROBE_HUB
return native_hub_control_status(rhport, request);
#else
return tud_control_status(rhport, request);
#endif
}

View file

@ -1,16 +1,18 @@
#pragma once
#include "model.h"
#define CFG_TUSB_RHPORT0_MODE (OPT_MODE_DEVICE | OPT_MODE_FULL_SPEED)
#ifndef CFG_TUSB_OS
#define CFG_TUSB_OS OPT_OS_NONE
#endif
#define CFG_TUD_ENDPOINT0_SIZE 64
#define CFG_TUD_HID 1
#define CFG_TUD_HID PROBE_CONTROLLER_COUNT
#define CFG_TUD_HID_EP_BUFSIZE 64
#define CFG_TUD_CDC 0
#define CFG_TUD_MSC 0
#define CFG_TUD_MIDI 0
#define CFG_TUD_VENDOR 1
#define CFG_TUD_VENDOR PROBE_CONTROLLER_COUNT
#define CFG_TUD_VENDOR_EPSIZE 64
#define CFG_TUD_VENDOR_RX_BUFSIZE 256
#define CFG_TUD_VENDOR_TX_BUFSIZE 256