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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -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>>,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user