diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-01 16:55:14 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-01 16:55:14 +0100 |
| commit | e53161d6b9191533caeea0ef6148f247f4e22441 (patch) | |
| tree | 4b6b251fe9fccca6520f9c0efffb3bb6c06495e3 | |
| parent | ehci: reserve the 64-bit QH/qTD layout (fixes hardware HSE) (diff) | |
| download | rs-dos-e53161d6b9191533caeea0ef6148f247f4e22441.tar.gz rs-dos-e53161d6b9191533caeea0ef6148f247f4e22441.zip | |
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.
| -rw-r--r-- | DEBUG_NOTES.md | 35 | ||||
| -rw-r--r-- | 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. | |||
| 664 | tail): HID qTD at QH+0x80; control qTDs at +0x100/+0x140/+0x180 and the setup | 664 | tail): HID qTD at QH+0x80; control qTDs at +0x100/+0x140/+0x180 and the setup |
| 665 | packet at +0x1C0. The failure QR now prints `c64=` and the DMA bases | 665 | packet at +0x1C0. The failure QR now prints `c64=` and the DMA bases |
| 666 | (async/xfer/setup/buf) so the next run is self-describing. | 666 | (async/xfer/setup/buf) so the next run is self-describing. |
| 667 | |||
| 668 | ### OptiPlex 7010 — second hardware QR (after the 64-bit layout fix) | ||
| 669 | |||
| 670 | ``` | ||
| 671 | EHCI fail stage=1 t=20ms sp=0 dev=0 in=true len=18 c64=1 | ||
| 672 | cmd=00000021 sts=00008003 | ||
| 673 | prtsc0=0x00001005 hp=0x00003000 | ||
| 674 | qh1=48084000 qh2=40800000 nqtd=012081c0 | ||
| 675 | setup=00080e42 data=80120d80 stat=80008c80 | ||
| 676 | dma a=01208000 x=01208080 s=01208240 b=0120b000 | ||
| 677 | ``` | ||
| 678 | |||
| 679 | - `sts=0x8003`: USBINT|USBERRINT|ASS — **no HSE**, the 64-bit QH/qTD layout is | ||
| 680 | fixed and the controller runs. | ||
| 681 | - `stage=1`: the **SETUP qTD halted**, `setup=0x00080e42` -> TBYTES 0, | ||
| 682 | Halted, C_ERR still 3, no XactErr/Babble/DBE (a split that got no valid | ||
| 683 | response). | ||
| 684 | - `prtsc0=0x1005`: device connected + powered, **PED=0** (port never | ||
| 685 | enabled), speed field reads full-speed. | ||
| 686 | |||
| 687 | On Intel PCH the USB2 physical ports sit behind the integrated Rate Matching | ||
| 688 | Hub, so a healthy root port shows the hub as a **high-speed** device | ||
| 689 | (`PED=1`). PED=0 with a connected device means the port did not enable, and | ||
| 690 | a full/low-speed split has no valid TT to talk to -> SETUP halts. | ||
| 691 | |||
| 692 | Follow-up hardening in the driver: | ||
| 693 | - 100 ms power-up settle (RMHs are slow) and stale port-change bits cleared | ||
| 694 | before the reset walk | ||
| 695 | - one reset retry after a settle | ||
| 696 | - explicit log line when a connected port stays disabled | ||
| 697 | - low-speed retry for a failed full-speed device (the PORTSC speed field is | ||
| 698 | only trustworthy once the port is enabled) | ||
| 699 | - persistent bottom-row summary: per controller `64b=`/`leg=` handoff result, | ||
| 700 | every connected port's PORTSC, and the HID device count — hardware runs | ||
| 701 | 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 { | |||
| 104 | qh_page: *mut u32, | 104 | qh_page: *mut u32, |
| 105 | buf_page: *mut u8, | 105 | buf_page: *mut u8, |
| 106 | caps64: bool, | 106 | caps64: bool, |
| 107 | legacy: u32, // USB legacy handoff result (0 released, 1 handed over, 2 timeout, 3 none) | ||
| 107 | } | 108 | } |
| 108 | 109 | ||
| 109 | impl Ctrl { | 110 | impl Ctrl { |
| @@ -117,6 +118,7 @@ impl Ctrl { | |||
| 117 | qh_page: core::ptr::null_mut(), | 118 | qh_page: core::ptr::null_mut(), |
| 118 | buf_page: core::ptr::null_mut(), | 119 | buf_page: core::ptr::null_mut(), |
| 119 | caps64: false, | 120 | caps64: false, |
| 121 | legacy: 3, | ||
| 120 | }; | 122 | }; |
| 121 | } | 123 | } |
| 122 | 124 | ||
| @@ -445,7 +447,8 @@ unsafe fn ctl_xfer( | |||
| 445 | ); | 447 | ); |
| 446 | if !QR_SHOWN.swap(true, Ordering::Relaxed) { | 448 | if !QR_SHOWN.swap(true, Ordering::Relaxed) { |
| 447 | let l1 = alloc::format!( | 449 | let l1 = alloc::format!( |
| 448 | "EHCI fail stage={} t={}ms sp={} dev={} in={} len={} c64={}", | 450 | "EHCI c{} fail stage={} t={}ms sp={} dev={} in={} len={} c64={}", |
| 451 | CUR.load(Ordering::Relaxed), | ||
| 449 | stage, | 452 | stage, |
| 450 | crate::interrupts::uptime_ms().wrapping_sub(t0), | 453 | crate::interrupts::uptime_ms().wrapping_sub(t0), |
| 451 | speed, | 454 | speed, |
| @@ -1014,12 +1017,24 @@ unsafe fn init_one(bus: u8, dev: u8, func: u8) -> bool { | |||
| 1014 | 1017 | ||
| 1015 | // BIOS legacy ownership: resetting the controller while SMM owns it | 1018 | // BIOS legacy ownership: resetting the controller while SMM owns it |
| 1016 | // kills the firmware keyboard path and can collide with our DMA. | 1019 | // kills the firmware keyboard path and can collide with our DMA. |
| 1017 | match crate::pci::claim_usb_legacy(bus, dev, func, base, 0x08, false) { | 1020 | let legacy = match crate::pci::claim_usb_legacy(bus, dev, func, base, 0x08, false) { |
| 1018 | 0 => crate::serial_println!("ehci: USB legacy: already released"), | 1021 | 0 => { |
| 1019 | 1 => crate::serial_println!("ehci: USB legacy handed over by BIOS"), | 1022 | crate::serial_println!("ehci: USB legacy: already released"); |
| 1020 | 2 => crate::serial_println!("ehci: USB legacy claim timed out"), | 1023 | 0 |
| 1021 | _ => crate::serial_println!("ehci: USB legacy: no capability"), | 1024 | } |
| 1022 | } | 1025 | 1 => { |
| 1026 | crate::serial_println!("ehci: USB legacy handed over by BIOS"); | ||
| 1027 | 1 | ||
| 1028 | } | ||
| 1029 | 2 => { | ||
| 1030 | crate::serial_println!("ehci: USB legacy claim timed out"); | ||
| 1031 | 2 | ||
| 1032 | } | ||
| 1033 | _ => { | ||
| 1034 | crate::serial_println!("ehci: USB legacy: no capability"); | ||
| 1035 | 3 | ||
| 1036 | } | ||
| 1037 | }; | ||
| 1023 | 1038 | ||
| 1024 | { | 1039 | { |
| 1025 | let c = ctrl_mut(ci); | 1040 | let c = ctrl_mut(ci); |
| @@ -1027,6 +1042,7 @@ unsafe fn init_one(bus: u8, dev: u8, func: u8) -> bool { | |||
| 1027 | c.opb = opb; | 1042 | c.opb = opb; |
| 1028 | c.nports = nports; | 1043 | c.nports = nports; |
| 1029 | c.caps64 = caps64; | 1044 | c.caps64 = caps64; |
| 1045 | c.legacy = legacy; | ||
| 1030 | } | 1046 | } |
| 1031 | 1047 | ||
| 1032 | // Halt, reset, wait for reset to clear. | 1048 | // Halt, reset, wait for reset to clear. |
| @@ -1112,7 +1128,17 @@ unsafe fn init_one(bus: u8, dev: u8, func: u8) -> bool { | |||
| 1112 | wr(psc, v | PRT_POWER); | 1128 | wr(psc, v | PRT_POWER); |
| 1113 | } | 1129 | } |
| 1114 | } | 1130 | } |
| 1115 | sleep_ms(20); | 1131 | // Rate-matching hubs need noticeably longer than a direct device to |
| 1132 | // come up after port power; give them time and clear stale change | ||
| 1133 | // bits before the reset walk. | ||
| 1134 | sleep_ms(100); | ||
| 1135 | for p in 0..nports as usize { | ||
| 1136 | let psc = prtsc(p); | ||
| 1137 | let v = rd(psc); | ||
| 1138 | if v & PRT_RWC != 0 { | ||
| 1139 | wr(psc, v | PRT_RWC); // change bits are write-1-to-clear | ||
| 1140 | } | ||
| 1141 | } | ||
| 1116 | crate::serial_println!("ehci: controller up ({} ports)", nports); | 1142 | crate::serial_println!("ehci: controller up ({} ports)", nports); |
| 1117 | 1143 | ||
| 1118 | N_CTRL.store((ci + 1) as u32, Ordering::Relaxed); | 1144 | N_CTRL.store((ci + 1) as u32, Ordering::Relaxed); |
| @@ -1124,7 +1150,14 @@ unsafe fn init_one(bus: u8, dev: u8, func: u8) -> bool { | |||
| 1124 | if v & PRT_CONNECT == 0 { | 1150 | if v & PRT_CONNECT == 0 { |
| 1125 | continue; | 1151 | continue; |
| 1126 | } | 1152 | } |
| 1127 | let hs = reset_port(p); | 1153 | let mut hs = reset_port(p); |
| 1154 | if !hs { | ||
| 1155 | // A device behind a rate-matching hub appears as high-speed; | ||
| 1156 | // if the first reset did not enable the port the hub may | ||
| 1157 | // still have been settling — retry once. | ||
| 1158 | sleep_ms(100); | ||
| 1159 | hs = reset_port(p); | ||
| 1160 | } | ||
| 1128 | if !hs && crate::uhci::controller_up() { | 1161 | if !hs && crate::uhci::controller_up() { |
| 1129 | crate::serial_println!( | 1162 | crate::serial_println!( |
| 1130 | "ehci: port {} full/low-speed — owned by companion UHCI", | 1163 | "ehci: port {} full/low-speed — owned by companion UHCI", |
| @@ -1133,6 +1166,13 @@ unsafe fn init_one(bus: u8, dev: u8, func: u8) -> bool { | |||
| 1133 | continue; | 1166 | continue; |
| 1134 | } | 1167 | } |
| 1135 | let speed = if hs { 2 } else { port_speed(p) }; | 1168 | let speed = if hs { 2 } else { port_speed(p) }; |
| 1169 | if !hs { | ||
| 1170 | crate::serial_println!( | ||
| 1171 | "ehci: port {} still not enabled (portsc={:#x}) — full/low-speed without a root TT (RMH not up?)", | ||
| 1172 | p, | ||
| 1173 | rd(prtsc(p)) | ||
| 1174 | ); | ||
| 1175 | } | ||
| 1136 | crate::serial_println!( | 1176 | crate::serial_println!( |
| 1137 | "ehci: port {} connected ({}{})", | 1177 | "ehci: port {} connected ({}{})", |
| 1138 | p, | 1178 | p, |
| @@ -1151,7 +1191,16 @@ unsafe fn init_one(bus: u8, dev: u8, func: u8) -> bool { | |||
| 1151 | port: (p + 1) as u8, | 1191 | port: (p + 1) as u8, |
| 1152 | }) | 1192 | }) |
| 1153 | }; | 1193 | }; |
| 1154 | enumerate(0, speed, tt, p); | 1194 | let mut found = enumerate(0, speed, tt, p); |
| 1195 | if found == 0 && !hs && speed == 0 { | ||
| 1196 | // The PORTSC speed field is only trustworthy once the port is | ||
| 1197 | // enabled; a low-speed device serviced as full-speed halts the | ||
| 1198 | // SETUP split. Retry once as low-speed. | ||
| 1199 | crate::serial_println!("ehci: port {} retrying as low-speed", p); | ||
| 1200 | sleep_ms(20); | ||
| 1201 | found = enumerate(0, 1, tt, p); | ||
| 1202 | } | ||
| 1203 | let _ = found; | ||
| 1155 | } | 1204 | } |
| 1156 | 1205 | ||
| 1157 | arm_periodic(ci); | 1206 | arm_periodic(ci); |
| @@ -1217,14 +1266,21 @@ unsafe fn reset_port(p: usize) -> bool { | |||
| 1217 | } | 1266 | } |
| 1218 | 1267 | ||
| 1219 | fn port_speed(p: usize) -> u32 { | 1268 | fn port_speed(p: usize) -> u32 { |
| 1269 | // Standard PORTSC speed field first (0=FS, 1=LS, 2=HS); PED is 0 for | ||
| 1270 | // FS/LS devices, where the field is technically only "valid" when the | ||
| 1271 | // port is enabled, so fall back to Intel's HOSTPC copy. | ||
| 1272 | let ps = unsafe { (rd(prtsc(p)) >> 26) & 3 }; | ||
| 1273 | if ps != 3 { | ||
| 1274 | return ps; | ||
| 1275 | } | ||
| 1220 | let hp = hostpc(p); | 1276 | let hp = hostpc(p); |
| 1221 | if hp != u32::MAX && hp != 0 { | 1277 | if hp != u32::MAX && hp != 0 { |
| 1222 | let f = (hp >> 25) & 3; | 1278 | let f = (hp >> 26) & 3; |
| 1223 | if f != 3 { | 1279 | if f != 3 { |
| 1224 | return f; | 1280 | return f; |
| 1225 | } | 1281 | } |
| 1226 | } | 1282 | } |
| 1227 | unsafe { (rd(prtsc(p)) >> 26) & 3 } | 1283 | 0 |
| 1228 | } | 1284 | } |
| 1229 | 1285 | ||
| 1230 | // --------------------------------------------------------------------------- | 1286 | // --------------------------------------------------------------------------- |
| @@ -1246,6 +1302,7 @@ pub fn init() -> bool { | |||
| 1246 | } | 1302 | } |
| 1247 | } | 1303 | } |
| 1248 | } | 1304 | } |
| 1305 | show_summary(); | ||
| 1249 | if !up { | 1306 | if !up { |
| 1250 | crate::serial_println!("ehci: no EHCI controller"); | 1307 | crate::serial_println!("ehci: no EHCI controller"); |
| 1251 | } else { | 1308 | } else { |
| @@ -1292,6 +1349,46 @@ pub fn rescan() { | |||
| 1292 | } | 1349 | } |
| 1293 | } | 1350 | } |
| 1294 | 1351 | ||
| 1352 | /// Persistent bottom-line summary: connected ports and their PORTSC, plus | ||
| 1353 | /// HID devices found. Hardware runs without serial still get the whole | ||
| 1354 | /// picture (the one-shot failure QR only names the first fault). | ||
| 1355 | fn show_summary() { | ||
| 1356 | let nc = N_CTRL.load(Ordering::Relaxed) as usize; | ||
| 1357 | let nd = N_DEV.load(Ordering::Relaxed) as usize; | ||
| 1358 | unsafe { | ||
| 1359 | for ci in 0..nc { | ||
| 1360 | let ct = &*core::ptr::addr_of!(CTRLS[ci]); | ||
| 1361 | let mut line = alloc::format!( | ||
| 1362 | "ehci c{} 64b={} leg={} ports:", | ||
| 1363 | ci, | ||
| 1364 | ct.caps64 as u8, | ||
| 1365 | ct.legacy | ||
| 1366 | ); | ||
| 1367 | for p in 0..(ct.nports.min(8) as usize) { | ||
| 1368 | let v = rd(ct.opb + OP_PRTSC_BASE + p * OP_PRTSC_STRIDE); | ||
| 1369 | if v & PRT_CONNECT != 0 { | ||
| 1370 | line.push_str(&alloc::format!(" p{}={:04x}", p, v & 0xffff)); | ||
| 1371 | } | ||
| 1372 | } | ||
| 1373 | crate::gop::boot_marker_at(&line, crate::gop::bottom_row(2 - ci)); | ||
| 1374 | } | ||
| 1375 | let mut mice = 0u32; | ||
| 1376 | let mut kbs = 0u32; | ||
| 1377 | for i in 0..nd { | ||
| 1378 | let d = &*core::ptr::addr_of!(DEVS[i]); | ||
| 1379 | if d.kind == Kind::Mouse { | ||
| 1380 | mice += 1; | ||
| 1381 | } else { | ||
| 1382 | kbs += 1; | ||
| 1383 | } | ||
| 1384 | } | ||
| 1385 | crate::gop::boot_marker_at( | ||
| 1386 | &alloc::format!("usb: mouse={} kbd={}", mice, kbs), | ||
| 1387 | crate::gop::bottom_row(0), | ||
| 1388 | ); | ||
| 1389 | } | ||
| 1390 | } | ||
| 1391 | |||
| 1295 | pub fn controller_up() -> bool { | 1392 | pub fn controller_up() -> bool { |
| 1296 | CTRL_UP.load(Ordering::Relaxed) | 1393 | CTRL_UP.load(Ordering::Relaxed) |
| 1297 | } | 1394 | } |
