From 2a3b0d4e0a0333bde577e780044f4e4187ce1a38 Mon Sep 17 00:00:00 2001 From: Red Bear OS Date: Mon, 20 Jul 2026 19:21:01 +0900 Subject: [PATCH] xhcid: per-device USB 2.0 hardware LPM (L1) enablement at attach (P7-A) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the Linux 7.1 xhci.c:4650 xhci_set_usb2_hardware_lpm() enable path, closing the P7-A gap on top of the P2-B substrate: - usb/bos.rs: BosUsb2ExtDesc bmAttributes accessors (LPM support, BESL support/validity, baseline/deep BESL fields) + 3 unit tests - xhci/mod.rs: - change_max_exit_latency(): MEL update via Evaluate Context (slot output context copied, SLOT add flag, DWORD1 low 16 bits = MEL) — Linux xhci.c:4520 - calculate_hird_besl(): HCS_PARAMS3 U2 latency + device BESL attributes — Linux xhci.c:4594 - enable_usb2_hw_lpm(): BESL vs HIRD parameter selection, MEL Evaluate Context, PORTHLPMC/PORTPMSC programming via the existing Port::enable_lpm — Linux xhci.c:4686-4722 - BESL_ENCODING_US table, XHCI_L1_TIMEOUT_US=512, XHCI_DEFAULT_BESL=4 - xhci/scheme.rs (get_desc): attach-time gate chain — hw_lpm_support && per-port HLC (USB 2.0 protocol only) && device USB_LPM_SUPPORT in BOS && non-hub class && root-hub-direct (get_desc only runs for root-port devices). LPM enable failure logs a warning and continues — LPM is opportunistic; the device works in U0 regardless. Verified: cargo check -Z build-std --target x86_64-unknown-redox -p xhcid clean (no new warnings). Unit tests compile-verified; execution requires redoxer like the crate's existing test suite. Runtime L1 entry validation requires LPM-capable hardware (QEMU's xHCI does not advertise HLC) — recorded as hardware-validation debt in the USB plan. --- drivers/usb/xhcid/src/usb/bos.rs | 77 ++++++++++++++++ drivers/usb/xhcid/src/usb/mod.rs | 4 +- drivers/usb/xhcid/src/xhci/mod.rs | 128 +++++++++++++++++++++++++++ drivers/usb/xhcid/src/xhci/scheme.rs | 46 ++++++++++ 4 files changed, 254 insertions(+), 1 deletion(-) diff --git a/drivers/usb/xhcid/src/usb/bos.rs b/drivers/usb/xhcid/src/usb/bos.rs index f6a095712d..f5d3d8fde0 100644 --- a/drivers/usb/xhcid/src/usb/bos.rs +++ b/drivers/usb/xhcid/src/usb/bos.rs @@ -60,6 +60,36 @@ pub struct BosUsb2ExtDesc { unsafe impl plain::Plain for BosUsb2ExtDesc {} +impl BosUsb2ExtDesc { + /// bmAttributes bit 1: device supports USB 2.0 Link Power Management. + /// USB 2.0 ECN LPM; Linux 7.1 ch9.h `USB_LPM_SUPPORT`. + pub fn lpm_supported(&self) -> bool { + self.attrs & (1 << 1) != 0 + } + /// bmAttributes bit 2: device supports BESL (Linux `USB_BESL_SUPPORT`). + pub fn besl_supported(&self) -> bool { + self.attrs & (1 << 2) != 0 + } + /// bmAttributes bit 3: Baseline BESL field is valid + /// (Linux `USB_BESL_BASELINE_VALID`). + pub fn besl_baseline_valid(&self) -> bool { + self.attrs & (1 << 3) != 0 + } + /// bmAttributes bit 4: Deep BESL field is valid + /// (Linux `USB_BESL_DEEP_VALID`). + pub fn besl_deep_valid(&self) -> bool { + self.attrs & (1 << 4) != 0 + } + /// bmAttributes bits 8-11: Baseline BESL (Linux `USB_GET_BESL_BASELINE`). + pub fn besl_baseline(&self) -> u8 { + ((self.attrs >> 8) & 0xF) as u8 + } + /// bmAttributes bits 12-15: Deep BESL (Linux `USB_GET_BESL_DEEP`). + pub fn besl_deep(&self) -> u8 { + ((self.attrs >> 12) & 0xF) as u8 + } +} + #[repr(u8)] pub enum DeviceCapability { Usb2Ext = 0x02, @@ -180,3 +210,50 @@ pub fn bos_capability_descs<'a>( BosAnyDevDescIter::from(&data[..desc.total_len as usize - std::mem::size_of_val(&desc)]) .take(desc.cap_count as usize) } + +#[cfg(test)] +mod tests { + use super::*; + + fn ext(attrs: u32) -> BosUsb2ExtDesc { + BosUsb2ExtDesc { + len: 7, + kind: 16, + cap_ty: DeviceCapability::Usb2Ext as u8, + attrs, + } + } + + #[test] + fn usb2_ext_lpm_bits() { + let attrs = (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4) | (5 << 8) | (9 << 12); + let d = ext(attrs); + assert!(d.lpm_supported()); + assert!(d.besl_supported()); + assert!(d.besl_baseline_valid()); + assert!(d.besl_deep_valid()); + assert_eq!(d.besl_baseline(), 5); + assert_eq!(d.besl_deep(), 9); + } + + #[test] + fn usb2_ext_zero_attrs() { + let d = ext(0); + assert!(!d.lpm_supported()); + assert!(!d.besl_supported()); + assert!(!d.besl_baseline_valid()); + assert!(!d.besl_deep_valid()); + assert_eq!(d.besl_baseline(), 0); + assert_eq!(d.besl_deep(), 0); + } + + #[test] + fn usb2_ext_field_isolation() { + let d = ext((0xA << 8) | (0x3 << 12)); + assert!(!d.lpm_supported()); + assert!(!d.besl_baseline_valid()); + assert_eq!(d.besl_baseline(), 0xA); + assert_eq!(d.besl_deep(), 0x3); + } +} + diff --git a/drivers/usb/xhcid/src/usb/mod.rs b/drivers/usb/xhcid/src/usb/mod.rs index d0d659882a..e080a602be 100644 --- a/drivers/usb/xhcid/src/usb/mod.rs +++ b/drivers/usb/xhcid/src/usb/mod.rs @@ -8,7 +8,9 @@ //! the documents that inform this implementation. //! //! See the crate-level documentation for the acronyms used to refer to specific documents. -pub use self::bos::{bos_capability_descs, BosAnyDevDesc, BosDescriptor, BosSuperSpeedDesc}; +pub use self::bos::{ + bos_capability_descs, BosAnyDevDesc, BosDescriptor, BosSuperSpeedDesc, BosUsb2ExtDesc, +}; pub use self::config::ConfigDescriptor; pub use self::device::{DeviceDescriptor, DeviceDescriptor8Byte}; pub use self::endpoint::{ diff --git a/drivers/usb/xhcid/src/xhci/mod.rs b/drivers/usb/xhcid/src/xhci/mod.rs index dd3c0edd0e..f73dba61cf 100644 --- a/drivers/usb/xhcid/src/xhci/mod.rs +++ b/drivers/usb/xhcid/src/xhci/mod.rs @@ -1415,6 +1415,134 @@ impl Xhci { Ok(()) } + /// BESL-to-microseconds encoding for USB 2.0 LPM. + /// Linux 7.1 xhci.c:4590 `xhci_besl_encoding`. + const BESL_ENCODING_US: [u16; 16] = [ + 125, 150, 200, 300, 400, 500, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000, + ]; + /// Default L1 inactivity timeout in microseconds (Linux `XHCI_L1_TIMEOUT`). + const XHCI_L1_TIMEOUT_US: u32 = 512; + /// Default BESL used when the device advertises no preferred value + /// (Linux `XHCI_DEFAULT_BESL`); works with mixed HIRD/BESL systems. + const XHCI_DEFAULT_BESL: u32 = 4; + + /// Change a slot's Max Exit Latency via an Evaluate Context command. + /// Linux 7.1 xhci.c:4520 `xhci_change_max_exit_latency`: the current + /// slot output context is copied into the input context, add flag + /// SLOT is set, and only the MEL field (slot context DWORD1 low 16 + /// bits) is modified. + pub async fn change_max_exit_latency(&self, slot_id: u8, mel: u16) -> Result<()> { + let mut input = unsafe { self.alloc_dma_zeroed::>()? }; + + let out = self + .dev_ctx + .contexts + .get(usize::from(slot_id)) + .ok_or(Error::new(EINVAL))?; + input.device.slot.a.write(out.slot.a.read()); + input.device.slot.b.write(out.slot.b.read()); + input.device.slot.c.write(out.slot.c.read()); + input.device.slot.d.write(out.slot.d.read()); + + input.add_context.write(1 << 0); + let b = input.device.slot.b.read(); + input.device.slot.b.write((b & !0xFFFF) | u32::from(mel)); + + let (event_trb, command_trb) = self + .execute_command(|trb, cycle| { + trb.evaluate_context(slot_id, input.physical(), false, cycle) + }) + .await; + + self::scheme::handle_event_trb("EVALUATE_CONTEXT", &event_trb, &command_trb)?; + + Ok(()) + } + + /// Compute the HIRD/BESL value for the PORTPMSC HIRD field when the + /// controller does not support BESL timing. + /// Linux 7.1 xhci.c:4594 `xhci_calculate_hird_besl`. + fn calculate_hird_besl(&self, ext: &usb::BosUsb2ExtDesc) -> u32 { + let u2del = u32::from(self.cap.u2_device_exit_latency()); + let mut besl_device = 0u32; + let besl_host; + + if ext.besl_supported() { + let mut host = 0u32; + while host < 16 && u32::from(Self::BESL_ENCODING_US[host as usize]) < u2del { + host += 1; + } + besl_host = host; + if ext.besl_baseline_valid() { + besl_device = u32::from(ext.besl_baseline()); + } else if ext.besl_deep_valid() { + besl_device = u32::from(ext.besl_deep()); + } + } else { + besl_host = if u2del <= 50 { 0 } else { (u2del - 51) / 75 + 1 }; + } + + (besl_host + besl_device).min(15) + } + + /// Enable USB 2.0 hardware LPM (L1) on a root-hub port for an + /// attached device. Caller has already evaluated the gate chain + /// (hw_lpm_support, per-port HLC, device LPM support, non-hub, + /// root-hub-direct). + /// Linux 7.1 xhci.c:4650 `xhci_set_usb2_hardware_lpm` enable path. + async fn enable_usb2_hw_lpm( + &self, + port_id: PortId, + slot: u8, + besl_capable: bool, + ext: &usb::BosUsb2ExtDesc, + ) -> Result<()> { + let hird; + let besld; + let hirdm; + // xHCI 1.0 section 5.4.11.2: L1 timeout is programmed in 256us steps. + let l1_timeout = Self::XHCI_L1_TIMEOUT_US / 256; + + if besl_capable { + hird = if ext.besl_supported() && ext.besl_baseline_valid() { + u32::from(ext.besl_baseline()) + } else { + Self::XHCI_DEFAULT_BESL + }; + // The slot's Max Exit Latency must cover the BESL resume time + // before the port is allowed into L1. + let mel = Self::BESL_ENCODING_US[hird as usize]; + self.change_max_exit_latency(slot, mel).await?; + if ext.besl_deep_valid() { + besld = u32::from(ext.besl_deep()); + hirdm = 1; + } else { + besld = 0; + hirdm = 0; + } + } else { + hird = self.calculate_hird_besl(ext); + besld = 0; + hirdm = 0; + } + + let mut ports = self.ports.lock().unwrap_or_else(|e| e.into_inner()); + let idx = port_id.root_hub_port_index().ok_or(Error::new(EINVAL))?; + let port = ports.get_mut(idx).ok_or(Error::new(ENOENT))?; + port.enable_lpm(slot, hird, besld, l1_timeout, hirdm); + log::info!( + "xhcid: port {} slot {} USB2 HW LPM enabled (hird={}, besld={}, hirdm={}, l1_timeout={})", + port_id, + slot, + hird, + besld, + hirdm, + l1_timeout + ); + + Ok(()) + } + pub async fn update_default_control_pipe( &self, input_context: &mut Dma>, diff --git a/drivers/usb/xhcid/src/xhci/scheme.rs b/drivers/usb/xhcid/src/xhci/scheme.rs index 6cab973dcd..bb6384a9ba 100644 --- a/drivers/usb/xhcid/src/xhci/scheme.rs +++ b/drivers/usb/xhcid/src/xhci/scheme.rs @@ -2135,6 +2135,52 @@ impl Xhci { let supports_superspeed = usb::bos_capability_descs(bos_desc, &bos_data).any(|desc| desc.is_superspeed()); let supports_superspeedplus = usb::bos_capability_descs(bos_desc, &bos_data).any(|desc| desc.is_superspeedplus()); + // USB 2.0 hardware LPM (P7-A): enable per-device at attach. + // Gate chain per Linux 7.1 xhci.c:4650 xhci_set_usb2_hardware_lpm: + // controller hw_lpm_support (HW_LPM_DISABLE quirk already folds + // into it) && this port's protocol advertises HLC (USB 2.0 + // protocol only — rev_major() != 3) && the device sets + // USB_LPM_SUPPORT in its BOS USB 2.0 extension && the device is + // not a hub && the device is directly on the root hub (get_desc + // only runs for root-port devices; hub children enumerate via + // usbhubd). + if self.hw_lpm_support && raw_dd.class != 0x09 { + if let Some(protocol) = self.supported_protocol(port_id) { + if protocol.rev_major() != 3 && protocol.hw_lpm_capable() { + let usb2_ext = usb::bos_capability_descs(bos_desc, &bos_data).find_map(|desc| { + match desc { + usb::BosAnyDevDesc::Usb2Ext(ext) => Some(ext), + _ => None, + } + }); + if let Some(ext) = usb2_ext { + if ext.lpm_supported() { + // LPM is opportunistic power saving: a failure + // here must not fail device enumeration — the + // device works in U0 regardless. Log and + // continue on error. + if let Err(err) = self + .enable_usb2_hw_lpm( + port_id, + slot, + protocol.besl_lpm_capable(), + &ext, + ) + .await + { + log::warn!( + "xhcid: port {} slot {} USB2 HW LPM enable failed: {}; device continues without L1", + port_id, + slot, + err + ); + } + } + } + } + } + } + let mut config_descs = SmallVec::new(); for index in 0..raw_dd.configurations {