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 {}
|
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)]
|
#[repr(u8)]
|
||||||
pub enum DeviceCapability {
|
pub enum DeviceCapability {
|
||||||
Usb2Ext = 0x02,
|
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)])
|
BosAnyDevDescIter::from(&data[..desc.total_len as usize - std::mem::size_of_val(&desc)])
|
||||||
.take(desc.cap_count as usize)
|
.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.
|
//! the documents that inform this implementation.
|
||||||
//!
|
//!
|
||||||
//! See the crate-level documentation for the acronyms used to refer to specific documents.
|
//! 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::config::ConfigDescriptor;
|
||||||
pub use self::device::{DeviceDescriptor, DeviceDescriptor8Byte};
|
pub use self::device::{DeviceDescriptor, DeviceDescriptor8Byte};
|
||||||
pub use self::endpoint::{
|
pub use self::endpoint::{
|
||||||
|
|||||||
@@ -1415,6 +1415,134 @@ impl<const N: usize> Xhci<N> {
|
|||||||
Ok(())
|
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(
|
pub async fn update_default_control_pipe(
|
||||||
&self,
|
&self,
|
||||||
input_context: &mut Dma<InputContext<N>>,
|
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_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());
|
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();
|
let mut config_descs = SmallVec::new();
|
||||||
|
|
||||||
for index in 0..raw_dd.configurations {
|
for index in 0..raw_dd.configurations {
|
||||||
|
|||||||
Reference in New Issue
Block a user