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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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