diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-01 16:48:40 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-01 16:48:40 +0100 |
| commit | 12549dabf96bd6eb08c6834134fedf9edc7452b5 (patch) | |
| tree | 2237ed1a05b731f0e1a39c2374440cec3c872258 | |
| parent | scripts: add test-usb; fix duplicate -bios in the test harness (diff) | |
| download | rs-dos-12549dabf96bd6eb08c6834134fedf9edc7452b5.tar.gz rs-dos-12549dabf96bd6eb08c6834134fedf9edc7452b5.zip | |
ehci: reserve the 64-bit QH/qTD layout (fixes hardware HSE)
First OptiPlex run: the controller halted with HSE (USBSTS bit4) 20 ms into
the first control transfer, with otherwise valid QH/qTD fields (control QH,
DTC, MPS 8, split hub 0/port 1, active qTD).
The PCH EHCI advertises HCCPARAMS bit0 (64-bit addressing capability), which
extends every QH with bufptr_hi[5] (17 dwords / 68 B) and every qTD the same
way (13 dwords / 52 B). Packing them 32-bit-style (48/32 B) made the
controller read the next qTD as the overlay's high buffer pointer and DMA to
a bogus 64-bit address -> Host System Error.
Space every structure for the 64-bit variant (32-bit controllers ignore the
tail): HID qTD at QH+0x80; control qTDs at QH+0x100/0x140/0x180 with the
setup packet at +0x1C0. The failure QR now reports the capability and the
DMA bases so the next hardware iteration is self-describing.
| -rw-r--r-- | DEBUG_NOTES.md | 33 | ||||
| -rw-r--r-- | rsnix/src/ehci.rs | 39 |
2 files changed, 63 insertions, 9 deletions
diff --git a/DEBUG_NOTES.md b/DEBUG_NOTES.md index b7c0e08..cd2b9ae 100644 --- a/DEBUG_NOTES.md +++ b/DEBUG_NOTES.md | |||
| @@ -631,3 +631,36 @@ Failure signatures to report: | |||
| 631 | - xHCI: no hub support; no USB3 warm-reset subtleties beyond the current | 631 | - xHCI: no hub support; no USB3 warm-reset subtleties beyond the current |
| 632 | port-reset sequence. | 632 | port-reset sequence. |
| 633 | - AHCI/PS2 behaviour unchanged (PS/2 remains the fallback input path). | 633 | - AHCI/PS2 behaviour unchanged (PS/2 remains the fallback input path). |
| 634 | |||
| 635 | ### OptiPlex 7010 — first hardware QR decode | ||
| 636 | |||
| 637 | The first hardware run booted to the shell with no USB input and displayed | ||
| 638 | the EHCI failure QR: | ||
| 639 | |||
| 640 | ``` | ||
| 641 | EHCI fail stage=4 t=20ms sp=0 dev=0 in=true len=18 | ||
| 642 | cmd=00000020 sts=0000b013 | ||
| 643 | prtsc0=0x00001005 hp=0x00003000 | ||
| 644 | qh1=48084000 qh2=40800000 nqtd=012080c0 | ||
| 645 | setup=00080e80 data=80120d80 stat=80008c80 | ||
| 646 | ``` | ||
| 647 | |||
| 648 | - `sts` bit4 = **HSE (Host System Error)** plus HCH: the controller aborted | ||
| 649 | the very first DMA pass after 20 ms, so this is a bus/DMA fault, not a | ||
| 650 | protocol error. | ||
| 651 | - `cmd=0x20`: RUNSTOP self-cleared, only ASE remains. | ||
| 652 | - `qh1/qh2/qTD tokens` are structurally correct: control QH, DTC, MPS 8, | ||
| 653 | split hub 0 / port 1, qTD active with TBYTES. | ||
| 654 | - `prtsc0` PED=0: full-speed device on the root port (companion/TT path). | ||
| 655 | |||
| 656 | **Root cause: 64-bit addressing capability.** The PCH EHCI advertises | ||
| 657 | HCCPARAMS bit0, which extends every QH with `bufptr_hi[5]` (68 B) and every | ||
| 658 | qTD with `bufptr_hi[5]` (52 B). The driver packed them 32-bit-style (48/32 | ||
| 659 | B), so the HC read the *next qTD* as the overlay's high buffer pointer and | ||
| 660 | DMA'd to a bogus 64-bit address -> HSE. QEMU reports `caps64=false`, so only | ||
| 661 | hardware exposed this. | ||
| 662 | |||
| 663 | **Fix:** always reserve the 64-bit layout (32-bit controllers ignore the | ||
| 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 | ||
| 666 | (async/xfer/setup/buf) so the next run is self-describing. | ||
diff --git a/rsnix/src/ehci.rs b/rsnix/src/ehci.rs index 5ee11cf..0aa7e0d 100644 --- a/rsnix/src/ehci.rs +++ b/rsnix/src/ehci.rs | |||
| @@ -77,6 +77,15 @@ const MAX_DEV: usize = 4; // mouse + keyboard on each controller | |||
| 77 | const DMA_MIN: usize = 0x100_0000; // avoid <16 MiB (PCI-master hostile region) | 77 | const DMA_MIN: usize = 0x100_0000; // avoid <16 MiB (PCI-master hostile region) |
| 78 | const DMA_MAX: usize = 0x1_0000_0000; // 32-bit EHCI DMA pointers | 78 | const DMA_MAX: usize = 0x1_0000_0000; // 32-bit EHCI DMA pointers |
| 79 | 79 | ||
| 80 | // Descriptor layout. A controller with HCCPARAMS bit0 (64-bit addressing) | ||
| 81 | // reads an extra bufptr_hi[5] after the 32-bit buffer list: QH = 17 dwords | ||
| 82 | // (68 B), qTD = 13 dwords (52 B). Space every structure for that variant — | ||
| 83 | // 32-bit controllers simply ignore the tail — otherwise the HC parses the | ||
| 84 | // next qTD as a high buffer pointer and dies with a Host System Error. | ||
| 85 | const QTD_SLOT: usize = 0x40; // 64-byte qTD slots (52 B + padding) | ||
| 86 | const CTRL_QTD_OFF: usize = 0x100; // control qTDs, offset from the control QH | ||
| 87 | const HID_QTD_OFF: usize = 0x80; // HID qTD, offset from the HID QH | ||
| 88 | |||
| 80 | const BUF_DEV_DESC: usize = 0x000; // 64 B | 89 | const BUF_DEV_DESC: usize = 0x000; // 64 B |
| 81 | const BUF_CFG: usize = 0x100; // 512 B (hubs can have long configs) | 90 | const BUF_CFG: usize = 0x100; // 512 B (hubs can have long configs) |
| 82 | const BUF_HUB_DESC: usize = 0x300; // 64 B | 91 | const BUF_HUB_DESC: usize = 0x300; // 64 B |
| @@ -94,6 +103,7 @@ struct Ctrl { | |||
| 94 | framelist: *mut u32, | 103 | framelist: *mut u32, |
| 95 | qh_page: *mut u32, | 104 | qh_page: *mut u32, |
| 96 | buf_page: *mut u8, | 105 | buf_page: *mut u8, |
| 106 | caps64: bool, | ||
| 97 | } | 107 | } |
| 98 | 108 | ||
| 99 | impl Ctrl { | 109 | impl Ctrl { |
| @@ -106,6 +116,7 @@ impl Ctrl { | |||
| 106 | framelist: core::ptr::null_mut(), | 116 | framelist: core::ptr::null_mut(), |
| 107 | qh_page: core::ptr::null_mut(), | 117 | qh_page: core::ptr::null_mut(), |
| 108 | buf_page: core::ptr::null_mut(), | 118 | buf_page: core::ptr::null_mut(), |
| 119 | caps64: false, | ||
| 109 | }; | 120 | }; |
| 110 | } | 121 | } |
| 111 | 122 | ||
| @@ -282,9 +293,9 @@ unsafe fn ctl_xfer( | |||
| 282 | let head = cur_ctrl().async_head; | 293 | let head = cur_ctrl().async_head; |
| 283 | let xfer = cur_ctrl().xfer_qh; | 294 | let xfer = cur_ctrl().xfer_qh; |
| 284 | 295 | ||
| 285 | let st = xfer.add(0x40 / 4); // SETUP qtd (32-byte aligned) | 296 | let st = xfer.add(CTRL_QTD_OFF / 4); // SETUP qtd (32-byte aligned) |
| 286 | let dt = st.add(0x20 / 4); // DATA qtd | 297 | let dt = st.add(QTD_SLOT / 4); // DATA qtd |
| 287 | let sp = dt.add(0x20 / 4); // STATUS qtd | 298 | let sp = dt.add(QTD_SLOT / 4); // STATUS qtd |
| 288 | 299 | ||
| 289 | core::ptr::write_volatile(st.add(0), 0u32); | 300 | core::ptr::write_volatile(st.add(0), 0u32); |
| 290 | core::ptr::write_volatile(st.add(1), QT_TERMINATE); | 301 | core::ptr::write_volatile(st.add(1), QT_TERMINATE); |
| @@ -295,7 +306,7 @@ unsafe fn ctl_xfer( | |||
| 295 | for i in 0..5 { | 306 | for i in 0..5 { |
| 296 | core::ptr::write_volatile(st.add(3 + i), 0u32); | 307 | core::ptr::write_volatile(st.add(3 + i), 0u32); |
| 297 | } | 308 | } |
| 298 | let sbuf = xfer.add(0x110 / 4); | 309 | let sbuf = xfer.add(0x1C0 / 4); |
| 299 | let sb = core::slice::from_raw_parts_mut(sbuf.cast::<u8>(), 8); | 310 | let sb = core::slice::from_raw_parts_mut(sbuf.cast::<u8>(), 8); |
| 300 | sb.copy_from_slice(req); | 311 | sb.copy_from_slice(req); |
| 301 | core::ptr::write_volatile(st.add(3), sbuf as u32); | 312 | core::ptr::write_volatile(st.add(3), sbuf as u32); |
| @@ -434,13 +445,14 @@ unsafe fn ctl_xfer( | |||
| 434 | ); | 445 | ); |
| 435 | if !QR_SHOWN.swap(true, Ordering::Relaxed) { | 446 | if !QR_SHOWN.swap(true, Ordering::Relaxed) { |
| 436 | let l1 = alloc::format!( | 447 | let l1 = alloc::format!( |
| 437 | "EHCI fail stage={} t={}ms sp={} dev={} in={} len={}", | 448 | "EHCI fail stage={} t={}ms sp={} dev={} in={} len={} c64={}", |
| 438 | stage, | 449 | stage, |
| 439 | crate::interrupts::uptime_ms().wrapping_sub(t0), | 450 | crate::interrupts::uptime_ms().wrapping_sub(t0), |
| 440 | speed, | 451 | speed, |
| 441 | dev, | 452 | dev, |
| 442 | dir_in, | 453 | dir_in, |
| 443 | len | 454 | len, |
| 455 | cur_ctrl().caps64 as u8 | ||
| 444 | ); | 456 | ); |
| 445 | let l2 = alloc::format!("cmd={:08x} sts={:08x}", rd(op(OPCMD)), rd(op(OPSTS))); | 457 | let l2 = alloc::format!("cmd={:08x} sts={:08x}", rd(op(OPCMD)), rd(op(OPSTS))); |
| 446 | let l3 = alloc::format!( | 458 | let l3 = alloc::format!( |
| @@ -456,7 +468,14 @@ unsafe fn ctl_xfer( | |||
| 456 | core::ptr::read_volatile(qh_next_qtd(xfer)) | 468 | core::ptr::read_volatile(qh_next_qtd(xfer)) |
| 457 | ); | 469 | ); |
| 458 | let l5 = alloc::format!("setup={:08x} data={:08x} stat={:08x}", stok, dtok, ptok); | 470 | let l5 = alloc::format!("setup={:08x} data={:08x} stat={:08x}", stok, dtok, ptok); |
| 459 | let all = [l1, l2, l3, l4, l5]; | 471 | let l6 = alloc::format!( |
| 472 | "dma a={:08x} x={:08x} s={:08x} b={:08x}", | ||
| 473 | cur_ctrl().async_head as usize, | ||
| 474 | xfer as usize, | ||
| 475 | sbuf as usize, | ||
| 476 | cur_ctrl().buf_page as usize | ||
| 477 | ); | ||
| 478 | let all = [l1, l2, l3, l4, l5, l6]; | ||
| 460 | let refs: alloc::vec::Vec<&str> = all.iter().map(|s| s.as_str()).collect(); | 479 | let refs: alloc::vec::Vec<&str> = all.iter().map(|s| s.as_str()).collect(); |
| 461 | crate::gop::dump_qr(&refs); | 480 | crate::gop::dump_qr(&refs); |
| 462 | crate::gop::qr_lock(); | 481 | crate::gop::qr_lock(); |
| @@ -654,7 +673,7 @@ unsafe fn register_hid( | |||
| 654 | } | 673 | } |
| 655 | let c = ctrl_mut(ci); | 674 | let c = ctrl_mut(ci); |
| 656 | let qh = c.qh_page.add(slot * 0x100 / 4); | 675 | let qh = c.qh_page.add(slot * 0x100 / 4); |
| 657 | let iqtd = qh.add(0x40 / 4); | 676 | let iqtd = qh.add(HID_QTD_OFF / 4); |
| 658 | let report = c.buf_page.add(BUF_REPORT + slot * 64); | 677 | let report = c.buf_page.add(BUF_REPORT + slot * 64); |
| 659 | core::ptr::write_bytes(qh as *mut u8, 0, 0x100); | 678 | core::ptr::write_bytes(qh as *mut u8, 0, 0x100); |
| 660 | core::ptr::write_bytes(report, 0, 64); | 679 | core::ptr::write_bytes(report, 0, 64); |
| @@ -981,6 +1000,7 @@ unsafe fn init_one(bus: u8, dev: u8, func: u8) -> bool { | |||
| 981 | let caplen = (rd(base) & 0xff) as usize; | 1000 | let caplen = (rd(base) & 0xff) as usize; |
| 982 | let opb = base + caplen; | 1001 | let opb = base + caplen; |
| 983 | let nports = rd(base + 4) & 0xf; | 1002 | let nports = rd(base + 4) & 0xf; |
| 1003 | let caps64 = (rd(base + 8) & 1) != 0; | ||
| 984 | crate::serial_println!( | 1004 | crate::serial_println!( |
| 985 | "ehci: {:02x}:{:02x}.{} mmio={:#x} caplen={:#x} ports={} 64-bit-caps={}", | 1005 | "ehci: {:02x}:{:02x}.{} mmio={:#x} caplen={:#x} ports={} 64-bit-caps={}", |
| 986 | bus, | 1006 | bus, |
| @@ -989,7 +1009,7 @@ unsafe fn init_one(bus: u8, dev: u8, func: u8) -> bool { | |||
| 989 | base, | 1009 | base, |
| 990 | caplen, | 1010 | caplen, |
| 991 | nports, | 1011 | nports, |
| 992 | (rd(base + 8) & 1) != 0 | 1012 | caps64 |
| 993 | ); | 1013 | ); |
| 994 | 1014 | ||
| 995 | // BIOS legacy ownership: resetting the controller while SMM owns it | 1015 | // BIOS legacy ownership: resetting the controller while SMM owns it |
| @@ -1006,6 +1026,7 @@ unsafe fn init_one(bus: u8, dev: u8, func: u8) -> bool { | |||
| 1006 | c.base = base; | 1026 | c.base = base; |
| 1007 | c.opb = opb; | 1027 | c.opb = opb; |
| 1008 | c.nports = nports; | 1028 | c.nports = nports; |
| 1029 | c.caps64 = caps64; | ||
| 1009 | } | 1030 | } |
| 1010 | 1031 | ||
| 1011 | // Halt, reset, wait for reset to clear. | 1032 | // Halt, reset, wait for reset to clear. |
