From d20557f72688ab91d68c2e7a882901526803172f Mon Sep 17 00:00:00 2001 From: Red Bear OS Date: Sat, 18 Jul 2026 22:33:38 +0900 Subject: [PATCH] xhcid: gate xHCI 1.1+ features on HCCPARAMS2 + protocol-caps bits (P2-B) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate xHCI 1.1+ features on their capability bits, cross-referenced with Linux 7.1 xhci driver behavior: LEC (HCC2_LEC, scheme.rs): - lec now uses Linux xhci-mem.c:1350 exact condition: hci_version > 0x100 && hcc_params2 & HCC2_LEC (HCCPARAMS2 register space is reserved on 1.0 controllers). - Max ESIT Payload Hi zeroed when LEC=0 (spec Table 6-8 RsvdZ). U3C (HCC2_U3C, mod.rs suspend_port): - Refuse SuperSpeed U3 entry with ENOSYS when hci_ver >= 0x110 and HCC2_U3C=0 (spec 4.15.1). USB2 suspend unaffected. Linux 7.1 defines but never gates this bit; xhcid follows the spec. CIC (HCC2_CIC): CIE gate pre-existing (set_cie from cic()); added the hci_ver > 0x100 version guard. HCC2 capability log block similarly guarded. HW LPM (extended.rs, mod.rs, port.rs): - New SupportedProtoCap::{l1_capable,hw_lpm_capable,besl_lpm_capable} reading protocol-defined bits (Linux xhci-ext-caps.h:62-66 L1C/HLC/BLC). - Xhci::hw_lpm_support computed per Linux xhci-mem.c:2137: hci_ver >= 0x100 && !HW_LPM_DISABLE && any USB2 protocol cap has HLC. - attach_device(): defensive LPM clear on USB 2.0 protocol ports (rev_major() != 3) when hw_lpm_support is false — Linux xhci.c:4725 disable path; USB3 excluded because PORTPMSC L1DS aliases U2 timeout. - Port::enable_lpm/disable_lpm register targets fixed: HLE/HIRD/RWE/ L1DS belong in PORTPMSC (offset 0x04), not PORTHLPMC (0x0C, bit 16 RsvdZ) — spec Tables 5-21/5-23, Linux xhci-port.h:135-158. Helpers rewritten to Linux xhci.c:4686-4737 two-register sequence. - Per-device L1 enablement (BESL, MEL Evaluate Context) defers to P3. Bug fix: removed bogus CapabilityRegs::hlc() + HCC_PARAMS1_HLC_BIT — they read xECP pointer bits 16-31 of HCCPARAMS1 (spec Table 5-13), not HLC. HLC lives in the Supported Protocol capability port_info DWORD. Verification: cargo check clean (138 warnings, -2 vs baseline: the new disable_lpm call site also revived PORT_HLE/PORT_HIRD_MASK), 43/43 tests pass. --- drivers/usb/xhcid/src/xhci/capability.rs | 15 +-- drivers/usb/xhcid/src/xhci/extended.rs | 19 ++++ drivers/usb/xhcid/src/xhci/mod.rs | 115 ++++++++++++++++++----- drivers/usb/xhcid/src/xhci/port.rs | 55 +++++++---- drivers/usb/xhcid/src/xhci/scheme.rs | 17 +++- 5 files changed, 174 insertions(+), 47 deletions(-) diff --git a/drivers/usb/xhcid/src/xhci/capability.rs b/drivers/usb/xhcid/src/xhci/capability.rs index 2dd256af72..ff4a748e6d 100644 --- a/drivers/usb/xhcid/src/xhci/capability.rs +++ b/drivers/usb/xhcid/src/xhci/capability.rs @@ -146,9 +146,14 @@ pub const HCC_PARAMS1_NSS_BIT: u32 = 1u32 << 7; pub const HCC_PARAMS1_SPC_BIT: u32 = 1u32 << 9; /// Bit 11 — Contiguous Frame ID Capability (HCC_CFC). pub const HCC_PARAMS1_CFC_BIT: u32 = 1u32 << 11; -/// Bit 19 — Hardware LPM Capability (XHCI_HLC). xHCI 1.1+. -/// When set, the controller supports USB 2.0 Link Power Management. -pub const HCC_PARAMS1_HLC_BIT: u32 = 1u32 << 19; + +// NOTE: HCCPARAMS1 has NO Hardware LPM bit — bits 16-31 are the xECP +// pointer (xHCI spec Table 5-13). USB 2.0 Hardware LPM Capability (HLC) +// is protocol-defined bit 3 of the Supported Protocol extended +// capability port_info DWORD (xHCI spec §7.2.2.1.3.2); see +// `SupportedProtoCap::hw_lpm_capable()` in extended.rs. An earlier +// revision mistakenly defined an HLC accessor reading bit 19 of this +// register (i.e. an xECP pointer bit) — removed. /// The mask to use to get the LEC bit from HCCParams2. See [CapabilityRegs] pub const HCC_PARAMS2_LEC_BIT: u32 = 1 << 4; @@ -272,10 +277,6 @@ impl CapabilityRegs { pub fn spc(&self) -> bool { self.hcc_params1.readf(HCC_PARAMS1_SPC_BIT) } /// Contiguous Frame ID Capability (bit 11). pub fn cfc(&self) -> bool { self.hcc_params1.readf(HCC_PARAMS1_CFC_BIT) } - /// USB 2.0 Hardware LPM Capability (bit 19, xHCI 1.1+). - /// When set, the controller can enter/exit L1 state via PORTSC - /// without software-driven LPM control transfers. - pub fn hlc(&self) -> bool { self.hcc_params1.readf(HCC_PARAMS1_HLC_BIT) } // -- HCSPARAMS3: U1/U2 exit latencies (xhci 1.1+ root-hub bos). -- /// Maximum U1 device exit latency in microseconds. Used by the root diff --git a/drivers/usb/xhcid/src/xhci/extended.rs b/drivers/usb/xhcid/src/xhci/extended.rs index e15ec2ec75..4b1f022dda 100644 --- a/drivers/usb/xhcid/src/xhci/extended.rs +++ b/drivers/usb/xhcid/src/xhci/extended.rs @@ -268,6 +268,25 @@ impl SupportedProtoCap { pub fn proto_defined(&self) -> u16 { ((self.c.read() & SUPP_PROTO_CAP_PROTO_DEF_MASK) >> SUPP_PROTO_CAP_PROTO_DEF_SHIFT) as u16 } + /// USB 2.0 L1 Capability (L1C) — protocol-defined bit 0 of the + /// Supported Protocol capability port_info DWORD. + /// Linux 7.1 xhci-ext-caps.h:62 `XHCI_L1C (1 << 16)`. + pub fn l1_capable(&self) -> bool { + self.proto_defined() & 0x1 != 0 + } + /// USB 2.0 Hardware LPM Capability (HLC) — protocol-defined bit 3. + /// Meaningful only on USB 2.0 protocol entries (rev_major() != 3); + /// USB3 ports use U1/U2 LPM instead. + /// Linux 7.1 xhci-ext-caps.h:65 `XHCI_HLC (1 << 19)` and + /// xhci-mem.c:2137 hw_lpm_support detection. + pub fn hw_lpm_capable(&self) -> bool { + self.proto_defined() & 0x8 != 0 + } + /// USB 2.0 BESL LPM Capability (BLC) — protocol-defined bit 4. + /// Linux 7.1 xhci-ext-caps.h:66 `XHCI_BLC (1 << 20)`. + pub fn besl_lpm_capable(&self) -> bool { + self.proto_defined() & 0x10 != 0 + } pub fn psic(&self) -> u8 { ((self.c.read() & SUPP_PROTO_CAP_PSIC_MASK) >> SUPP_PROTO_CAP_PSIC_SHIFT) as u8 } diff --git a/drivers/usb/xhcid/src/xhci/mod.rs b/drivers/usb/xhcid/src/xhci/mod.rs index b38907fe51..71eca86f7e 100644 --- a/drivers/usb/xhcid/src/xhci/mod.rs +++ b/drivers/usb/xhcid/src/xhci/mod.rs @@ -16,7 +16,7 @@ use std::sync::atomic::AtomicUsize; use std::sync::{Arc, Mutex}; use std::{mem, process, slice, thread}; -use syscall::error::{Error, Result, EBADF, EBADFD, EBADMSG, EINVAL, EIO, ENOENT}; +use syscall::error::{Error, Result, EBADF, EBADFD, EBADMSG, EINVAL, EIO, ENOENT, ENOSYS}; use syscall::{EAGAIN, PAGE_SIZE}; use chashmap::CHashMap; @@ -261,6 +261,13 @@ pub struct Xhci { /// Per-controller hardware quirks ported from Linux 7.1 xhci-pci.c. /// Set once at construction and read by hot paths to decide workarounds. pub quirks: crate::xhci::quirks::XhciQuirks, + /// Whether USB 2.0 hardware Link Power Management (L1) is usable on + /// this controller. Computed at init from + /// `hci_version >= 0x100 && !HW_LPM_DISABLE && any USB 2.0 Supported + /// Protocol cap has HLC set` — mirrors Linux 7.1 xhci-mem.c:2137 + /// `hw_lpm_support` detection. Consumed by per-device LPM enablement + /// at attach time (hub work, P3). + hw_lpm_support: bool, //page_size: usize, // XXX: It would be really useful to be able to mutably access individual elements of a slice, @@ -476,6 +483,7 @@ impl Xhci { cap, quirks, + hw_lpm_support: false, // computed in init() //page_size, op: Mutex::new(op), ports: Mutex::new(ports), @@ -540,19 +548,33 @@ impl Xhci { self.op.get_mut().unwrap().config.read() & 0xFF ); - // Log HCC2/HCS3 features the controller advertises. These are - // xHCI 1.1+ extensions; on a 1.0 controller all are zero. - // Future phases (P2-C, P3, P7) gate behavior on these bits. - if self.cap.hcc2_u3c() { log::info!("xhcid: HCC2: U3 entry supported"); } - if self.cap.hcc2_cmc() { log::info!("xhcid: HCC2: Configure Endpoint MaxExitLat too-large supported"); } - if self.cap.hcc2_fsc() { log::info!("xhcid: HCC2: Force Save Context supported"); } - if self.cap.hcc2_etc() { log::info!("xhcid: HCC2: Extended TBC supported"); } - if self.cap.hcc2_gsc() { log::info!("xhcid: HCC2: Get/Set Extended Property supported"); } - if self.cap.hcc2_vtc() { log::info!("xhcid: HCC2: Virtualization-based Trusted I/O supported"); } - if self.cap.hcc2_e2v2c() { log::info!("xhcid: HCC2: eUSB2V2 supported"); } - // HCCPARAMS1: USB 2.0 Hardware LPM (xHCI 1.1+). - if self.cap.hlc() { - log::info!("xhcid: HCC1: USB 2.0 Hardware LPM supported"); + // HCCPARAMS2 exists only on xHCI 1.1+; on 1.0 controllers its + // register space is reserved, so every hcc2_* read is gated on + // the version. Linux uses the same guard (xhci-mem.c:1350: + // `hci_version > 0x100 && hcc_params2 & HCC2_LEC`). + let hci_version = self.cap.hci_ver.read(); + + // USB 2.0 hardware LPM availability (Linux 7.1 xhci-mem.c:2137): + // xHCI 1.0+ required, HW_LPM_DISABLE quirk vetoes, and at least + // one USB 2.0 Supported Protocol capability must advertise HLC. + self.hw_lpm_support = hci_version >= 0x100 + && !self.quirks.contains(crate::xhci::quirks::XhciQuirks::HW_LPM_DISABLE) + && self + .supported_protocols_iter() + .any(|proto| proto.rev_major() != 3 && proto.hw_lpm_capable()); + + // Log HCC2/HCS3 features the controller advertises. + if hci_version > 0x100 { + if self.cap.hcc2_u3c() { log::info!("xhcid: HCC2: U3 entry supported"); } + if self.cap.hcc2_cmc() { log::info!("xhcid: HCC2: Configure Endpoint MaxExitLat too-large supported"); } + if self.cap.hcc2_fsc() { log::info!("xhcid: HCC2: Force Save Context supported"); } + if self.cap.hcc2_etc() { log::info!("xhcid: HCC2: Extended TBC supported"); } + if self.cap.hcc2_gsc() { log::info!("xhcid: HCC2: Get/Set Extended Property supported"); } + if self.cap.hcc2_vtc() { log::info!("xhcid: HCC2: Virtualization-based Trusted I/O supported"); } + if self.cap.hcc2_e2v2c() { log::info!("xhcid: HCC2: eUSB2V2 supported"); } + } + if self.hw_lpm_support { + log::info!("xhcid: USB 2.0 Hardware LPM supported (HLC in protocol caps)"); } log::info!( "xhcid: HCS3 U1_dev_exit_lat={}us U2_dev_exit_lat={}us", @@ -637,7 +659,13 @@ impl Xhci { debug!("XHCI initialized."); - self.op.get_mut().unwrap().set_cie(self.cap.cic()); + // CIE enables Configuration Information in the input control + // context (xHCI 1.1 §6.2.5.1); HCCPARAMS2.CIC is only defined on + // xHCI 1.1+, hence the version guard. + self.op + .get_mut() + .unwrap() + .set_cie(hci_version > 0x100 && self.cap.cic()); self.print_port_capabilities(); @@ -649,13 +677,20 @@ impl Xhci { log::info!("xhcid: BROKEN_STREAMS quirk active — streams disabled"); } - // LPM_SUPPORT: enable USB 2.0 Hardware Link Power Management. - // Required for Intel host controllers. HW_LPM_DISABLE - // overrides: some AMD/ASMedia controllers have broken LPM. + // USB 2.0 hardware LPM state was computed above into + // self.hw_lpm_support (mirrors Linux 7.1 xhci-mem.c:2137). + // LPM_SUPPORT (Intel Panther Point and later) means the + // controller is expected to advertise HLC; HW_LPM_DISABLE + // (broken AMD/ASMedia LPM) vetoes it unconditionally. Actual + // per-device L1 enablement happens at attach time once the + // device BOS descriptor (lpm_capable) and BESL parameters are + // known — hub-level work tracked as P3. if self.quirks.contains(crate::xhci::quirks::XhciQuirks::HW_LPM_DISABLE) { - log::info!("xhcid: HW_LPM_DISABLE quirk active — LPM disabled"); - } else if self.quirks.contains(crate::xhci::quirks::XhciQuirks::LPM_SUPPORT) { - log::info!("xhcid: LPM_SUPPORT quirk active — USB 2.0 LPM enabled"); + log::info!("xhcid: HW_LPM_DISABLE quirk active — hw_lpm_support=false"); + } else if self.quirks.contains(crate::xhci::quirks::XhciQuirks::LPM_SUPPORT) + && self.hw_lpm_support + { + log::info!("xhcid: LPM_SUPPORT quirk + HLC capability — USB 2.0 HW LPM available"); } // U2_DISABLE_WAKE: skip U2 wake configuration. @@ -945,6 +980,17 @@ impl Xhci { // The port speed field (bits 13:10) gives the current link speed. let speed = port.speed(); let usb3 = speed >= 4; // SuperSpeed (4) or SuperSpeedPlus (5+) + // HCC2_U3C (xHCI 1.1+, spec §4.15.1): when clear, the controller + // does not support U3 entry on the SuperSpeed link. Refusing the + // transition here is the spec-correct behavior; USB 2.0 port + // suspend uses the bus-level suspended state and is unaffected. + if usb3 && self.cap.hci_ver.read() >= 0x110 && !self.cap.hcc2_u3c() { + log::warn!( + "xhcid: refusing U3 suspend on port {} — controller is xHCI 1.1+ without U3 Entry Capability (HCC2_U3C=0)", + port_id + ); + return Err(Error::new(ENOSYS)); + } log::info!("xhcid: suspend port {} to U3 (USB3={})", port_id, usb3); port.suspend(usb3); Ok(()) @@ -1113,7 +1159,8 @@ impl Xhci { ); if flags.contains(port::PortFlags::CCS) { - let slot_ty = match self.supported_protocol(port_id) { + let protocol = self.supported_protocol(port_id); + let slot_ty = match protocol { Some(protocol) => protocol.proto_slot_ty(), None => { warn!("Failed to find supported protocol information for port"); @@ -1121,6 +1168,30 @@ impl Xhci { } }; + // Defensive LPM clear (Linux 7.1 xhci.c:4725 disable path): + // when the controller lacks HW LPM support — or HW_LPM_DISABLE + // vetoed it — scrub any firmware-leftover HLE state on USB 2.0 + // ports. USB 2.0 protocol ports only (rev_major() != 3, the + // Linux test): on USB 3.0 ports the PORTPMSC L1DS field + // aliases the U2 timeout field, so a blanket clear would + // corrupt U2 LPM configuration. + if !self.hw_lpm_support { + if let Some(proto) = protocol { + if proto.rev_major() != 3 { + let mut ports = self.ports.lock().unwrap_or_else(|e| e.into_inner()); + if let Some(idx) = port_id.root_hub_port_index() { + if let Some(port) = ports.get_mut(idx) { + port.disable_lpm(); + log::debug!( + "xhcid: port {} HW LPM explicitly disabled (hw_lpm_support=false)", + port_id + ); + } + } + } + } + } + debug!("Slot type: {}", slot_ty); debug!("Enabling slot."); let slot = match self.enable_port_slot(slot_ty).await { diff --git a/drivers/usb/xhcid/src/xhci/port.rs b/drivers/usb/xhcid/src/xhci/port.rs index 4b82f9c9f7..763b198f54 100644 --- a/drivers/usb/xhcid/src/xhci/port.rs +++ b/drivers/usb/xhcid/src/xhci/port.rs @@ -55,15 +55,23 @@ pub struct Port { pub porthlpmc: Mmio, } -// PORTHLPMC register bits (USB 2.0 LPM). -// Cross-referenced with Linux 7.1 drivers/usb/host/xhci-port.h:135-173. -pub const PORT_HLE: u32 = 1u32 << 16; // Hardware LPM Enable -pub const PORT_HIRD_MASK: u32 = 0xFu32 << 4; // Host Initiated Resume Duration -pub const PORT_L1_TIMEOUT_MASK: u32 = 0xFFu32 << 2; -pub const PORT_BESLD_MASK: u32 = 0xFu32 << 10; // Best Effort Service Latency Deep -pub const PORT_HIRDM_MASK: u32 = 0x3u32; // Host Initiated Resume Duration Mode +// PORTPMSC register bits for USB 2.0 hardware LPM (xHCI spec Table 5-21; +// Linux 7.1 xhci-port.h:135-143). HLE/HIRD/L1DS/RWE live in PORTPMSC +// (offset 0x04), NOT in PORTHLPMC — an earlier revision of this file +// wrote them to PORTHLPMC (offset 0x0C), where bit 16 is RsvdZ. +pub const PORT_RWE: u32 = 1u32 << 3; // Remote Wake Enable +pub const PORT_HIRD_MASK: u32 = 0xFu32 << 4; // Host Initiated Resume Duration (bits 7:4) +pub const PORT_L1DS_MASK: u32 = 0xFFu32 << 8; // L1 Device Slot (bits 15:8) +pub const PORT_HLE: u32 = 1u32 << 16; // Hardware LPM Enable + +// PORTHLPMC register bits for USB 2.0 hardware LPM (xHCI spec Table 5-23; +// Linux 7.1 xhci-port.h:156-158). +pub const PORT_HIRDM_MASK: u32 = 0x3u32; // Host Initiated Resume Duration Mode (bits 1:0) +pub const PORT_L1_TIMEOUT_MASK: u32 = 0xFFu32 << 2; // L1 timeout (bits 9:2) +pub const PORT_BESLD_MASK: u32 = 0xFu32 << 10; // Best Effort Service Latency Deep (bits 13:10) + pub const XHCI_DEFAULT_BESL: u32 = 4; -pub const XHCI_L1_TIMEOUT: u32 = 512; // microseconds +pub const XHCI_L1_TIMEOUT: u32 = 512; // microseconds // USB 3.0 Port Link States (PORTSC PLS field, bits 8:5). // Cross-referenced with Linux 7.1 drivers/usb/host/xhci-port.h:18-21. @@ -146,18 +154,33 @@ impl Port { self.flags() & preserved } - /// Enable USB 2.0 Hardware LPM on this port. - /// Linux 7.1: xhci_set_usb2_hardware_lpm() → sets PORT_HLE. - pub fn enable_lpm(&mut self, hird: u32, l1_timeout: u32) { - let val = PORT_HLE - | ((hird & 0xF) << 4) // PORT_HIRD - | ((l1_timeout & 0xFF) << 2); // PORT_L1_TIMEOUT - self.porthlpmc.write(val); + /// Enable USB 2.0 Hardware LPM (L1) on this port. + /// Linux 7.1 xhci.c:4686-4722 xhci_set_usb2_hardware_lpm() enable + /// path: PORTHLPMC ← HIRDM | L1_TIMEOUT | BESLD, then PORTPMSC ← + /// HIRD | RWE | L1DS(slot), then PORTPMSC |= HLE to commit. + pub fn enable_lpm(&mut self, slot_id: u8, hird: u32, besld: u32, l1_timeout: u32, hirdm: u32) { + let hlpmc = (hirdm & PORT_HIRDM_MASK) + | ((l1_timeout << 2) & PORT_L1_TIMEOUT_MASK) + | ((besld << 10) & PORT_BESLD_MASK); + self.porthlpmc.write(hlpmc); + + let mut pm = self.portpmsc.read(); + pm &= !(PORT_HIRD_MASK | PORT_RWE | PORT_L1DS_MASK); + pm |= ((hird << 4) & PORT_HIRD_MASK) + | PORT_RWE + | ((u32::from(slot_id) << 8) & PORT_L1DS_MASK); + self.portpmsc.write(pm); + self.portpmsc.write(pm | PORT_HLE); } /// Disable USB 2.0 Hardware LPM on this port. + /// Linux 7.1 xhci.c:4725-4737 disable path: clear HLE, RWE, HIRD + /// and L1DS in PORTPMSC. PORTHLPMC contents are don't-care once + /// HLE is clear. pub fn disable_lpm(&mut self) { - self.porthlpmc.write(0); + let pm = + self.portpmsc.read() & !(PORT_HLE | PORT_RWE | PORT_HIRD_MASK | PORT_L1DS_MASK); + self.portpmsc.write(pm); } /// Program U1/U2 inactivity timeout for USB 3.0 ports. diff --git a/drivers/usb/xhcid/src/xhci/scheme.rs b/drivers/usb/xhcid/src/xhci/scheme.rs index f6bb9a30b2..968a2eb837 100644 --- a/drivers/usb/xhcid/src/xhci/scheme.rs +++ b/drivers/usb/xhcid/src/xhci/scheme.rs @@ -1570,7 +1570,12 @@ impl Xhci { config_desc.configuration_value, ) }; - let lec = self.cap.lec(); + // LEC gates both the isoch Mult field and the Max ESIT Payload + // Hi byte. Linux 7.1 xhci-mem.c:1350 xhci_get_endpoint_mult(): + // `lec = hci_version > 0x100 && (hcc_params2 & HCC2_LEC)` — the + // version guard is required because HCCPARAMS2 is reserved space + // on xHCI 1.0 controllers. + let lec = self.cap.hci_ver.read() > 0x100 && self.cap.lec(); let log_max_psa_size = self.cap.max_psa_size(); let port_speed_id = port.root_hub_port_index() @@ -1671,7 +1676,15 @@ impl Xhci { max_burst_size, ); let max_esit_payload_lo = max_esit_payload as u16; - let max_esit_payload_hi = ((max_esit_payload & 0x00FF_0000) >> 16) as u8; + // xHCI 1.1 spec Table 6-8: Max ESIT Payload Hi (Endpoint + // Context dw0 bits 31:24) is RsvdZ when LEC=0 — must be + // zeroed on 1.0 controllers and 1.1+ controllers without the + // Large ESIT Payload Capability. + let max_esit_payload_hi = if lec { + ((max_esit_payload & 0x00FF_0000) >> 16) as u8 + } else { + 0 + }; let interval = Self::endp_ctx_interval(speed_id, endp_desc);