diff options
Diffstat (limited to 'rsnix/src/ehci.rs')
| -rw-r--r-- | rsnix/src/ehci.rs | 1274 |
1 files changed, 1274 insertions, 0 deletions
diff --git a/rsnix/src/ehci.rs b/rsnix/src/ehci.rs new file mode 100644 index 0000000..6209155 --- /dev/null +++ b/rsnix/src/ehci.rs | |||
| @@ -0,0 +1,1274 @@ | |||
| 1 | // --------------------------------------------------------------------------- | ||
| 2 | // rsdos EHCI (USB2.0 host controller) driver — mouse only. | ||
| 3 | // | ||
| 4 | // Enumerates devices connected to an Intel-class EHCI (direct root-port | ||
| 5 | // connect), discovers a HID boot-mouse interrupt-IN endpoint, and drains its | ||
| 6 | // reports into ps2::MouseEvent. Uses the EHCI async schedule for control | ||
| 7 | // transfers and the periodic (frame-list) schedule for the interrupt IN | ||
| 8 | // endpoint. Polled: no IRQ/MSI — keep exactly one report outstanding and | ||
| 9 | // drain it from the cursor task. | ||
| 10 | // | ||
| 11 | // Why EHCI: on real hardware (e.g. a Q67/Q77-era OptiPlex) a USB mouse is a | ||
| 12 | // USB2 (full/low-speed) device attached to the EHCI USB2 controllers, not to | ||
| 13 | // the xHCI's USB3/SS ports. QEMU only exercises the xHCI path, so this class | ||
| 14 | // of real hardware requires its own driver. | ||
| 15 | // | ||
| 16 | // Identity map (phys==virt) means any kernel-heap pointer is a valid DMA | ||
| 17 | // address; data structures must be 32-byte aligned for the EHCI. | ||
| 18 | // --------------------------------------------------------------------------- | ||
| 19 | |||
| 20 | use crate::ps2::MouseEvent; | ||
| 21 | use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; | ||
| 22 | |||
| 23 | // ---- Registers (host controller registers; opbase = base + CAPLENGTH) ---- | ||
| 24 | const OPCMD: usize = 0x00; | ||
| 25 | const OPSTS: usize = 0x04; | ||
| 26 | const OPINTR: usize = 0x08; | ||
| 27 | const OPFRIDX: usize = 0x0C; | ||
| 28 | const OPCFGFLAG: usize = 0x40; | ||
| 29 | const OP_PRTSC_BASE: usize = 0x44; // PORTSC #1 | ||
| 30 | const OP_PRTSC_STRIDE: usize = 0x04; | ||
| 31 | |||
| 32 | const CMD_RUN: u32 = 1 << 0; | ||
| 33 | const CMD_HCRESET: u32 = 1 << 1; | ||
| 34 | const CMD_PSE: u32 = 1 << 4; | ||
| 35 | const CMD_ASE: u32 = 1 << 5; | ||
| 36 | |||
| 37 | const STS_HCH: u32 = 1 << 12; | ||
| 38 | const STS_SE: u32 = 1 << 4; | ||
| 39 | const STS_RECL: u32 = 1 << 14; | ||
| 40 | |||
| 41 | const PRT_CONNECT: u32 = 1 << 0; | ||
| 42 | const PRT_PED: u32 = 1 << 1; | ||
| 43 | const PRT_RESET: u32 = 1 << 8; | ||
| 44 | const PRT_POWER: u32 = 1 << 12; | ||
| 45 | const PRT_RWC: u32 = (1 << 17) | (1 << 18) | (1 << 16) | (1 << 20); // CSC|PEC|CRC|OCC | ||
| 46 | |||
| 47 | // ---- QH / QTD fields ---- | ||
| 48 | const Q_TYPE_QH: u32 = 1 << 1; | ||
| 49 | const QT_TERMINATE: u32 = 1; // terminate = 1 in a link pointer | ||
| 50 | const QH_HEAD: u32 = 1 << 15; | ||
| 51 | const QH_CONTROL_EP: u32 = 1 << 27; | ||
| 52 | const QH_DTC: u32 = 1 << 14; // data toggle comes from the qTDs (control endpoints) | ||
| 53 | const QH_RL: u32 = 4 << 28; // NAK reload count (Linux EHCI_TUNE_RL_{HS,TT}) | ||
| 54 | const QH_EPS_HIGH: u32 = 2 << 12; | ||
| 55 | const QH_EPS_FULL: u32 = 0 << 12; | ||
| 56 | const QH_EPS_LOW: u32 = 1 << 12; | ||
| 57 | const QH_MULT: u32 = 1 << 30; // MULT=1: one split transaction per uframe | ||
| 58 | |||
| 59 | const TOK_IOC: u32 = 1 << 15; | ||
| 60 | const TOK_ACTIVE: u32 = 1 << 7; | ||
| 61 | const TOK_HALT: u32 = 1 << 6; | ||
| 62 | const TOK_XACT: u32 = 1 << 3; | ||
| 63 | const TOK_MMF: u32 = 1 << 2; // missed microframe: split did not complete in its window | ||
| 64 | const PID_IN: u32 = 1; | ||
| 65 | const PID_SETUP: u32 = 2; | ||
| 66 | |||
| 67 | const CCERR_3: u32 = 3 << 10; | ||
| 68 | |||
| 69 | // ---- Static control state ---- | ||
| 70 | static EHCI_BASE: AtomicU32 = AtomicU32::new(0); | ||
| 71 | static N_PORTS: AtomicU32 = AtomicU32::new(0); | ||
| 72 | static CTRL_UP: AtomicBool = AtomicBool::new(false); | ||
| 73 | static MOUSE_PRES: AtomicBool = AtomicBool::new(false); | ||
| 74 | static DEV_ADDR: AtomicU32 = AtomicU32::new(0); | ||
| 75 | static HID_EP: AtomicU32 = AtomicU32::new(0); // EP number (1..15), IN | ||
| 76 | static HID_MPS: AtomicU32 = AtomicU32::new(0); | ||
| 77 | static ARMED: AtomicBool = AtomicBool::new(false); | ||
| 78 | // When the last enumerate attempt ran (ms): a failed/glitched mouse must | ||
| 79 | // never trigger a re-enumeration hot loop. | ||
| 80 | static LAST_ENUM: AtomicU32 = AtomicU32::new(0); | ||
| 81 | // Root ports observed in full/low-speed mode with a UHCI companion present. | ||
| 82 | // Those ports are owned by the companion controller — the EHCI can never | ||
| 83 | // talk to them, so they are skipped instead of failing a 5 s descriptor | ||
| 84 | // timeout every second (the "stuck in a loop" the OptiPlex showed). | ||
| 85 | static FS_LS_PORTS: AtomicU32 = AtomicU32::new(0); | ||
| 86 | // One-shot: the first transfer failure renders a diagnostic QR on screen | ||
| 87 | // (photograph it); retries keep dumping serial but must not re-render. | ||
| 88 | static QR_SHOWN: AtomicBool = AtomicBool::new(false); | ||
| 89 | // DEBUG: run the one-boot transfer variant matrix (real-hardware bisection | ||
| 90 | // of the EHCI System Error trigger). MATRIX_DONE gates it to one pass; | ||
| 91 | // XFER_TWEAK flips construction variables inside ctl_xfer. | ||
| 92 | static DEBUG_MATRIX: bool = true; | ||
| 93 | static MATRIX_DONE: AtomicBool = AtomicBool::new(false); | ||
| 94 | static XFER_TWEAK: AtomicU32 = AtomicU32::new(0); | ||
| 95 | // Low-region (<1 MB) control QH for the matrix's low-region variant. | ||
| 96 | static mut LOW_XFER: *mut u32 = core::ptr::null_mut(); | ||
| 97 | |||
| 98 | // DMA buffers. Allocated once at init; base pointers recorded here. | ||
| 99 | static mut ASYNC_HEAD: *mut u32 = core::ptr::null_mut(); // QH (32 bytes min, align 32) | ||
| 100 | static mut XFER_QH: *mut u32 = core::ptr::null_mut(); // control QH | ||
| 101 | static mut FRAMELIST: *mut u32 = core::ptr::null_mut(); // 1024 entries, 4K aligned | ||
| 102 | static mut HID_QH: *mut u32 = core::ptr::null_mut(); | ||
| 103 | |||
| 104 | // One dedicated 4 KiB DMA page for every transfer buffer (device descriptor, | ||
| 105 | // config descriptor, HID report). All DMA targets live in controller-visible | ||
| 106 | // pages — never in .bss adjacent to kernel statics — so an overrun (report | ||
| 107 | // longer than MPS, descriptor longer than requested, short-packet arithmetic | ||
| 108 | // slip) can only clobber spare DMA space, on QEMU or on real silicon. | ||
| 109 | static mut BUF_PAGE: *mut u8 = core::ptr::null_mut(); | ||
| 110 | const BUF_DEV_DESC: usize = 0x000; // 64 B incl. red zone | ||
| 111 | const BUF_CFG: usize = 0x100; // 320 B incl. red zone | ||
| 112 | const BUF_HID_REPORT: usize = 0x400; // 64 B incl. red zone | ||
| 113 | |||
| 114 | unsafe fn rd(a: usize) -> u32 { | ||
| 115 | unsafe { (a as *const u32).read_volatile() } | ||
| 116 | } | ||
| 117 | unsafe fn wr(a: usize, v: u32) { | ||
| 118 | unsafe { (a as *mut u32).write_volatile(v) } | ||
| 119 | } | ||
| 120 | |||
| 121 | fn opbase() -> usize { | ||
| 122 | unsafe { | ||
| 123 | EHCI_BASE.load(Ordering::Relaxed) as usize | ||
| 124 | + (rd(EHCI_BASE.load(Ordering::Relaxed) as usize) & 0xff) as usize | ||
| 125 | } | ||
| 126 | } | ||
| 127 | fn op(off: usize) -> usize { | ||
| 128 | opbase() + off | ||
| 129 | } | ||
| 130 | fn prtsc(p: usize) -> usize { | ||
| 131 | opbase() + OP_PRTSC_BASE + p * OP_PRTSC_STRIDE | ||
| 132 | } | ||
| 133 | |||
| 134 | // ---- QH/QTD helpers: all writes go through raw dword pointers ---- | ||
| 135 | fn qh_next(qh: *mut u32) -> *mut u32 { | ||
| 136 | qh.wrapping_add(0) | ||
| 137 | } | ||
| 138 | fn qh_info1(qh: *mut u32) -> *mut u32 { | ||
| 139 | qh.wrapping_add(1) | ||
| 140 | } | ||
| 141 | fn qh_info2(qh: *mut u32) -> *mut u32 { | ||
| 142 | qh.wrapping_add(2) | ||
| 143 | } | ||
| 144 | fn qh_current(qh: *mut u32) -> *mut u32 { | ||
| 145 | qh.wrapping_add(3) | ||
| 146 | } | ||
| 147 | fn qh_next_qtd(qh: *mut u32) -> *mut u32 { | ||
| 148 | qh.wrapping_add(4) | ||
| 149 | } | ||
| 150 | fn qh_alt_qtd(qh: *mut u32) -> *mut u32 { | ||
| 151 | qh.wrapping_add(5) | ||
| 152 | } | ||
| 153 | fn qh_token(qh: *mut u32) -> *mut u32 { | ||
| 154 | qh.wrapping_add(6) | ||
| 155 | } | ||
| 156 | fn qh_buf(qh: *mut u32) -> *mut u32 { | ||
| 157 | qh.wrapping_add(7) | ||
| 158 | } | ||
| 159 | |||
| 160 | fn link_of(ptr: usize) -> u32 { | ||
| 161 | (ptr as u32) | Q_TYPE_QH | ||
| 162 | } | ||
| 163 | |||
| 164 | unsafe fn wait_recl(timeout: usize) { | ||
| 165 | unsafe { | ||
| 166 | let t0 = crate::interrupts::uptime_ms(); | ||
| 167 | while t0 + timeout as u64 > crate::interrupts::uptime_ms() | ||
| 168 | && (rd(op(OPSTS)) & STS_RECL) == 0 | ||
| 169 | { | ||
| 170 | core::hint::spin_loop(); | ||
| 171 | } | ||
| 172 | } | ||
| 173 | } | ||
| 174 | |||
| 175 | /// Issue a single control transfer on the async schedule. `dev` is the USB | ||
| 176 | /// device address (0 for the first GET_DESCRIPTOR), `mps` the EP0 max packet, | ||
| 177 | /// `speed` the device speed (0=full, 1=low, 2=high), `port` the root port | ||
| 178 | /// index (0-based; used only for full/low-speed split QHs), `req` the 8-byte | ||
| 179 | /// setup (bmRequestType,bRequest,..), `dir_in` true for a device->host data | ||
| 180 | /// stage, `buf/len` the data buffer. Returns bytes transferred, or a | ||
| 181 | /// negative error code (-1 timeout, -2 HALT, -3 XactErr, -4 missed uframe). | ||
| 182 | unsafe fn ctl_xfer( | ||
| 183 | dev: u32, | ||
| 184 | mps: u32, | ||
| 185 | speed: u32, | ||
| 186 | port: usize, | ||
| 187 | req: &[u8; 8], | ||
| 188 | dir_in: bool, | ||
| 189 | buf: *mut u8, | ||
| 190 | len: u32, | ||
| 191 | ) -> i32 { | ||
| 192 | // DEBUG matrix hook: XFER_TWEAK bits flip construction variables. | ||
| 193 | // bit0: no-ASE arm; bit1: no-split fields; bit2: no qtd chain; | ||
| 194 | // bit3: zero-length (no DATA stage); bit6: short timeout (1.2 s). | ||
| 195 | let len = if XFER_TWEAK.load(Ordering::Relaxed) & 8 != 0 { | ||
| 196 | 0 | ||
| 197 | } else { | ||
| 198 | len | ||
| 199 | }; | ||
| 200 | unsafe { | ||
| 201 | let head = ASYNC_HEAD; | ||
| 202 | let tw0 = XFER_TWEAK.load(Ordering::Relaxed); | ||
| 203 | // DEBUG matrix: execute the same transfer from the low (<1 MB) page. | ||
| 204 | let xfer = if tw0 & 0x80 != 0 && !LOW_XFER.is_null() { | ||
| 205 | LOW_XFER | ||
| 206 | } else { | ||
| 207 | XFER_QH | ||
| 208 | }; | ||
| 209 | |||
| 210 | // QH occupies dwords 0..11 (48 bytes) incl. the 5-dword buffer list. | ||
| 211 | // QEMU's NLPTR_GET masks qtd addresses to 32-byte alignment | ||
| 212 | // (& 0xffffffe0), so every qtd must sit on a 32-byte boundary. | ||
| 213 | // Place them at +0x40 / +0x60 / +0x80 (each 8 dwords = 32 bytes). | ||
| 214 | let st = xfer.add(0x40 / 4); // SETUP qtd at QH+0x40 (32-aligned) | ||
| 215 | let dt = st.add(0x20 / 4); // DATA qtd at QH+0x60 (32-aligned) | ||
| 216 | let sp = dt.add(0x20 / 4); // STATUS qtd at QH+0x80 (32-aligned) | ||
| 217 | |||
| 218 | // SETUP token: toggle 0, len 8, PID_SETUP, active, IOC off. | ||
| 219 | core::ptr::write_volatile(st.add(0), 0u32); // next (chained below) | ||
| 220 | core::ptr::write_volatile(st.add(1), QT_TERMINATE); // alt: terminate | ||
| 221 | core::ptr::write_volatile( | ||
| 222 | st.add(2), | ||
| 223 | (8 << 16) | CCERR_3 | (PID_SETUP << 8) | TOK_ACTIVE, | ||
| 224 | ); | ||
| 225 | for i in 0..5 { | ||
| 226 | core::ptr::write_volatile(st.add(3 + i), 0u32); | ||
| 227 | } | ||
| 228 | // Setup packet buffer: 8 bytes at +0x110 (clear of the QH + qtds). | ||
| 229 | let sbuf = xfer.add(0x110 / 4); | ||
| 230 | let sb = core::slice::from_raw_parts_mut(sbuf.cast::<u8>(), 8); | ||
| 231 | sb.copy_from_slice(req); | ||
| 232 | core::ptr::write_volatile(st.add(3), sbuf as u32); | ||
| 233 | |||
| 234 | // DATA qtd. EHCI token PID field (bits 9:8): OUT=0, IN=1, SETUP=2. | ||
| 235 | let data_pid_w = (if dir_in { 1u32 } else { 0u32 }) << 8; | ||
| 236 | if len == 0 { | ||
| 237 | // no data stage | ||
| 238 | core::ptr::write_volatile(dt.add(2), 0u32); | ||
| 239 | } else { | ||
| 240 | core::ptr::write_volatile(dt.add(0), sp as u32); | ||
| 241 | // altnext MUST be terminate (1): QEMU's ADVANCEQUEUE follows altnext | ||
| 242 | // whenever TBYTES != 0 after an IN (e.g. a short packet). altnext=0 | ||
| 243 | // is treated as *valid* (bit0==0) so the controller would jump to | ||
| 244 | // qtd address 0 and hang. Terminate keeps short transfers chained | ||
| 245 | // to the STATUS qtd, as required for control transfers. | ||
| 246 | core::ptr::write_volatile(dt.add(1), QT_TERMINATE); | ||
| 247 | core::ptr::write_volatile( | ||
| 248 | dt.add(2), | ||
| 249 | ((len & 0x7fff) << 16) | (1 << 31) | CCERR_3 | (data_pid_w) | TOK_ACTIVE, | ||
| 250 | ); | ||
| 251 | for i in 0..5 { | ||
| 252 | core::ptr::write_volatile(dt.add(3 + i), 0u32); | ||
| 253 | } | ||
| 254 | // data buffer | ||
| 255 | core::ptr::write_volatile(dt.add(3), buf as u32); | ||
| 256 | } | ||
| 257 | |||
| 258 | // STATUS qtd: toggle 1, len 0, PID opposite of the data stage; for a | ||
| 259 | // zero-length control the status is opposite of the setup direction. | ||
| 260 | let setup_in = (req[0] & 0x80) != 0; | ||
| 261 | let status_pid_w = if len > 0 { | ||
| 262 | if dir_in { | ||
| 263 | 0u32 | ||
| 264 | } else { | ||
| 265 | 1u32 | ||
| 266 | } // opposite of data | ||
| 267 | } else if setup_in { | ||
| 268 | 0u32 | ||
| 269 | } else { | ||
| 270 | 1u32 | ||
| 271 | } << 8; | ||
| 272 | core::ptr::write_volatile(sp.add(0), 1u32); // terminate | ||
| 273 | core::ptr::write_volatile(sp.add(1), QT_TERMINATE); | ||
| 274 | core::ptr::write_volatile( | ||
| 275 | sp.add(2), | ||
| 276 | (1 << 31) | CCERR_3 | (status_pid_w) | TOK_ACTIVE | TOK_IOC, | ||
| 277 | ); | ||
| 278 | for i in 0..5 { | ||
| 279 | core::ptr::write_volatile(sp.add(3 + i), 0u32); | ||
| 280 | } | ||
| 281 | |||
| 282 | // Chain the qtd list. | ||
| 283 | if len == 0 { | ||
| 284 | core::ptr::write_volatile(st.add(0), sp as u32); | ||
| 285 | } else { | ||
| 286 | core::ptr::write_volatile(st.add(0), dt as u32); | ||
| 287 | core::ptr::write_volatile(dt.add(0), sp as u32); | ||
| 288 | } | ||
| 289 | |||
| 290 | // Configure the control QH (EP0). | ||
| 291 | let eps = match speed { | ||
| 292 | 2 => QH_EPS_HIGH, | ||
| 293 | 1 => QH_EPS_LOW, | ||
| 294 | _ => QH_EPS_FULL, | ||
| 295 | }; | ||
| 296 | let control_flag = if speed == 2 { 0 } else { QH_CONTROL_EP }; | ||
| 297 | // FS/LS split QH: DTC (toggles come from the qTDs), NAK reload, MULT=1, | ||
| 298 | // hub addr 0 (integrated root-hub TT) and the 1-based root port number | ||
| 299 | // in bits 29:23. The controller runs SSPLIT/CSPLIT pairs for every qTD | ||
| 300 | // on this QH; no S/C masks are needed on the async list (Linux and | ||
| 301 | // FreeBSD both leave them 0 for control/bulk). | ||
| 302 | let info1 = (mps & 0x7ff) << 16 | QH_RL | QH_DTC | control_flag | eps | dev; // devaddr in low 7 bits | ||
| 303 | *qh_info1(xfer) = info1; | ||
| 304 | *qh_info2(xfer) = if speed == 2 || XFER_TWEAK.load(Ordering::Relaxed) & 2 != 0 { | ||
| 305 | 0 | ||
| 306 | } else { | ||
| 307 | QH_MULT | (((port as u32) + 1) << 23) | ||
| 308 | }; | ||
| 309 | *qh_current(xfer) = 0; | ||
| 310 | *qh_next_qtd(xfer) = st as u32; | ||
| 311 | *qh_alt_qtd(xfer) = QT_TERMINATE; | ||
| 312 | *qh_token(xfer) = 0; | ||
| 313 | for i in 0..5 { | ||
| 314 | *qh_buf(xfer).add(i) = 0; | ||
| 315 | } | ||
| 316 | |||
| 317 | // Link: head -> xfer -> head (reclamation cycle). | ||
| 318 | *qh_next(xfer) = link_of(head as usize); | ||
| 319 | *qh_next(head) = link_of(xfer as usize); | ||
| 320 | if XFER_TWEAK.load(Ordering::Relaxed) & 4 != 0 { | ||
| 321 | *qh_next_qtd(xfer) = 1; // no qtd chain: HC treats the QH as idle | ||
| 322 | } | ||
| 323 | |||
| 324 | // Ensure the controller is running and both schedules enabled. | ||
| 325 | // Re-assert every transfer: QEMU may drop RUNSTOP, and re-setting the | ||
| 326 | // bits is what makes it (re)schedule the async walk. | ||
| 327 | // | ||
| 328 | // Clear the W1C status bits first so a failure dump reflects only | ||
| 329 | // this transfer's window (USBINT|USBERR|PCD|FR|SE). | ||
| 330 | wr(op(OPSTS), 0x1F); | ||
| 331 | // Fence: make all DMA writes globally visible before the controller reads. | ||
| 332 | core::sync::atomic::fence(core::sync::atomic::Ordering::SeqCst); | ||
| 333 | let tw = XFER_TWEAK.load(Ordering::Relaxed); | ||
| 334 | if tw & 0x20 != 0 { | ||
| 335 | // wire-off: disable the port so no transaction can reach the wire. | ||
| 336 | wr(prtsc(port), rd(prtsc(port)) & !(1 << 1)); // PED=0 | ||
| 337 | } | ||
| 338 | wr( | ||
| 339 | op(OPCMD), | ||
| 340 | rd(op(OPCMD)) | CMD_RUN | (if tw & 1 == 0 { CMD_ASE } else { 0 }), | ||
| 341 | ); // no PSE — periodic not needed for control | ||
| 342 | let ctl_timeout = if tw & 0x40 != 0 { 1200u64 } else { 5000u64 }; | ||
| 343 | // Poll for completion. Watch the whole chain: a HALT on the SETUP | ||
| 344 | // or DATA qtd stops the queue and the STATUS qtd would never clear | ||
| 345 | // (which previously read as a plain timeout). Exit early naming the | ||
| 346 | // failing stage, and on any failure dump the controller + queue state | ||
| 347 | // for real-hardware iteration. | ||
| 348 | let t0 = crate::interrupts::uptime_ms(); | ||
| 349 | let mut stage: i32 = 0; // 0=ok, 1=setup halted, 2=data halted, 3=timeout, 4=hc halted/SE | ||
| 350 | loop { | ||
| 351 | let stok = core::ptr::read_volatile(st.add(2).cast::<u32>()); | ||
| 352 | let ptok = core::ptr::read_volatile(sp.add(2).cast::<u32>()); | ||
| 353 | if ptok & TOK_ACTIVE == 0 { | ||
| 354 | break; // status completed | ||
| 355 | } | ||
| 356 | if stok & TOK_HALT != 0 { | ||
| 357 | stage = 1; | ||
| 358 | break; | ||
| 359 | } | ||
| 360 | if len > 0 { | ||
| 361 | let dtok = core::ptr::read_volatile(dt.add(2).cast::<u32>()); | ||
| 362 | if dtok & TOK_HALT != 0 { | ||
| 363 | stage = 2; | ||
| 364 | break; | ||
| 365 | } | ||
| 366 | } | ||
| 367 | // Controller-level fault: the HC halted itself (HCH) or threw a | ||
| 368 | // system error (SE). Polling further is pointless — the qtds | ||
| 369 | // would sit ACTIVE forever and read back as a plain timeout. | ||
| 370 | // Catching it here names the window precisely. | ||
| 371 | if rd(op(OPSTS)) & (STS_HCH | STS_SE) != 0 { | ||
| 372 | stage = 4; | ||
| 373 | break; | ||
| 374 | } | ||
| 375 | if t0 + ctl_timeout < crate::interrupts::uptime_ms() { | ||
| 376 | stage = 3; | ||
| 377 | break; | ||
| 378 | } | ||
| 379 | core::hint::spin_loop(); | ||
| 380 | } | ||
| 381 | if stage != 0 { | ||
| 382 | let stok = core::ptr::read_volatile(st.add(2).cast::<u32>()); | ||
| 383 | let dtok = if len > 0 { | ||
| 384 | core::ptr::read_volatile(dt.add(2).cast::<u32>()) | ||
| 385 | } else { | ||
| 386 | 0 | ||
| 387 | }; | ||
| 388 | let ptok = core::ptr::read_volatile(sp.add(2).cast::<u32>()); | ||
| 389 | crate::serial_println!( | ||
| 390 | "ehci: xfer fail stage={} (1=setup 2=data 3=timeout) speed={} dev={} dir_in={} len={}", | ||
| 391 | stage, | ||
| 392 | speed, | ||
| 393 | dev, | ||
| 394 | dir_in, | ||
| 395 | len | ||
| 396 | ); | ||
| 397 | let base = EHCI_BASE.load(Ordering::Relaxed) as usize; | ||
| 398 | crate::serial_println!( | ||
| 399 | "ehci: cmd={:#x} sts={:#x} prtsc{}={:#x} hostpc={:#x}", | ||
| 400 | rd(op(OPCMD)), | ||
| 401 | rd(op(OPSTS)), | ||
| 402 | port, | ||
| 403 | rd(prtsc(port)), | ||
| 404 | rd(base + 0x84 + port * 4) | ||
| 405 | ); | ||
| 406 | crate::serial_println!( | ||
| 407 | "ehci: qh i1={:#x} i2={:#x} cur={:#x} nqtd={:#x}", | ||
| 408 | core::ptr::read_volatile(qh_info1(xfer)), | ||
| 409 | core::ptr::read_volatile(qh_info2(xfer)), | ||
| 410 | core::ptr::read_volatile(qh_current(xfer)), | ||
| 411 | core::ptr::read_volatile(qh_next_qtd(xfer)) | ||
| 412 | ); | ||
| 413 | crate::serial_println!( | ||
| 414 | "ehci: tok setup={:#x} data={:#x} stat={:#x}", | ||
| 415 | stok, | ||
| 416 | dtok, | ||
| 417 | ptok | ||
| 418 | ); | ||
| 419 | if stage == 4 { | ||
| 420 | crate::serial_println!( | ||
| 421 | "ehci: HC halted/SE after {} ms since arm (frindex={:#x} asyncbase={:#x})", | ||
| 422 | crate::interrupts::uptime_ms().wrapping_sub(t0), | ||
| 423 | rd(op(OPFRIDX)), | ||
| 424 | rd(op(0x18)) | ||
| 425 | ); | ||
| 426 | } | ||
| 427 | // One-shot on-screen QR of the whole diagnostic: band text plus a | ||
| 428 | // scannable matrix below. Used on hardware with no serial pipe — | ||
| 429 | // point a phone at the screen. Lock the klog screen echo so the | ||
| 430 | // retry loop cannot overwrite it (serial keeps logging). | ||
| 431 | if !QR_SHOWN.swap(true, Ordering::Relaxed) { | ||
| 432 | let l1 = alloc::format!( | ||
| 433 | "EHCI ctl fail stage={} t={}ms speed={} dev={} in={} len={}", | ||
| 434 | stage, | ||
| 435 | crate::interrupts::uptime_ms().wrapping_sub(t0), | ||
| 436 | speed, | ||
| 437 | dev, | ||
| 438 | dir_in, | ||
| 439 | len | ||
| 440 | ); | ||
| 441 | let l2 = alloc::format!("cmd={:08x} sts={:08x}", rd(op(OPCMD)), rd(op(OPSTS))); | ||
| 442 | let l3 = alloc::format!( | ||
| 443 | "prtsc{}=0x{:08x} hp=0x{:08x}", | ||
| 444 | port, | ||
| 445 | rd(prtsc(port)), | ||
| 446 | rd(base + 0x84 + port * 4) | ||
| 447 | ); | ||
| 448 | let l4 = alloc::format!( | ||
| 449 | "qh1={:08x} qh2={:08x} cur={:08x} nqtd={:08x}", | ||
| 450 | core::ptr::read_volatile(qh_info1(xfer)), | ||
| 451 | core::ptr::read_volatile(qh_info2(xfer)), | ||
| 452 | core::ptr::read_volatile(qh_current(xfer)), | ||
| 453 | core::ptr::read_volatile(qh_next_qtd(xfer)) | ||
| 454 | ); | ||
| 455 | let l5 = alloc::format!("setup={:08x} data={:08x} stat={:08x}", stok, dtok, ptok); | ||
| 456 | let l6 = alloc::format!( | ||
| 457 | "dma a={:08x} x={:08x} f={:08x} h={:08x} b={:08x}", | ||
| 458 | ASYNC_HEAD as usize, | ||
| 459 | XFER_QH as usize, | ||
| 460 | FRAMELIST as usize, | ||
| 461 | HID_QH as usize, | ||
| 462 | BUF_PAGE as usize | ||
| 463 | ); | ||
| 464 | let all = [l1, l2, l3, l4, l5, l6]; | ||
| 465 | let refs: alloc::vec::Vec<&str> = all.iter().map(|s| s.as_str()).collect(); | ||
| 466 | crate::gop::dump_qr(&refs); | ||
| 467 | crate::gop::qr_lock(); | ||
| 468 | } | ||
| 469 | *qh_next(head) = link_of(head as usize); | ||
| 470 | wait_recl(100); | ||
| 471 | return match stage { | ||
| 472 | 1 => -3, // setup halted | ||
| 473 | 2 => -4, // data halted | ||
| 474 | _ => -1, // timeout | ||
| 475 | }; | ||
| 476 | } | ||
| 477 | // Status completed — check its error bits. | ||
| 478 | let tok = core::ptr::read_volatile(sp.add(2).cast::<u32>()); | ||
| 479 | if tok & TOK_HALT != 0 { | ||
| 480 | *qh_next(head) = link_of(head as usize); | ||
| 481 | wait_recl(100); | ||
| 482 | return -2; // HALT — qtd halted on error (babble/DBE/...) | ||
| 483 | } | ||
| 484 | if tok & TOK_XACT != 0 { | ||
| 485 | *qh_next(head) = link_of(head as usize); | ||
| 486 | wait_recl(100); | ||
| 487 | return -5; // XactErr — CRC/timeout on the transaction | ||
| 488 | } | ||
| 489 | if tok & TOK_MMF != 0 { | ||
| 490 | *qh_next(head) = link_of(head as usize); | ||
| 491 | wait_recl(100); | ||
| 492 | return -6; // missed microframe — split window missed (retryable) | ||
| 493 | } | ||
| 494 | // Bytes transferred = requested - remaining on the DATA qtd. | ||
| 495 | let mut got = len; | ||
| 496 | if len > 0 { | ||
| 497 | let dtok = core::ptr::read_volatile(dt.add(2).cast::<u32>()); | ||
| 498 | let rem = ((dtok >> 16) & 0x7fff) as u32; | ||
| 499 | got = len - rem.min(len); | ||
| 500 | } | ||
| 501 | // Unlink: idle self-loop must keep the QH type bit, otherwise QEMU | ||
| 502 | // parses it as an iTD and resets the HC ("non queue head request"). | ||
| 503 | *qh_next(head) = link_of(head as usize); | ||
| 504 | wait_recl(100); | ||
| 505 | let _ = qh_next_qtd; | ||
| 506 | got as i32 | ||
| 507 | } | ||
| 508 | } | ||
| 509 | |||
| 510 | // Control-transfer convenience using the standard control request layout. | ||
| 511 | fn ctrl_req(bmreq: u8, breq: u8, wval: u16, widx: u16, wlen: u16) -> [u8; 8] { | ||
| 512 | [ | ||
| 513 | bmreq, | ||
| 514 | breq, | ||
| 515 | (wval & 0xff) as u8, | ||
| 516 | (wval >> 8) as u8, | ||
| 517 | (widx & 0xff) as u8, | ||
| 518 | (widx >> 8) as u8, | ||
| 519 | (wlen & 0xff) as u8, | ||
| 520 | (wlen >> 8) as u8, | ||
| 521 | ] | ||
| 522 | } | ||
| 523 | |||
| 524 | // --------------------------------------------------------------------------- | ||
| 525 | // Controller bring-up | ||
| 526 | // --------------------------------------------------------------------------- | ||
| 527 | |||
| 528 | /// Find an EHCI controller (class 0x0C class 0x03, prog_if 0x20). | ||
| 529 | fn find_ehci() -> Option<(u8, u8, u8)> { | ||
| 530 | for d in crate::pci::probe() { | ||
| 531 | if d.class == 0x0C && d.subclass == 0x03 && d.prog_if == 0x20 { | ||
| 532 | return Some((d.bus, d.device, d.func)); | ||
| 533 | } | ||
| 534 | } | ||
| 535 | None | ||
| 536 | } | ||
| 537 | |||
| 538 | /// Return the number of the first root port with a device attached, or usize::MAX. | ||
| 539 | fn find_connected_port() -> usize { | ||
| 540 | let np = N_PORTS.load(Ordering::Relaxed) as usize; | ||
| 541 | let skip = FS_LS_PORTS.load(Ordering::Relaxed); | ||
| 542 | for p in 0..np { | ||
| 543 | if skip & (1 << p) != 0 { | ||
| 544 | continue; // full/low-speed — owned by the companion UHCI | ||
| 545 | } | ||
| 546 | let v = unsafe { rd(prtsc(p)) }; | ||
| 547 | if v & PRT_CONNECT != 0 { | ||
| 548 | return p; | ||
| 549 | } | ||
| 550 | } | ||
| 551 | usize::MAX | ||
| 552 | } | ||
| 553 | |||
| 554 | /// Clear the FS/LS port mask so discovery retries (shell `usb rescan`). | ||
| 555 | pub fn rescan() { | ||
| 556 | FS_LS_PORTS.store(0, Ordering::Relaxed); | ||
| 557 | } | ||
| 558 | |||
| 559 | pub fn init() -> bool { | ||
| 560 | let Some((b, d, f)) = find_ehci() else { | ||
| 561 | crate::serial_println!("ehci: no EHCI controller"); | ||
| 562 | return false; | ||
| 563 | }; | ||
| 564 | crate::serial_println!("ehci: USB2 EHCI at {:02x}:{:02x}.{}", b, d, f); | ||
| 565 | |||
| 566 | unsafe { | ||
| 567 | // Enable Memory Space + Bus Master, and disable INTx assertion | ||
| 568 | // (bit 10) — the driver is polled (OPINTR=0), and an unhandled | ||
| 569 | // EHCI interrupt on real hardware would fire a stray IRQ that the | ||
| 570 | // kernel's catch-all handler EOI's but cannot service. | ||
| 571 | let cmd = crate::pci::config_read(b, d, f, 0x04); | ||
| 572 | crate::pci::config_write( | ||
| 573 | b, | ||
| 574 | d, | ||
| 575 | f, | ||
| 576 | 0x04, | ||
| 577 | (cmd & 0xFFFF_0000) | (cmd & 0xFFFF) | 0x6 | 0x400, | ||
| 578 | ); | ||
| 579 | let bar = crate::pci::bar64(b, d, f, 0); | ||
| 580 | let base = bar.unwrap_or(0) as usize; | ||
| 581 | EHCI_BASE.store(base as u32, Ordering::Relaxed); | ||
| 582 | let cap = rd(base); | ||
| 583 | let sp = rd(base + 4); | ||
| 584 | let hc = rd(base + 8); | ||
| 585 | let np = sp & 0xf; | ||
| 586 | N_PORTS.store(np, Ordering::Relaxed); | ||
| 587 | crate::serial_println!( | ||
| 588 | "ehci: mmio={:#x} caplen={:#x} ports={} 64-bit-caps={}", | ||
| 589 | base, | ||
| 590 | cap & 0xff, | ||
| 591 | np, | ||
| 592 | (hc & 1) != 0 | ||
| 593 | ); | ||
| 594 | if base == 0 { | ||
| 595 | return false; | ||
| 596 | } | ||
| 597 | |||
| 598 | // Claim USB Legacy ownership from the BIOS before resetting the | ||
| 599 | // controller. The PCH BIOS drives USB keyboards through SMM | ||
| 600 | // legacy emulation while it owns the EHCI; resetting/reprogramming | ||
| 601 | // the controller underneath it silently kills the legacy keyboard | ||
| 602 | // (observed on the 7010) and the still-active SMI poll can collide | ||
| 603 | // with our async DMA — a Host System Error. The capability lives | ||
| 604 | // in PCI config space (EHCI spec §5.1); the claim walks it exactly | ||
| 605 | // like Linux/FreeBSD. Bounded, no-panic; reports what happened. | ||
| 606 | match crate::pci::claim_usb_legacy(b, d, f, base, 0x08, false) { | ||
| 607 | 0 => crate::serial_println!("ehci: USB legacy: already released (no claim needed)"), | ||
| 608 | 1 => crate::serial_println!("ehci: USB legacy handed over by BIOS"), | ||
| 609 | 2 => crate::serial_println!("ehci: USB legacy claim timed out (BIOS sem stuck)"), | ||
| 610 | _ => crate::serial_println!("ehci: USB legacy: no capability found"), | ||
| 611 | } | ||
| 612 | |||
| 613 | // Halt the controller. | ||
| 614 | wr(op(OPCMD), 0); | ||
| 615 | let mut t = 0; | ||
| 616 | while t < 1_000_000 && (rd(op(OPSTS)) & STS_HCH) == 0 { | ||
| 617 | t += 1; | ||
| 618 | core::hint::spin_loop(); | ||
| 619 | } | ||
| 620 | crate::serial_println!( | ||
| 621 | "ehci: halted {}", | ||
| 622 | if rd(op(OPSTS)) & STS_HCH != 0 { | ||
| 623 | "ok" | ||
| 624 | } else { | ||
| 625 | "TIMEOUT" | ||
| 626 | } | ||
| 627 | ); | ||
| 628 | // Ensure not in reset already; then issue HC reset. | ||
| 629 | wr(op(OPCMD), CMD_HCRESET); | ||
| 630 | t = 0; | ||
| 631 | while t < 10_000_000 && (rd(op(OPCMD)) & CMD_HCRESET) != 0 { | ||
| 632 | t += 1; | ||
| 633 | core::hint::spin_loop(); | ||
| 634 | } | ||
| 635 | crate::serial_println!( | ||
| 636 | "ehci: hcreset {}", | ||
| 637 | if rd(op(OPCMD)) & CMD_HCRESET == 0 { | ||
| 638 | "ok" | ||
| 639 | } else { | ||
| 640 | "TIMEOUT" | ||
| 641 | } | ||
| 642 | ); | ||
| 643 | t = 0; | ||
| 644 | while t < 10_000_000 && (rd(op(OPSTS)) & STS_HCH) == 0 { | ||
| 645 | t += 1; | ||
| 646 | core::hint::spin_loop(); | ||
| 647 | } | ||
| 648 | crate::serial_println!( | ||
| 649 | "ehci: post-reset halt {}", | ||
| 650 | if rd(op(OPSTS)) & STS_HCH != 0 { | ||
| 651 | "ok" | ||
| 652 | } else { | ||
| 653 | "TIMEOUT" | ||
| 654 | } | ||
| 655 | ); | ||
| 656 | // Disable interrupts (polled). | ||
| 657 | wr(op(OPINTR), 0); | ||
| 658 | // Clear HCHalted? After reset the HC is halted; running is set later. | ||
| 659 | |||
| 660 | // Allocate the async head QH + control QH + frame list + HID structures. | ||
| 661 | ASYNC_HEAD = dma_alloc(0x400, 0x20) as *mut u32; // 1KB region | ||
| 662 | XFER_QH = ASYNC_HEAD.add(0x80 / 4); // control QH at +0x80 | ||
| 663 | // qtds live after XFER_QH's 11 dwords (44 bytes); space reserved. | ||
| 664 | FRAMELIST = dma_alloc(0x1000, 0x1000) as *mut u32; | ||
| 665 | HID_QH = dma_alloc(0x400, 0x20) as *mut u32; | ||
| 666 | // DEBUG matrix: low-region control QH (sub-1MB page) to test whether | ||
| 667 | // the SE is tied to the 16 MB DMA region (write protection) or not. | ||
| 668 | if let Some(lp) = crate::memory::alloc_pages_above(2, 0) { | ||
| 669 | LOW_XFER = lp as *mut u32; | ||
| 670 | crate::serial_println!("ehci: low-xfer debug page at {:#x}", lp); | ||
| 671 | } | ||
| 672 | BUF_PAGE = dma_alloc(0x1000, 0x20); | ||
| 673 | if BUF_PAGE.is_null() { | ||
| 674 | crate::serial_println!("ehci: buffer page allocation failed"); | ||
| 675 | return false; | ||
| 676 | } | ||
| 677 | crate::serial_println!( | ||
| 678 | "ehci: dma async={:#x} xfer={:#x} framelist={:#x} hid={:#x} buf={:#x}", | ||
| 679 | ASYNC_HEAD as usize, | ||
| 680 | XFER_QH as usize, | ||
| 681 | FRAMELIST as usize, | ||
| 682 | HID_QH as usize, | ||
| 683 | BUF_PAGE as usize | ||
| 684 | ); | ||
| 685 | |||
| 686 | // Init async head QH: self-linked, marked as head of reclamation. | ||
| 687 | *qh_next(ASYNC_HEAD) = link_of(XFER_QH as usize); // circular: head → control QH → head | ||
| 688 | *qh_info1(ASYNC_HEAD) = QH_HEAD; | ||
| 689 | *qh_info2(ASYNC_HEAD) = 0; | ||
| 690 | *qh_current(ASYNC_HEAD) = 0; | ||
| 691 | *qh_next_qtd(ASYNC_HEAD) = 0; | ||
| 692 | |||
| 693 | // Configure CONTROL QH as an idle EP0 (filled per transfer). | ||
| 694 | *qh_next(XFER_QH) = link_of(ASYNC_HEAD as usize); | ||
| 695 | *qh_info1(XFER_QH) = QH_EPS_FULL | (64 << 16); // HS mps 64 | ||
| 696 | *qh_info2(XFER_QH) = 0; | ||
| 697 | *qh_current(XFER_QH) = 0; | ||
| 698 | *qh_next_qtd(XFER_QH) = 0; // idle | ||
| 699 | *qh_alt_qtd(XFER_QH) = QT_TERMINATE; | ||
| 700 | *qh_token(XFER_QH) = 0; | ||
| 701 | |||
| 702 | // Async schedule address. | ||
| 703 | wr(op(OPFRIDX), 0); | ||
| 704 | // Set ASYNCLISTADDR (32-bit). Controller supports 64-bit (hcc&1) but we | ||
| 705 | // keep everything below 4 GB; CTRLDSSEGMENT=0 suffices. | ||
| 706 | // ASYNCLISTADDR low 32 bits: address of HEAD QH (lower 5 bits zero). | ||
| 707 | // With hcc 64-bit set, the controller reads the 64-bit address | ||
| 708 | // CTRLDSSEGMENT:ASYNCLISTADDR. Zero DSEG + low 32-bit pointer is fine | ||
| 709 | // for addresses < 4 GB. | ||
| 710 | let head_addr = ASYNC_HEAD as usize; | ||
| 711 | wr(op(0x10), 0); // CTRLDSSEGMENT | ||
| 712 | // ASYNCLISTADDR is at op+0x18 | ||
| 713 | wr(op(0x18), (head_addr as u32) & !0x1F); | ||
| 714 | // Periodic frame list address. | ||
| 715 | // Zero the frame list to avoid stale QH/ITD pointers causing | ||
| 716 | // QEMU to reset the controller when PSE is later enabled. | ||
| 717 | let fl = FRAMELIST as usize; | ||
| 718 | for i in 0..1024 { | ||
| 719 | core::ptr::write_volatile(FRAMELIST.add(i), 0u32); | ||
| 720 | } | ||
| 721 | wr(op(0x14), (fl as u32) & !0x3FF); | ||
| 722 | // Frame list size: FLS field (bits 2-3) stays 0 → 1024 entries. | ||
| 723 | // Intel/QEMU layout: PSE=bit4, ASE=bit5. | ||
| 724 | // NOTE: do NOT enable PSE here — the frame list is empty (all zeros). | ||
| 725 | // Enabling PSE on an empty frame list is fine (QEMU skips zero entries), | ||
| 726 | // but we want to avoid any race with stale OVMF entries. PSE will be | ||
| 727 | // enabled later in poll_mouse when the HID QH is linked into the list. | ||
| 728 | wr(op(OPCMD), 0x01u32); // CMD_RUN only, no PSE | ||
| 729 | wr(op(OPCMD), rd(op(OPCMD)) | CMD_RUN); | ||
| 730 | // Wait to leave halt. | ||
| 731 | let mut t = 0; | ||
| 732 | while t < 1_000_000 && (rd(op(OPSTS)) & STS_HCH) != 0 { | ||
| 733 | t += 1; | ||
| 734 | core::hint::spin_loop(); | ||
| 735 | } | ||
| 736 | crate::serial_println!( | ||
| 737 | "ehci: running {}", | ||
| 738 | if rd(op(OPSTS)) & STS_HCH == 0 { | ||
| 739 | "ok" | ||
| 740 | } else { | ||
| 741 | "STUCK HALTED" | ||
| 742 | } | ||
| 743 | ); | ||
| 744 | // Configure flag: route all ports to the host (CF=1) and power them. | ||
| 745 | wr(op(OPCFGFLAG), 1); | ||
| 746 | for p in 0..np as usize { | ||
| 747 | let psc = prtsc(p); | ||
| 748 | let v = rd(psc); | ||
| 749 | if v & PRT_POWER == 0 { | ||
| 750 | wr(psc, v | PRT_POWER); | ||
| 751 | } | ||
| 752 | } | ||
| 753 | CTRL_UP.store(true, Ordering::Relaxed); | ||
| 754 | crate::serial_println!("ehci: controller up ({} ports)", np); | ||
| 755 | |||
| 756 | // DIAGNOSTIC: run the async schedule with an idle head (self-loop, | ||
| 757 | // no qtds) for 150 ms. If this faults (HCH/SE), the SE happens on | ||
| 758 | // schedule/DMA mechanics alone — nothing to do with the split | ||
| 759 | // transfer. If clean, the fault is specific to transfer execution. | ||
| 760 | *qh_next(ASYNC_HEAD) = link_of(ASYNC_HEAD as usize); | ||
| 761 | wr(op(OPSTS), 0x1F); | ||
| 762 | core::sync::atomic::fence(core::sync::atomic::Ordering::SeqCst); | ||
| 763 | wr(op(OPCMD), rd(op(OPCMD)) | CMD_RUN | CMD_ASE); | ||
| 764 | let pt0 = crate::interrupts::uptime_ms(); | ||
| 765 | loop { | ||
| 766 | let s = rd(op(OPSTS)); | ||
| 767 | if s & (STS_HCH | STS_SE) != 0 { | ||
| 768 | crate::serial_println!( | ||
| 769 | "ehci: idle-probe FAULTED {}ms: cmd={:#x} sts={:#x} frindex={:#x} asyncbase={:#x} prtsc0={:#x}", | ||
| 770 | crate::interrupts::uptime_ms().wrapping_sub(pt0), | ||
| 771 | rd(op(OPCMD)), | ||
| 772 | s, | ||
| 773 | rd(op(OPFRIDX)), | ||
| 774 | rd(op(0x18)), | ||
| 775 | rd(prtsc(0)) | ||
| 776 | ); | ||
| 777 | break; | ||
| 778 | } | ||
| 779 | if crate::interrupts::uptime_ms().wrapping_sub(pt0) >= 150 { | ||
| 780 | crate::serial_println!("ehci: idle-probe ok (150ms, no fault)"); | ||
| 781 | break; | ||
| 782 | } | ||
| 783 | core::hint::spin_loop(); | ||
| 784 | } | ||
| 785 | } | ||
| 786 | true | ||
| 787 | } | ||
| 788 | |||
| 789 | /// Port reset on USB2 EHCI. Assert PR, hold it briefly (real EHCI clears | ||
| 790 | /// PR itself once reset signaling finishes; QEMU needs the driver to | ||
| 791 | /// deassert), then wait for the port to enable. Returns true if PED set | ||
| 792 | /// (high-speed device). Full/low-speed stay PED=0 and need a TT path. | ||
| 793 | unsafe fn reset_port(p: usize) -> bool { | ||
| 794 | unsafe { | ||
| 795 | let psc = prtsc(p); | ||
| 796 | let v = rd(psc); | ||
| 797 | // Put port in reset. | ||
| 798 | wr(psc, (v | PRT_RESET) & !PRT_RWC); | ||
| 799 | // Hold ~50 ms, bailing early if the HC cleared PR on its own. | ||
| 800 | let t0 = crate::interrupts::uptime_ms(); | ||
| 801 | while crate::interrupts::uptime_ms().saturating_sub(t0) < 50 { | ||
| 802 | core::hint::spin_loop(); | ||
| 803 | if rd(psc) & PRT_RESET == 0 { | ||
| 804 | break; | ||
| 805 | } | ||
| 806 | } | ||
| 807 | // Deassert explicitly (QEMU only completes the reset on deassert). | ||
| 808 | wr(psc, (rd(psc) & !PRT_RESET) & !PRT_RWC); | ||
| 809 | // Wait briefly for the port to enable (PED). High-speed devices set | ||
| 810 | // PED on reset; full/low-speed stay PED=0 (they need split/TT). Do | ||
| 811 | // not spin forever — decide on the line state instead. | ||
| 812 | let mut t = 0; | ||
| 813 | while t < 2_000_000 && (rd(psc) & PRT_PED) == 0 { | ||
| 814 | t += 1; | ||
| 815 | core::hint::spin_loop(); | ||
| 816 | } | ||
| 817 | // A high-speed device gains PED. Full/low-speed won't (needs companion/TT). | ||
| 818 | rd(psc) & PRT_PED != 0 | ||
| 819 | } | ||
| 820 | } | ||
| 821 | |||
| 822 | /// Distinguish full- vs low-speed on a connected root port (for the split QH | ||
| 823 | /// EPS field). Intel PCH EHCIs expose the speed in HOSTPC (base+0x84+4p, | ||
| 824 | /// bits 27:26: 0=full, 1=low, 2=high); other controllers fall back to the | ||
| 825 | /// PORTSC bits 27:26. Only meaningful after the port has completed reset. | ||
| 826 | unsafe fn port_speed(p: usize) -> u32 { | ||
| 827 | unsafe { | ||
| 828 | let base = EHCI_BASE.load(Ordering::Relaxed) as usize; | ||
| 829 | let hp = rd(base + 0x84 + p * 4); | ||
| 830 | if hp != u32::MAX && hp != 0 { | ||
| 831 | let f = (hp >> 25) & 3; | ||
| 832 | if f != 3 { | ||
| 833 | return f; | ||
| 834 | } | ||
| 835 | } | ||
| 836 | (rd(prtsc(p)) >> 26) & 3 | ||
| 837 | } | ||
| 838 | } | ||
| 839 | |||
| 840 | // --------------------------------------------------------------------------- | ||
| 841 | // Enumeration + HID discovery | ||
| 842 | // --------------------------------------------------------------------------- | ||
| 843 | |||
| 844 | const HID_CLASS: u8 = 0x03; | ||
| 845 | |||
| 846 | fn find_hid_ep(cfg: &[u8]) -> Option<(u8, u32, u32, u8)> { | ||
| 847 | // returns (epaddr, mps, interval_bint, epnum) | ||
| 848 | let mut i = 0usize; | ||
| 849 | let mut in_iface = false; | ||
| 850 | while i + 2 <= cfg.len() { | ||
| 851 | let blen = cfg[i] as usize; | ||
| 852 | if blen == 0 { | ||
| 853 | break; | ||
| 854 | } | ||
| 855 | let bt = cfg[i + 1]; | ||
| 856 | if bt == 4 && i + 9 <= cfg.len() { | ||
| 857 | in_iface = cfg[i + 5] == HID_CLASS; | ||
| 858 | } else if bt == 5 && in_iface && i + 7 <= cfg.len() { | ||
| 859 | let addr = cfg[i + 2]; | ||
| 860 | let attrs = cfg[i + 3]; | ||
| 861 | let mps = (cfg[i + 4] as u32) | ((cfg[i + 5] as u32) << 8); | ||
| 862 | let bint = cfg[i + 6] as u32; | ||
| 863 | if (addr & 0x80) != 0 && (attrs & 0x03) == 0x03 { | ||
| 864 | return Some((addr, mps, bint, addr & 0x0f)); | ||
| 865 | } | ||
| 866 | } | ||
| 867 | i += blen; | ||
| 868 | } | ||
| 869 | None | ||
| 870 | } | ||
| 871 | |||
| 872 | fn enumerate() -> bool { | ||
| 873 | let port = find_connected_port(); | ||
| 874 | if port == usize::MAX { | ||
| 875 | return false; | ||
| 876 | } | ||
| 877 | // DEBUG: one-boot transfer variant matrix replaces normal enumeration. | ||
| 878 | if DEBUG_MATRIX && !MATRIX_DONE.swap(true, Ordering::Relaxed) { | ||
| 879 | unsafe { | ||
| 880 | debug_xfer_matrix(port); | ||
| 881 | } | ||
| 882 | return false; // diagnostic build | ||
| 883 | } | ||
| 884 | unsafe { | ||
| 885 | // If a periodic schedule is live (a previous arm), stop it cleanly | ||
| 886 | // before touching the controller: point every frame at terminate, | ||
| 887 | // clear PSE and wait for the reclaim handshake. Re-enumerating while | ||
| 888 | // the old HID QH is still being walked interleaves the periodic walk | ||
| 889 | // with control transfers — fragile on any HC. | ||
| 890 | if rd(op(OPCMD)) & CMD_PSE != 0 { | ||
| 891 | let fl = FRAMELIST; | ||
| 892 | for f in 0..1024 { | ||
| 893 | core::ptr::write_volatile(fl.add(f), QT_TERMINATE); | ||
| 894 | } | ||
| 895 | core::sync::atomic::fence(core::sync::atomic::Ordering::SeqCst); | ||
| 896 | wr(op(OPCMD), rd(op(OPCMD)) & !CMD_PSE); | ||
| 897 | wait_recl(1000); | ||
| 898 | } | ||
| 899 | // Discovery state: nothing armed/present until the arm at the end. | ||
| 900 | ARMED.store(false, Ordering::Relaxed); | ||
| 901 | MOUSE_PRES.store(false, Ordering::Relaxed); | ||
| 902 | let hs = reset_port(port); | ||
| 903 | if !hs && crate::uhci::controller_up() { | ||
| 904 | // A real (non-QEMU) EHCI can never transfer to a full/low-speed | ||
| 905 | // device: the port is owned by the companion UHCI. Mark the | ||
| 906 | // port and stop — a descriptor timeout here costs 5 s and | ||
| 907 | // repeats every second, which is exactly the "stuck in a loop" | ||
| 908 | // the OptiPlex showed. (QEMU's usb-ehci accepts PED=0 with a | ||
| 909 | // high-speed device behind it, so this skip only triggers when | ||
| 910 | // a real UHCI companion is actually present.) | ||
| 911 | FS_LS_PORTS.store( | ||
| 912 | FS_LS_PORTS.load(Ordering::Relaxed) | (1 << port), | ||
| 913 | Ordering::Relaxed, | ||
| 914 | ); | ||
| 915 | crate::serial_println!( | ||
| 916 | "ehci: port {} full/low-speed — owned by companion UHCI (uhci driver handles it)", | ||
| 917 | port | ||
| 918 | ); | ||
| 919 | return false; | ||
| 920 | } | ||
| 921 | crate::serial_println!( | ||
| 922 | "ehci: port {} resolved{}", | ||
| 923 | port, | ||
| 924 | if hs { | ||
| 925 | " (high-speed)" | ||
| 926 | } else { | ||
| 927 | " (full/low — TT path)" | ||
| 928 | } | ||
| 929 | ); | ||
| 930 | // Speed for the QH EPS field: 2=HS, 1=LS, 0=FS. FS/LS on an EHCI | ||
| 931 | // root port must be serviced with split transactions (integrated TT). | ||
| 932 | let speed = if hs { 2 } else { port_speed(port) }; | ||
| 933 | let speed_name = match speed { | ||
| 934 | 2 => "high-speed", | ||
| 935 | 1 => "low-speed (split)", | ||
| 936 | _ => "full-speed (split)", | ||
| 937 | }; | ||
| 938 | let hostpc = unsafe { rd(EHCI_BASE.load(Ordering::Relaxed) as usize + 0x84 + port * 4) }; | ||
| 939 | crate::serial_println!( | ||
| 940 | "ehci: port {} speed: {} (hostpc={:#x})", | ||
| 941 | port, | ||
| 942 | speed_name, | ||
| 943 | hostpc | ||
| 944 | ); | ||
| 945 | let mps = if hs { 64u32 } else { 8u32 }; | ||
| 946 | |||
| 947 | // 1. GET_DESCRIPTOR (device) — 18 bytes to learn idVendor etc. | ||
| 948 | let req = ctrl_req(0x80, 6, 0x0100, 0, 18); | ||
| 949 | let dd = BUF_PAGE.add(BUF_DEV_DESC); | ||
| 950 | let mut got = ctl_xfer(0, mps, speed, port, &req, true, dd, 18); | ||
| 951 | if got < 0 { | ||
| 952 | crate::serial_println!("ehci: get-dev-desc failed code={}", got); | ||
| 953 | return false; | ||
| 954 | } | ||
| 955 | if got < 8 { | ||
| 956 | crate::serial_println!("ehci: short dev-desc ({})", got); | ||
| 957 | return false; | ||
| 958 | } | ||
| 959 | let dd = dd as *const u8; | ||
| 960 | let mps0 = core::ptr::read_volatile(dd.add(7)) as u32; // bMaxPacketSize0 | ||
| 961 | let _ = mps0; | ||
| 962 | crate::serial_println!( | ||
| 963 | "ehci: dev {:04x}:{:04x} class={:#x} mps0={}", | ||
| 964 | (core::ptr::read_volatile(dd.add(9)) as u16) | ||
| 965 | | ((core::ptr::read_volatile(dd.add(8)) as u16) << 8), | ||
| 966 | (core::ptr::read_volatile(dd.add(11)) as u16) | ||
| 967 | | ((core::ptr::read_volatile(dd.add(10)) as u16) << 8), | ||
| 968 | core::ptr::read_volatile(dd.add(4)), | ||
| 969 | core::ptr::read_volatile(dd.add(7)) | ||
| 970 | ); | ||
| 971 | |||
| 972 | // 2. SET_ADDRESS to 1. | ||
| 973 | let req = ctrl_req(0x00, 5, 1, 0, 0); | ||
| 974 | if ctl_xfer(0, mps, speed, port, &req, false, core::ptr::null_mut(), 0) < 0 { | ||
| 975 | crate::serial_println!("ehci: set-address failed"); | ||
| 976 | return false; | ||
| 977 | } | ||
| 978 | DEV_ADDR.store(1, Ordering::Relaxed); | ||
| 979 | |||
| 980 | // 3. GET_CONFIG(0) — 256 bytes, enough for a HID descriptor. | ||
| 981 | let cfg = BUF_PAGE.add(BUF_CFG); | ||
| 982 | core::ptr::write_bytes(cfg, 0, 256); | ||
| 983 | let req = ctrl_req(0x80, 6, 0x0200, 0, 256); | ||
| 984 | got = ctl_xfer(1, mps, speed, port, &req, true, cfg, 256); | ||
| 985 | if got < 0 { | ||
| 986 | crate::serial_println!("ehci: get-config failed code={}", got); | ||
| 987 | return false; | ||
| 988 | } | ||
| 989 | let Some((epaddr, epmps, bint, epnum)) = | ||
| 990 | find_hid_ep(core::slice::from_raw_parts(cfg as *const u8, 256)) | ||
| 991 | else { | ||
| 992 | crate::serial_println!("ehci: no HID mouse endpoint in config"); | ||
| 993 | return false; | ||
| 994 | }; | ||
| 995 | crate::serial_println!( | ||
| 996 | "ehci: HID mouse IN ep{}({:#x}) mps={} interval={}", | ||
| 997 | epnum, | ||
| 998 | epaddr, | ||
| 999 | epmps, | ||
| 1000 | bint | ||
| 1001 | ); | ||
| 1002 | |||
| 1003 | // 4. SET_CONFIGURATION(1). | ||
| 1004 | let req = ctrl_req(0x00, 9, 1, 0, 0); | ||
| 1005 | if ctl_xfer(1, mps, speed, port, &req, false, core::ptr::null_mut(), 0) < 0 { | ||
| 1006 | crate::serial_println!("ehci: set-config failed"); | ||
| 1007 | return false; | ||
| 1008 | } | ||
| 1009 | |||
| 1010 | // 5. Arm the HID interrupt-IN endpoint on the periodic schedule. | ||
| 1011 | HID_EP.store(epnum as u32, Ordering::Relaxed); | ||
| 1012 | HID_MPS.store(epmps, Ordering::Relaxed); | ||
| 1013 | // Build HID QH for EP<epnum> IN on device 1. | ||
| 1014 | let hq = HID_QH; | ||
| 1015 | // eps: speed (0/1/2) maps directly onto the QH EPS field (bits 13:12). | ||
| 1016 | let info1 = QH_RL | ||
| 1017 | | (speed << 12) | ||
| 1018 | | ((epmps.max(4) & 0x7ff) << 16) | ||
| 1019 | | (DEV_ADDR.load(Ordering::Relaxed) & 0x7f) | ||
| 1020 | | (((epnum as u32) & 0x0f) << 8); | ||
| 1021 | // Poll every frame. HS: SMASK covers every uframe. FS/LS: split — | ||
| 1022 | // SSPLIT at uframe 0, CSPLIT at uframes 2..4 (Linux NEWSCHED), hub | ||
| 1023 | // addr 0 (integrated root TT), 1-based root port in bits 29:23. | ||
| 1024 | let (smask, cmask, split) = if speed == 2 { | ||
| 1025 | (0xFFu32, 0u32, 0u32) | ||
| 1026 | } else { | ||
| 1027 | (0x01u32, 0x1Cu32 << 8, QH_MULT | (((port as u32) + 1) << 23)) | ||
| 1028 | }; | ||
| 1029 | *qh_next(hq) = QT_TERMINATE; // end of this frame's periodic list | ||
| 1030 | *qh_info1(hq) = info1; | ||
| 1031 | *qh_info2(hq) = smask | cmask | split; | ||
| 1032 | *qh_current(hq) = 0; | ||
| 1033 | let iqtd = hq.add(0x40 / 4); // qtd at +0x40 (must be 32-byte aligned) | ||
| 1034 | // HID report qtd: len = epmps, PID IN. | ||
| 1035 | core::ptr::write_volatile(iqtd.wrapping_add(0), QT_TERMINATE); | ||
| 1036 | core::ptr::write_volatile(iqtd.wrapping_add(1), QT_TERMINATE); | ||
| 1037 | core::ptr::write_volatile( | ||
| 1038 | iqtd.wrapping_add(2), | ||
| 1039 | ((epmps.max(4) & 0x7fff) << 16) | (PID_IN << 8) | TOK_ACTIVE | TOK_IOC, | ||
| 1040 | ); | ||
| 1041 | for i in 0..5 { | ||
| 1042 | core::ptr::write_volatile(iqtd.add(3 + i), 0u32); | ||
| 1043 | } | ||
| 1044 | core::ptr::write_volatile(iqtd.wrapping_add(3), BUF_PAGE.add(BUF_HID_REPORT) as u32); | ||
| 1045 | *qh_next_qtd(hq) = iqtd as u32; | ||
| 1046 | *qh_alt_qtd(hq) = QT_TERMINATE; | ||
| 1047 | *qh_token(hq) = 0; | ||
| 1048 | // QEMU reads the report buffer from the qtd's bufptr (ehci_init_transfer), | ||
| 1049 | // so the QH overlay's buffer list need not be pre-populated. | ||
| 1050 | // Point every period frame entry at the HID QH. | ||
| 1051 | let fl = FRAMELIST; | ||
| 1052 | for f in 0..1024 { | ||
| 1053 | core::ptr::write_volatile(fl.add(f), link_of(hq as usize)); | ||
| 1054 | } | ||
| 1055 | // Enable the periodic schedule so the frame list is actually walked. | ||
| 1056 | // PSE was deliberately off until now (empty frame list during init). | ||
| 1057 | core::sync::atomic::fence(core::sync::atomic::Ordering::SeqCst); | ||
| 1058 | wr(op(OPCMD), rd(op(OPCMD)) | CMD_RUN | CMD_PSE); | ||
| 1059 | ARMED.store(true, Ordering::Relaxed); | ||
| 1060 | MOUSE_PRES.store(true, Ordering::Relaxed); | ||
| 1061 | crate::serial_println!("ehci: HID boot mouse armed (device 1)"); | ||
| 1062 | true | ||
| 1063 | } | ||
| 1064 | } | ||
| 1065 | |||
| 1066 | // --------------------------------------------------------------------------- | ||
| 1067 | // DEBUG: one-boot transfer variant matrix (real-hardware SE bisection). | ||
| 1068 | // Each variant does a full HC reset + port reset, then one ctl_xfer with a | ||
| 1069 | // single construction variable flipped (see XFER_TWEAK in ctl_xfer), and | ||
| 1070 | // logs the outcome. A wedged port from an earlier SE cannot confound the | ||
| 1071 | // comparisons because every variant restarts the controller. | ||
| 1072 | // --------------------------------------------------------------------------- | ||
| 1073 | unsafe fn hc_restart() { | ||
| 1074 | unsafe { | ||
| 1075 | wr(op(OPCMD), 0); | ||
| 1076 | let mut t = 0; | ||
| 1077 | while t < 1_000_000 && (rd(op(OPSTS)) & STS_HCH) == 0 { | ||
| 1078 | t += 1; | ||
| 1079 | core::hint::spin_loop(); | ||
| 1080 | } | ||
| 1081 | wr(op(OPCMD), CMD_HCRESET); | ||
| 1082 | t = 0; | ||
| 1083 | while t < 10_000_000 && (rd(op(OPCMD)) & CMD_HCRESET) != 0 { | ||
| 1084 | t += 1; | ||
| 1085 | core::hint::spin_loop(); | ||
| 1086 | } | ||
| 1087 | t = 0; | ||
| 1088 | while t < 10_000_000 && (rd(op(OPSTS)) & STS_HCH) == 0 { | ||
| 1089 | t += 1; | ||
| 1090 | core::hint::spin_loop(); | ||
| 1091 | } | ||
| 1092 | wr(op(0x18), (ASYNC_HEAD as u32) & !0x1F); | ||
| 1093 | wr(op(0x14), (FRAMELIST as u32) & !0x3FF); | ||
| 1094 | wr(op(OPCFGFLAG), 1); | ||
| 1095 | let np = N_PORTS.load(Ordering::Relaxed); | ||
| 1096 | for p in 0..np as usize { | ||
| 1097 | let psc = prtsc(p); | ||
| 1098 | let v = rd(psc); | ||
| 1099 | if v & PRT_POWER == 0 { | ||
| 1100 | wr(psc, v | PRT_POWER); | ||
| 1101 | } | ||
| 1102 | } | ||
| 1103 | wr(op(OPSTS), 0x1F); | ||
| 1104 | wr(op(OPCMD), rd(op(OPCMD)) | CMD_RUN); | ||
| 1105 | t = 0; | ||
| 1106 | while t < 1_000_000 && (rd(op(OPSTS)) & STS_HCH) != 0 { | ||
| 1107 | t += 1; | ||
| 1108 | core::hint::spin_loop(); | ||
| 1109 | } | ||
| 1110 | } | ||
| 1111 | } | ||
| 1112 | |||
| 1113 | unsafe fn debug_xfer_matrix(port: usize) { | ||
| 1114 | unsafe { | ||
| 1115 | QR_SHOWN.store(true, Ordering::Relaxed); // serial-only diagnostics | ||
| 1116 | crate::serial_println!( | ||
| 1117 | "ehci: ===== DEBUG xfer variant matrix (port {}) =====", | ||
| 1118 | port | ||
| 1119 | ); | ||
| 1120 | let tweaks: [u32; 7] = [0, 1, 2, 4, 8, 0x20, 0x80]; | ||
| 1121 | let names = [ | ||
| 1122 | "base", | ||
| 1123 | "no-ASE", | ||
| 1124 | "no-split", | ||
| 1125 | "no-qtd", | ||
| 1126 | "len0", | ||
| 1127 | "wire-off", | ||
| 1128 | "low-region", | ||
| 1129 | ]; | ||
| 1130 | let ebase = EHCI_BASE.load(Ordering::Relaxed) as usize; | ||
| 1131 | for (i, &tw) in tweaks.iter().enumerate() { | ||
| 1132 | hc_restart(); | ||
| 1133 | let _hs = reset_port(port); | ||
| 1134 | let req: [u8; 8] = [0x80, 0x06, 0x00, 0x01, 0, 0, 0x12, 0]; // get_desc dev, 18B | ||
| 1135 | let mut buf = [0u8; 18]; | ||
| 1136 | XFER_TWEAK.store(tw | 0x40, Ordering::Relaxed); // + short timeout | ||
| 1137 | let t0 = crate::interrupts::uptime_ms(); | ||
| 1138 | let r = ctl_xfer(0, 8, 0, port, &req, true, buf.as_mut_ptr(), 18); | ||
| 1139 | let dt = crate::interrupts::uptime_ms().wrapping_sub(t0); | ||
| 1140 | crate::serial_println!( | ||
| 1141 | "ehci: v{} {:<10}: ret={} t={}ms sts={:#x} cmd={:#x} prtsc0={:#x} hp={:#x}", | ||
| 1142 | i, | ||
| 1143 | names[i], | ||
| 1144 | r, | ||
| 1145 | dt, | ||
| 1146 | rd(op(OPSTS)), | ||
| 1147 | rd(op(OPCMD)), | ||
| 1148 | rd(prtsc(0)), | ||
| 1149 | rd(ebase + 0x84) | ||
| 1150 | ); | ||
| 1151 | XFER_TWEAK.store(0, Ordering::Relaxed); | ||
| 1152 | let w0 = crate::interrupts::uptime_ms(); | ||
| 1153 | while crate::interrupts::uptime_ms().wrapping_sub(w0) < 600 { | ||
| 1154 | core::hint::spin_loop(); | ||
| 1155 | } | ||
| 1156 | } | ||
| 1157 | crate::serial_println!("ehci: ===== matrix done ====="); | ||
| 1158 | } | ||
| 1159 | } | ||
| 1160 | |||
| 1161 | // --------------------------------------------------------------------------- | ||
| 1162 | // Active interface (called by cursor task / shell) | ||
| 1163 | // --------------------------------------------------------------------------- | ||
| 1164 | |||
| 1165 | pub fn controller_up() -> bool { | ||
| 1166 | CTRL_UP.load(Ordering::Relaxed) | ||
| 1167 | } | ||
| 1168 | pub fn mouse_present() -> bool { | ||
| 1169 | MOUSE_PRES.load(Ordering::Relaxed) | ||
| 1170 | } | ||
| 1171 | |||
| 1172 | pub fn poll_mouse() -> Option<MouseEvent> { | ||
| 1173 | unsafe { | ||
| 1174 | // Hot path first. The qtd's ACTIVE bit (in controller-visible DMA | ||
| 1175 | // memory) is the ground truth for "report outstanding"; a kernel | ||
| 1176 | // static must never gate report delivery. | ||
| 1177 | if ARMED.load(Ordering::Relaxed) { | ||
| 1178 | let hq = HID_QH; | ||
| 1179 | let iqtd = hq.add(0x40 / 4); // same 32-byte-aligned location as arming | ||
| 1180 | let tok = core::ptr::read_volatile(iqtd.wrapping_add(2).cast::<u32>()); | ||
| 1181 | if tok & TOK_ACTIVE == 0 { | ||
| 1182 | if tok & (TOK_HALT | TOK_XACT) != 0 { | ||
| 1183 | // Transfer error: drop the arm; a bounded re-enum follows. | ||
| 1184 | ARMED.store(false, Ordering::Relaxed); | ||
| 1185 | return None; | ||
| 1186 | } | ||
| 1187 | // Completed: decode the report from the dedicated DMA page. | ||
| 1188 | let hb = BUF_PAGE.add(BUF_HID_REPORT) as *const u8; | ||
| 1189 | let b0 = core::ptr::read_volatile(hb); | ||
| 1190 | let b1 = core::ptr::read_volatile(hb.add(1)); | ||
| 1191 | let b2 = core::ptr::read_volatile(hb.add(2)); | ||
| 1192 | let ev = Some(MouseEvent { | ||
| 1193 | dx: (b1 as i8) as i16, | ||
| 1194 | dy: (b2 as i8) as i16, | ||
| 1195 | left: b0 & 0x01 != 0, | ||
| 1196 | right: b0 & 0x02 != 0, | ||
| 1197 | middle: b0 & 0x04 != 0, | ||
| 1198 | }); | ||
| 1199 | // Re-arm the periodic interrupt-IN qtd: restore its token to | ||
| 1200 | // ACTIVE and re-link it as the QH's next qtd so the HC fetches | ||
| 1201 | // it again next frame (canonical one-qtd periodic re-arm). | ||
| 1202 | core::ptr::write_volatile( | ||
| 1203 | iqtd.wrapping_add(2), | ||
| 1204 | ((HID_MPS.load(Ordering::Relaxed).max(4) & 0x7fff) << 16) | ||
| 1205 | | (PID_IN << 8) | ||
| 1206 | | TOK_ACTIVE | ||
| 1207 | | TOK_IOC, | ||
| 1208 | ); | ||
| 1209 | *qh_next_qtd(hq) = iqtd as u32; | ||
| 1210 | return ev; | ||
| 1211 | } | ||
| 1212 | return None; | ||
| 1213 | } | ||
| 1214 | // Not armed: bring the mouse up lazily, but never hot-loop — at most | ||
| 1215 | // one enumerate attempt per second, and only while the controller is | ||
| 1216 | // up. A transient glitch can delay a report; it must not wedge the | ||
| 1217 | // system in a re-enumeration loop. | ||
| 1218 | if CTRL_UP.load(Ordering::Relaxed) { | ||
| 1219 | let now = crate::interrupts::uptime_ms() as u32; | ||
| 1220 | if now.wrapping_sub(LAST_ENUM.load(Ordering::Relaxed)) >= 1000 { | ||
| 1221 | LAST_ENUM.store(now, Ordering::Relaxed); | ||
| 1222 | enumerate(); | ||
| 1223 | } | ||
| 1224 | } | ||
| 1225 | } | ||
| 1226 | None | ||
| 1227 | } | ||
| 1228 | |||
| 1229 | /// Allocate DMA-safe memory above 16 MB. Uses the page allocator | ||
| 1230 | /// (physical, identity-mapped addresses) instead of the heap allocator | ||
| 1231 | /// (virtual addresses) because the EHCI controller's DMA engine needs | ||
| 1232 | /// physical addresses that match what the CPU sees. The 16 MB floor | ||
| 1233 | /// keeps the schedule structures out of the first 640 K, which some real | ||
| 1234 | /// chipsets reject from PCI masters (controller System Error halt). | ||
| 1235 | unsafe fn dma_alloc(bytes: usize, _align: usize) -> *mut u8 { | ||
| 1236 | unsafe { | ||
| 1237 | let pages = bytes.div_ceil(4096); | ||
| 1238 | let Some(phys) = crate::memory::alloc_pages_above(pages, 0x100_0000) else { | ||
| 1239 | return core::ptr::null_mut(); | ||
| 1240 | }; | ||
| 1241 | let p = phys as *mut u8; | ||
| 1242 | core::ptr::write_bytes(p, 0, pages * 4096); | ||
| 1243 | p | ||
| 1244 | } | ||
| 1245 | } | ||
| 1246 | |||
| 1247 | /// One-line status for the shell `usb` command. | ||
| 1248 | pub fn diagnose() -> alloc::string::String { | ||
| 1249 | let mut s = alloc::string::String::new(); | ||
| 1250 | s.push_str(&alloc::format!( | ||
| 1251 | "ehci mmio={:#x} ports={} mouse={} dev={} hid_ep={} armed={}\n", | ||
| 1252 | EHCI_BASE.load(Ordering::Relaxed), | ||
| 1253 | N_PORTS.load(Ordering::Relaxed), | ||
| 1254 | if MOUSE_PRES.load(Ordering::Relaxed) { | ||
| 1255 | "yes" | ||
| 1256 | } else { | ||
| 1257 | "no" | ||
| 1258 | }, | ||
| 1259 | DEV_ADDR.load(Ordering::Relaxed), | ||
| 1260 | HID_EP.load(Ordering::Relaxed), | ||
| 1261 | ARMED.load(Ordering::Relaxed) | ||
| 1262 | )); | ||
| 1263 | let np = N_PORTS.load(Ordering::Relaxed) as usize; | ||
| 1264 | for p in 0..np.min(8) { | ||
| 1265 | let v = unsafe { rd(prtsc(p)) }; | ||
| 1266 | s.push_str(&alloc::format!( | ||
| 1267 | " eport{}: {}{:#x}\n", | ||
| 1268 | p, | ||
| 1269 | if v & PRT_CONNECT != 0 { "*" } else { "" }, | ||
| 1270 | v & 0xffff | ||
| 1271 | )); | ||
| 1272 | } | ||
| 1273 | s | ||
| 1274 | } | ||
