usbhubd: full Linux 7.1 hub enumeration state machine (P3-A)

Port the Linux 7.1 hub.c port state machine into usbhubd, replacing the
minimal connect/reset handling:

New module port_ops.rs (pure logic, side effects injected, 14 unit
tests):
- debounce_until_connected: hub_port_debounce_be_connected() port
  (hub.c:4696-4737) — 25ms polls, connection stable for 100ms, 2s
  budget, connection-change bit cleared in-loop.
- wait_for_reset: hub_port_wait_reset() port (hub.c:2953-3047) — 10ms
  polls until RESET clears with CONNECTION set, escalate to 200ms after
  two short waits, 800ms budget; then 50ms TRSTRCY recovery (hub.c:3159)
  and C_PORT_RESET clear. Replaces the previous bare sleep(10ms).
- wait_for_u0: USB 3.0 polling→U0 wait after port power-on — 36ms steps,
  400ms ceiling (tPollingLFPSTimeout = 360ms; Linux hub.c:1226 debounce
  path).
- accumulate_hub_delay_ns: wHubDelay chain rule (hub.c:1507-1519:
  wHubDelay + parent->hub_delay + 40ns, cap 65535ns).

main.rs wiring:
- Port status normalized to PortStatusSnapshot (decouples the state
  machine from the V2/V3 wire formats; V3 link state extracted from
  bits 8:5).
- Debounce on connection-change before attach; C_PORT_ENABLE cleared
  once handled (Linux port_event semantics).
- Reset path uses wait_for_reset instead of sleep(10ms).
- USB 3 power-on path waits for U0 before proceeding.
- wHubDelay: ancestor-chain walk fetching USB 3 ancestor hub
  descriptors, accumulated per Linux; delivered to newly attached
  SuperSpeed children via SET_ISOCH_DELAY (USB 3.0 9.4.11; Linux
  message.c:1142 — hubs and non-SS skipped, children inherit the hub's
  accumulated delay verbatim per hub.c:5128-5129).
- attach/detach failure logs now identify the port.

hub.rs (xhcid usb module):
- HubDescriptorV3 extended with device_removable: u16 — the SS hub
  descriptor is 12 bytes (spec Table 10-15); the old struct under-read
  by 2 bytes. Stale TODO corrected: SS descriptors have no
  PortPwrCtrlMask (that is USB 2.0-only, still unparsed).

Verified: cargo check clean (0 usbhubd warnings), 14/14 usbhubd tests,
xhcid unaffected (43/43 tests).
This commit is contained in:
Red Bear OS
2026-07-19 00:27:49 +09:00
parent 260003331e
commit bd379e0771
3 changed files with 785 additions and 105 deletions
+246 -97
View File
@@ -1,10 +1,108 @@
use std::{env, thread, time};
use xhcid_interface::{
plain, usb, ConfigureEndpointsReq, DevDesc, DeviceReqData, EndpointTy, EndpDesc,
EndpDirection, PortId, PortReqRecipient, PortReqTy, XhciClientHandle, XhciEndpHandle,
plain, usb, ConfigureEndpointsReq, DevDesc, DeviceReqData, EndpointTy, EndpDirection, PortId,
PortReqRecipient, PortReqTy, XhciClientHandle, XhciClientHandleError, XhciEndpHandle,
};
mod port_ops;
use port_ops::PortStatusSnapshot;
/// Issue GET_STATUS(Class, Other, port) on the hub's control pipe and
/// normalize the wire-format status into a `PortStatusSnapshot`.
/// Linux 7.1 hub.c:600 `get_port_status()`.
fn fetch_status(
handle: &XhciClientHandle,
port: u8,
usb_3: bool,
) -> Result<PortStatusSnapshot, XhciClientHandleError> {
if usb_3 {
let mut sts = usb::HubPortStatusV3::default();
handle.device_request(
PortReqTy::Class,
PortReqRecipient::Other,
usb::SetupReq::GetStatus as u8,
0,
port as u16,
DeviceReqData::In(unsafe { plain::as_mut_bytes(&mut sts) }),
)?;
Ok(PortStatusSnapshot::from(&usb::HubPortStatus::V3(sts)))
} else {
let mut sts = usb::HubPortStatusV2::default();
handle.device_request(
PortReqTy::Class,
PortReqRecipient::Other,
usb::SetupReq::GetStatus as u8,
0,
port as u16,
DeviceReqData::In(unsafe { plain::as_mut_bytes(&mut sts) }),
)?;
Ok(PortStatusSnapshot::from(&usb::HubPortStatus::V2(sts)))
}
}
/// SET_FEATURE(Class, Other, feature, port). Linux `set_port_feature()`.
fn set_feature(
handle: &XhciClientHandle,
port: u8,
feature: usb::HubPortFeature,
) -> Result<(), XhciClientHandleError> {
handle.device_request(
PortReqTy::Class,
PortReqRecipient::Other,
usb::SetupReq::SetFeature as u8,
feature as u16,
port as u16,
DeviceReqData::NoData,
)
}
/// CLEAR_FEATURE(Class, Other, feature, port). Linux `clear_port_feature()`.
fn clear_feature(
handle: &XhciClientHandle,
port: u8,
feature: usb::HubPortFeature,
) -> Result<(), XhciClientHandleError> {
handle.device_request(
PortReqTy::Class,
PortReqRecipient::Other,
usb::SetupReq::ClearFeature as u8,
feature as u16,
port as u16,
DeviceReqData::NoData,
)
}
/// Deliver the accumulated hub delay to a newly attached SuperSpeed child
/// via SET_ISOCH_DELAY (USB 3.0 §9.4.11). Mirrors Linux 7.1
/// `usb_set_isoch_delay()` (message.c:1142-1158): skipped for hubs (a
/// child hub propagates the chain through its own daemon) and non-SS
/// devices. The value is this hub's accumulated `hub_delay` — children
/// inherit it verbatim (hub.c:5128-5129).
fn maybe_send_isoch_delay(handle: &XhciClientHandle, hub_delay_ns: u32) {
let Ok(desc) = handle.get_standard_descs() else {
return;
};
const USB_CLASS_HUB: u8 = 0x09;
if desc.class == USB_CLASS_HUB || desc.major_version() < 3 {
return;
}
let delay = hub_delay_ns.min(port_ops::USB_TP_TRANSMISSION_DELAY_MAX_NS) as u16;
const SET_ISOCH_DELAY: u8 = 0x31;
match handle.device_request(
PortReqTy::Standard,
PortReqRecipient::Device,
SET_ISOCH_DELAY,
delay,
0,
DeviceReqData::NoData,
) {
Ok(()) => log::info!("usbhubd: SET_ISOCH_DELAY {} ns delivered", delay),
Err(e) => log::warn!("usbhubd: SET_ISOCH_DELAY({} ns) failed: {}", delay, e),
}
}
fn main() {
common::init();
let mut args = env::args().skip(1);
@@ -61,7 +159,7 @@ fn main() {
.expect("Failed to find suitable configuration");
// Read hub descriptor
let (ports, usb_3, b_pwr_on_2_pwr_good, w_hub_delay) = if desc.major_version() >= 3 {
let (ports, usb_3, b_pwr_on_2_pwr_good, w_hub_delay, device_removable) = if desc.major_version() >= 3 {
let mut hub_desc = usb::HubDescriptorV3::default();
handle
.device_request(
@@ -80,6 +178,7 @@ fn main() {
// the spec says to use 10 (20ms) as default.
10u8,
u16::from(hub_desc.delay),
hub_desc.device_removable,
)
} else {
let mut hub_desc = usb::HubDescriptorV2::default();
@@ -98,6 +197,9 @@ fn main() {
false,
hub_desc.power_on_good,
0u16, // wHubDelay only exists in USB 3 hub descriptors
// USB 2.0 device_removable bitmaps are variable-length and
// not parsed (see hub.rs).
0u16,
)
};
@@ -105,6 +207,65 @@ fn main() {
"usbhubd: {} port(s) detected, PwrOn2PwrGood={}*2ms, wHubDelay={}",
ports, b_pwr_on_2_pwr_good, w_hub_delay,
);
if device_removable != 0 {
let fixed: Vec<u8> = (1..=ports)
.filter(|&p| (device_removable >> (p - 1)) & 1 == 1)
.collect();
log::info!("usbhubd: non-removable (integrated) ports: {:?}", fixed);
}
// wHubDelay chain accumulation (Linux 7.1 hub.c:1507-1519):
// hub_delay = wHubDelay + parent->hub_delay + tTPTransmissionDelay(40ns),
// clamped to 65535 ns. Walk the PortId ancestor chain so multi-tier
// topologies accumulate correctly; each USB 3 ancestor hub contributes
// its own wHubDelay plus one transmission-delay hop. Children behind
// this hub inherit the result via SET_ISOCH_DELAY at attach time.
let hub_delay_ns = if usb_3 {
let mut ancestors: Vec<PortId> = Vec::new();
let mut cur = port_id.parent();
while let Some((p, _port_num_on_parent)) = cur {
ancestors.push(p);
cur = p.parent();
}
ancestors.reverse(); // root-first, matching Linux's recursive chain
let mut acc = 0u32;
for ancestor in ancestors {
let Ok(ah) = XhciClientHandle::new(scheme.clone(), ancestor) else {
continue;
};
let Ok(adesc) = ah.get_standard_descs() else {
continue;
};
const USB_CLASS_HUB: u8 = 0x09;
if adesc.class != USB_CLASS_HUB || adesc.major_version() < 3 {
continue;
}
let mut ancestor_desc = usb::HubDescriptorV3::default();
if ah
.device_request(
PortReqTy::Class,
PortReqRecipient::Device,
usb::SetupReq::GetDescriptor as u8,
u16::from(usb::HubDescriptorV3::DESCRIPTOR_KIND) << 8,
0,
DeviceReqData::In(unsafe { plain::as_mut_bytes(&mut ancestor_desc) }),
)
.is_ok()
{
acc = port_ops::accumulate_hub_delay_ns(u32::from(u16::from(ancestor_desc.delay)), acc);
}
}
let total = port_ops::accumulate_hub_delay_ns(u32::from(w_hub_delay), acc);
log::info!(
"usbhubd: accumulated hub delay {} ns (depth {})",
total,
port_id.hub_depth()
);
total
} else {
0
};
// Configure as hub device. USB 3 hubs do not need explicit
// interface_desc / alternate_setting in ConfigureEndpointsReq —
@@ -172,7 +333,7 @@ fn main() {
// Initialize states
struct PortState {
port_id: PortId,
port_sts: usb::HubPortStatus,
last_status: Option<PortStatusSnapshot>,
handle: XhciClientHandle,
attached: bool,
}
@@ -187,7 +348,7 @@ fn main() {
match self.handle.attach() {
Ok(()) => {}
Err(e) => {
log::warn!("usbhubd: attach failed for port: {}", e);
log::warn!("usbhubd: attach failed for port {}: {}", self.port_id, e);
return;
}
}
@@ -195,7 +356,7 @@ fn main() {
match self.handle.detach() {
Ok(()) => {}
Err(e) => {
log::warn!("usbhubd: detach failed for port: {}", e);
log::warn!("usbhubd: detach failed for port {}: {}", self.port_id, e);
return;
}
}
@@ -210,11 +371,7 @@ fn main() {
let child_port_id = port_id.child(port).expect("Cannot get child port ID");
states.push(PortState {
port_id: child_port_id,
port_sts: if usb_3 {
usb::HubPortStatus::V3(usb::HubPortStatusV3::default())
} else {
usb::HubPortStatus::V2(usb::HubPortStatusV2::default())
},
last_status: None,
handle: XhciClientHandle::new(scheme.clone(), child_port_id)
.expect("Failed to open XhciClientHandle"),
attached: false,
@@ -278,87 +435,50 @@ fn main() {
None => continue,
};
let port_sts = if usb_3 {
let mut port_sts = usb::HubPortStatusV3::default();
match handle.device_request(
PortReqTy::Class,
PortReqRecipient::Other,
usb::SetupReq::GetStatus as u8,
0,
port as u16,
DeviceReqData::In(unsafe { plain::as_mut_bytes(&mut port_sts) }),
) {
Ok(()) => usb::HubPortStatus::V3(port_sts),
Err(e) => {
log::warn!("usbhubd: GetPortStatus failed for port {}: {} — detaching", port, e);
state.ensure_attached(false);
continue;
}
}
} else {
let mut port_sts = usb::HubPortStatusV2::default();
match handle.device_request(
PortReqTy::Class,
PortReqRecipient::Other,
usb::SetupReq::GetStatus as u8,
0,
port as u16,
DeviceReqData::In(unsafe { plain::as_mut_bytes(&mut port_sts) }),
) {
Ok(()) => usb::HubPortStatus::V2(port_sts),
Err(e) => {
log::warn!("usbhubd: GetPortStatus failed for port {}: {} — detaching", port, e);
state.ensure_attached(false);
continue;
}
let mut snap = match fetch_status(&handle, port, usb_3) {
Ok(s) => s,
Err(e) => {
log::warn!("usbhubd: GetPortStatus failed for port {}: {} — detaching", port, e);
state.ensure_attached(false);
continue;
}
};
if state.port_sts != port_sts {
state.port_sts = port_sts;
log::info!("port {} status {:X?}", port, port_sts);
if state.last_status != Some(snap) {
state.last_status = Some(snap);
log::info!("port {} status {:?}", port, snap);
}
// Ensure port is powered on.
// Linux 7.1 hub_power_on(): issue SET_FEATURE(PORT_POWER),
// then sleep bPwrOn2PwrGood * 2ms (minimum 100ms).
if !port_sts.is_powered() {
// then sleep max(bPwrOn2PwrGood * 2ms, 100ms).
if !snap.powered {
log::info!("power on port {port}");
if let Err(e) = handle.device_request(
PortReqTy::Class,
PortReqRecipient::Other,
usb::SetupReq::SetFeature as u8,
usb::HubPortFeature::PortPower as u16,
port as u16,
DeviceReqData::NoData,
) {
if let Err(e) = set_feature(&handle, port, usb::HubPortFeature::PortPower) {
log::warn!("usbhubd: SetPortPower failed for port {}: {}", port, e);
continue;
}
state.ensure_attached(false);
thread::sleep(time::Duration::from_millis(power_on_delay_ms));
// USB 3.0: the link can sit in Polling after power-on;
// wait for it to reach U0 before touching the port
// (tPollingLFPSTimeout = 360ms; Linux hub.c:1226).
if usb_3 {
if let Err(e) = port_ops::wait_for_u0(
| | fetch_status(&handle, port, true),
|ms| thread::sleep(time::Duration::from_millis(ms)),
) {
log::warn!("usbhubd: port {} polling→U0 wait: {}", port, e);
}
}
continue;
}
// Linux 7.1: over-current detection and recovery.
// C_PORT_OVERCURRENT → log, clear, power-cycle.
if port_sts.is_over_current_changed() {
if snap.over_current_changed {
log::warn!("usbhubd: over-current change on port {} — power cycling", port);
let _ = handle.device_request(
PortReqTy::Class,
PortReqRecipient::Other,
usb::SetupReq::ClearFeature as u8,
usb::HubPortFeature::CPortOverCurrent as u16,
port as u16,
DeviceReqData::NoData,
);
let _ = handle.device_request(
PortReqTy::Class,
PortReqRecipient::Other,
usb::SetupReq::ClearFeature as u8,
usb::HubPortFeature::PortPower as u16,
port as u16,
DeviceReqData::NoData,
);
let _ = clear_feature(&handle, port, usb::HubPortFeature::CPortOverCurrent);
let _ = clear_feature(&handle, port, usb::HubPortFeature::PortPower);
state.ensure_attached(false);
thread::sleep(time::Duration::from_millis(power_on_delay_ms));
continue;
@@ -366,52 +486,81 @@ fn main() {
// Linux 7.1: port indicator — set amber during reset/power-on,
// green when enabled. Helps diagnose which port is active.
if port_sts.is_connected() && port_sts.is_enabled() && !port_sts.is_resetting() {
let _ = handle.device_request(
PortReqTy::Class,
PortReqRecipient::Other,
usb::SetupReq::SetFeature as u8,
usb::HubPortFeature::PortIndicator as u16,
port as u16,
DeviceReqData::NoData,
);
if snap.connected && snap.enabled && !snap.resetting {
let _ = set_feature(&handle, port, usb::HubPortFeature::PortIndicator);
}
// Ignore disconnected port
if !port_sts.is_connected() {
if !snap.connected {
state.ensure_attached(false);
continue;
}
// Ignore port in reset
if port_sts.is_resetting() {
if snap.resetting {
state.ensure_attached(false);
continue;
}
// Linux 7.1 hub_port_debounce_be_connected(): on a
// connection-change event, require the connection bit to stay
// stable for 100ms (25ms steps, 2s budget) before enumerating.
if snap.connection_changed && !state.attached {
match port_ops::debounce_until_connected(
| | fetch_status(&handle, port, usb_3),
| | clear_feature(&handle, port, usb::HubPortFeature::CPortConnection),
|ms| thread::sleep(time::Duration::from_millis(ms)),
) {
Ok(s) => {
snap = s;
}
Err(e) => {
log::warn!("usbhubd: port {} debounce: {}", port, e);
state.ensure_attached(false);
continue;
}
}
if !snap.connected {
state.ensure_attached(false);
continue;
}
}
// Ensure port is enabled.
// Linux 7.1 hub_port_reset(): issue SET_FEATURE(PORT_RESET),
// then wait for reset completion (up to USB_PORT_RESET_TIMEOUT
// = 5000ms for USB 3, 1000ms for USB 2).
if !port_sts.is_enabled() {
// then hub_port_wait_reset(): poll until RESET clears with
// CONNECTION set (10ms steps escalating to 200ms, 800ms budget),
// then 50ms TRSTRCY recovery and C_PORT_RESET clear.
if !snap.enabled {
log::info!("reset port {port}");
if let Err(e) = handle.device_request(
PortReqTy::Class,
PortReqRecipient::Other,
usb::SetupReq::SetFeature as u8,
usb::HubPortFeature::PortReset as u16,
port as u16,
DeviceReqData::NoData,
) {
if let Err(e) = set_feature(&handle, port, usb::HubPortFeature::PortReset) {
log::warn!("usbhubd: SetPortReset failed for port {}: {}", port, e);
continue;
}
state.ensure_attached(false);
thread::sleep(time::Duration::from_millis(10));
if let Err(e) = port_ops::wait_for_reset(
| | fetch_status(&handle, port, usb_3),
| | clear_feature(&handle, port, usb::HubPortFeature::CPortReset),
|ms| thread::sleep(time::Duration::from_millis(ms)),
) {
log::warn!("usbhubd: port {} reset wait: {}", port, e);
}
continue;
}
// Clear the enable-change bit once handled (Linux port_event).
if snap.enable_changed {
let _ = clear_feature(&handle, port, usb::HubPortFeature::CPortEnable);
}
let was_attached = state.attached;
state.ensure_attached(true);
// New SuperSpeed child: deliver the accumulated hub delay so the
// device can compensate isochronous scheduling (Linux
// usb_set_isoch_delay after enumeration, hub.c:5126-5131).
if !was_attached && state.attached && usb_3 && hub_delay_ns > 0 {
maybe_send_isoch_delay(&state.handle, hub_delay_ns);
}
}
}
}
+527
View File
@@ -0,0 +1,527 @@
//! Hub port state-machine operations ported from Linux 7.1
//! `drivers/usb/core/hub.c`.
//!
//! Pure logic with side effects injected as closures: production wiring
//! in `main.rs` supplies real hub-class control transfers and sleeps;
//! unit tests supply scripted fakes. Covered algorithms:
//! - `debounce_until_connected` — `hub_port_debounce_be_connected()` (hub.c:4696)
//! - `wait_for_reset` — `hub_port_wait_reset()` (hub.c:2953) + TRSTRCY recovery + C_PORT_RESET clear
//! - `wait_for_u0` — USB 3.0 polling→U0 wait after port power-on (hub.c:1226, tPollingLFPSTimeout)
//! - `accumulate_hub_delay_ns` — wHubDelay chain rule (hub.c:1507-1519)
use std::fmt;
use xhcid_interface::usb;
// ---- Linux 7.1 timing constants ----
/// hub.c:138 — maximum total debounce time.
pub const HUB_DEBOUNCE_TIMEOUT_MS: u64 = 2000;
/// hub.c:139 — debounce poll interval.
pub const HUB_DEBOUNCE_STEP_MS: u64 = 25;
/// hub.c:140 — required stable-connection duration.
pub const HUB_DEBOUNCE_STABLE_MS: u64 = 100;
/// hub.c:2902 — initial reset wait step.
pub const HUB_SHORT_RESET_TIME_MS: u64 = 10;
/// hub.c:2904 — escalated reset wait step after two short waits.
pub const HUB_LONG_RESET_TIME_MS: u64 = 200;
/// hub.c:2905 — total reset wait budget.
pub const HUB_RESET_TIMEOUT_MS: u64 = 800;
/// hub.c:3159 — post-reset recovery: TRSTRCY (10 ms) + 40 ms slack.
pub const RESET_RECOVERY_TIME_MS: u64 = 50;
/// USB 3.0 tPollingLFPSTimeout = 360 ms, polled as 10 × 36 ms.
pub const USB3_U0_WAIT_STEP_MS: u64 = 36;
/// Bounded ceiling for the U0 wait (spec timeout + slack).
pub const USB3_U0_WAIT_TIMEOUT_MS: u64 = 400;
/// xHCI PLS value for the Polling link state (USB 3.0 port status bits 8:5).
pub const USB_SS_PORT_LS_POLLING: u8 = 0x7;
// ---- wHubDelay chain constants (hub.c:62-63) ----
/// tTPTransmissionDelay per hub hop.
pub const USB_TP_TRANSMISSION_DELAY_NS: u32 = 40;
/// Spec ceiling for the accumulated delay (u16 transport).
pub const USB_TP_TRANSMISSION_DELAY_MAX_NS: u32 = 65535;
/// Normalized view of one hub port status read, covering every field the
/// state machines consume. Decoupled from the wire format so tests can
/// construct sequences directly; production builds it from
/// `usb::HubPortStatus`.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct PortStatusSnapshot {
pub connected: bool,
pub enabled: bool,
pub resetting: bool,
pub powered: bool,
pub connection_changed: bool,
pub enable_changed: bool,
pub reset_changed: bool,
pub over_current_changed: bool,
/// USB 3.0 link state (port status bits 8:5); `None` on USB 2.0 hubs.
pub link_state: Option<u8>,
}
impl From<&usb::HubPortStatus> for PortStatusSnapshot {
fn from(sts: &usb::HubPortStatus) -> Self {
let (connection_changed, enable_changed, reset_changed, link_state) = match sts {
usb::HubPortStatus::V2(x) => (
x.contains(usb::HubPortStatusV2::CONNECTION_CHANGED),
x.contains(usb::HubPortStatusV2::ENABLE_CHANGED),
x.contains(usb::HubPortStatusV2::RESET_CHANGED),
None,
),
usb::HubPortStatus::V3(x) => (
x.contains(usb::HubPortStatusV3::CONNECTION_CHANGED),
// USB 3.0 port status has no ENABLE_CHANGED bit (bits 17-18 reserved).
false,
x.contains(usb::HubPortStatusV3::RESET_CHANGED),
Some(((x.bits() >> 5) & 0xF) as u8),
),
};
Self {
connected: sts.is_connected(),
enabled: sts.is_enabled(),
resetting: sts.is_resetting(),
powered: sts.is_powered(),
connection_changed,
enable_changed,
reset_changed,
over_current_changed: sts.is_over_current_changed(),
link_state,
}
}
}
/// Failure modes shared by the wait state machines.
#[derive(Debug, PartialEq, Eq)]
pub enum WaitError<E> {
/// Total time budget exhausted (Linux: -ETIMEDOUT / -EBUSY).
Timeout,
/// Connection lost during the wait (Linux: -ENOTCONN).
Disconnected,
/// The injected status-fetch or feature-clear closure failed.
Io(E),
}
impl<E: fmt::Debug> fmt::Display for WaitError<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Timeout => write!(f, "timed out"),
Self::Disconnected => write!(f, "device disconnected during wait"),
Self::Io(e) => write!(f, "I/O error: {:?}", e),
}
}
}
impl<E: fmt::Debug> std::error::Error for WaitError<E> {}
/// Linux 7.1 `hub_port_debounce_be_connected()` (hub.c:4696-4737).
///
/// Poll the port until the connection bit stays continuously set for
/// `HUB_DEBOUNCE_STABLE_MS`, sampling every `HUB_DEBOUNCE_STEP_MS` with a
/// total budget of `HUB_DEBOUNCE_TIMEOUT_MS`. Any connection-change event
/// or state flip restarts the stable window. The change bit is cleared
/// during the loop so subsequent reads reflect hardware, not the
/// original event. Returns the final stable status.
pub fn debounce_until_connected<E>(
mut fetch: impl FnMut() -> Result<PortStatusSnapshot, E>,
mut clear_connection_change: impl FnMut() -> Result<(), E>,
mut sleep: impl FnMut(u64),
) -> Result<PortStatusSnapshot, WaitError<E>> {
let mut stable_ms = 0u64;
let mut total_ms = 0u64;
// Linux seeds with 0xffff ("unknown") so the first read always
// (re)starts the window.
let mut connection: Option<bool> = None;
loop {
let cur = fetch().map_err(WaitError::Io)?;
if !cur.connection_changed && connection == Some(cur.connected) {
stable_ms += HUB_DEBOUNCE_STEP_MS;
if stable_ms >= HUB_DEBOUNCE_STABLE_MS {
return if cur.connected {
Ok(cur)
} else {
Err(WaitError::Disconnected)
};
}
} else {
stable_ms = 0;
connection = Some(cur.connected);
}
if cur.connection_changed {
clear_connection_change().map_err(WaitError::Io)?;
}
if total_ms >= HUB_DEBOUNCE_TIMEOUT_MS {
return Err(WaitError::Timeout);
}
total_ms += HUB_DEBOUNCE_STEP_MS;
sleep(HUB_DEBOUNCE_STEP_MS);
}
}
/// Linux 7.1 `hub_port_wait_reset()` (hub.c:2953-3047) followed by the
/// TRSTRCY recovery sleep from `hub_port_reset()` (hub.c:3159) and the
/// C_PORT_RESET clear from `port_event()` (hub.c:5803).
///
/// Call after SET_FEATURE(PORT_RESET). Polls until RESET clears with
/// CONNECTION still set: 10 ms steps, escalating to 200 ms steps after
/// two short waits, within an 800 ms total budget. On completion, sleeps
/// `RESET_RECOVERY_TIME_MS` (50 ms) and clears the reset-change bit.
/// A reset that completes without ENABLE is not treated as an error
/// here — the caller's outer loop re-issues the reset, matching Linux's
/// PORT_RESET_TRIES retry through `hub_port_reset()`.
pub fn wait_for_reset<E>(
mut fetch: impl FnMut() -> Result<PortStatusSnapshot, E>,
mut clear_reset_change: impl FnMut() -> Result<(), E>,
mut sleep: impl FnMut(u64),
) -> Result<PortStatusSnapshot, WaitError<E>> {
let mut total_ms = 0u64;
let mut delay = HUB_SHORT_RESET_TIME_MS;
let mut cur = fetch().map_err(WaitError::Io)?;
loop {
if !cur.resetting && cur.connected {
sleep(RESET_RECOVERY_TIME_MS);
if cur.reset_changed {
clear_reset_change().map_err(WaitError::Io)?;
}
return Ok(cur);
}
if !cur.connected {
return Err(WaitError::Disconnected);
}
if total_ms >= HUB_RESET_TIMEOUT_MS {
return Err(WaitError::Timeout);
}
sleep(delay);
total_ms += delay;
// hub.c:2993-2995 — escalate after two short waits.
if total_ms >= 2 * HUB_SHORT_RESET_TIME_MS {
delay = HUB_LONG_RESET_TIME_MS;
}
cur = fetch().map_err(WaitError::Io)?;
}
}
/// USB 3.0 polling→U0 wait after port power-on (hub.c:1226-1229;
/// tPollingLFPSTimeout = 360 ms from USB 3.0 §9.4.2.1).
///
/// A downstream port can sit in the Polling link state for some time
/// after power is applied; enumeration must wait for the link to leave
/// Polling. Polls every `USB3_U0_WAIT_STEP_MS` up to
/// `USB3_U0_WAIT_TIMEOUT_MS`. Returns immediately when the port is not
/// in Polling (including USB 2 hubs and empty ports).
pub fn wait_for_u0<E>(
mut fetch: impl FnMut() -> Result<PortStatusSnapshot, E>,
mut sleep: impl FnMut(u64),
) -> Result<(), WaitError<E>> {
let mut total_ms = 0u64;
loop {
let cur = fetch().map_err(WaitError::Io)?;
let polling = cur.link_state == Some(USB_SS_PORT_LS_POLLING);
if !polling || !cur.connected {
return Ok(());
}
if total_ms >= USB3_U0_WAIT_TIMEOUT_MS {
return Err(WaitError::Timeout);
}
total_ms += USB3_U0_WAIT_STEP_MS;
sleep(USB3_U0_WAIT_STEP_MS);
}
}
/// Linux 7.1 hub.c:1507-1519 chain rule for a hub's own accumulated
/// delay: `wHubDelay + parent->hub_delay + tTPTransmissionDelay`,
/// clamped to 65535 ns. Child devices behind this hub inherit the
/// result verbatim for SetIsochDelay (hub.c:5128-5129).
pub fn accumulate_hub_delay_ns(own_w_hub_delay: u32, parent_accumulated: u32) -> u32 {
own_w_hub_delay
.saturating_add(parent_accumulated)
.saturating_add(USB_TP_TRANSMISSION_DELAY_NS)
.min(USB_TP_TRANSMISSION_DELAY_MAX_NS)
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
use std::collections::VecDeque;
fn snap() -> PortStatusSnapshot {
PortStatusSnapshot::default()
}
fn connected() -> PortStatusSnapshot {
PortStatusSnapshot {
connected: true,
powered: true,
..snap()
}
}
/// Scripted test harness: queued status reads, recorded sleeps and clears.
/// Once the queue is exhausted, the last queued snapshot repeats forever.
struct Rig {
reads: VecDeque<PortStatusSnapshot>,
last: PortStatusSnapshot,
sleeps: Vec<u64>,
clears: u32,
}
impl Rig {
fn new(reads: impl IntoIterator<Item = PortStatusSnapshot>) -> Self {
let reads: VecDeque<_> = reads.into_iter().collect();
let last = reads.front().copied().unwrap_or_default();
Self {
reads,
last,
sleeps: Vec::new(),
clears: 0,
}
}
fn fetch(&mut self) -> Result<PortStatusSnapshot, ()> {
if let Some(s) = self.reads.pop_front() {
self.last = s;
}
Ok(self.last)
}
fn clear(&mut self) -> Result<(), ()> {
self.clears += 1;
Ok(())
}
fn sleep(&mut self, ms: u64) {
self.sleeps.push(ms);
}
}
#[test]
fn debounce_stable_immediately() {
let rig = RefCell::new(Rig::new([connected(), connected(), connected(), connected()]));
let out = debounce_until_connected(
|| rig.borrow_mut().fetch(),
|| rig.borrow_mut().clear(),
|ms| rig.borrow_mut().sleep(ms),
);
let rig = rig.into_inner();
assert_eq!(out, Ok(connected()));
// First read seeds the window; four stable steps of 25 ms reach 100 ms.
assert_eq!(rig.sleeps, vec![25, 25, 25, 25]);
assert_eq!(rig.clears, 0);
}
#[test]
fn debounce_flap_restarts_window() {
let rig = RefCell::new(Rig::new([
connected(),
snap(), // flap: disconnect
connected(), // window restarts here
connected(),
connected(),
connected(),
]));
let out = debounce_until_connected(
|| rig.borrow_mut().fetch(),
|| rig.borrow_mut().clear(),
|ms| rig.borrow_mut().sleep(ms),
);
let rig = rig.into_inner();
assert_eq!(out, Ok(connected()));
assert_eq!(rig.sleeps.len(), 6);
assert!(rig.sleeps.iter().all(|&s| s == HUB_DEBOUNCE_STEP_MS));
}
#[test]
fn debounce_stable_disconnect_errors() {
let rig = RefCell::new(Rig::new([snap(), snap(), snap(), snap()]));
let out = debounce_until_connected(
|| rig.borrow_mut().fetch(),
|| rig.borrow_mut().clear(),
|ms| rig.borrow_mut().sleep(ms),
);
assert_eq!(out, Err(WaitError::Disconnected));
}
#[test]
fn debounce_change_bit_cleared_and_timeout() {
let flapping = PortStatusSnapshot {
connection_changed: true,
..connected()
};
let rig = RefCell::new(Rig::new(
(0..200).map(|i| if i % 2 == 0 { flapping } else { connected() }),
));
let out = debounce_until_connected(
|| rig.borrow_mut().fetch(),
|| rig.borrow_mut().clear(),
|ms| rig.borrow_mut().sleep(ms),
);
let rig = rig.into_inner();
assert_eq!(out, Err(WaitError::Timeout));
assert!(rig.clears > 0);
assert_eq!(rig.sleeps.iter().sum::<u64>(), HUB_DEBOUNCE_TIMEOUT_MS);
}
#[test]
fn reset_completes_after_two_polls() {
let resetting = PortStatusSnapshot {
resetting: true,
..connected()
};
let done = PortStatusSnapshot {
enabled: true,
reset_changed: true,
..connected()
};
let rig = RefCell::new(Rig::new([resetting, resetting, done]));
let out = wait_for_reset(
|| rig.borrow_mut().fetch(),
|| rig.borrow_mut().clear(),
|ms| rig.borrow_mut().sleep(ms),
);
let rig = rig.into_inner();
assert_eq!(out, Ok(done));
assert_eq!(rig.sleeps, vec![10, 10, RESET_RECOVERY_TIME_MS]);
assert_eq!(rig.clears, 1);
}
#[test]
fn reset_escalates_to_long_step() {
let resetting = PortStatusSnapshot {
resetting: true,
..connected()
};
let done = connected();
let rig = RefCell::new(Rig::new([resetting, resetting, resetting, resetting, done]));
let out = wait_for_reset(
|| rig.borrow_mut().fetch(),
|| rig.borrow_mut().clear(),
|ms| rig.borrow_mut().sleep(ms),
);
let rig = rig.into_inner();
assert_eq!(out, Ok(done));
assert_eq!(rig.sleeps[0], HUB_SHORT_RESET_TIME_MS);
assert_eq!(rig.sleeps[1], HUB_SHORT_RESET_TIME_MS);
assert_eq!(rig.sleeps[2], HUB_LONG_RESET_TIME_MS);
assert_eq!(*rig.sleeps.last().unwrap(), RESET_RECOVERY_TIME_MS);
}
#[test]
fn reset_timeout() {
let resetting = PortStatusSnapshot {
resetting: true,
..connected()
};
let rig = RefCell::new(Rig::new([resetting]));
let out = wait_for_reset(
|| rig.borrow_mut().fetch(),
|| rig.borrow_mut().clear(),
|ms| rig.borrow_mut().sleep(ms),
);
assert_eq!(out, Err(WaitError::Timeout));
}
#[test]
fn reset_disconnect_mid_wait() {
let resetting = PortStatusSnapshot {
resetting: true,
..connected()
};
let rig = RefCell::new(Rig::new([resetting, snap()]));
let out = wait_for_reset(
|| rig.borrow_mut().fetch(),
|| rig.borrow_mut().clear(),
|ms| rig.borrow_mut().sleep(ms),
);
let rig = rig.into_inner();
assert_eq!(out, Err(WaitError::Disconnected));
assert_eq!(rig.sleeps, vec![HUB_SHORT_RESET_TIME_MS]);
}
#[test]
fn u0_wait_leaves_polling() {
let polling = PortStatusSnapshot {
link_state: Some(USB_SS_PORT_LS_POLLING),
..connected()
};
let u0 = PortStatusSnapshot {
link_state: Some(0),
..connected()
};
let rig = RefCell::new(Rig::new([polling, polling, polling, u0]));
let out = wait_for_u0(
|| rig.borrow_mut().fetch(),
|ms| rig.borrow_mut().sleep(ms),
);
let rig = rig.into_inner();
assert_eq!(out, Ok(()));
assert_eq!(rig.sleeps, vec![36, 36, 36]);
}
#[test]
fn u0_wait_skips_non_polling_and_empty_ports() {
let rx_detect = PortStatusSnapshot {
link_state: Some(5),
..snap()
};
let rig = RefCell::new(Rig::new([rx_detect]));
let out = wait_for_u0(
|| rig.borrow_mut().fetch(),
|ms| rig.borrow_mut().sleep(ms),
);
let rig = rig.into_inner();
assert_eq!(out, Ok(()));
assert!(rig.sleeps.is_empty());
}
#[test]
fn u0_wait_timeout() {
let polling = PortStatusSnapshot {
link_state: Some(USB_SS_PORT_LS_POLLING),
..connected()
};
let rig = RefCell::new(Rig::new([polling]));
let out = wait_for_u0(
|| rig.borrow_mut().fetch(),
|ms| rig.borrow_mut().sleep(ms),
);
assert_eq!(out, Err(WaitError::Timeout));
}
#[test]
fn hub_delay_chain_and_cap() {
// First-tier hub: parent (root) accumulated = 0.
assert_eq!(accumulate_hub_delay_ns(100, 0), 140);
// Second-tier: own + parent chain + 40.
assert_eq!(accumulate_hub_delay_ns(200, 140), 380);
// Cap at 65535 ns.
assert_eq!(accumulate_hub_delay_ns(65500, 65500), 65535);
}
#[test]
fn snapshot_from_v3_extracts_link_state() {
let raw = usb::HubPortStatusV3::CONNECTION
| usb::HubPortStatusV3::POWER
| usb::HubPortStatusV3::LINK_STATE_0
| usb::HubPortStatusV3::LINK_STATE_1; // link state 0b0011 = 3
let snap = PortStatusSnapshot::from(&usb::HubPortStatus::V3(raw));
assert!(snap.connected);
assert!(snap.powered);
assert_eq!(snap.link_state, Some(3));
assert!(!snap.enable_changed); // reserved on V3
}
#[test]
fn snapshot_from_v2_has_no_link_state() {
let raw = usb::HubPortStatusV2::CONNECTION
| usb::HubPortStatusV2::POWER
| usb::HubPortStatusV2::ENABLE_CHANGED;
let snap = PortStatusSnapshot::from(&usb::HubPortStatus::V2(raw));
assert!(snap.connected);
assert!(snap.enable_changed);
assert_eq!(snap.link_state, None);
}
}
+12 -8
View File
@@ -47,17 +47,23 @@ pub struct HubDescriptorV3 {
pub current: u8,
pub decode_latency: u8,
pub delay: u16,
/*TODO: USB 2 and 3 disagree on the descriptor, so some fields are disabled
// device_removable: bitmap of ports, maximum of 256 bits (32 bytes)
// power_control_mask: bitmap of ports, maximum of 256 bits (32 bytes)
bitmaps: [u8; 64],
*/
/// Bitmap of non-removable ports: bit N set = port N+1 is permanently
/// attached. USB 3.0 spec Table 10-15 — this is the final field of
/// the 12-byte SuperSpeed hub descriptor (SS descriptors have no
/// PortPwrCtrlMask; that exists only in the variable-length USB 2.0
/// descriptor, whose bitmaps are still not parsed).
pub device_removable: u16,
}
unsafe impl plain::Plain for HubDescriptorV3 {}
impl HubDescriptorV3 {
pub const DESCRIPTOR_KIND: u8 = 0x2A;
/// Whether the given 1-based port number is marked non-removable.
pub fn is_port_non_removable(&self, port: u8) -> bool {
port >= 1 && (self.device_removable >> (port - 1)) & 1 == 1
}
}
impl Default for HubDescriptorV3 {
@@ -71,9 +77,7 @@ impl Default for HubDescriptorV3 {
current: 0,
decode_latency: 0,
delay: 0,
/*
bitmaps: [0; 64],
*/
device_removable: 0,
}
}
}