From e53161d6b9191533caeea0ef6148f247f4e22441 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 1 Nov 2026 16:55:14 +0100 Subject: ehci: diagnostics + hardening for the port-not-enabled case Second OptiPlex QR: the Host System Error is gone (the 64-bit QH/qTD layout fix worked), but the SETUP qTD now halts with TBYTES=0, C_ERR=3 and no XactErr/Babble/DBE -- a split transaction with no valid response. PORTSC shows CCS=1, PP=1 but PED=0, i.e. the port never enabled, so the split has no TT to address. On Intel PCH a healthy USB2 port shows the rate-matching hub as high-speed (PED=1), so the next step is to see every port's state. - settle 100 ms after powering ports and clear stale change bits before the reset walk (RMHs come up slower than direct devices) - retry the port reset once after settling - log a clear line when a connected port remains disabled - retry a failed full-speed device as low-speed (the PORTSC speed field is only valid once the port is enabled) - persistent bottom-row summary: controller count, caps64, USB legacy handoff result, every connected port's PORTSC and the HID device count. A hardware run without serial now reports the whole picture instead of one QR. --- DEBUG_NOTES.md | 35 ++++++++++++++++ rsnix/src/ehci.rs | 121 ++++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 144 insertions(+), 12 deletions(-) diff --git a/DEBUG_NOTES.md b/DEBUG_NOTES.md index cd2b9ae..c9efdc1 100644 --- a/DEBUG_NOTES.md +++ b/DEBUG_NOTES.md @@ -664,3 +664,38 @@ hardware exposed this. tail): HID qTD at QH+0x80; control qTDs at +0x100/+0x140/+0x180 and the setup packet at +0x1C0. The failure QR now prints `c64=` and the DMA bases (async/xfer/setup/buf) so the next run is self-describing. + +### OptiPlex 7010 — second hardware QR (after the 64-bit layout fix) + +``` +EHCI fail stage=1 t=20ms sp=0 dev=0 in=true len=18 c64=1 +cmd=00000021 sts=00008003 +prtsc0=0x00001005 hp=0x00003000 +qh1=48084000 qh2=40800000 nqtd=012081c0 +setup=00080e42 data=80120d80 stat=80008c80 +dma a=01208000 x=01208080 s=01208240 b=0120b000 +``` + +- `sts=0x8003`: USBINT|USBERRINT|ASS — **no HSE**, the 64-bit QH/qTD layout is + fixed and the controller runs. +- `stage=1`: the **SETUP qTD halted**, `setup=0x00080e42` -> TBYTES 0, + Halted, C_ERR still 3, no XactErr/Babble/DBE (a split that got no valid + response). +- `prtsc0=0x1005`: device connected + powered, **PED=0** (port never + enabled), speed field reads full-speed. + +On Intel PCH the USB2 physical ports sit behind the integrated Rate Matching +Hub, so a healthy root port shows the hub as a **high-speed** device +(`PED=1`). PED=0 with a connected device means the port did not enable, and +a full/low-speed split has no valid TT to talk to -> SETUP halts. + +Follow-up hardening in the driver: +- 100 ms power-up settle (RMHs are slow) and stale port-change bits cleared + before the reset walk +- one reset retry after a settle +- explicit log line when a connected port stays disabled +- low-speed retry for a failed full-speed device (the PORTSC speed field is + only trustworthy once the port is enabled) +- persistent bottom-row summary: per controller `64b=`/`leg=` handoff result, + every connected port's PORTSC, and the HID device count — hardware runs + without serial now show the whole picture, not just the first QR diff --git a/rsnix/src/ehci.rs b/rsnix/src/ehci.rs index 0aa7e0d..2d16d37 100644 --- a/rsnix/src/ehci.rs +++ b/rsnix/src/ehci.rs @@ -104,6 +104,7 @@ struct Ctrl { qh_page: *mut u32, buf_page: *mut u8, caps64: bool, + legacy: u32, // USB legacy handoff result (0 released, 1 handed over, 2 timeout, 3 none) } impl Ctrl { @@ -117,6 +118,7 @@ impl Ctrl { qh_page: core::ptr::null_mut(), buf_page: core::ptr::null_mut(), caps64: false, + legacy: 3, }; } @@ -445,7 +447,8 @@ unsafe fn ctl_xfer( ); if !QR_SHOWN.swap(true, Ordering::Relaxed) { let l1 = alloc::format!( - "EHCI fail stage={} t={}ms sp={} dev={} in={} len={} c64={}", + "EHCI c{} fail stage={} t={}ms sp={} dev={} in={} len={} c64={}", + CUR.load(Ordering::Relaxed), stage, crate::interrupts::uptime_ms().wrapping_sub(t0), speed, @@ -1014,12 +1017,24 @@ unsafe fn init_one(bus: u8, dev: u8, func: u8) -> bool { // BIOS legacy ownership: resetting the controller while SMM owns it // kills the firmware keyboard path and can collide with our DMA. - match crate::pci::claim_usb_legacy(bus, dev, func, base, 0x08, false) { - 0 => crate::serial_println!("ehci: USB legacy: already released"), - 1 => crate::serial_println!("ehci: USB legacy handed over by BIOS"), - 2 => crate::serial_println!("ehci: USB legacy claim timed out"), - _ => crate::serial_println!("ehci: USB legacy: no capability"), - } + let legacy = match crate::pci::claim_usb_legacy(bus, dev, func, base, 0x08, false) { + 0 => { + crate::serial_println!("ehci: USB legacy: already released"); + 0 + } + 1 => { + crate::serial_println!("ehci: USB legacy handed over by BIOS"); + 1 + } + 2 => { + crate::serial_println!("ehci: USB legacy claim timed out"); + 2 + } + _ => { + crate::serial_println!("ehci: USB legacy: no capability"); + 3 + } + }; { let c = ctrl_mut(ci); @@ -1027,6 +1042,7 @@ unsafe fn init_one(bus: u8, dev: u8, func: u8) -> bool { c.opb = opb; c.nports = nports; c.caps64 = caps64; + c.legacy = legacy; } // Halt, reset, wait for reset to clear. @@ -1112,7 +1128,17 @@ unsafe fn init_one(bus: u8, dev: u8, func: u8) -> bool { wr(psc, v | PRT_POWER); } } - sleep_ms(20); + // Rate-matching hubs need noticeably longer than a direct device to + // come up after port power; give them time and clear stale change + // bits before the reset walk. + sleep_ms(100); + for p in 0..nports as usize { + let psc = prtsc(p); + let v = rd(psc); + if v & PRT_RWC != 0 { + wr(psc, v | PRT_RWC); // change bits are write-1-to-clear + } + } crate::serial_println!("ehci: controller up ({} ports)", nports); N_CTRL.store((ci + 1) as u32, Ordering::Relaxed); @@ -1124,7 +1150,14 @@ unsafe fn init_one(bus: u8, dev: u8, func: u8) -> bool { if v & PRT_CONNECT == 0 { continue; } - let hs = reset_port(p); + let mut hs = reset_port(p); + if !hs { + // A device behind a rate-matching hub appears as high-speed; + // if the first reset did not enable the port the hub may + // still have been settling — retry once. + sleep_ms(100); + hs = reset_port(p); + } if !hs && crate::uhci::controller_up() { crate::serial_println!( "ehci: port {} full/low-speed — owned by companion UHCI", @@ -1133,6 +1166,13 @@ unsafe fn init_one(bus: u8, dev: u8, func: u8) -> bool { continue; } let speed = if hs { 2 } else { port_speed(p) }; + if !hs { + crate::serial_println!( + "ehci: port {} still not enabled (portsc={:#x}) — full/low-speed without a root TT (RMH not up?)", + p, + rd(prtsc(p)) + ); + } crate::serial_println!( "ehci: port {} connected ({}{})", p, @@ -1151,7 +1191,16 @@ unsafe fn init_one(bus: u8, dev: u8, func: u8) -> bool { port: (p + 1) as u8, }) }; - enumerate(0, speed, tt, p); + let mut found = enumerate(0, speed, tt, p); + if found == 0 && !hs && speed == 0 { + // The PORTSC speed field is only trustworthy once the port is + // enabled; a low-speed device serviced as full-speed halts the + // SETUP split. Retry once as low-speed. + crate::serial_println!("ehci: port {} retrying as low-speed", p); + sleep_ms(20); + found = enumerate(0, 1, tt, p); + } + let _ = found; } arm_periodic(ci); @@ -1217,14 +1266,21 @@ unsafe fn reset_port(p: usize) -> bool { } fn port_speed(p: usize) -> u32 { + // Standard PORTSC speed field first (0=FS, 1=LS, 2=HS); PED is 0 for + // FS/LS devices, where the field is technically only "valid" when the + // port is enabled, so fall back to Intel's HOSTPC copy. + let ps = unsafe { (rd(prtsc(p)) >> 26) & 3 }; + if ps != 3 { + return ps; + } let hp = hostpc(p); if hp != u32::MAX && hp != 0 { - let f = (hp >> 25) & 3; + let f = (hp >> 26) & 3; if f != 3 { return f; } } - unsafe { (rd(prtsc(p)) >> 26) & 3 } + 0 } // --------------------------------------------------------------------------- @@ -1246,6 +1302,7 @@ pub fn init() -> bool { } } } + show_summary(); if !up { crate::serial_println!("ehci: no EHCI controller"); } else { @@ -1292,6 +1349,46 @@ pub fn rescan() { } } +/// Persistent bottom-line summary: connected ports and their PORTSC, plus +/// HID devices found. Hardware runs without serial still get the whole +/// picture (the one-shot failure QR only names the first fault). +fn show_summary() { + let nc = N_CTRL.load(Ordering::Relaxed) as usize; + let nd = N_DEV.load(Ordering::Relaxed) as usize; + unsafe { + for ci in 0..nc { + let ct = &*core::ptr::addr_of!(CTRLS[ci]); + let mut line = alloc::format!( + "ehci c{} 64b={} leg={} ports:", + ci, + ct.caps64 as u8, + ct.legacy + ); + for p in 0..(ct.nports.min(8) as usize) { + let v = rd(ct.opb + OP_PRTSC_BASE + p * OP_PRTSC_STRIDE); + if v & PRT_CONNECT != 0 { + line.push_str(&alloc::format!(" p{}={:04x}", p, v & 0xffff)); + } + } + crate::gop::boot_marker_at(&line, crate::gop::bottom_row(2 - ci)); + } + let mut mice = 0u32; + let mut kbs = 0u32; + for i in 0..nd { + let d = &*core::ptr::addr_of!(DEVS[i]); + if d.kind == Kind::Mouse { + mice += 1; + } else { + kbs += 1; + } + } + crate::gop::boot_marker_at( + &alloc::format!("usb: mouse={} kbd={}", mice, kbs), + crate::gop::bottom_row(0), + ); + } +} + pub fn controller_up() -> bool { CTRL_UP.load(Ordering::Relaxed) } -- cgit v1.2.3