From bd379e0771e7cfffdf2b74ccee207d3195a0544a Mon Sep 17 00:00:00 2001 From: Red Bear OS Date: Sun, 19 Jul 2026 00:27:49 +0900 Subject: [PATCH] usbhubd: full Linux 7.1 hub enumeration state machine (P3-A) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- drivers/usb/usbhubd/src/main.rs | 343 +++++++++++++----- drivers/usb/usbhubd/src/port_ops.rs | 527 ++++++++++++++++++++++++++++ drivers/usb/xhcid/src/usb/hub.rs | 20 +- 3 files changed, 785 insertions(+), 105 deletions(-) create mode 100644 drivers/usb/usbhubd/src/port_ops.rs diff --git a/drivers/usb/usbhubd/src/main.rs b/drivers/usb/usbhubd/src/main.rs index 74958fc278..ec4efec6d2 100644 --- a/drivers/usb/usbhubd/src/main.rs +++ b/drivers/usb/usbhubd/src/main.rs @@ -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 { + 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 = (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 = 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, 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); + } } } } diff --git a/drivers/usb/usbhubd/src/port_ops.rs b/drivers/usb/usbhubd/src/port_ops.rs new file mode 100644 index 0000000000..f846f1ab1a --- /dev/null +++ b/drivers/usb/usbhubd/src/port_ops.rs @@ -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, +} + +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 { + /// 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 fmt::Display for WaitError { + 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 std::error::Error for WaitError {} + +/// 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( + mut fetch: impl FnMut() -> Result, + mut clear_connection_change: impl FnMut() -> Result<(), E>, + mut sleep: impl FnMut(u64), +) -> Result> { + 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 = 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( + mut fetch: impl FnMut() -> Result, + mut clear_reset_change: impl FnMut() -> Result<(), E>, + mut sleep: impl FnMut(u64), +) -> Result> { + 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( + mut fetch: impl FnMut() -> Result, + mut sleep: impl FnMut(u64), +) -> Result<(), WaitError> { + 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, + last: PortStatusSnapshot, + sleeps: Vec, + clears: u32, + } + + impl Rig { + fn new(reads: impl IntoIterator) -> 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 { + 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::(), 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); + } +} diff --git a/drivers/usb/xhcid/src/usb/hub.rs b/drivers/usb/xhcid/src/usb/hub.rs index 688672fd41..0c3337d4a7 100644 --- a/drivers/usb/xhcid/src/usb/hub.rs +++ b/drivers/usb/xhcid/src/usb/hub.rs @@ -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, } } }