xhcid: per-device USB 2.0 hardware LPM (L1) enablement at attach (P7-A)

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.
This commit is contained in:
Red Bear OS
2026-07-20 19:21:01 +09:00
parent 28afc1fefd
commit 2a3b0d4e0a
4 changed files with 254 additions and 1 deletions
+77
View File
@@ -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);
}
}
+3 -1
View File
@@ -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::{
+128
View File
@@ -1415,6 +1415,134 @@ impl<const N: usize> Xhci<N> {
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::<InputContext<N>>()? };
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<InputContext<N>>,
+46
View File
@@ -2135,6 +2135,52 @@ impl<const N: usize> Xhci<N> {
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 {