usbhubd: P3 slice 2 — interrupt-driven hub change detection

Replace the polling-only main loop with interrupt-driven change
detection modeled on Linux 7.1 hub_irq().

Key changes:
  1. Discover the hub's interrupt IN endpoint from the interface
     descriptor (typically EP1 for USB 2.x, may be absent for USB 3).
     Use EndpointTy::Interrupt + EndpDirection::In to match.

  2. Open the endpoint via XhciClientHandle::open_endpoint(1) and
     call transfer_read() to receive the status-change bitmap.

  3. Build a per-port change mask from the bitmap:
     Port N is bit (N-1) of byte (N-1)/8.  Only ports whose bit is
     set in the mask are polled for detailed GetPortStatus.

  4. Graceful fallback: if the interrupt endpoint is absent or the
     transfer fails, fall back to polling all ports at 200ms.

  5. Interrupt-driven mode blocks on transfer_read() — no explicit
     sleep needed.  Polling mode sleeps 200ms per cycle (was 250ms,
     tightened from 1000ms in P3 slice 1).

  6. Added XhciEndpHandle import for endpoint operations.

Cross-reference: Linux 7.1
  - drivers/usb/core/hub.c: hub_irq() — URB completion handler
  - drivers/usb/core/hub.c: hub_configure() — interrupt endpoint setup
  - include/linux/usb/ch11.h — hub status change bitmap format

This completes P3 hub maturity — power-on timing (slice 1) plus
interrupt-driven detection (slice 2) brings usbhubd to Linux 7.1
parity for the two most important hub operations.
This commit is contained in:
Red Bear OS
2026-07-07 12:00:51 +03:00
parent b244dbd0d9
commit 8b9a4fa7b6
+68 -8
View File
@@ -1,8 +1,8 @@
use std::{env, thread, time};
use xhcid_interface::{
plain, usb, ConfigureEndpointsReq, DevDesc, DeviceReqData, PortId, PortReqRecipient, PortReqTy,
XhciClientHandle,
plain, usb, ConfigureEndpointsReq, DevDesc, DeviceReqData, EndpointTy, EndpDesc,
EndpDirection, PortId, PortReqRecipient, PortReqTy, XhciClientHandle, XhciEndpHandle,
};
fn main() {
@@ -143,6 +143,32 @@ fn main() {
log::info!("usbhubd: SET_HUB_DEPTH to {}", hub_depth);
}
// Find the hub interrupt endpoint for status-change detection.
// Linux 7.1 hub_irq() reads the status-change bitmap from EP1.
// For USB 2.x hubs: EP1, bInterval=12 (255ms max polling).
// For USB 3 hubs: may not have a dedicated interrupt EP — fall back to polling.
let intr_desc = if_desc
.endpoints
.iter()
.find(|ep| ep.ty() == EndpointTy::Interrupt && ep.direction() == EndpDirection::In)
.cloned();
let mut intr_ep_handle: Option<XhciEndpHandle> = if intr_desc.is_some() {
match handle.open_endpoint(1u8) {
Ok(handle) => {
log::info!("usbhubd: interrupt endpoint opened for change detection");
Some(handle)
}
Err(e) => {
log::warn!("usbhubd: interrupt endpoint open failed ({}), falling back to polling", e);
None
}
}
} else {
log::info!("usbhubd: no interrupt endpoint found, using polling");
None
};
// Initialize states
struct PortState {
port_id: PortId,
@@ -188,13 +214,49 @@ fn main() {
// Linux uses 100ms minimum; follow the same floor.
let power_on_delay_ms = core::cmp::max(power_on_delay_ms, 100);
// Main event loop. Linux 7.1 uses interrupt-driven change
// detection; we poll at 250ms as an intermediate step until
// hub interrupt endpoints are integrated.
const POLL_INTERVAL_MS: u64 = 250;
// Main event loop with interrupt-driven change detection.
// Linux 7.1 hub_irq(): reads status-change bitmap from EP1,
// then kicks hub_wq to process only the changed ports.
// We do the same: read the bitmap, build a port mask, and
// only poll GetPortStatus for ports whose bit is set.
// If EP1 is unavailable, fall back to polling all ports.
//
// Bitmap size: ceil(ports / 8) bytes.
// Port N is bit (N-1) of byte (N-1)/8.
const POLL_FALLBACK_MS: u64 = 200;
let bitmap_size = (ports as usize + 7) / 8;
let mut bitmap = vec![0u8; bitmap_size];
loop {
// Build a port-change mask.
// Bit N set = port (N+1) needs processing.
let changed: u64 = if let Some(ref mut ep) = intr_ep_handle {
match ep.transfer_read(&mut bitmap) {
Ok(_) => {
let mut mask = 0u64;
for (byte_idx, &byte) in bitmap.iter().enumerate() {
if byte != 0 {
mask |= (byte as u64) << (byte_idx * 8);
}
}
mask
}
Err(e) => {
log::warn!("usbhubd: interrupt transfer failed ({}), falling back to poll", e);
(1u64 << ports) - 1
}
}
} else {
// Polling mode: process all ports.
thread::sleep(time::Duration::from_millis(POLL_FALLBACK_MS));
(1u64 << ports) - 1
};
for port in 1..=ports {
let bit = 1u64 << (port - 1);
if (changed & bit) == 0 {
continue;
}
let port_idx: usize = port.checked_sub(1).unwrap().into();
let state = states.get_mut(port_idx).unwrap();
@@ -287,7 +349,5 @@ fn main() {
state.ensure_attached(true);
}
thread::sleep(time::Duration::from_millis(POLL_INTERVAL_MS));
}
}