Add stock-USB native Joy-Con R/L hub bridge
This commit is contained in:
parent
9f6dddb790
commit
1748910316
41 changed files with 7101 additions and 909 deletions
1062
tools/native_joycon_hub_check.py
Executable file
1062
tools/native_joycon_hub_check.py
Executable file
File diff suppressed because it is too large
Load diff
45
tools/pico_usb_address_probe/CMakeLists.txt
Normal file
45
tools/pico_usb_address_probe/CMakeLists.txt
Normal 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)
|
||||
288
tools/pico_usb_address_probe/host_probe.py
Executable file
288
tools/pico_usb_address_probe/host_probe.py
Executable 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())
|
||||
62
tools/pico_usb_address_probe/main.c
Normal file
62
tools/pico_usb_address_probe/main.c
Normal 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);
|
||||
}
|
||||
}
|
||||
738
tools/pico_usb_address_probe/router.c
Normal file
738
tools/pico_usb_address_probe/router.c
Normal 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
|
||||
}
|
||||
}
|
||||
48
tools/pico_usb_address_probe/router.h
Normal file
48
tools/pico_usb_address_probe/router.h
Normal 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);
|
||||
13
tools/pico_usb_address_probe/tusb_config.h
Normal file
13
tools/pico_usb_address_probe/tusb_config.h
Normal 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
|
||||
839
tools/pico_usb_address_probe/usb_probe.c
Normal file
839
tools/pico_usb_address_probe/usb_probe.c
Normal 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);
|
||||
}
|
||||
5
tools/pico_usb_address_probe/usb_probe.h
Normal file
5
tools/pico_usb_address_probe/usb_probe.h
Normal 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);
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
94
tools/switch2_usb_probe/model.h
Normal file
94
tools/switch2_usb_probe/model.h
Normal 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;
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
101
tools/switch2_usb_probe/transport.h
Normal file
101
tools/switch2_usb_probe/transport.h
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue