diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-17 14:53:52 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-17 14:53:52 +0200 |
| commit | be868a43185826b02bf6d091019bb779678525bb (patch) | |
| tree | 586988c15f1d700b1621c7f0208584b30878a56c | |
| parent | v0.5.10: change em-dashes to dashes (diff) | |
| download | rs-dos-be868a43185826b02bf6d091019bb779678525bb.tar.gz rs-dos-be868a43185826b02bf6d091019bb779678525bb.zip | |
USB2/1.1 (EHCI/UHCI) drivers + real-hardware bring-up hardening
- Add polled EHCI (USB2) and UHCI (USB1.1) host-controller drivers with
HID boot mouse support, wired into cursor polling and the 'usb' shell cmd
- Add USB Legacy ownership handoff (claim_usb_legacy) plus config_write_byte
- Harden boot: verify the PIT/timer ticks before shell entry, EOI stray
interrupts in the LAPIC to stop interrupt storms, bound PS/2 controller
waits, and INTx-disable AHCI (polled driver)
- Add boot stage markers, klog screen echo, and QR lock; scheduler
mark_shell_running; NO_USB stopgap to keep BIOS USB-Legacy input alive
- alloc_pages_above for DMA buffers that must avoid low conventional memory
- Expand DEBUG_NOTES with EHCI/QEMU bring-up findings
| -rw-r--r-- | DEBUG_NOTES.md | 207 | ||||
| -rw-r--r-- | rsdos/src/main.rs | 62 | ||||
| -rw-r--r-- | rsnix/src/ahci.rs | 7 | ||||
| -rw-r--r-- | rsnix/src/cursor.rs | 6 | ||||
| -rw-r--r-- | rsnix/src/ehci.rs | 1274 | ||||
| -rw-r--r-- | rsnix/src/gop.rs | 18 | ||||
| -rw-r--r-- | rsnix/src/interrupts.rs | 36 | ||||
| -rw-r--r-- | rsnix/src/klog.rs | 10 | ||||
| -rw-r--r-- | rsnix/src/lib.rs | 2 | ||||
| -rw-r--r-- | rsnix/src/memory.rs | 44 | ||||
| -rw-r--r-- | rsnix/src/pci.rs | 125 | ||||
| -rw-r--r-- | rsnix/src/ps2.rs | 78 | ||||
| -rw-r--r-- | rsnix/src/sched.rs | 103 | ||||
| -rw-r--r-- | rsnix/src/tty.rs | 28 | ||||
| -rw-r--r-- | rsnix/src/uhci.rs | 1129 | ||||
| -rw-r--r-- | rsnix/src/usb.rs | 147 | ||||
| -rw-r--r-- | rsnix/src/vt.rs | 51 | ||||
| -rw-r--r-- | shell/src/lib.rs | 11 |
18 files changed, 3295 insertions, 43 deletions
diff --git a/DEBUG_NOTES.md b/DEBUG_NOTES.md index 19fe5cf..5730ca5 100644 --- a/DEBUG_NOTES.md +++ b/DEBUG_NOTES.md | |||
| @@ -284,6 +284,165 @@ sequence clean. `0x00AAAAAA` = gray text colour; shadow far rows stay 0.) | |||
| 284 | 284 | ||
| 285 | --- | 285 | --- |
| 286 | 286 | ||
| 287 | ## π EHCI driver debug β in progress (`rsnix/src/ehci.rs`) | ||
| 288 | |||
| 289 | ### Background | ||
| 290 | |||
| 291 | The USB mouse attaches to the **EHCI** (USB2) controllers on the real | ||
| 292 | OptiPlex (`00:1a.0`, `00:1d.0`), NOT the xHCI (`00:14.0` = USB3 only). | ||
| 293 | A full EHCI driver was written in `rsnix/src/ehci.rs` (~26 KB), wired | ||
| 294 | into `lib.rs`, `main.rs`, `cursor.rs`, `shell/src/lib.rs` (`usb` cmd). | ||
| 295 | |||
| 296 | ### What works | ||
| 297 | |||
| 298 | - PCI discovery, MMIO probe, HC reset, async+periodic list init. | ||
| 299 | - Port reset + device detection: FS device found on port 0. | ||
| 300 | - `ctl_xfer` builds SETUP/DATA/STATUS qtd chain; async list is | ||
| 301 | circular (head β xfer_qh β head). | ||
| 302 | - QEMU EHCI bench: async schedule processes our control QH correctly. | ||
| 303 | |||
| 304 | ### The bug: QTD DMA reads return all-zeros | ||
| 305 | |||
| 306 | The async schedule fetches our XFER_QH at `0x01b6e7c0` and reads its | ||
| 307 | `next_qtd` field correctly (`0x01b6e7f0`). Then `ADVANCEQUEUE` sets | ||
| 308 | `q->qtdaddr = 0x01b6e7f0`. But `FETCHQTD` reads from `0x01b6e7e0` | ||
| 309 | (**16 bytes BEFORE** the expected QTD address) and gets all-zeros. | ||
| 310 | |||
| 311 | ``` | ||
| 312 | FETCHQH @ 0x01b6e7c0: next_qtd=0x01b6e7f0 β correct | ||
| 313 | ADVANCEQUEUE β qtdaddr=0x01b6e7f0 β correct | ||
| 314 | FETCHQTD @ 0x01b6e7e0: all zeros β WRONG address + zeros | ||
| 315 | ``` | ||
| 316 | |||
| 317 | The QH's buffer pointer area (offset 28β44) is at `0x01b6e7dcβ0x01b6e7ec`. | ||
| 318 | `0x01b6e7e0` falls inside the QH's `bufptr[1]` field β which we zeroed. | ||
| 319 | So the DMA is reading from inside our own QH structure, not the QTD that | ||
| 320 | follows it. | ||
| 321 | |||
| 322 | ### Root cause hypothesis: DMA address β CPU address | ||
| 323 | |||
| 324 | `dma_alloc` uses `alloc::alloc::alloc()` (the Rust global heap allocator). | ||
| 325 | This returns a **virtual address**. On UEFI, the page tables from | ||
| 326 | `adopt_tables()` copy the firmware's identity map β so virt == phys for | ||
| 327 | most RAM. BUT: | ||
| 328 | |||
| 329 | 1. The heap pages allocated by the page allocator may have been remapped | ||
| 330 | or the identity map may not cover all heap pages. | ||
| 331 | 2. The EHCI controller does DMA to/from the address we write to | ||
| 332 | `ASYNCLISTADDR`. If the heap allocation's physical address differs | ||
| 333 | from the virtual address the CPU sees, DMA reads will fetch zeros | ||
| 334 | (unmapped or wrong page). | ||
| 335 | |||
| 336 | **Evidence**: CPU can read back `*st.add(2) = 0x80e80` (the SETUP token) | ||
| 337 | via `read_volatile`, but QEMU's EHCI DMA engine reads 0 from the same | ||
| 338 | address. The CPU sees the data; the DMA engine does not. | ||
| 339 | |||
| 340 | ### What's NOT the problem | ||
| 341 | |||
| 342 | - **USBCMD bits**: Fixed (PSE=bit4, ASE=bit5, RUN=bit0). | ||
| 343 | - **Async list linkage**: Fixed (headβxferβhead circular, Q_TYPE_QH tags). | ||
| 344 | - **QH/QTD struct layout**: Matches QEMU's `EHCIqh`/`EHCIqtd` exactly. | ||
| 345 | - **Periodic schedule interference**: Disabled (no PSE). OVMF's stale | ||
| 346 | periodic entries caused crashes; now avoided. | ||
| 347 | - **Build cache**: Full `cargo clean` done; binary matches source. | ||
| 348 | - **Identity mapping**: `adopt_tables` copies UEFI page tables β should | ||
| 349 | be identity-mapped. But the evidence says otherwise for our DMA buffers. | ||
| 350 | |||
| 351 | ### Debug QEMU command | ||
| 352 | |||
| 353 | ```bash | ||
| 354 | qemu-system-x86_64 \ | ||
| 355 | -bios /usr/share/edk2/x64/OVMF.4m.fd \ | ||
| 356 | -drive file=fat:rw:esp,format=raw \ | ||
| 357 | -serial file:/tmp/rsdos_q.log \ | ||
| 358 | -monitor unix:/tmp/rsdos_mon.sock,server,nowait \ | ||
| 359 | -m 256M -machine q35 -cpu qemu64 -no-reboot \ | ||
| 360 | -device ich9-usb-ehci1 -device usb-mouse \ | ||
| 361 | -trace "usb_ehci*" >/tmp/rsdos_trace.txt 2>&1 | ||
| 362 | ``` | ||
| 363 | |||
| 364 | ### Key QEMU EHCI facts | ||
| 365 | |||
| 366 | - **Register layout**: `CAPLEN +0, HCSPARAMS +4, HCCPARAMS +8`; | ||
| 367 | operational base = `base + CAPLEN`. `USBCMD=+0, USBSTS=+4, | ||
| 368 | ASYNCLISTADDR=+0x18, CONFIGFLAG=+0x40, PORTSC#1=+0x44`. | ||
| 369 | - **USBCMD bits**: `RUNSTOP=bit0, HCRESET=bit1, FLS=bits2-3 (2-bit), | ||
| 370 | PSE=bit4, ASE=bit5, IAAD=bit6, ITC=bits16-23`. | ||
| 371 | - **QEMU async BH**: triggered by USBCMD writes (bit-change detection) | ||
| 372 | and frame timer. `ehci_work_bh` processes periodic frames first, then | ||
| 373 | advances async schedule. | ||
| 374 | - **QEMU strips FLS** if guest sets it (FCR register). | ||
| 375 | - **Port reset**: deassert PR; PED stays 0 for FS (no HS speed mask). | ||
| 376 | - **`ehci_find_queue_by_qh`** reuses queue objects; `q->seen > 1` stops | ||
| 377 | re-processing the same QH (reclamation detection). | ||
| 378 | |||
| 379 | ### Key QEMU EHCI structs (from `h110.h` / `hw/usb/hcd-ehci.h`) | ||
| 380 | |||
| 381 | ```c | ||
| 382 | // QH: 12 dwords (48 bytes) | ||
| 383 | typedef struct EHCIqh { | ||
| 384 | uint32_t next; // dword0: horizontal link pointer | ||
| 385 | uint32_t epchar; // dword1: dev(0-6), i(7), ep(8-10), eps(12-13), dtc(14), h(15), mps(16-26) | ||
| 386 | uint32_t epcap; // dword2 | ||
| 387 | uint32_t current_qtd; // dword3 | ||
| 388 | uint32_t next_qtd; // dword4 | ||
| 389 | uint32_t altnext_qtd; // dword5 | ||
| 390 | uint32_t token; // dword6: tbytes(16-30), cerr(10-11), pid(8-9), ioc(16), active(7), halt(6) | ||
| 391 | uint32_t bufptr[5]; // dword7-11 | ||
| 392 | } EHCIqh; | ||
| 393 | |||
| 394 | // QTD: 8 dwords (32 bytes) | ||
| 395 | typedef struct EHCIqtd { | ||
| 396 | uint32_t next; // dword0 | ||
| 397 | uint32_t altnext; // dword1 | ||
| 398 | uint32_t token; // dword2: tbytes(16-30), cpage(12-15), cerr(10-11), pid(8-9), ioc(16), active(7) | ||
| 399 | uint32_t bufptr[5]; // dword3-7 | ||
| 400 | } EHCIqtd; | ||
| 401 | |||
| 402 | #define NLPTR_GET(x) ((x) & ~0xf) | ||
| 403 | #define NLPTR_TBIT(x) ((x) & 1) | ||
| 404 | #define NLPTR_TYPE_GET(x) (((x) >> 1) & 3) | ||
| 405 | #define Q_TYPE_QH 1 // NLPTR_TYPE value for QH links | ||
| 406 | ``` | ||
| 407 | |||
| 408 | ### What to try next | ||
| 409 | |||
| 410 | 1. **Allocate DMA buffers from a known-safe region**: Instead of | ||
| 411 | `alloc::alloc`, use a dedicated DMA pool at a known physical address | ||
| 412 | (e.g., the first page allocator pages, or a statically allocated | ||
| 413 | buffer). Verify the physical address matches the virtual address by | ||
| 414 | reading the PML4/PDPT/PD/PT entries for the buffer's page. | ||
| 415 | |||
| 416 | 2. **Add a page-table walk diagnostic**: Walk the 4-level page tables for | ||
| 417 | the DMA buffer address and print the physical address. If virt β phys, | ||
| 418 | that's the bug. Add this to `ehci.rs` init or to `paging.rs`. | ||
| 419 | |||
| 420 | 3. **Use the page allocator directly** (which returns physical addresses | ||
| 421 | that are identity-mapped by construction) instead of the heap allocator. | ||
| 422 | `alloc_pages(n)` in `memory.rs` returns a `*mut u8` β verify this is | ||
| 423 | identity-mapped by checking a known page table entry. | ||
| 424 | |||
| 425 | 4. **Check if the heap allocator's pages lose identity mapping**: The heap | ||
| 426 | is built from the UEFI memory map. `adopt_tables` copies the firmware | ||
| 427 | page tables. But if the heap allocator later allocates pages that were | ||
| 428 | NOT in the firmware's map (e.g., from a free region), those pages | ||
| 429 | might not have identity-mapped PTEs. | ||
| 430 | |||
| 431 | 5. **Simpler test**: Allocate a small buffer (e.g., 64 bytes) with | ||
| 432 | `dma_alloc`, write a known pattern, and check if the EHCI controller | ||
| 433 | can DMA-read it. If even a small buffer fails, the problem is | ||
| 434 | fundamental (identity mapping broken for heap pages). | ||
| 435 | |||
| 436 | ### Key code locations | ||
| 437 | |||
| 438 | - `rsnix/src/ehci.rs:751` β `dma_alloc()` (uses `alloc::alloc`) | ||
| 439 | - `rsnix/src/ehci.rs:420` β `init()` (sets up ASYNCLISTADDR, head QH, XFER_QH) | ||
| 440 | - `rsnix/src/ehci.rs:230` β `ctl_xfer()` (builds QH + QTD chain, enables schedule) | ||
| 441 | - `rsnix/src/paging.rs:48` β `adopt_tables()` (copies UEFI page tables) | ||
| 442 | - `rsnix/src/memory.rs` β `alloc_pages()` (page allocator) | ||
| 443 | |||
| 444 | --- | ||
| 445 | |||
| 287 | ## β v0.5.10 β USB HID mouse driver (polled xHCI, `rsnix/src/usb.rs`) | 446 | ## β v0.5.10 β USB HID mouse driver (polled xHCI, `rsnix/src/usb.rs`) |
| 288 | 447 | ||
| 289 | Handoff: HEAD = `v0.5.10` (workspace Cargo.toml + boot banner). Adds a | 448 | Handoff: HEAD = `v0.5.10` (workspace Cargo.toml + boot banner). Adds a |
| @@ -292,6 +451,7 @@ polled xHCI controller driver (mouse-only) announced via `usb: xHCI at | |||
| 292 | so a USB mouse supersedes PS/2. New shell cmd: `usb` (diagnose), `usb rescan`. | 451 | so a USB mouse supersedes PS/2. New shell cmd: `usb` (diagnose), `usb rescan`. |
| 293 | 452 | ||
| 294 | ### How it works | 453 | ### How it works |
| 454 | |||
| 295 | - Bring up controller (`OPC=0`, set `RCS`+`CRR`, `CNR`β0), discover a | 455 | - Bring up controller (`OPC=0`, set `RCS`+`CRR`, `CNR`β0), discover a |
| 296 | mouse asynchronously from the cursor task (`maybe_discover`), reset the | 456 | mouse asynchronously from the cursor task (`maybe_discover`), reset the |
| 297 | port, enable slot, address device, read device + config descriptors, | 457 | port, enable slot, address device, read device + config descriptors, |
| @@ -301,6 +461,7 @@ so a USB mouse supersedes PS/2. New shell cmd: `usb` (diagnose), `usb rescan`. | |||
| 301 | heap pointers; no IRQ β keep one IN report armed and drain on each poll. | 461 | heap pointers; no IRQ β keep one IN report armed and drain on each poll. |
| 302 | 462 | ||
| 303 | ### QEMU bring-up observations / gotchas that bit us | 463 | ### QEMU bring-up observations / gotchas that bit us |
| 464 | |||
| 304 | - **MaxPorts = bits 31:24 of `HCSPARAMS1`**, not 16:23 (`MaxIntrs`). Using | 465 | - **MaxPorts = bits 31:24 of `HCSPARAMS1`**, not 16:23 (`MaxIntrs`). Using |
| 305 | the wrong nibble silently yielded 0 ports ("ports=0", every PORTSC read 0). | 466 | the wrong nibble silently yielded 0 ports ("ports=0", every PORTSC read 0). |
| 306 | - **Operational registers** (xHCI >=1.1, matches QEMU/Linux): `CRCR=0x18`, | 467 | - **Operational registers** (xHCI >=1.1, matches QEMU/Linux): `CRCR=0x18`, |
| @@ -321,9 +482,10 @@ so a USB mouse supersedes PS/2. New shell cmd: `usb` (diagnose), `usb rescan`. | |||
| 321 | - A long-lived QEMU session sometimes stopped forwarding `mouse_move` | 482 | - A long-lived QEMU session sometimes stopped forwarding `mouse_move` |
| 322 | after heavy use; a fresh relaunch always revived it β not a driver bug. | 483 | after heavy use; a fresh relaunch always revived it β not a driver bug. |
| 323 | - Diagnose cmd `usb` prints `xhci mmio=β¦ mapped=β¦ ports=β¦ mouse=β¦ slot=β¦` | 484 | - Diagnose cmd `usb` prints `xhci mmio=β¦ mapped=β¦ ports=β¦ mouse=β¦ slot=β¦` |
| 324 | plus per-port `PORTS` dwords and a ` hid ep-index=β¦ mps=β¦ armed=true` line. | 485 | plus per-port `PORTS` dwords and a `hid ep-index=β¦ mps=β¦ armed=true` line. |
| 325 | 486 | ||
| 326 | ### Boot log (good path) | 487 | ### Boot log (good path) |
| 488 | |||
| 327 | ``` | 489 | ``` |
| 328 | usb: xHCI at 00:03.0 | 490 | usb: xHCI at 00:03.0 |
| 329 | usb: controller up (8 ports) β mouse discovery deferred to cursor task | 491 | usb: controller up (8 ports) β mouse discovery deferred to cursor task |
| @@ -334,5 +496,48 @@ usb: HID boot mouse armed (slot 1) | |||
| 334 | ``` | 496 | ``` |
| 335 | 497 | ||
| 336 | ### Known limitations | 498 | ### Known limitations |
| 499 | |||
| 337 | - Mouse-only, no HID descriptor protocol selection (`SET_PROTOCOL` boot), | 500 | - Mouse-only, no HID descriptor protocol selection (`SET_PROTOCOL` boot), |
| 338 | no high-rate intervals, no keyboard/tablet, no hot-unplug handling, no MSI. | 501 | no high-rate intervals, no keyboard/tablet, no hot-unplug handling, no MSI. |
| 502 | |||
| 503 | ### EHCI (usb2) bring-up β QEMU quirks that bit us (all now fixed) | ||
| 504 | |||
| 505 | - **QTDs must be 32-byte aligned.** QEMU's `NLPTR_GET(x) = x & 0xffffffe0` | ||
| 506 | masks the LOW 5 bits, so a qtd at QH+0x30 (e.g. `0x01b6e7f0`) gets rounded | ||
| 507 | down to `0x01b6e7e0` and the transfer executes at the wrong address | ||
| 508 | (seen as a 16-byte jump in the FETCHQTD trace). Control qtds now live at | ||
| 509 | QH+0x40/0x60/0x80; the HID report qtd at HID_QH+0x40. | ||
| 510 | - **Unlink must self-loop WITH the QH type bit**: `link_of(head)` (addr|0x02). | ||
| 511 | A bare `head as u32` self-loop is parsed by QEMU as an iTD β | ||
| 512 | "non queue head request in async schedule" β HC reset. | ||
| 513 | - **DATA qtd altnext must be `1` (terminate)**, not 0. On a short IN | ||
| 514 | (e.g. config descriptor < 256 bytes) TBYTES stays != 0 so ADVANCEQUEUE | ||
| 515 | follows altnext; QEMU treats altnext=0 as a *valid* address β jump to 0. | ||
| 516 | - **Periodic HID QH must set the EP number (epchar bits 8-11).** The QH was | ||
| 517 | built with devaddr|EPS|MPS but EP=0, so every periodic IN went to the | ||
| 518 | control endpoint: the first transfer "completed" oddly, then the qtd | ||
| 519 | HALTed every frame (`usb_ehci_qtd_bits` showed `halt 1`, packet on ep 0). | ||
| 520 | Fix: `| ((epnum & 0x0f) << 8)`. | ||
| 521 | - **Idle periodic qtd with correct EP just NAKs** (device has no report) β | ||
| 522 | that is correct; don't mistake "no reports" for a broken schedule. | ||
| 523 | - Headless QEMU 11 **cannot inject input into `usb-mouse`**: it has no | ||
| 524 | `display=` property (only usb-tablet/usb-kbd do), so it is never bound to | ||
| 525 | a display and `input-send-event`/`send-key` events (traced as `con -1`) | ||
| 526 | never reach it. First-report delivery was verified with `usb-kbd` + | ||
| 527 | `send-key` instead (b2 carried the real HID keycode, e.g. `0x04` for 'a'). | ||
| 528 | - **Controller halt+reset before init** is required to clear OVMF's stale | ||
| 529 | async schedule left in the controller. | ||
| 530 | |||
| 531 | ### EHCI boot log (good path, usb-mouse) | ||
| 532 | |||
| 533 | ``` | ||
| 534 | ehci: USB2 EHCI at 00:04.0 | ||
| 535 | ehci: mmio=0x81060000 caplen=0x20 ports=6 64-bit-caps=false | ||
| 536 | ehci: controller up (6 ports) | ||
| 537 | > ehci: port 0 resolved (full/low β TT path) | ||
| 538 | ehci: dev 2706:0100 class=0x0 mps0=64 | ||
| 539 | ehci: HID mouse IN ep1(0x81) mps=4 interval=7 | ||
| 540 | ehci: HID boot mouse armed (device 1) | ||
| 541 | ``` | ||
| 542 | |||
| 543 | Periodic schedule: 2200+ EXECUTEs/frame with 0 qtd halts; idle mouse NAKs. | ||
diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs index 27dab06..0e159e6 100644 --- a/rsdos/src/main.rs +++ b/rsdos/src/main.rs | |||
| @@ -247,6 +247,25 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut | |||
| 247 | } | 247 | } |
| 248 | rsnix::interrupts::init(); | 248 | rsnix::interrupts::init(); |
| 249 | rsnix::serial_println!("intr: IDT loaded, PIC remapped, timer ~100 Hz"); | 249 | rsnix::serial_println!("intr: IDT loaded, PIC remapped, timer ~100 Hz"); |
| 250 | // Prove the timer actually ticks before handing the machine to the | ||
| 251 | // scheduler: if the PIC/APIC route is broken on real hardware, the | ||
| 252 | // first `hlt` (shell input, idle) never wakes and the system silently | ||
| 253 | // freezes. Spin briefly with interrupts on and require uptime to | ||
| 254 | // advance; a dead timer becomes a visible panic instead. | ||
| 255 | let t0 = rsnix::interrupts::uptime_ms(); | ||
| 256 | let mut timer_ok = false; | ||
| 257 | for _ in 0..2_000_000 { | ||
| 258 | core::hint::spin_loop(); | ||
| 259 | if rsnix::interrupts::uptime_ms() != t0 { | ||
| 260 | timer_ok = true; | ||
| 261 | break; | ||
| 262 | } | ||
| 263 | } | ||
| 264 | if timer_ok { | ||
| 265 | rsnix::serial_println!("intr: timer verified (uptime advances)"); | ||
| 266 | } else { | ||
| 267 | panic!("timer: no PIT tick within 2M spins β interrupt routing broken"); | ||
| 268 | } | ||
| 250 | rsnix::gop::boot_marker("rsnix: serial+ps2+gdt+intr ok"); | 269 | rsnix::gop::boot_marker("rsnix: serial+ps2+gdt+intr ok"); |
| 251 | 270 | ||
| 252 | dump_memory_summary(); | 271 | dump_memory_summary(); |
| @@ -347,13 +366,50 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut | |||
| 347 | 366 | ||
| 348 | rsnix::serial_println!("boot: entering shell"); | 367 | rsnix::serial_println!("boot: entering shell"); |
| 349 | rsnix::gop::boot_marker("rsnix: boot done β shell"); | 368 | rsnix::gop::boot_marker("rsnix: boot done β shell"); |
| 350 | // USB (xHCI) mouse is polled; fall back to PS/2 if no USB mouse. | 369 | // USB (xHCI) / EHCI mouse is polled; fall back to PS/2 if no USB mouse. |
| 351 | rsnix::usb::init(); | 370 | // Each stage draws its own marker: a real-hardware freeze leaves the |
| 352 | if rsnix::ps2::mouse_present() || rsnix::usb::controller_up() { | 371 | // last completed stage on the screen, so the hang point is identifiable |
| 372 | // without a serial console. | ||
| 373 | // STOPGAP: skip ALL USB host-controller init so the BIOS USB-Legacy SMI | ||
| 374 | // keeps emulating the keyboard/mouse on the 8042 (PS/2). Resetting the | ||
| 375 | // EHCI without a completed USBLEGSUP ownership handshake killed the | ||
| 376 | // firmware's legacy emulation, leaving the OptiPlex 7010 with zero input. | ||
| 377 | // While NO_USB is set, USB input is served entirely by the BIOS (IRQ1/ | ||
| 378 | // IRQ12 + our PS/2 driver) and no EHCI/xHCI/UHCI register is touched. | ||
| 379 | const NO_USB: bool = false; | ||
| 380 | rsnix::gop::boot_marker("rsnix: usb init"); | ||
| 381 | if !NO_USB { | ||
| 382 | rsnix::usb::init(); | ||
| 383 | } else { | ||
| 384 | rsnix::serial_println!("usb: init skipped (stopgap) β BIOS USB-Legacy owns input"); | ||
| 385 | } | ||
| 386 | rsnix::gop::boot_marker("rsnix: ehci init"); | ||
| 387 | if !NO_USB { | ||
| 388 | rsnix::ehci::init(); | ||
| 389 | } else { | ||
| 390 | rsnix::serial_println!("ehci: init skipped (stopgap)"); | ||
| 391 | } | ||
| 392 | rsnix::gop::boot_marker("rsnix: uhci init"); | ||
| 393 | if !NO_USB { | ||
| 394 | rsnix::uhci::init(); | ||
| 395 | } else { | ||
| 396 | rsnix::serial_println!("uhci: init skipped (stopgap)"); | ||
| 397 | } | ||
| 398 | if rsnix::ps2::mouse_present() | ||
| 399 | || rsnix::usb::controller_up() | ||
| 400 | || rsnix::ehci::controller_up() | ||
| 401 | || rsnix::uhci::controller_up() | ||
| 402 | { | ||
| 403 | rsnix::gop::boot_marker("rsnix: cursor spawn"); | ||
| 353 | rsnix::cursor::spawn(); | 404 | rsnix::cursor::spawn(); |
| 354 | } else { | 405 | } else { |
| 355 | rsnix::serial_println!("cursor: no mouse β overlay disabled"); | 406 | rsnix::serial_println!("cursor: no mouse β overlay disabled"); |
| 356 | } | 407 | } |
| 408 | rsnix::gop::boot_marker("rsnix: shell run"); | ||
| 409 | // The shell REPL is about to start: from here on task 0 may legitimately | ||
| 410 | // park in poll_key with a fixed frame, so the watchdog's boot-phase spin | ||
| 411 | // rule for task 0 is disarmed (boot-phase hangs already panicked). | ||
| 412 | rsnix::sched::mark_shell_running(); | ||
| 357 | // Shell-entry self-check: verify the vt grid structs are sane right | 413 | // Shell-entry self-check: verify the vt grid structs are sane right |
| 358 | // before the shell's first print. The OptiPlex crashes in the first | 414 | // before the shell's first print. The OptiPlex crashes in the first |
| 359 | // banner write when the cells VecData (or the shadow) holds the | 415 | // banner write when the cells VecData (or the shadow) holds the |
diff --git a/rsnix/src/ahci.rs b/rsnix/src/ahci.rs index a77b341..543bb21 100644 --- a/rsnix/src/ahci.rs +++ b/rsnix/src/ahci.rs | |||
| @@ -230,9 +230,12 @@ pub fn init() -> bool { | |||
| 230 | let Some((dev, func)) = find_ahci() else { | 230 | let Some((dev, func)) = find_ahci() else { |
| 231 | return false; | 231 | return false; |
| 232 | }; | 232 | }; |
| 233 | // Enable memory space + bus mastering so MMIO and DMA work. | 233 | // Enable memory space + bus mastering so MMIO and DMA work, plus INTx |
| 234 | // disable (bit 10): the driver polls command completion (no ISR, no | ||
| 235 | // MSI), so the HBA must not assert INTx β a stray SATA interrupt would | ||
| 236 | // re-fire forever on real hardware. | ||
| 234 | let cmd_reg = pci::config_read(0, dev, func, 0x04); | 237 | let cmd_reg = pci::config_read(0, dev, func, 0x04); |
| 235 | pci::config_write(0, dev, func, 0x04, cmd_reg | 0x7); | 238 | pci::config_write(0, dev, func, 0x04, cmd_reg | 0x7 | 0x400); |
| 236 | 239 | ||
| 237 | let Some(abar) = pci::bar64(0, dev, func, 5) else { | 240 | let Some(abar) = pci::bar64(0, dev, func, 5) else { |
| 238 | return false; | 241 | return false; |
diff --git a/rsnix/src/cursor.rs b/rsnix/src/cursor.rs index d9d97bc..31b919e 100644 --- a/rsnix/src/cursor.rs +++ b/rsnix/src/cursor.rs | |||
| @@ -50,7 +50,11 @@ fn forward_target() -> Option<usize> { | |||
| 50 | 50 | ||
| 51 | fn task_main() { | 51 | fn task_main() { |
| 52 | loop { | 52 | loop { |
| 53 | while let Some(ev) = crate::usb::poll_mouse().or_else(crate::ps2::try_poll_mouse) { | 53 | while let Some(ev) = crate::usb::poll_mouse() |
| 54 | .or_else(crate::ehci::poll_mouse) | ||
| 55 | .or_else(crate::uhci::poll_mouse) | ||
| 56 | .or_else(crate::ps2::try_poll_mouse) | ||
| 57 | { | ||
| 54 | crate::gop::cursor_move(ev.dx, ev.dy); | 58 | crate::gop::cursor_move(ev.dx, ev.dy); |
| 55 | crate::gop::cursor_set_buttons(btn_bits(&ev)); | 59 | crate::gop::cursor_set_buttons(btn_bits(&ev)); |
| 56 | if let Some(id) = forward_target() | 60 | if let Some(id) = forward_target() |
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 | } | ||
diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs index ccc6509..676814e 100644 --- a/rsnix/src/gop.rs +++ b/rsnix/src/gop.rs | |||
| @@ -7,7 +7,7 @@ use crate::font; | |||
| 7 | use crate::uefi::{GopModeInfo, GraphicsOutputProtocol, EFI_SUCCESS, PIXEL_BGR, PIXEL_RGB}; | 7 | use crate::uefi::{GopModeInfo, GraphicsOutputProtocol, EFI_SUCCESS, PIXEL_BGR, PIXEL_RGB}; |
| 8 | use core::fmt; | 8 | use core::fmt; |
| 9 | use core::ptr; | 9 | use core::ptr; |
| 10 | use core::sync::atomic::{AtomicU32, AtomicU64, AtomicU8, Ordering}; | 10 | use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicU8, Ordering}; |
| 11 | 11 | ||
| 12 | /// Framebuffer state captured before ExitBootServices. | 12 | /// Framebuffer state captured before ExitBootServices. |
| 13 | pub struct Framebuffer { | 13 | pub struct Framebuffer { |
| @@ -91,6 +91,19 @@ pub fn last_boot_stage() -> alloc::string::String { | |||
| 91 | /// the preemptive scheduler. Set via the shell `stall` command. | 91 | /// the preemptive scheduler. Set via the shell `stall` command. |
| 92 | pub static DISPLAY_STALL: AtomicU32 = AtomicU32::new(0); | 92 | pub static DISPLAY_STALL: AtomicU32 = AtomicU32::new(0); |
| 93 | 93 | ||
| 94 | /// Set once a diagnostic QR has been rendered on screen. While locked, the | ||
| 95 | /// klog screen echo is suppressed so subsequent log lines cannot overwrite | ||
| 96 | /// the QR; the serial console keeps the full stream. Never released (the | ||
| 97 | /// QR is the "photograph this" surface for hardware with no serial pipe). | ||
| 98 | pub static QR_LOCKED: AtomicBool = AtomicBool::new(false); | ||
| 99 | |||
| 100 | pub fn qr_lock() { | ||
| 101 | QR_LOCKED.store(true, Ordering::Relaxed); | ||
| 102 | } | ||
| 103 | pub fn qr_locked() -> bool { | ||
| 104 | QR_LOCKED.load(Ordering::Relaxed) | ||
| 105 | } | ||
| 106 | |||
| 94 | #[inline] | 107 | #[inline] |
| 95 | fn stall_display() { | 108 | fn stall_display() { |
| 96 | let n = DISPLAY_STALL.load(Ordering::Relaxed); | 109 | let n = DISPLAY_STALL.load(Ordering::Relaxed); |
| @@ -308,7 +321,8 @@ pub fn shadow_late_init() { | |||
| 308 | /// boot stage is written straight to the framebuffer β the last visible | 321 | /// boot stage is written straight to the framebuffer β the last visible |
| 309 | /// marker before a hang identifies where the kernel died. Uses the | 322 | /// marker before a hang identifies where the kernel died. Uses the |
| 310 | /// direct-display glyph path (the shadow may not exist yet in early | 323 | /// direct-display glyph path (the shadow may not exist yet in early |
| 311 | /// boot); the shell banner overwrites row 0 at first paint. | 324 | /// boot). Row 0 stays a persistent status line: the shell starts its |
| 325 | /// output at row 1 so the marker survives a post-boot freeze. | ||
| 312 | pub fn boot_marker(msg: &str) { | 326 | pub fn boot_marker(msg: &str) { |
| 313 | boot_marker_at(msg, 0); | 327 | boot_marker_at(msg, 0); |
| 314 | } | 328 | } |
diff --git a/rsnix/src/interrupts.rs b/rsnix/src/interrupts.rs index 927e4f1..9872389 100644 --- a/rsnix/src/interrupts.rs +++ b/rsnix/src/interrupts.rs | |||
| @@ -210,6 +210,10 @@ impl ByteRing { | |||
| 210 | 210 | ||
| 211 | static KEYBUF: ByteRing = ByteRing::new(); | 211 | static KEYBUF: ByteRing = ByteRing::new(); |
| 212 | static MOUSEBUF: ByteRing = ByteRing::new(); | 212 | static MOUSEBUF: ByteRing = ByteRing::new(); |
| 213 | /// One-shot flag: the first unhandled interrupt logs its vector, so real | ||
| 214 | /// hardware shows which device raised a stray IRQ (before the LAPIC EOI | ||
| 215 | /// drops it). | ||
| 216 | static STRAY_LOGGED: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false); | ||
| 213 | 217 | ||
| 214 | // --------------------------------------------------------------------------- | 218 | // --------------------------------------------------------------------------- |
| 215 | // Interrupt stubs (assembly) and dispatch | 219 | // Interrupt stubs (assembly) and dispatch |
| @@ -412,7 +416,7 @@ pub fn enter_dump() { | |||
| 412 | } | 416 | } |
| 413 | } | 417 | } |
| 414 | 418 | ||
| 415 | fn in_dump() -> bool { | 419 | pub(crate) fn in_dump() -> bool { |
| 416 | unsafe { IN_DUMP } | 420 | unsafe { IN_DUMP } |
| 417 | } | 421 | } |
| 418 | 422 | ||
| @@ -488,7 +492,35 @@ unsafe extern "C" fn isr_dispatch(frame: &IntFrame) -> usize { | |||
| 488 | } | 492 | } |
| 489 | crate::sched::fault_dump("exception", 0, frame) | 493 | crate::sched::fault_dump("exception", 0, frame) |
| 490 | } | 494 | } |
| 491 | _ => fp, | 495 | _ => { |
| 496 | // Unhandled/stray interrupt. On real hardware an | ||
| 497 | // IO-APIC-delivered IRQ (USB/SATA/NIC INTx) that is never | ||
| 498 | // EOI'd stays pending in the local APIC and re-fires the | ||
| 499 | // instant we iretq β an interrupt storm that starves the | ||
| 500 | // timer tick (higher vector priority) and freezes the whole | ||
| 501 | // machine with no panic. EOI the LAPIC so an | ||
| 502 | // edge-triggered stray drops (a genuinely-spurious LAPIC | ||
| 503 | // IRQ needs no EOI, so that case is a benign no-op). | ||
| 504 | // Level-triggered strays are additionally stopped by the | ||
| 505 | // drivers masking their PCI INTx / interrupt enables. | ||
| 506 | if !STRAY_LOGGED.swap(true, Ordering::Relaxed) { | ||
| 507 | crate::serial_println!("intr: stray vector {:#x} β LAPIC EOI'd", frame.vector); | ||
| 508 | } | ||
| 509 | // EOI the local APIC when it is enabled (MSR 0x1B, bit 11). | ||
| 510 | let (lo, hi): (u32, u32); | ||
| 511 | core::arch::asm!( | ||
| 512 | "rdmsr", | ||
| 513 | in("ecx") 0x1Bu32, | ||
| 514 | out("eax") lo, | ||
| 515 | out("edx") hi, | ||
| 516 | options(nostack, preserves_flags) | ||
| 517 | ); | ||
| 518 | let apic = ((hi as u64) << 32) | lo as u64; | ||
| 519 | if apic & (1 << 11) != 0 { | ||
| 520 | core::ptr::write_volatile(0xFEE0_00B0usize as *mut u32, 0); | ||
| 521 | } | ||
| 522 | fp | ||
| 523 | } | ||
| 492 | } | 524 | } |
| 493 | }; | 525 | }; |
| 494 | crate::sched::set_in_isr(false); | 526 | crate::sched::set_in_isr(false); |
diff --git a/rsnix/src/klog.rs b/rsnix/src/klog.rs index 2e1fc82..300a597 100644 --- a/rsnix/src/klog.rs +++ b/rsnix/src/klog.rs | |||
| @@ -95,6 +95,16 @@ pub fn emit(args: core::fmt::Arguments<'_>) { | |||
| 95 | crate::serial::write_raw(line.as_str()); | 95 | crate::serial::write_raw(line.as_str()); |
| 96 | crate::serial::write_raw("\r\n"); | 96 | crate::serial::write_raw("\r\n"); |
| 97 | append(line.as_str(), ms); | 97 | append(line.as_str(), ms); |
| 98 | // Screen echo: mirror the serial line to the VGA console, so the whole | ||
| 99 | // boot log is visible on real hardware without a serial console. The | ||
| 100 | // last line on screen when the machine freezes is exactly where boot | ||
| 101 | // stopped (far more precise than the stage marker). Skipped while a | ||
| 102 | // fault dump is rendering its own band (row 0 = boot marker, terminal | ||
| 103 | // lives below it). | ||
| 104 | if crate::vt::ready() && !crate::interrupts::in_dump() && !crate::gop::qr_locked() { | ||
| 105 | crate::vt::write_str(line.as_str()); | ||
| 106 | crate::vt::write_str("\n"); | ||
| 107 | } | ||
| 98 | crate::sched::unlock(); | 108 | crate::sched::unlock(); |
| 99 | } | 109 | } |
| 100 | 110 | ||
diff --git a/rsnix/src/lib.rs b/rsnix/src/lib.rs index bc2553d..f5c3419 100644 --- a/rsnix/src/lib.rs +++ b/rsnix/src/lib.rs | |||
| @@ -17,6 +17,8 @@ pub mod pci; | |||
| 17 | pub mod qr; | 17 | pub mod qr; |
| 18 | pub mod power; | 18 | pub mod power; |
| 19 | pub mod ps2; | 19 | pub mod ps2; |
| 20 | pub mod uhci; | ||
| 21 | pub mod ehci; | ||
| 20 | pub mod usb; | 22 | pub mod usb; |
| 21 | pub mod rtc; | 23 | pub mod rtc; |
| 22 | pub mod sched; | 24 | pub mod sched; |
diff --git a/rsnix/src/memory.rs b/rsnix/src/memory.rs index 099f607..2a32da8 100644 --- a/rsnix/src/memory.rs +++ b/rsnix/src/memory.rs | |||
| @@ -1194,6 +1194,50 @@ pub fn alloc_pages(count: usize) -> Option<usize> { | |||
| 1194 | alloc_pages_aligned(count, 1) | 1194 | alloc_pages_aligned(count, 1) |
| 1195 | } | 1195 | } |
| 1196 | 1196 | ||
| 1197 | /// Allocate `count` contiguous physical pages at or above `min_addr` | ||
| 1198 | /// (first-fit upward from there). Used for controller DMA buffers that | ||
| 1199 | /// must avoid low conventional memory β the first 640 K on some real | ||
| 1200 | /// chipsets is hostile to PCI masters and shows up as a controller | ||
| 1201 | /// "System Error" (SE) halt. Page-aligned like `alloc_pages`. | ||
| 1202 | pub fn alloc_pages_above(count: usize, min_addr: usize) -> Option<usize> { | ||
| 1203 | if count == 0 { | ||
| 1204 | return None; | ||
| 1205 | } | ||
| 1206 | unsafe { | ||
| 1207 | if BITMAP.is_null() { | ||
| 1208 | return None; | ||
| 1209 | } | ||
| 1210 | let span = SPAN_PAGES; | ||
| 1211 | let min_page = min_addr.div_ceil(PAGE_SIZE).max(BASE_PAGE); | ||
| 1212 | if min_page >= BASE_PAGE + span { | ||
| 1213 | return None; | ||
| 1214 | } | ||
| 1215 | let start_idx = min_page - BASE_PAGE; | ||
| 1216 | let mut run_start = 0usize; | ||
| 1217 | let mut run_len = 0usize; | ||
| 1218 | for idx in start_idx..span { | ||
| 1219 | let free = (*BITMAP.add(idx / 8) >> (idx % 8)) & 1 == 0; | ||
| 1220 | if free { | ||
| 1221 | if run_len == 0 { | ||
| 1222 | run_start = idx; | ||
| 1223 | } | ||
| 1224 | run_len += 1; | ||
| 1225 | if run_len >= count { | ||
| 1226 | let p = (BASE_PAGE + run_start) * PAGE_SIZE; | ||
| 1227 | for k in run_start..run_start + count { | ||
| 1228 | *BITMAP.add(k / 8) |= 1 << (k % 8); | ||
| 1229 | } | ||
| 1230 | FREE_PAGES -= count; | ||
| 1231 | return Some(p); | ||
| 1232 | } | ||
| 1233 | } else { | ||
| 1234 | run_len = 0; | ||
| 1235 | } | ||
| 1236 | } | ||
| 1237 | None | ||
| 1238 | } | ||
| 1239 | } | ||
| 1240 | |||
| 1197 | /// Allocate `count` contiguous physical pages whose start address is | 1241 | /// Allocate `count` contiguous physical pages whose start address is |
| 1198 | /// aligned to `align_pages` (a power of two, in page units). | 1242 | /// aligned to `align_pages` (a power of two, in page units). |
| 1199 | pub fn alloc_pages_aligned(count: usize, align_pages: usize) -> Option<usize> { | 1243 | pub fn alloc_pages_aligned(count: usize, align_pages: usize) -> Option<usize> { |
diff --git a/rsnix/src/pci.rs b/rsnix/src/pci.rs index b9c2265..738c5e9 100644 --- a/rsnix/src/pci.rs +++ b/rsnix/src/pci.rs | |||
| @@ -61,13 +61,32 @@ pub fn config_read(bus: u8, device: u8, func: u8, reg: u8) -> u32 { | |||
| 61 | outl(CONFIG_ADDRESS, config_addr(bus, device, func, reg)); | 61 | outl(CONFIG_ADDRESS, config_addr(bus, device, func, reg)); |
| 62 | inl(CONFIG_DATA) | 62 | inl(CONFIG_DATA) |
| 63 | } | 63 | } |
| 64 | |||
| 65 | /// Write a dword to a device's config space (`reg` must be dword-aligned). | 64 | /// Write a dword to a device's config space (`reg` must be dword-aligned). |
| 66 | pub fn config_write(bus: u8, device: u8, func: u8, reg: u8, val: u32) { | 65 | pub fn config_write(bus: u8, device: u8, func: u8, reg: u8, val: u32) { |
| 67 | outl(CONFIG_ADDRESS, config_addr(bus, device, func, reg)); | 66 | outl(CONFIG_ADDRESS, config_addr(bus, device, func, reg)); |
| 68 | outl(CONFIG_DATA, val) | 67 | outl(CONFIG_DATA, val) |
| 69 | } | 68 | } |
| 70 | 69 | ||
| 70 | fn outb(port: u16, val: u8) { | ||
| 71 | unsafe { | ||
| 72 | core::arch::asm!( | ||
| 73 | "out dx, al", | ||
| 74 | in("dx") port, | ||
| 75 | in("al") val, | ||
| 76 | options(nomem, nostack, preserves_flags) | ||
| 77 | ); | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
| 81 | /// Write a single byte to config space (any byte offset). The PCI | ||
| 82 | /// byte-enable cycle goes out on port 0xCFC + lane, exactly like `pci_ | ||
| 83 | /// write_config_byte` everywhere else; QEMU and real chipsets both decode | ||
| 84 | /// the low two address bits of the config-data port. | ||
| 85 | pub fn config_write_byte(bus: u8, device: u8, func: u8, reg: u8, val: u8) { | ||
| 86 | outl(CONFIG_ADDRESS, config_addr(bus, device, func, reg)); | ||
| 87 | outb(CONFIG_DATA + ((reg & 3) as u16), val) | ||
| 88 | } | ||
| 89 | |||
| 71 | /// Read a memory BAR (index 0-5) as a 64-bit address, handling 32/64-bit | 90 | /// Read a memory BAR (index 0-5) as a 64-bit address, handling 32/64-bit |
| 72 | /// BARs. Returns None for I/O BARs or invalid values. | 91 | /// BARs. Returns None for I/O BARs or invalid values. |
| 73 | pub fn bar64(bus: u8, device: u8, func: u8, bar_index: u8) -> Option<u64> { | 92 | pub fn bar64(bus: u8, device: u8, func: u8, bar_index: u8) -> Option<u64> { |
| @@ -90,6 +109,110 @@ pub fn bar64(bus: u8, device: u8, func: u8, bar_index: u8) -> Option<u64> { | |||
| 90 | } | 109 | } |
| 91 | } | 110 | } |
| 92 | 111 | ||
| 112 | /// Claim USB Legacy ownership from the BIOS on a controller whose MMIO base | ||
| 113 | /// exposes the USB Legacy Support extended capability (USBLEGSUP, cap id 1). | ||
| 114 | /// | ||
| 115 | /// EHCI/xHCI βUSB Legacy Supportβ ownership handoff (EHCI spec Β§5.1). | ||
| 116 | /// | ||
| 117 | /// The 2012-era PCH BIOS drives USB keyboards through SMM USB-legacy | ||
| 118 | /// emulation while it owns the controller: its SMI handler polls the EHCI | ||
| 119 | /// over USB-error/port-change events. If the OS reprograms the controller | ||
| 120 | /// and runs async DMA underneath the still-active BIOS module, the SMM poll | ||
| 121 | /// collides with the OS transfer at an unpredictable moment β a classic | ||
| 122 | /// real-silicon Host System Error (SE) that QEMU cannot reproduce (no SMM). | ||
| 123 | /// | ||
| 124 | /// CRITICAL: the extended-capability pointer is a PCI *config-space* | ||
| 125 | /// offset, not an MMIO offset. Read it from an MMIO register (`eccp_src` | ||
| 126 | /// = MMIO dword offset; EHCI: HCCPARAMS @+0x08 bits 15:8; xHCI: HCSPARAMS1 | ||
| 127 | /// @+0x04 bits 31:16), then walk config space β exactly Linux's | ||
| 128 | /// `quirk_usb_handoff_ehci` (pci-quirks.c) and FreeBSD's `ehci_pci_attach`. | ||
| 129 | /// Our previous implementation treated EECP as an MMIO offset, read a | ||
| 130 | /// reserved register (0), and silently never claimed ownership β the | ||
| 131 | /// observed βclaim skippedβ on the 7010. | ||
| 132 | /// | ||
| 133 | /// Handshake (Linux/FreeBSD-identical): if the BIOS-owned semaphore | ||
| 134 | /// (bit 16 of the cap dword) is set, write the OS-owned semaphore byte | ||
| 135 | /// (cap+3) = 1 and spin (bounded ~1 s) for bit 16 to clear. USBLEGCTLSTS | ||
| 136 | /// (cap+4) is deliberately left alone β Linux notes some systems βget | ||
| 137 | /// upsetβ if it's touched. | ||
| 138 | /// | ||
| 139 | /// Returns: 0 = already released/owned (no claim needed), 1 = handed over, | ||
| 140 | /// 2 = BIOS never released, 3 = no legacy capability / chain out of range. | ||
| 141 | pub fn claim_usb_legacy( | ||
| 142 | bus: u8, | ||
| 143 | dev: u8, | ||
| 144 | func: u8, | ||
| 145 | mmio_base: usize, | ||
| 146 | eccp_src: usize, | ||
| 147 | eccp_bits_high: bool, | ||
| 148 | ) -> u8 { | ||
| 149 | let src = unsafe { (mmio_base as *const u32).add(eccp_src).read_volatile() }; | ||
| 150 | crate::serial_println!( | ||
| 151 | "usb-legacy: probe mmio=+{:#x} hcc={:#010x} high={}", | ||
| 152 | eccp_src, | ||
| 153 | src, | ||
| 154 | eccp_bits_high | ||
| 155 | ); | ||
| 156 | let mut offset = if eccp_bits_high { | ||
| 157 | ((src >> 16) & 0xFFFF) as u8 // xHCI: XECP (u8 caps the walk at 0xFF config) | ||
| 158 | } else { | ||
| 159 | ((src >> 8) & 0xFF) as u8 // EHCI: EECP | ||
| 160 | }; | ||
| 161 | if offset == 0 { | ||
| 162 | return 3; | ||
| 163 | } | ||
| 164 | let mut guard = 32; // bounded walk, no panics | ||
| 165 | while offset != 0 && guard > 0 { | ||
| 166 | guard -= 1; | ||
| 167 | let cap = config_read(bus, dev, func, offset); | ||
| 168 | crate::serial_println!( | ||
| 169 | "usb-legacy: cap@{:#x} id={:#04x} next={:#04x} d={:#010x}", | ||
| 170 | offset, | ||
| 171 | cap & 0xFF, | ||
| 172 | (cap >> 8) & 0xFF, | ||
| 173 | cap | ||
| 174 | ); | ||
| 175 | let id = cap & 0xFF; | ||
| 176 | let next = ((cap >> 8) & 0xFF) as u8; | ||
| 177 | match id { | ||
| 178 | 0 => return 3, // illegal reserved cap terminates the chain | ||
| 179 | 1 => { | ||
| 180 | // USB Legacy Support capability. | ||
| 181 | let ctlsts = config_read(bus, dev, func, offset + 4); | ||
| 182 | crate::serial_println!( | ||
| 183 | "usb-legacy: cap@{:#x} bios_sem={} os_sem={} ctlsts={:#x}", | ||
| 184 | offset, | ||
| 185 | (cap >> 16) & 1, | ||
| 186 | (cap >> 24) & 1, | ||
| 187 | ctlsts | ||
| 188 | ); | ||
| 189 | if cap & (1 << 24) != 0 { | ||
| 190 | return 0; // OS already owns | ||
| 191 | } | ||
| 192 | if cap & (1 << 16) == 0 { | ||
| 193 | return 0; // BIOS not claiming it | ||
| 194 | } | ||
| 195 | crate::serial_println!("usb-legacy: BIOS owns controller - claiming (OS sem)"); | ||
| 196 | config_write_byte(bus, dev, func, offset + 3, 1); | ||
| 197 | let t0 = crate::interrupts::uptime_ms(); | ||
| 198 | let mut handed = false; | ||
| 199 | while crate::interrupts::uptime_ms().wrapping_sub(t0) < 1000 { | ||
| 200 | if config_read(bus, dev, func, offset) & (1 << 16) == 0 { | ||
| 201 | handed = true; | ||
| 202 | break; | ||
| 203 | } | ||
| 204 | core::hint::spin_loop(); | ||
| 205 | } | ||
| 206 | return if handed { 1 } else { 2 }; | ||
| 207 | } | ||
| 208 | _ => { | ||
| 209 | offset = next; | ||
| 210 | } | ||
| 211 | } | ||
| 212 | } | ||
| 213 | 3 | ||
| 214 | } | ||
| 215 | |||
| 93 | /// Enumerate every device/function present on all buses. | 216 | /// Enumerate every device/function present on all buses. |
| 94 | pub fn probe() -> alloc::vec::Vec<DeviceInfo> { | 217 | pub fn probe() -> alloc::vec::Vec<DeviceInfo> { |
| 95 | let mut out = alloc::vec::Vec::new(); | 218 | let mut out = alloc::vec::Vec::new(); |
diff --git a/rsnix/src/ps2.rs b/rsnix/src/ps2.rs index 5f4a804..965e4a0 100644 --- a/rsnix/src/ps2.rs +++ b/rsnix/src/ps2.rs | |||
| @@ -169,14 +169,19 @@ pub fn init() { | |||
| 169 | 169 | ||
| 170 | // Enable the keyboard interrupt on the PS/2 controller. | 170 | // Enable the keyboard interrupt on the PS/2 controller. |
| 171 | // Read Configuration Byte (command 0x20), set bit 0 (IRQ1 enable), | 171 | // Read Configuration Byte (command 0x20), set bit 0 (IRQ1 enable), |
| 172 | // write back (command 0x60). | 172 | // write back (command 0x60). Every wait is bounded; a wedged |
| 173 | // controller bails out instead of hanging the boot. | ||
| 173 | unsafe { | 174 | unsafe { |
| 174 | // Tell the keyboard controller we want to read the config byte. | 175 | // Tell the keyboard controller we want to read the config byte. |
| 175 | // Need to wait for the controller to be ready first. | 176 | // Need to wait for the controller to be ready first. |
| 176 | wait_write(); | 177 | if !wait_write() { |
| 178 | return; | ||
| 179 | } | ||
| 177 | core::arch::asm!("out dx, al", in("dx") 0x64u16, in("al") 0x20u8, | 180 | core::arch::asm!("out dx, al", in("dx") 0x64u16, in("al") 0x20u8, |
| 178 | options(nostack, preserves_flags)); | 181 | options(nostack, preserves_flags)); |
| 179 | wait_read(); | 182 | if !wait_read() { |
| 183 | return; | ||
| 184 | } | ||
| 180 | let mut cfg: u8; | 185 | let mut cfg: u8; |
| 181 | core::arch::asm!("in al, dx", out("al") cfg, in("dx") 0x60u16, | 186 | core::arch::asm!("in al, dx", out("al") cfg, in("dx") 0x60u16, |
| 182 | options(nostack, preserves_flags)); | 187 | options(nostack, preserves_flags)); |
| @@ -184,48 +189,60 @@ pub fn init() { | |||
| 184 | cfg |= 0x01; // enable keyboard interrupt | 189 | cfg |= 0x01; // enable keyboard interrupt |
| 185 | 190 | ||
| 186 | // Write config byte back | 191 | // Write config byte back |
| 187 | wait_write(); | 192 | if !wait_write() { |
| 193 | return; | ||
| 194 | } | ||
| 188 | core::arch::asm!("out dx, al", in("dx") 0x64u16, in("al") 0x60u8, | 195 | core::arch::asm!("out dx, al", in("dx") 0x64u16, in("al") 0x60u8, |
| 189 | options(nostack, preserves_flags)); | 196 | options(nostack, preserves_flags)); |
| 190 | wait_write(); | 197 | if !wait_write() { |
| 198 | return; | ||
| 199 | } | ||
| 191 | core::arch::asm!("out dx, al", in("dx") 0x60u16, in("al") cfg, | 200 | core::arch::asm!("out dx, al", in("dx") 0x60u16, in("al") cfg, |
| 192 | options(nostack, preserves_flags)); | 201 | options(nostack, preserves_flags)); |
| 193 | } | 202 | } |
| 194 | } | 203 | } |
| 195 | 204 | ||
| 196 | /// Wait until the PS/2 controller is ready to receive a command. | 205 | /// Wait until the PS/2 controller is ready to receive a command. |
| 197 | fn wait_write() { | 206 | /// Bounded: a wedged or missing 8042 must not hang the boot (the classic |
| 207 | /// real-hardware freeze β the controller reports busy forever on some | ||
| 208 | /// machines). Returns false on timeout. | ||
| 209 | fn wait_write() -> bool { | ||
| 198 | unsafe { | 210 | unsafe { |
| 199 | loop { | 211 | for _ in 0..1_000_000 { |
| 200 | let status: u8; | 212 | let status: u8; |
| 201 | core::arch::asm!("in al, dx", out("al") status, in("dx") 0x64u16, | 213 | core::arch::asm!("in al, dx", out("al") status, in("dx") 0x64u16, |
| 202 | options(nostack, preserves_flags)); | 214 | options(nostack, preserves_flags)); |
| 203 | if status & 0x02 == 0 { | 215 | if status & 0x02 == 0 { |
| 204 | // Input buffer empty β controller ready to receive | 216 | // Input buffer empty β controller ready to receive |
| 205 | break; | 217 | return true; |
| 206 | } | 218 | } |
| 207 | } | 219 | } |
| 208 | } | 220 | } |
| 221 | false | ||
| 209 | } | 222 | } |
| 210 | 223 | ||
| 211 | /// Wait until the PS/2 controller has data available to read. | 224 | /// Wait until the PS/2 controller has data available to read. |
| 212 | fn wait_read() { | 225 | /// Bounded like `wait_write`. |
| 226 | fn wait_read() -> bool { | ||
| 213 | unsafe { | 227 | unsafe { |
| 214 | loop { | 228 | for _ in 0..1_000_000 { |
| 215 | let status: u8; | 229 | let status: u8; |
| 216 | core::arch::asm!("in al, dx", out("al") status, in("dx") 0x64u16, | 230 | core::arch::asm!("in al, dx", out("al") status, in("dx") 0x64u16, |
| 217 | options(nostack, preserves_flags)); | 231 | options(nostack, preserves_flags)); |
| 218 | if status & 0x01 != 0 { | 232 | if status & 0x01 != 0 { |
| 219 | // Output buffer full β data ready to read | 233 | // Output buffer full β data ready to read |
| 220 | break; | 234 | return true; |
| 221 | } | 235 | } |
| 222 | } | 236 | } |
| 223 | } | 237 | } |
| 238 | false | ||
| 224 | } | 239 | } |
| 225 | 240 | ||
| 241 | /// Drain any pending bytes. Bounded so a stuck output-full flag (or a | ||
| 242 | /// controller that keeps raising it with no data) cannot hang the boot. | ||
| 226 | fn drain_input() { | 243 | fn drain_input() { |
| 227 | unsafe { | 244 | unsafe { |
| 228 | loop { | 245 | for _ in 0..32 { |
| 229 | let status: u8; | 246 | let status: u8; |
| 230 | core::arch::asm!("in al, dx", out("al") status, in("dx") STATUS_PORT, | 247 | core::arch::asm!("in al, dx", out("al") status, in("dx") STATUS_PORT, |
| 231 | options(nostack, preserves_flags)); | 248 | options(nostack, preserves_flags)); |
| @@ -268,6 +285,15 @@ fn kbd() -> &'static mut KbdState { | |||
| 268 | unsafe { &mut *core::ptr::addr_of_mut!(KBD) } | 285 | unsafe { &mut *core::ptr::addr_of_mut!(KBD) } |
| 269 | } | 286 | } |
| 270 | 287 | ||
| 288 | /// Feed one scancode-set-1 byte into the shared decoder state machine. | ||
| 289 | /// Used by the PS/2 IRQ path (raw bytes) AND the USB HID keyboard driver | ||
| 290 | /// (which translates HID usage codes to scancode-set-1 make/break bytes so | ||
| 291 | /// the same shift/ctrl/caps decoding applies). Returns a KeyEvent on a | ||
| 292 | /// visible press; releases and modifier-only changes return None. | ||
| 293 | pub(crate) fn feed_scancode(byte: u8) -> Option<KeyEvent> { | ||
| 294 | process_byte(byte) | ||
| 295 | } | ||
| 296 | |||
| 271 | fn process_byte(byte: u8) -> Option<KeyEvent> { | 297 | fn process_byte(byte: u8) -> Option<KeyEvent> { |
| 272 | let state = kbd(); | 298 | let state = kbd(); |
| 273 | 299 | ||
| @@ -466,25 +492,35 @@ pub fn mouse_init() -> bool { | |||
| 466 | drain_input(); | 492 | drain_input(); |
| 467 | unsafe { | 493 | unsafe { |
| 468 | // 1. Enable the auxiliary port itself. | 494 | // 1. Enable the auxiliary port itself. |
| 469 | wait_write(); | 495 | if !wait_write() { |
| 496 | return false; | ||
| 497 | } | ||
| 470 | core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_AUX_ENABLE, | 498 | core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_AUX_ENABLE, |
| 471 | options(nostack, preserves_flags)); | 499 | options(nostack, preserves_flags)); |
| 472 | 500 | ||
| 473 | // 2. Configuration Byte: enable IRQ12 + the aux port clock. Read, | 501 | // 2. Configuration Byte: enable IRQ12 + the aux port clock. Read, |
| 474 | // OR the two bits, write back (preserves the keyboard IRQ bit | 502 | // OR the two bits, write back (preserves the keyboard IRQ bit |
| 475 | // that ps2::init set). | 503 | // that ps2::init set). |
| 476 | wait_write(); | 504 | if !wait_write() { |
| 505 | return false; | ||
| 506 | } | ||
| 477 | core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_READ_CFG, | 507 | core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_READ_CFG, |
| 478 | options(nostack, preserves_flags)); | 508 | options(nostack, preserves_flags)); |
| 479 | wait_read(); | 509 | if !wait_read() { |
| 510 | return false; | ||
| 511 | } | ||
| 480 | let mut cfg: u8; | 512 | let mut cfg: u8; |
| 481 | core::arch::asm!("in al, dx", out("al") cfg, in("dx") DATA_PORT, | 513 | core::arch::asm!("in al, dx", out("al") cfg, in("dx") DATA_PORT, |
| 482 | options(nostack, preserves_flags)); | 514 | options(nostack, preserves_flags)); |
| 483 | cfg |= CFG_AUX_IRQ | CFG_AUX_CLOCK; | 515 | cfg |= CFG_AUX_IRQ | CFG_AUX_CLOCK; |
| 484 | wait_write(); | 516 | if !wait_write() { |
| 517 | return false; | ||
| 518 | } | ||
| 485 | core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_WRITE_CFG, | 519 | core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_WRITE_CFG, |
| 486 | options(nostack, preserves_flags)); | 520 | options(nostack, preserves_flags)); |
| 487 | wait_write(); | 521 | if !wait_write() { |
| 522 | return false; | ||
| 523 | } | ||
| 488 | core::arch::asm!("out dx, al", in("dx") DATA_PORT, in("al") cfg, | 524 | core::arch::asm!("out dx, al", in("dx") DATA_PORT, in("al") cfg, |
| 489 | options(nostack, preserves_flags)); | 525 | options(nostack, preserves_flags)); |
| 490 | 526 | ||
| @@ -504,10 +540,14 @@ pub fn mouse_init() -> bool { | |||
| 504 | /// The wait is bounded so a missing device cannot hang the boot. | 540 | /// The wait is bounded so a missing device cannot hang the boot. |
| 505 | fn aux_command(cmd: u8) -> bool { | 541 | fn aux_command(cmd: u8) -> bool { |
| 506 | unsafe { | 542 | unsafe { |
| 507 | wait_write(); | 543 | if !wait_write() { |
| 544 | return false; | ||
| 545 | } | ||
| 508 | core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_SEND_TO_AUX, | 546 | core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_SEND_TO_AUX, |
| 509 | options(nostack, preserves_flags)); | 547 | options(nostack, preserves_flags)); |
| 510 | wait_write(); | 548 | if !wait_write() { |
| 549 | return false; | ||
| 550 | } | ||
| 511 | core::arch::asm!("out dx, al", in("dx") DATA_PORT, in("al") cmd, | 551 | core::arch::asm!("out dx, al", in("dx") DATA_PORT, in("al") cmd, |
| 512 | options(nostack, preserves_flags)); | 552 | options(nostack, preserves_flags)); |
| 513 | for _ in 0..100_000 { | 553 | for _ in 0..100_000 { |
diff --git a/rsnix/src/sched.rs b/rsnix/src/sched.rs index dd1409e..259ddf6 100644 --- a/rsnix/src/sched.rs +++ b/rsnix/src/sched.rs | |||
| @@ -158,6 +158,47 @@ static IN_ISR: AtomicBool = AtomicBool::new(false); | |||
| 158 | static PREEMPT_DEPTH: AtomicU64 = AtomicU64::new(0); | 158 | static PREEMPT_DEPTH: AtomicU64 = AtomicU64::new(0); |
| 159 | 159 | ||
| 160 | // --------------------------------------------------------------------------- | 160 | // --------------------------------------------------------------------------- |
| 161 | // Watchdog β turn wedged states into visible panics instead of silent | ||
| 162 | // freezes. Real hardware has no serial console, so the panic handler | ||
| 163 | // renders to the framebuffer and writes the crash log; a freeze that would | ||
| 164 | // otherwise look like a dead machine becomes a reported panic. | ||
| 165 | // | ||
| 166 | // The watchdog only runs when the PIT tick is firing (interrupts on). A | ||
| 167 | // hang with interrupts disabled is invisible to it, so driver waits must | ||
| 168 | // stay bounded (timeout loops) β enforced at the driver level. | ||
| 169 | // --------------------------------------------------------------------------- | ||
| 170 | |||
| 171 | /// Consecutive ticks where the preemption lock was held (depth > 0). The | ||
| 172 | /// tick cannot switch tasks while the lock is held, so a hold this long is | ||
| 173 | /// a deadlock (a driver wedged inside the serial/klog lock, ...). | ||
| 174 | static STALL_TICKS: AtomicU64 = AtomicU64::new(0); | ||
| 175 | const WATCHDOG_LOCK_TICKS: u64 = 500; // 5 s at 100 Hz | ||
| 176 | |||
| 177 | /// Per-task "same interrupted frame" counter for spin detection. | ||
| 178 | struct SpinWatch { | ||
| 179 | last_rsp: usize, | ||
| 180 | same: u32, | ||
| 181 | } | ||
| 182 | const SPIN_WATCH_INIT: SpinWatch = SpinWatch { last_rsp: 0, same: 0 }; | ||
| 183 | static mut SPIN_WATCH: [SpinWatch; MAX_TASKS] = [SPIN_WATCH_INIT; MAX_TASKS]; | ||
| 184 | /// A spinning background task is interrupted at roughly half the tick rate | ||
| 185 | /// (it alternates with idle), so 1500 hits is ~30 s of wall time. | ||
| 186 | const WATCHDOG_SPIN_TICKS: u32 = 1500; | ||
| 187 | /// Boot-phase threshold for task 0 (the shell): before the REPL is live, | ||
| 188 | /// task 0 runs init code where a fixed frame means a driver/controller | ||
| 189 | /// hang β panic after ~15 s wall. The boot must finish well inside this. | ||
| 190 | const WATCHDOG_BOOT_SPIN_TICKS: u32 = 750; | ||
| 191 | |||
| 192 | /// True once the shell REPL is live (set by main.rs before shell::run()). | ||
| 193 | /// The shell legitimately parks in poll_key with a fixed frame when it is; | ||
| 194 | /// before that, task 0 running boot code must not sit in a fixed frame. | ||
| 195 | static SHELL_RUNNING: AtomicBool = AtomicBool::new(false); | ||
| 196 | |||
| 197 | pub fn mark_shell_running() { | ||
| 198 | SHELL_RUNNING.store(true, Ordering::Relaxed); | ||
| 199 | } | ||
| 200 | |||
| 201 | // --------------------------------------------------------------------------- | ||
| 161 | // Preemption lock | 202 | // Preemption lock |
| 162 | // --------------------------------------------------------------------------- | 203 | // --------------------------------------------------------------------------- |
| 163 | 204 | ||
| @@ -193,12 +234,49 @@ pub fn current() -> usize { | |||
| 193 | /// Receives the interrupted task's frame pointer; returns the frame | 234 | /// Receives the interrupted task's frame pointer; returns the frame |
| 194 | /// pointer the asm should switch to (the same one when no switch). | 235 | /// pointer the asm should switch to (the same one when no switch). |
| 195 | pub fn on_tick(cur: usize) -> usize { | 236 | pub fn on_tick(cur: usize) -> usize { |
| 196 | if !is_ready() || PREEMPT_DEPTH.load(Ordering::Relaxed) > 0 { | 237 | if !is_ready() { |
| 197 | return cur; | 238 | return cur; |
| 198 | } | 239 | } |
| 199 | unsafe { | 240 | unsafe { |
| 241 | // Watchdog: the tick fired, so the CPU is alive. A state we cannot | ||
| 242 | // leave is a hang, not a sleep β surface it as a visible panic. | ||
| 243 | if PREEMPT_DEPTH.load(Ordering::Relaxed) > 0 { | ||
| 244 | let n = STALL_TICKS.fetch_add(1, Ordering::Relaxed) + 1; | ||
| 245 | if n >= WATCHDOG_LOCK_TICKS { | ||
| 246 | watchdog_panic(cur, "preemption lock held"); | ||
| 247 | } | ||
| 248 | return cur; | ||
| 249 | } | ||
| 250 | STALL_TICKS.store(0, Ordering::Relaxed); | ||
| 251 | |||
| 200 | TASKS[CURRENT].saved_rsp = cur; | 252 | TASKS[CURRENT].saved_rsp = cur; |
| 201 | TASKS[CURRENT].ticks += 1; | 253 | TASKS[CURRENT].ticks += 1; |
| 254 | // Spin detection: a task whose interrupted frame never moves | ||
| 255 | // (identical RSP on every tick) is stuck in a hot loop β no yield, | ||
| 256 | // no sleep, no block. The shell (id 0) legitimately parks in | ||
| 257 | // poll_key with a fixed frame, and idle (id 1) hlt's; both exempt. | ||
| 258 | let id = CURRENT; | ||
| 259 | let w = &mut SPIN_WATCH[id]; | ||
| 260 | if w.last_rsp == cur { | ||
| 261 | w.same += 1; | ||
| 262 | // Background tasks (id >= 2): spinning is never legitimate. | ||
| 263 | // Task 0 (shell): exempt once the REPL is live (poll_key parks | ||
| 264 | // with a fixed frame); during the boot phase a fixed frame | ||
| 265 | // means usb/ehci init is wedged on real hardware. | ||
| 266 | let threshold = if id == SHELL_TID && !SHELL_RUNNING.load(Ordering::Relaxed) { | ||
| 267 | WATCHDOG_BOOT_SPIN_TICKS | ||
| 268 | } else if id >= 2 { | ||
| 269 | WATCHDOG_SPIN_TICKS | ||
| 270 | } else { | ||
| 271 | u32::MAX // idle hlt's with a fixed frame forever β fine | ||
| 272 | }; | ||
| 273 | if w.same >= threshold { | ||
| 274 | watchdog_panic(cur, "task spinning without progress"); | ||
| 275 | } | ||
| 276 | } else { | ||
| 277 | w.last_rsp = cur; | ||
| 278 | w.same = 1; | ||
| 279 | } | ||
| 202 | let now = crate::interrupts::uptime_ms(); | 280 | let now = crate::interrupts::uptime_ms(); |
| 203 | for t in 0..MAX_TASKS { | 281 | for t in 0..MAX_TASKS { |
| 204 | match TASKS[t].state { | 282 | match TASKS[t].state { |
| @@ -219,6 +297,29 @@ pub fn on_tick(cur: usize) -> usize { | |||
| 219 | } | 297 | } |
| 220 | } | 298 | } |
| 221 | 299 | ||
| 300 | /// Render a watchdog verdict through the normal panic path (framebuffer + | ||
| 301 | /// serial + crash log) and halt. Runs in tick/ISR context: `cur` is the | ||
| 302 | /// interrupted task's IntFrame, so the reported RIP pinpoints the hang. | ||
| 303 | fn watchdog_panic(cur: usize, why: &str) -> ! { | ||
| 304 | let id = unsafe { CURRENT }; | ||
| 305 | let name = name_str(id); | ||
| 306 | let rip = unsafe { (cur as *const IntFrame).read().rip }; | ||
| 307 | let up = crate::interrupts::uptime_ms(); | ||
| 308 | if id == SHELL_TID { | ||
| 309 | // Boot-phase hang: report the last completed boot stage so the | ||
| 310 | // wedged init step is identifiable without serial. | ||
| 311 | let stage = crate::gop::last_boot_stage(); | ||
| 312 | panic!( | ||
| 313 | "watchdog: {} β task {} \"{}\" rip={:#x} (uptime {} ms) boot-stage: {}", | ||
| 314 | why, id, name, rip, up, stage | ||
| 315 | ); | ||
| 316 | } | ||
| 317 | panic!( | ||
| 318 | "watchdog: {} β task {} \"{}\" rip={:#x} (uptime {} ms)", | ||
| 319 | why, id, name, rip, up | ||
| 320 | ); | ||
| 321 | } | ||
| 322 | |||
| 222 | /// Software yield (vector 0x60). | 323 | /// Software yield (vector 0x60). |
| 223 | pub fn on_yield(cur: usize) -> usize { | 324 | pub fn on_yield(cur: usize) -> usize { |
| 224 | if !is_ready() || PREEMPT_DEPTH.load(Ordering::Relaxed) > 0 { | 325 | if !is_ready() || PREEMPT_DEPTH.load(Ordering::Relaxed) > 0 { |
diff --git a/rsnix/src/tty.rs b/rsnix/src/tty.rs index 7304925..0783521 100644 --- a/rsnix/src/tty.rs +++ b/rsnix/src/tty.rs | |||
| @@ -1,4 +1,8 @@ | |||
| 1 | //! Line discipline over the PS/2 keyboard β the kernel's tty layer. | 1 | //! Line discipline over the keyboard β the kernel's tty layer. |
| 2 | //! | ||
| 3 | //! Keys come from either the PS/2 controller (IRQ-fed scancode ring) or the | ||
| 4 | //! USB HID boot keyboard (polled UHCI driver). Both are translated to the | ||
| 5 | //! same `KeyEvent` space, so the shell is input-source agnostic. | ||
| 2 | //! | 6 | //! |
| 3 | //! Two modes, like a real UNIX terminal: | 7 | //! Two modes, like a real UNIX terminal: |
| 4 | //! - **canonical** (`read_line`): input is echoed, buffered until Enter, | 8 | //! - **canonical** (`read_line`): input is echoed, buffered until Enter, |
| @@ -34,6 +38,20 @@ pub enum LineInput { | |||
| 34 | static mut RAW_QUEUE: [u8; 8] = [0; 8]; | 38 | static mut RAW_QUEUE: [u8; 8] = [0; 8]; |
| 35 | static mut RAW_LEN: usize = 0; | 39 | static mut RAW_LEN: usize = 0; |
| 36 | 40 | ||
| 41 | /// Combined blocking key poll: USB HID keyboard first (polled UHCI), then | ||
| 42 | /// the PS/2 path (which parks via `hlt` until an IRQ-fed scancode lands). | ||
| 43 | fn poll_key() -> KeyEvent { | ||
| 44 | if let Some(ev) = crate::uhci::try_poll_key() { | ||
| 45 | return ev; | ||
| 46 | } | ||
| 47 | ps2::poll_key() | ||
| 48 | } | ||
| 49 | |||
| 50 | /// Combined non-blocking key poll (USB first, then PS/2). | ||
| 51 | fn try_poll_key() -> Option<KeyEvent> { | ||
| 52 | crate::uhci::try_poll_key().or_else(ps2::try_poll_key) | ||
| 53 | } | ||
| 54 | |||
| 37 | // --------------------------------------------------------------------------- | 55 | // --------------------------------------------------------------------------- |
| 38 | // canonical mode | 56 | // canonical mode |
| 39 | // --------------------------------------------------------------------------- | 57 | // --------------------------------------------------------------------------- |
| @@ -46,7 +64,7 @@ pub fn read_line() -> LineInput { | |||
| 46 | crate::vt::sync_scroll(); | 64 | crate::vt::sync_scroll(); |
| 47 | let mut buf = strbuf::String::<LINE_BUF_SIZE>::new(); | 65 | let mut buf = strbuf::String::<LINE_BUF_SIZE>::new(); |
| 48 | loop { | 66 | loop { |
| 49 | match ps2::poll_key() { | 67 | match poll_key() { |
| 50 | KeyEvent::Enter => { | 68 | KeyEvent::Enter => { |
| 51 | gop::put_char('\n'); | 69 | gop::put_char('\n'); |
| 52 | serial::write_str("\r\n"); | 70 | serial::write_str("\r\n"); |
| @@ -104,7 +122,7 @@ pub fn read_line() -> LineInput { | |||
| 104 | /// streaming commands (cat/hexdump) so a long dump can be interrupted. | 122 | /// streaming commands (cat/hexdump) so a long dump can be interrupted. |
| 105 | pub fn interrupt_pressed() -> bool { | 123 | pub fn interrupt_pressed() -> bool { |
| 106 | let mut pressed = false; | 124 | let mut pressed = false; |
| 107 | while let Some(ev) = ps2::try_poll_key() { | 125 | while let Some(ev) = try_poll_key() { |
| 108 | if matches!(ev, KeyEvent::Ctrl('c')) { | 126 | if matches!(ev, KeyEvent::Ctrl('c')) { |
| 109 | pressed = true; | 127 | pressed = true; |
| 110 | } | 128 | } |
| @@ -126,7 +144,7 @@ pub fn read_byte() -> u8 { | |||
| 126 | if let Some(b) = pop_raw() { | 144 | if let Some(b) = pop_raw() { |
| 127 | return b; | 145 | return b; |
| 128 | } | 146 | } |
| 129 | let ev = ps2::poll_key(); | 147 | let ev = poll_key(); |
| 130 | enqueue_key(ev); | 148 | enqueue_key(ev); |
| 131 | } | 149 | } |
| 132 | } | 150 | } |
| @@ -136,7 +154,7 @@ pub fn try_read_byte() -> Option<u8> { | |||
| 136 | if let Some(b) = pop_raw() { | 154 | if let Some(b) = pop_raw() { |
| 137 | return Some(b); | 155 | return Some(b); |
| 138 | } | 156 | } |
| 139 | match ps2::try_poll_key() { | 157 | match try_poll_key() { |
| 140 | Some(ev) => { | 158 | Some(ev) => { |
| 141 | enqueue_key(ev); | 159 | enqueue_key(ev); |
| 142 | pop_raw() | 160 | pop_raw() |
diff --git a/rsnix/src/uhci.rs b/rsnix/src/uhci.rs new file mode 100644 index 0000000..87ec9c6 --- /dev/null +++ b/rsnix/src/uhci.rs | |||
| @@ -0,0 +1,1129 @@ | |||
| 1 | // --------------------------------------------------------------------------- | ||
| 2 | // rsdos UHCI (USB 1.1) companion driver β HID boot keyboard + mouse. | ||
| 3 | // | ||
| 4 | // Real Intel PCH (e.g. OptiPlex 7010) USB2 ports are handled by an EHCI | ||
| 5 | // (high-speed devices) plus its companion UHCI pair (full/low-speed | ||
| 6 | // devices). A real USB keyboard/mouse is full- or low-speed, so it lives | ||
| 7 | // on the UHCI β the EHCI cannot talk to it without a root-hub TT (Intel | ||
| 8 | // PCH EHCI has none; the ports belong to the companion UHCI). QEMU's | ||
| 9 | // virtual usb-mouse/usb-kbd are high-speed when attached to usb-ehci, so | ||
| 10 | // this class of real hardware was never exercised by the xHCI/EHCI paths. | ||
| 11 | // | ||
| 12 | // This driver is polled (no IRQ), exactly like ehci.rs: the frame list is | ||
| 13 | // walked every 1 ms by the controller; we keep one armed interrupt-IN TD | ||
| 14 | // per device and drain reports from the shell task (keyboard) and cursor | ||
| 15 | // task (mouse). Both devices share one control pipeline, serialized by a | ||
| 16 | // spin lock so the shell and cursor tasks cannot corrupt each other's | ||
| 17 | // transfers. | ||
| 18 | // | ||
| 19 | // Data structures follow the UHCI spec as implemented by QEMU's hcd-uhci | ||
| 20 | // and Linux's uhci-hcd (they agree field-for-field): | ||
| 21 | // TD: link(0x00) ctrl(0x04) token(0x08) buffer(0x0C) | ||
| 22 | // QH: link(0x00) el_link(0x04) | ||
| 23 | // ctrl: ACTIVE=1<<23 IOC=1<<24 IOS=1<<25 LS=1<<26 C_ERR=3<<27 SPD=1<<29 | ||
| 24 | // ACTLEN=bits 10:0 (n-1, written by the HC) | ||
| 25 | // token: PID=bits 7:0 (full byte: SETUP=0x2D IN=0x69 OUT=0xE1) | ||
| 26 | // DEVADDR=bits 14:8, EP=bits 18:15, TOGGLE=bit 19, | ||
| 27 | // EXPLEN=bits 31:21 (n-1) | ||
| 28 | // link ptrs: bit0=terminate, bit1=QH type, bit2=depth-first | ||
| 29 | // --------------------------------------------------------------------------- | ||
| 30 | |||
| 31 | use crate::ps2::MouseEvent; | ||
| 32 | use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; | ||
| 33 | |||
| 34 | // ---- Registers (I/O space; 16-bit unless noted) ---- | ||
| 35 | const USBCMD: u16 = 0x00; | ||
| 36 | const USBSTS: u16 = 0x02; | ||
| 37 | const USBINTR: u16 = 0x04; | ||
| 38 | const FRNUM: u16 = 0x06; | ||
| 39 | const FRBASEADD: u16 = 0x08; // 32-bit | ||
| 40 | const SOFMOD: u16 = 0x0C; | ||
| 41 | const PORTSC_BASE: u16 = 0x10; // PORTSC1, PORTSC2 (16-bit each) | ||
| 42 | |||
| 43 | const CMD_RUN: u16 = 1 << 0; | ||
| 44 | const CMD_HCRESET: u16 = 1 << 1; | ||
| 45 | const CMD_CF: u16 = 1 << 6; | ||
| 46 | |||
| 47 | const STS_HCHALTED: u16 = 1 << 5; | ||
| 48 | |||
| 49 | const PSC_CCS: u16 = 1 << 0; // current connect status | ||
| 50 | const PSC_CSC: u16 = 1 << 1; // connect change (write 1 to clear) | ||
| 51 | const PSC_PE: u16 = 1 << 2; // port enabled | ||
| 52 | const PSC_PEDC: u16 = 1 << 3; // enable change (write 1 to clear) | ||
| 53 | const PSC_LSDA: u16 = 1 << 8; // low-speed device attached | ||
| 54 | const PSC_R: u16 = 1 << 9; // reset | ||
| 55 | const PSC_SUSP: u16 = 1 << 12; // suspend | ||
| 56 | |||
| 57 | // ---- TD / QH fields ---- | ||
| 58 | const TD_ACTIVE: u32 = 1 << 23; | ||
| 59 | const TD_IOC: u32 = 1 << 24; | ||
| 60 | const TD_LS: u32 = 1 << 26; | ||
| 61 | const TD_ERR: u32 = 3 << 27; // C_ERR = 3 retries | ||
| 62 | const TD_STALLED: u32 = 1 << 22; | ||
| 63 | const TD_BABBLE: u32 = 1 << 20; | ||
| 64 | const TD_TIMEO: u32 = 1 << 18; | ||
| 65 | const TD_ACTLEN: u32 = 0x7FF; // actual length, n-1 | ||
| 66 | |||
| 67 | const TOK_DEV_SHIFT: u32 = 8; | ||
| 68 | const TOK_EP_SHIFT: u32 = 15; | ||
| 69 | const TOK_TOGGLE: u32 = 1 << 19; | ||
| 70 | const TOK_EXPLEN_SHIFT: u32 = 21; | ||
| 71 | |||
| 72 | const PID_SETUP: u32 = 0x2D; | ||
| 73 | const PID_IN: u32 = 0x69; | ||
| 74 | const PID_OUT: u32 = 0xE1; | ||
| 75 | |||
| 76 | const PTR_TERM: u32 = 0x1; | ||
| 77 | const PTR_QH: u32 = 0x2; | ||
| 78 | |||
| 79 | // ---- HID boot requests ---- | ||
| 80 | const RT_CLASS_IFACE: u8 = 0x21; | ||
| 81 | const REQ_SET_IDLE: u8 = 0x0A; | ||
| 82 | const REQ_SET_PROTOCOL: u8 = 0x0B; | ||
| 83 | |||
| 84 | // ---- Static state ---- | ||
| 85 | static IO_BASE: AtomicU32 = AtomicU32::new(0); | ||
| 86 | static N_PORTS: AtomicU32 = AtomicU32::new(0); | ||
| 87 | static CTRL_UP: AtomicBool = AtomicBool::new(false); | ||
| 88 | static MOUSE_PRES: AtomicBool = AtomicBool::new(false); | ||
| 89 | static KBD_ARMED: AtomicBool = AtomicBool::new(false); | ||
| 90 | static MOUSE_ARMED: AtomicBool = AtomicBool::new(false); | ||
| 91 | // Port index each device was found on (usize::MAX = none). Used so the | ||
| 92 | // keyboard scan skips the mouse's port and vice versa. | ||
| 93 | static KBD_PORT: AtomicU32 = AtomicU32::new(u32::MAX); | ||
| 94 | static MOUSE_PORT: AtomicU32 = AtomicU32::new(u32::MAX); | ||
| 95 | static DEV_KBD: AtomicU32 = AtomicU32::new(0); | ||
| 96 | static DEV_MOUSE: AtomicU32 = AtomicU32::new(0); | ||
| 97 | static KBD_EP: AtomicU32 = AtomicU32::new(1); | ||
| 98 | static KBD_MPS: AtomicU32 = AtomicU32::new(8); | ||
| 99 | static MOUSE_EP: AtomicU32 = AtomicU32::new(1); | ||
| 100 | static MOUSE_MPS: AtomicU32 = AtomicU32::new(4); | ||
| 101 | // Low-speed flag per device (TD_LS needed for 1.5 Mb/s). | ||
| 102 | static KBD_LS: AtomicBool = AtomicBool::new(false); | ||
| 103 | static MOUSE_LS: AtomicBool = AtomicBool::new(false); | ||
| 104 | // Next USB device address to assign (1, 2, ...). | ||
| 105 | static DEV_SEQ: AtomicU32 = AtomicU32::new(1); | ||
| 106 | // Throttles for the lazy re-enumeration paths (ms). | ||
| 107 | static LAST_KBD_ENUM: AtomicU32 = AtomicU32::new(0); | ||
| 108 | static LAST_MOUSE_ENUM: AtomicU32 = AtomicU32::new(0); | ||
| 109 | // Serializes control transfers between the shell task (keyboard) and the | ||
| 110 | // cursor task (mouse). Bounded spin; on timeout the transfer is skipped | ||
| 111 | // and retried on the next throttled pass. | ||
| 112 | static CTRL_LOCK: AtomicBool = AtomicBool::new(false); | ||
| 113 | // Toggle bit the device expects next (alternates after each report). | ||
| 114 | static KBD_TOGGLE: AtomicU32 = AtomicU32::new(0); | ||
| 115 | static MOUSE_TOGGLE: AtomicU32 = AtomicU32::new(0); | ||
| 116 | |||
| 117 | // ---- DMA: frame list + one struct page ---- | ||
| 118 | static mut FRAMELIST: *mut u32 = core::ptr::null_mut(); | ||
| 119 | static mut P: *mut u32 = core::ptr::null_mut(); // QHs, TDs, buffers | ||
| 120 | |||
| 121 | // Offsets within the struct page (16-byte-aligned QHs/TDs). | ||
| 122 | const OFF_CTRL_QH: usize = 0x000; | ||
| 123 | const OFF_KBD_QH: usize = 0x010; | ||
| 124 | const OFF_MOUSE_QH: usize = 0x020; | ||
| 125 | const OFF_SETUP_TD: usize = 0x030; | ||
| 126 | const OFF_DATA_TD: usize = 0x040; | ||
| 127 | const OFF_STAT_TD: usize = 0x050; | ||
| 128 | const OFF_KBD_TD: usize = 0x060; | ||
| 129 | const OFF_MOUSE_TD: usize = 0x070; | ||
| 130 | const OFF_REQ: usize = 0x100; // 8-byte setup packet | ||
| 131 | const OFF_DESC: usize = 0x200; // device descriptor (64 B) | ||
| 132 | const OFF_CFG: usize = 0x300; // config descriptor (256 B) | ||
| 133 | const OFF_KBD_REPORT: usize = 0x500; // 8 B | ||
| 134 | const OFF_MOUSE_REPORT: usize = 0x520; // 8 B | ||
| 135 | |||
| 136 | // Keyboard: previous report + pending scancode FIFO (fed to ps2 decoder). | ||
| 137 | static mut KBD_PREV: [u8; 8] = [0; 8]; | ||
| 138 | const KBDQ_SIZE: usize = 64; | ||
| 139 | static mut KBDQ: [u8; KBDQ_SIZE] = [0; KBDQ_SIZE]; | ||
| 140 | static KBDQ_HEAD: AtomicU32 = AtomicU32::new(0); | ||
| 141 | static KBDQ_LEN: AtomicU32 = AtomicU32::new(0); | ||
| 142 | |||
| 143 | // ---- port I/O ---- | ||
| 144 | #[inline] | ||
| 145 | unsafe fn inw(port: u16) -> u16 { | ||
| 146 | let v: u16; | ||
| 147 | unsafe { | ||
| 148 | core::arch::asm!("in ax, dx", out("ax") v, in("dx") port, options(nostack, preserves_flags)); | ||
| 149 | } | ||
| 150 | v | ||
| 151 | } | ||
| 152 | #[inline] | ||
| 153 | unsafe fn outw(port: u16, v: u16) { | ||
| 154 | unsafe { | ||
| 155 | core::arch::asm!("out dx, ax", in("dx") port, in("ax") v, options(nostack, preserves_flags)); | ||
| 156 | } | ||
| 157 | } | ||
| 158 | #[inline] | ||
| 159 | unsafe fn outl(port: u16, v: u32) { | ||
| 160 | unsafe { | ||
| 161 | core::arch::asm!("out dx, eax", in("dx") port, in("eax") v, options(nostack, preserves_flags)); | ||
| 162 | } | ||
| 163 | } | ||
| 164 | |||
| 165 | fn io() -> u16 { | ||
| 166 | IO_BASE.load(Ordering::Relaxed) as u16 | ||
| 167 | } | ||
| 168 | /// Absolute I/O port of root port `p`'s PORTSC register. | ||
| 169 | fn prtsc(p: usize) -> u16 { | ||
| 170 | io() + PORTSC_BASE + (p as u16) * 2 | ||
| 171 | } | ||
| 172 | |||
| 173 | // ---- DMA helpers (mirror ehci.rs) ---- | ||
| 174 | unsafe fn dma_alloc(bytes: usize) -> *mut u8 { | ||
| 175 | unsafe { | ||
| 176 | let pages = bytes.div_ceil(4096); | ||
| 177 | let Some(phys) = crate::memory::alloc_pages(pages) else { | ||
| 178 | return core::ptr::null_mut(); | ||
| 179 | }; | ||
| 180 | let p = phys as *mut u8; | ||
| 181 | core::ptr::write_bytes(p, 0, pages * 4096); | ||
| 182 | p | ||
| 183 | } | ||
| 184 | } | ||
| 185 | |||
| 186 | unsafe fn rd(addr: *const u32) -> u32 { | ||
| 187 | unsafe { addr.read_volatile() } | ||
| 188 | } | ||
| 189 | unsafe fn wr(addr: *mut u32, v: u32) { | ||
| 190 | unsafe { addr.write_volatile(v) } | ||
| 191 | } | ||
| 192 | |||
| 193 | fn td_addr(o: usize) -> *mut u32 { | ||
| 194 | unsafe { P.add(o / 4) } | ||
| 195 | } | ||
| 196 | fn buf_addr(o: usize) -> *mut u8 { | ||
| 197 | unsafe { P.add(o / 4) as *mut u8 } | ||
| 198 | } | ||
| 199 | |||
| 200 | #[inline] | ||
| 201 | fn ms_now() -> u64 { | ||
| 202 | crate::interrupts::uptime_ms() | ||
| 203 | } | ||
| 204 | |||
| 205 | /// Busy-wait for `ms` milliseconds (interrupts stay enabled; the tick keeps | ||
| 206 | /// the scheduler alive). | ||
| 207 | fn ms_spin(ms: u64) { | ||
| 208 | let t0 = ms_now(); | ||
| 209 | while ms_now().wrapping_sub(t0) < ms { | ||
| 210 | core::hint::spin_loop(); | ||
| 211 | } | ||
| 212 | } | ||
| 213 | |||
| 214 | /// Build an 8-byte USB setup packet. | ||
| 215 | fn ctrl_req(bm: u8, breq: u8, val: u16, idx: u16, len: u16) -> [u8; 8] { | ||
| 216 | let mut r = [0u8; 8]; | ||
| 217 | r[0] = bm; | ||
| 218 | r[1] = breq; | ||
| 219 | r[2] = val as u8; | ||
| 220 | r[3] = (val >> 8) as u8; | ||
| 221 | r[4] = idx as u8; | ||
| 222 | r[5] = (idx >> 8) as u8; | ||
| 223 | r[6] = len as u8; | ||
| 224 | r[7] = (len >> 8) as u8; | ||
| 225 | r | ||
| 226 | } | ||
| 227 | |||
| 228 | // --------------------------------------------------------------------------- | ||
| 229 | // Control transfer pipeline (shared, lock-protected) | ||
| 230 | // --------------------------------------------------------------------------- | ||
| 231 | |||
| 232 | fn lock_ctrl() -> bool { | ||
| 233 | for _ in 0..200_000 { | ||
| 234 | if !CTRL_LOCK.swap(true, Ordering::Acquire) { | ||
| 235 | return true; | ||
| 236 | } | ||
| 237 | core::hint::spin_loop(); | ||
| 238 | } | ||
| 239 | false | ||
| 240 | } | ||
| 241 | fn unlock_ctrl() { | ||
| 242 | CTRL_LOCK.store(false, Ordering::Release); | ||
| 243 | } | ||
| 244 | |||
| 245 | /// Issue one control transfer on EP0 of `dev`. `mps` is the device's EP0 | ||
| 246 | /// max packet, `low_speed` selects the TD_CTRL_LS bit, `req` the setup | ||
| 247 | /// packet, `dir_in` a device->host data phase, `buf/len` the data buffer. | ||
| 248 | /// Returns bytes received (IN) or 0 (OUT/setup-only), or a negative error. | ||
| 249 | /// Completion is polled (STATUS TD active bit) with a 5 s timeout. | ||
| 250 | unsafe fn ctl_xfer( | ||
| 251 | dev: u32, | ||
| 252 | _mps: u32, | ||
| 253 | low_speed: bool, | ||
| 254 | req: &[u8; 8], | ||
| 255 | dir_in: bool, | ||
| 256 | buf: *mut u8, | ||
| 257 | len: usize, | ||
| 258 | ) -> i32 { | ||
| 259 | unsafe { | ||
| 260 | // The controller DMA's the setup packet straight from the request | ||
| 261 | // buffer β copy it there first. | ||
| 262 | core::ptr::copy_nonoverlapping(req.as_ptr(), buf_addr(OFF_REQ), 8); | ||
| 263 | core::sync::atomic::fence(Ordering::SeqCst); | ||
| 264 | let ls = if low_speed { TD_LS } else { 0 }; | ||
| 265 | |||
| 266 | // SETUP TD (toggle 0, 8 bytes, always from the request buffer). | ||
| 267 | let st = td_addr(OFF_SETUP_TD); | ||
| 268 | wr(st.add(0), 0); // link: filled below | ||
| 269 | wr(st.add(1), TD_ACTIVE | TD_ERR | ls); | ||
| 270 | wr( | ||
| 271 | st.add(2), | ||
| 272 | PID_SETUP | (dev << TOK_DEV_SHIFT) | (7 << TOK_EXPLEN_SHIFT), // 8 bytes | ||
| 273 | ); | ||
| 274 | wr(st.add(3), buf_addr(OFF_REQ) as u32); | ||
| 275 | |||
| 276 | // STATUS TD (toggle 1, opposite direction of the data phase). | ||
| 277 | let sp = td_addr(OFF_STAT_TD); | ||
| 278 | let status_pid = if dir_in { PID_OUT } else { PID_IN }; | ||
| 279 | wr(sp.add(0), PTR_TERM); | ||
| 280 | wr(sp.add(1), TD_ACTIVE | TD_ERR | ls); | ||
| 281 | wr( | ||
| 282 | sp.add(2), | ||
| 283 | status_pid | (dev << TOK_DEV_SHIFT) | TOK_TOGGLE | (0 << TOK_EXPLEN_SHIFT), | ||
| 284 | ); | ||
| 285 | wr(sp.add(3), buf_addr(OFF_REQ) as u32); | ||
| 286 | |||
| 287 | if len == 0 { | ||
| 288 | // Setup-only transfer: SETUP -> STATUS. | ||
| 289 | wr(st.add(0), td_addr(OFF_STAT_TD) as u32); | ||
| 290 | } else { | ||
| 291 | // DATA TD (toggle 1). | ||
| 292 | let dt = td_addr(OFF_DATA_TD); | ||
| 293 | let dpid = if dir_in { PID_IN } else { PID_OUT }; | ||
| 294 | wr(dt.add(0), td_addr(OFF_STAT_TD) as u32); | ||
| 295 | wr(dt.add(1), TD_ACTIVE | TD_ERR | ls); | ||
| 296 | wr( | ||
| 297 | dt.add(2), | ||
| 298 | dpid | (dev << TOK_DEV_SHIFT) | ||
| 299 | | TOK_TOGGLE | ||
| 300 | | (((len as u32).saturating_sub(1) & 0x7FF) << TOK_EXPLEN_SHIFT), | ||
| 301 | ); | ||
| 302 | wr(dt.add(3), buf as u32); | ||
| 303 | wr(st.add(0), dt as u32); | ||
| 304 | } | ||
| 305 | |||
| 306 | // Point the control QH at the new chain and let the HC run it. | ||
| 307 | core::sync::atomic::fence(Ordering::SeqCst); | ||
| 308 | wr(td_addr(OFF_CTRL_QH).add(1), td_addr(OFF_SETUP_TD) as u32); | ||
| 309 | core::sync::atomic::fence(Ordering::SeqCst); | ||
| 310 | |||
| 311 | // Wait for completion (STATUS TD clears ACTIVE). | ||
| 312 | let t0 = ms_now(); | ||
| 313 | loop { | ||
| 314 | let c = rd(sp.add(1)); | ||
| 315 | if c & TD_ACTIVE == 0 { | ||
| 316 | // Error checks: the status TD mirrors the chain's fate. | ||
| 317 | if c & (TD_STALLED | TD_BABBLE | TD_TIMEO) != 0 { | ||
| 318 | return -2; | ||
| 319 | } | ||
| 320 | let got = if len > 0 && dir_in { | ||
| 321 | ((rd(td_addr(OFF_DATA_TD).add(1)) & TD_ACTLEN) + 1) as i32 | ||
| 322 | } else { | ||
| 323 | 0 | ||
| 324 | }; | ||
| 325 | return got.min(len as i32); | ||
| 326 | } | ||
| 327 | if ms_now().wrapping_sub(t0) >= 5000 { | ||
| 328 | return -1; // timeout | ||
| 329 | } | ||
| 330 | core::hint::spin_loop(); | ||
| 331 | } | ||
| 332 | } | ||
| 333 | } | ||
| 334 | |||
| 335 | // --------------------------------------------------------------------------- | ||
| 336 | // Port management | ||
| 337 | // --------------------------------------------------------------------------- | ||
| 338 | |||
| 339 | /// Reset a connected root port and enable it. Returns the low-speed flag | ||
| 340 | /// (false = full-speed) on success. | ||
| 341 | unsafe fn reset_port(p: usize) -> Option<bool> { | ||
| 342 | unsafe { | ||
| 343 | let r = prtsc(p); | ||
| 344 | // Clear connect/enable change bits (write 1s). | ||
| 345 | outw(r, PSC_CSC | PSC_PEDC); | ||
| 346 | // Start reset. | ||
| 347 | outw(r, PSC_R); | ||
| 348 | ms_spin(50); | ||
| 349 | // End reset. | ||
| 350 | outw(r, 0); | ||
| 351 | // Enable the port (device must be connected). | ||
| 352 | outw(r, PSC_PE); | ||
| 353 | let t0 = ms_now(); | ||
| 354 | while ms_now().wrapping_sub(t0) < 100 { | ||
| 355 | let psc = inw(r); | ||
| 356 | if psc & PSC_PE != 0 { | ||
| 357 | return Some(psc & PSC_LSDA != 0); | ||
| 358 | } | ||
| 359 | ms_spin(1); | ||
| 360 | } | ||
| 361 | None | ||
| 362 | } | ||
| 363 | } | ||
| 364 | |||
| 365 | // --------------------------------------------------------------------------- | ||
| 366 | // Config-descriptor parsing: find a HID boot interface with the wanted | ||
| 367 | // protocol and its first interrupt IN endpoint. | ||
| 368 | // --------------------------------------------------------------------------- | ||
| 369 | |||
| 370 | struct HidEp { | ||
| 371 | epnum: u8, | ||
| 372 | mps: u16, | ||
| 373 | iface: u8, | ||
| 374 | } | ||
| 375 | |||
| 376 | /// Walk a config descriptor for a boot-protocol HID interface (class 3, | ||
| 377 | /// subclass 1) whose protocol matches `want` (1 = keyboard, 2 = mouse), | ||
| 378 | /// returning its first interrupt IN endpoint. | ||
| 379 | fn find_hid_ep(cfg: &[u8], want: u8) -> Option<HidEp> { | ||
| 380 | let mut i = 0; | ||
| 381 | while i + 1 < cfg.len() && cfg[i] != 0 { | ||
| 382 | let blen = cfg[i] as usize; | ||
| 383 | let btype = cfg[i + 1]; | ||
| 384 | if blen < 2 || i + blen > cfg.len() { | ||
| 385 | return None; | ||
| 386 | } | ||
| 387 | if btype == 4 { | ||
| 388 | // Interface descriptor. | ||
| 389 | if blen >= 9 | ||
| 390 | && cfg[i + 5] == 3 // bInterfaceClass = HID | ||
| 391 | && cfg[i + 6] == 1 // bInterfaceSubClass = boot | ||
| 392 | && cfg[i + 7] == want | ||
| 393 | // bInterfaceProtocol | ||
| 394 | { | ||
| 395 | let iface = cfg[i + 2]; | ||
| 396 | // Scan this interface's endpoints. | ||
| 397 | let mut j = i + blen; | ||
| 398 | while j + 1 < cfg.len() && cfg[j] != 0 { | ||
| 399 | let elen = cfg[j] as usize; | ||
| 400 | let etype = cfg[j + 1]; | ||
| 401 | if etype == 4 { | ||
| 402 | break; // next interface | ||
| 403 | } | ||
| 404 | if etype == 5 && elen >= 7 { | ||
| 405 | let epa = cfg[j + 2]; | ||
| 406 | let attrs = cfg[j + 3]; | ||
| 407 | if epa & 0x80 != 0 && attrs & 0x03 == 0x03 { | ||
| 408 | let mps = (cfg[j + 4] as u16) | ((cfg[j + 5] as u16) << 8); | ||
| 409 | return Some(HidEp { | ||
| 410 | epnum: epa & 0x0F, | ||
| 411 | mps: mps.max(1), | ||
| 412 | iface, | ||
| 413 | }); | ||
| 414 | } | ||
| 415 | } | ||
| 416 | if elen < 2 { | ||
| 417 | return None; | ||
| 418 | } | ||
| 419 | j += elen; | ||
| 420 | } | ||
| 421 | } | ||
| 422 | } | ||
| 423 | i += blen; | ||
| 424 | } | ||
| 425 | None | ||
| 426 | } | ||
| 427 | |||
| 428 | // --------------------------------------------------------------------------- | ||
| 429 | // Device enumeration (one root port) | ||
| 430 | // --------------------------------------------------------------------------- | ||
| 431 | |||
| 432 | /// Enumerate the device on root port `p` and arm the interrupt-IN endpoint | ||
| 433 | /// if it is a HID boot device of protocol `want` (1 = keyboard, 2 = mouse). | ||
| 434 | /// Returns true when armed. | ||
| 435 | unsafe fn enumerate_port(p: usize, want: u8) -> bool { | ||
| 436 | unsafe { | ||
| 437 | let Some(low_speed) = reset_port(p) else { | ||
| 438 | return false; | ||
| 439 | }; | ||
| 440 | |||
| 441 | // 1. GET_DESCRIPTOR(device) at address 0 β 8 bytes for EP0 MPS. | ||
| 442 | let dd = buf_addr(OFF_DESC); | ||
| 443 | core::ptr::write_bytes(dd, 0, 64); | ||
| 444 | let req = ctrl_req(0x80, 6, 0x0100, 0, 8); | ||
| 445 | if ctl_xfer(0, 8, low_speed, &req, true, dd, 8) < 0 { | ||
| 446 | crate::serial_println!("uhci: port {} get-dev-desc failed (want {})", p, want); | ||
| 447 | return false; | ||
| 448 | } | ||
| 449 | let mps0 = core::ptr::read_volatile(dd.add(7)).max(8) as u32; | ||
| 450 | |||
| 451 | // 2. SET_ADDRESS. | ||
| 452 | let dev = DEV_SEQ.fetch_add(1, Ordering::Relaxed); | ||
| 453 | let req = ctrl_req(0x00, 5, dev as u16, 0, 0); | ||
| 454 | if ctl_xfer(0, mps0, low_speed, &req, false, core::ptr::null_mut(), 0) < 0 { | ||
| 455 | crate::serial_println!("uhci: set-address failed on port {}", p); | ||
| 456 | return false; | ||
| 457 | } | ||
| 458 | |||
| 459 | // 3. Full device descriptor at the new address. | ||
| 460 | core::ptr::write_bytes(dd, 0, 64); | ||
| 461 | let req = ctrl_req(0x80, 6, 0x0100, 0, 18); | ||
| 462 | let got = ctl_xfer(dev, mps0, low_speed, &req, true, dd, 18); | ||
| 463 | if got < 0 { | ||
| 464 | crate::serial_println!("uhci: re-get dev-desc failed on port {}", p); | ||
| 465 | return false; | ||
| 466 | } | ||
| 467 | let class = core::ptr::read_volatile(dd.add(4)); | ||
| 468 | let _ = class; // HID devices have device class 0x00; the HID class | ||
| 469 | // lives in the interface descriptor (checked below). | ||
| 470 | |||
| 471 | // 4. Full config descriptor. | ||
| 472 | let cfg = buf_addr(OFF_CFG); | ||
| 473 | core::ptr::write_bytes(cfg, 0, 256); | ||
| 474 | let req = ctrl_req(0x80, 6, 0x0200, 0, 256); | ||
| 475 | let got = ctl_xfer(dev, mps0, low_speed, &req, true, cfg, 256); | ||
| 476 | if got < 0 { | ||
| 477 | crate::serial_println!( | ||
| 478 | "uhci: get-config failed on port {} code={} (data ctrl={:#x} stat ctrl={:#x})", | ||
| 479 | p, | ||
| 480 | got, | ||
| 481 | rd(td_addr(OFF_DATA_TD).add(1)), | ||
| 482 | rd(td_addr(OFF_STAT_TD).add(1)) | ||
| 483 | ); | ||
| 484 | return false; | ||
| 485 | } | ||
| 486 | let Some(ep) = find_hid_ep(core::slice::from_raw_parts(cfg, got as usize), want) else { | ||
| 487 | return false; // not the device we're looking for | ||
| 488 | }; | ||
| 489 | |||
| 490 | // 5. SET_CONFIGURATION(1). | ||
| 491 | let req = ctrl_req(0x00, 9, 1, 0, 0); | ||
| 492 | if ctl_xfer(dev, mps0, low_speed, &req, false, core::ptr::null_mut(), 0) < 0 { | ||
| 493 | crate::serial_println!("uhci: set-config failed on port {}", p); | ||
| 494 | return false; | ||
| 495 | } | ||
| 496 | |||
| 497 | // 6. Boot protocol + no idle. Some devices stall these; tolerate. | ||
| 498 | let req = ctrl_req(RT_CLASS_IFACE, REQ_SET_PROTOCOL, 1, ep.iface as u16, 0); | ||
| 499 | if ctl_xfer(dev, mps0, low_speed, &req, false, core::ptr::null_mut(), 0) < 0 { | ||
| 500 | crate::serial_println!("uhci: set-protocol stalled on port {}", p); | ||
| 501 | } | ||
| 502 | let req = ctrl_req(RT_CLASS_IFACE, REQ_SET_IDLE, 0, ep.iface as u16, 0); | ||
| 503 | if ctl_xfer(dev, mps0, low_speed, &req, false, core::ptr::null_mut(), 0) < 0 { | ||
| 504 | crate::serial_println!("uhci: set-idle stalled on port {}", p); | ||
| 505 | } | ||
| 506 | |||
| 507 | // 7. Arm the interrupt-IN TD. | ||
| 508 | arm_in_td(want, dev, &ep, low_speed, p); | ||
| 509 | crate::serial_println!( | ||
| 510 | "uhci: HID boot {} armed (port {} dev {} ep{}({:#x}) mps={} {})", | ||
| 511 | if want == 1 { "keyboard" } else { "mouse" }, | ||
| 512 | p, | ||
| 513 | dev, | ||
| 514 | ep.epnum, | ||
| 515 | if want == 1 { 0x81 } else { 0x81 }, | ||
| 516 | ep.mps, | ||
| 517 | if low_speed { "low-speed" } else { "full-speed" } | ||
| 518 | ); | ||
| 519 | true | ||
| 520 | } | ||
| 521 | } | ||
| 522 | |||
| 523 | /// Build the interrupt-IN TD for a device and point its QH element at it. | ||
| 524 | unsafe fn arm_in_td(want: u8, dev: u32, ep: &HidEp, low_speed: bool, port: usize) { | ||
| 525 | unsafe { | ||
| 526 | let (qho, tdo, buf_off, mps, is_kbd) = if want == 1 { | ||
| 527 | ( | ||
| 528 | OFF_KBD_QH, | ||
| 529 | OFF_KBD_TD, | ||
| 530 | OFF_KBD_REPORT, | ||
| 531 | (ep.mps as u32).max(8), | ||
| 532 | true, | ||
| 533 | ) | ||
| 534 | } else { | ||
| 535 | ( | ||
| 536 | OFF_MOUSE_QH, | ||
| 537 | OFF_MOUSE_TD, | ||
| 538 | OFF_MOUSE_REPORT, | ||
| 539 | (ep.mps as u32).max(4), | ||
| 540 | false, | ||
| 541 | ) | ||
| 542 | }; | ||
| 543 | let qh = td_addr(qho); | ||
| 544 | let td = td_addr(tdo); | ||
| 545 | let ls = if low_speed { TD_LS } else { 0 }; | ||
| 546 | let explen = if is_kbd { 8 } else { 3 }; | ||
| 547 | let toggle = 0; // endpoint toggles start at 0 after SET_CONFIG | ||
| 548 | |||
| 549 | wr(td.add(0), PTR_TERM); | ||
| 550 | wr(td.add(1), TD_ACTIVE | TD_ERR | TD_IOC | ls); | ||
| 551 | wr( | ||
| 552 | td.add(2), | ||
| 553 | PID_IN | ||
| 554 | | (dev << TOK_DEV_SHIFT) | ||
| 555 | | ((ep.epnum as u32) << TOK_EP_SHIFT) | ||
| 556 | | (toggle << 19) | ||
| 557 | | (explen << TOK_EXPLEN_SHIFT), | ||
| 558 | ); | ||
| 559 | wr(td.add(3), buf_addr(buf_off) as u32); | ||
| 560 | // QH: el_link points at the TD (link/el_link written last). | ||
| 561 | wr(qh.add(0), PTR_TERM); // no next QH (per-device QH at chain end) | ||
| 562 | core::sync::atomic::fence(Ordering::SeqCst); | ||
| 563 | wr(qh.add(1), td as u32); | ||
| 564 | core::sync::atomic::fence(Ordering::SeqCst); | ||
| 565 | |||
| 566 | match is_kbd { | ||
| 567 | true => { | ||
| 568 | KBD_ARMED.store(true, Ordering::Relaxed); | ||
| 569 | KBD_PORT.store(port as u32, Ordering::Relaxed); | ||
| 570 | DEV_KBD.store(dev, Ordering::Relaxed); | ||
| 571 | KBD_EP.store(ep.epnum as u32, Ordering::Relaxed); | ||
| 572 | KBD_MPS.store(mps, Ordering::Relaxed); | ||
| 573 | KBD_LS.store(low_speed, Ordering::Relaxed); | ||
| 574 | KBD_TOGGLE.store(0, Ordering::Relaxed); | ||
| 575 | } | ||
| 576 | false => { | ||
| 577 | MOUSE_ARMED.store(true, Ordering::Relaxed); | ||
| 578 | MOUSE_PORT.store(port as u32, Ordering::Relaxed); | ||
| 579 | DEV_MOUSE.store(dev, Ordering::Relaxed); | ||
| 580 | MOUSE_EP.store(ep.epnum as u32, Ordering::Relaxed); | ||
| 581 | MOUSE_MPS.store(mps, Ordering::Relaxed); | ||
| 582 | MOUSE_LS.store(low_speed, Ordering::Relaxed); | ||
| 583 | MOUSE_TOGGLE.store(0, Ordering::Relaxed); | ||
| 584 | MOUSE_PRES.store(true, Ordering::Relaxed); | ||
| 585 | } | ||
| 586 | } | ||
| 587 | } | ||
| 588 | } | ||
| 589 | |||
| 590 | /// Scan all root ports and arm the first device of the wanted protocol. | ||
| 591 | unsafe fn arm_device(want: u8) -> bool { | ||
| 592 | unsafe { | ||
| 593 | if !lock_ctrl() { | ||
| 594 | return false; | ||
| 595 | } | ||
| 596 | let np = N_PORTS.load(Ordering::Relaxed) as usize; | ||
| 597 | let other = if want == 1 { | ||
| 598 | MOUSE_PORT.load(Ordering::Relaxed) | ||
| 599 | } else { | ||
| 600 | KBD_PORT.load(Ordering::Relaxed) | ||
| 601 | }; | ||
| 602 | for p in 0..np { | ||
| 603 | if p as u32 == other { | ||
| 604 | continue; // claimed by the other device | ||
| 605 | } | ||
| 606 | let psc = inw(prtsc(p)); | ||
| 607 | if psc & PSC_CCS == 0 { | ||
| 608 | continue; | ||
| 609 | } | ||
| 610 | if enumerate_port(p, want) { | ||
| 611 | unlock_ctrl(); | ||
| 612 | return true; | ||
| 613 | } | ||
| 614 | } | ||
| 615 | unlock_ctrl(); | ||
| 616 | false | ||
| 617 | } | ||
| 618 | } | ||
| 619 | |||
| 620 | // --------------------------------------------------------------------------- | ||
| 621 | // Keyboard: HID report -> scancode-set-1 make/break bytes -> ps2 decoder | ||
| 622 | // --------------------------------------------------------------------------- | ||
| 623 | |||
| 624 | /// HID usage -> scancode set 1 (make code). High byte 0xE0 = E0-prefixed. | ||
| 625 | /// 0 = unmapped. Table covers the common keyboard usages (0x04..0x63). | ||
| 626 | static HID_TO_SCAN: [u16; 0x64] = { | ||
| 627 | let mut t = [0u16; 0x64]; | ||
| 628 | let mut i = 0; | ||
| 629 | while i < 0x64 { | ||
| 630 | t[i] = 0; | ||
| 631 | i += 1; | ||
| 632 | } | ||
| 633 | // Letters a-z | ||
| 634 | t[0x04] = 0x1E; | ||
| 635 | t[0x05] = 0x30; | ||
| 636 | t[0x06] = 0x2E; | ||
| 637 | t[0x07] = 0x20; | ||
| 638 | t[0x08] = 0x12; | ||
| 639 | t[0x09] = 0x21; | ||
| 640 | t[0x0A] = 0x22; | ||
| 641 | t[0x0B] = 0x23; | ||
| 642 | t[0x0C] = 0x17; | ||
| 643 | t[0x0D] = 0x24; | ||
| 644 | t[0x0E] = 0x25; | ||
| 645 | t[0x0F] = 0x26; | ||
| 646 | t[0x10] = 0x32; | ||
| 647 | t[0x11] = 0x31; | ||
| 648 | t[0x12] = 0x18; | ||
| 649 | t[0x13] = 0x19; | ||
| 650 | t[0x14] = 0x10; | ||
| 651 | t[0x15] = 0x13; | ||
| 652 | t[0x16] = 0x1F; | ||
| 653 | t[0x17] = 0x14; | ||
| 654 | t[0x18] = 0x16; | ||
| 655 | t[0x19] = 0x2F; | ||
| 656 | t[0x1A] = 0x11; | ||
| 657 | t[0x1B] = 0x2D; | ||
| 658 | t[0x1C] = 0x15; | ||
| 659 | t[0x1D] = 0x2C; | ||
| 660 | // Digits + punctuation | ||
| 661 | t[0x1E] = 0x02; | ||
| 662 | t[0x1F] = 0x03; | ||
| 663 | t[0x20] = 0x04; | ||
| 664 | t[0x21] = 0x05; | ||
| 665 | t[0x22] = 0x06; | ||
| 666 | t[0x23] = 0x07; | ||
| 667 | t[0x24] = 0x08; | ||
| 668 | t[0x25] = 0x09; | ||
| 669 | t[0x26] = 0x0A; | ||
| 670 | t[0x27] = 0x0B; | ||
| 671 | t[0x28] = 0x1C; // Enter | ||
| 672 | t[0x29] = 0x01; // Esc | ||
| 673 | t[0x2A] = 0x0E; // Backspace | ||
| 674 | t[0x2B] = 0x0F; // Tab | ||
| 675 | t[0x2C] = 0x39; // Space | ||
| 676 | t[0x2D] = 0x0C; // - | ||
| 677 | t[0x2E] = 0x0D; // = | ||
| 678 | t[0x2F] = 0x1A; // [ | ||
| 679 | t[0x30] = 0x1B; // ] | ||
| 680 | t[0x31] = 0x2B; // backslash | ||
| 681 | t[0x33] = 0x27; // ; | ||
| 682 | t[0x34] = 0x28; // ' | ||
| 683 | t[0x35] = 0x29; // ` | ||
| 684 | t[0x36] = 0x33; // , | ||
| 685 | t[0x37] = 0x34; // . | ||
| 686 | t[0x38] = 0x35; // / | ||
| 687 | t[0x39] = 0x3A; // CapsLock | ||
| 688 | // F-keys | ||
| 689 | t[0x3A] = 0x3B; // F1 | ||
| 690 | t[0x3B] = 0x3C; | ||
| 691 | t[0x3C] = 0x3D; | ||
| 692 | t[0x3D] = 0x3E; | ||
| 693 | t[0x3E] = 0x3F; | ||
| 694 | t[0x3F] = 0x40; | ||
| 695 | t[0x40] = 0x41; | ||
| 696 | t[0x41] = 0x42; | ||
| 697 | t[0x42] = 0x43; | ||
| 698 | t[0x43] = 0x44; // F10 | ||
| 699 | t[0x44] = 0xE057; // F11 | ||
| 700 | t[0x45] = 0xE058; // F12 | ||
| 701 | // Nav cluster | ||
| 702 | t[0x49] = 0xE052; // Insert | ||
| 703 | t[0x4A] = 0xE047; // Home | ||
| 704 | t[0x4B] = 0xE049; // PageUp | ||
| 705 | t[0x4C] = 0xE053; // Delete | ||
| 706 | t[0x4D] = 0xE04F; // End | ||
| 707 | t[0x4E] = 0xE051; // PageDown | ||
| 708 | t[0x4F] = 0xE04D; // Right | ||
| 709 | t[0x50] = 0xE04B; // Left | ||
| 710 | t[0x51] = 0xE050; // Down | ||
| 711 | t[0x52] = 0xE048; // Up | ||
| 712 | // Keypad | ||
| 713 | t[0x53] = 0x45; // NumLock | ||
| 714 | t[0x54] = 0xE035; // / | ||
| 715 | t[0x55] = 0x37; // * | ||
| 716 | t[0x56] = 0x4A; // - | ||
| 717 | t[0x57] = 0x4E; // + | ||
| 718 | t[0x58] = 0xE01C; // Enter | ||
| 719 | t[0x59] = 0x4F; // 1 | ||
| 720 | t[0x5A] = 0x50; // 2 | ||
| 721 | t[0x5B] = 0x51; // 3 | ||
| 722 | t[0x5C] = 0x4B; // 4 | ||
| 723 | t[0x5D] = 0x4C; // 5 | ||
| 724 | t[0x5E] = 0x4D; // 6 | ||
| 725 | t[0x5F] = 0x47; // 7 | ||
| 726 | t[0x60] = 0x48; // 8 | ||
| 727 | t[0x61] = 0x49; // 9 | ||
| 728 | t[0x62] = 0x52; // 0 | ||
| 729 | t[0x63] = 0x53; // . | ||
| 730 | t | ||
| 731 | }; | ||
| 732 | |||
| 733 | /// Modifier bit -> scancode (set 1, high byte 0xE0 = E0-prefixed). | ||
| 734 | static MOD_TO_SCAN: [u16; 8] = [ | ||
| 735 | 0x1D, // 0 LCtrl | ||
| 736 | 0x2A, // 1 LShift | ||
| 737 | 0x38, // 2 LAlt | ||
| 738 | 0xE05B, // 3 LGui | ||
| 739 | 0xE01D, // 4 RCtrl | ||
| 740 | 0x36, // 5 RShift | ||
| 741 | 0xE038, // 6 RAlt | ||
| 742 | 0xE05C, // 7 RGui | ||
| 743 | ]; | ||
| 744 | |||
| 745 | fn kbdq_push(b: u8) { | ||
| 746 | unsafe { | ||
| 747 | let h = KBDQ_HEAD.load(Ordering::Relaxed) as usize; | ||
| 748 | let l = KBDQ_LEN.load(Ordering::Relaxed) as usize; | ||
| 749 | if l >= KBDQ_SIZE { | ||
| 750 | return; | ||
| 751 | } | ||
| 752 | KBDQ[(h + l) % KBDQ_SIZE] = b; | ||
| 753 | KBDQ_LEN.store((l + 1) as u32, Ordering::Relaxed); | ||
| 754 | } | ||
| 755 | } | ||
| 756 | fn kbdq_pop() -> Option<u8> { | ||
| 757 | unsafe { | ||
| 758 | let l = KBDQ_LEN.load(Ordering::Relaxed) as usize; | ||
| 759 | if l == 0 { | ||
| 760 | return None; | ||
| 761 | } | ||
| 762 | let h = KBDQ_HEAD.load(Ordering::Relaxed) as usize; | ||
| 763 | let b = KBDQ[h]; | ||
| 764 | KBDQ_HEAD.store(((h + 1) % KBDQ_SIZE) as u32, Ordering::Relaxed); | ||
| 765 | KBDQ_LEN.store((l - 1) as u32, Ordering::Relaxed); | ||
| 766 | Some(b) | ||
| 767 | } | ||
| 768 | } | ||
| 769 | |||
| 770 | /// Push the make (press) or break (release) scancode sequence for a | ||
| 771 | /// scancode-map entry (0 = unmapped, 0x00XX = plain, 0xE0XX = E0-prefixed). | ||
| 772 | fn push_scancode(scan: u16, press: bool) { | ||
| 773 | if scan == 0 { | ||
| 774 | return; | ||
| 775 | } | ||
| 776 | if scan & 0xFF00 == 0xE000 { | ||
| 777 | kbdq_push(0xE0); | ||
| 778 | let code = (scan & 0xFF) as u8; | ||
| 779 | kbdq_push(if press { code } else { code | 0x80 }); | ||
| 780 | } else { | ||
| 781 | let code = scan as u8; | ||
| 782 | kbdq_push(if press { code } else { code | 0x80 }); | ||
| 783 | } | ||
| 784 | } | ||
| 785 | |||
| 786 | /// Diff the completed HID report against the previous one and queue the | ||
| 787 | /// scancode transitions (make/break) for the shared PS/2 decoder. | ||
| 788 | unsafe fn diff_report(report: &[u8]) { | ||
| 789 | unsafe { | ||
| 790 | let prev = &mut *core::ptr::addr_of_mut!(KBD_PREV); | ||
| 791 | let new_mods = report[0]; | ||
| 792 | let old_mods = prev[0]; | ||
| 793 | for bit in 0..8 { | ||
| 794 | let new_set = new_mods & (1 << bit) != 0; | ||
| 795 | let old_set = old_mods & (1 << bit) != 0; | ||
| 796 | if new_set != old_set { | ||
| 797 | push_scancode(MOD_TO_SCAN[bit], new_set); | ||
| 798 | } | ||
| 799 | } | ||
| 800 | let new_keys = &report[2..8]; | ||
| 801 | let old_keys = &prev[2..8]; | ||
| 802 | // Releases first (old keys not in new set). | ||
| 803 | for &k in old_keys { | ||
| 804 | if k == 0 || new_keys.contains(&k) { | ||
| 805 | continue; | ||
| 806 | } | ||
| 807 | if k as usize >= HID_TO_SCAN.len() { | ||
| 808 | continue; | ||
| 809 | } | ||
| 810 | push_scancode(HID_TO_SCAN[k as usize], false); | ||
| 811 | } | ||
| 812 | // Then presses (new keys not in the old set). | ||
| 813 | for &k in new_keys { | ||
| 814 | if k == 0 || old_keys.contains(&k) { | ||
| 815 | continue; | ||
| 816 | } | ||
| 817 | if k as usize >= HID_TO_SCAN.len() { | ||
| 818 | continue; | ||
| 819 | } | ||
| 820 | push_scancode(HID_TO_SCAN[k as usize], true); | ||
| 821 | } | ||
| 822 | prev.copy_from_slice(report); | ||
| 823 | } | ||
| 824 | } | ||
| 825 | |||
| 826 | /// Re-arm a completed interrupt-IN TD (one-outstanding report pattern). | ||
| 827 | unsafe fn rearm_td(qho: usize, tdo: usize, toggle: u32, ls: bool, mps: usize, explen: u32) { | ||
| 828 | unsafe { | ||
| 829 | let qh = td_addr(qho); | ||
| 830 | let td = td_addr(tdo); | ||
| 831 | let t = rd(td.add(2)); | ||
| 832 | let dev = (t >> TOK_DEV_SHIFT) & 0x7F; | ||
| 833 | let ep = (t >> TOK_EP_SHIFT) & 0xF; | ||
| 834 | wr( | ||
| 835 | td.add(1), | ||
| 836 | TD_ACTIVE | TD_ERR | TD_IOC | if ls { TD_LS } else { 0 }, | ||
| 837 | ); | ||
| 838 | wr( | ||
| 839 | td.add(2), | ||
| 840 | PID_IN | ||
| 841 | | (dev << TOK_DEV_SHIFT) | ||
| 842 | | (ep << TOK_EP_SHIFT) | ||
| 843 | | (toggle << 19) | ||
| 844 | | (explen << TOK_EXPLEN_SHIFT), | ||
| 845 | ); | ||
| 846 | core::sync::atomic::fence(Ordering::SeqCst); | ||
| 847 | wr(qh.add(1), td as u32); | ||
| 848 | core::sync::atomic::fence(Ordering::SeqCst); | ||
| 849 | let _ = mps; | ||
| 850 | } | ||
| 851 | } | ||
| 852 | |||
| 853 | // --------------------------------------------------------------------------- | ||
| 854 | // Public interface | ||
| 855 | // --------------------------------------------------------------------------- | ||
| 856 | |||
| 857 | pub fn controller_up() -> bool { | ||
| 858 | CTRL_UP.load(Ordering::Relaxed) | ||
| 859 | } | ||
| 860 | pub fn mouse_present() -> bool { | ||
| 861 | MOUSE_PRES.load(Ordering::Relaxed) | ||
| 862 | } | ||
| 863 | |||
| 864 | /// Find the first UHCI companion controller (class 0x0C, subclass 0x03, | ||
| 865 | /// prog_if 0x00). | ||
| 866 | fn find_uhci() -> Option<(u8, u8, u8)> { | ||
| 867 | for d in crate::pci::probe() { | ||
| 868 | if d.class == 0x0C && d.subclass == 0x03 && d.prog_if == 0x00 { | ||
| 869 | return Some((d.bus, d.device, d.func)); | ||
| 870 | } | ||
| 871 | } | ||
| 872 | None | ||
| 873 | } | ||
| 874 | |||
| 875 | /// Bring up the first UHCI controller: claim it, reset it, run the frame | ||
| 876 | /// list, and enumerate any connected boot HID devices. | ||
| 877 | pub fn init() -> bool { | ||
| 878 | let Some((b, d, f)) = find_uhci() else { | ||
| 879 | crate::serial_println!("uhci: no UHCI controller"); | ||
| 880 | return false; | ||
| 881 | }; | ||
| 882 | crate::serial_println!("uhci: USB1.1 UHCI at {:02x}:{:02x}.{}", b, d, f); | ||
| 883 | |||
| 884 | unsafe { | ||
| 885 | // I/O space + bus master (polled: no INTx assertion needed, but | ||
| 886 | // clear it anyway so a stray legacy IRQ can never storm). | ||
| 887 | let cmd = crate::pci::config_read(b, d, f, 0x04); | ||
| 888 | crate::pci::config_write( | ||
| 889 | b, | ||
| 890 | d, | ||
| 891 | f, | ||
| 892 | 0x04, | ||
| 893 | (cmd & 0xFFFF_0000) | (cmd & 0xFFFF) | 0x5 | 0x400, | ||
| 894 | ); | ||
| 895 | let mut base: u32 = 0; | ||
| 896 | // BAR0 first (Intel ICH), then BAR4 (PIIX3/PIIX4-style). Take the | ||
| 897 | // first nonzero I/O BAR. | ||
| 898 | for bar_idx in [0u8, 4] { | ||
| 899 | let bar = crate::pci::config_read(b, d, f, 0x10 + bar_idx * 4); | ||
| 900 | let cand = bar & !0x3; | ||
| 901 | if cand != 0 { | ||
| 902 | base = cand; | ||
| 903 | break; | ||
| 904 | } | ||
| 905 | } | ||
| 906 | if base == 0 { | ||
| 907 | crate::serial_println!("uhci: no I/O BAR"); | ||
| 908 | return false; | ||
| 909 | } | ||
| 910 | IO_BASE.store(base, Ordering::Relaxed); | ||
| 911 | |||
| 912 | // Controller reset: stop, then HCRESET (UHCI clears HCRESET itself | ||
| 913 | // within microseconds). | ||
| 914 | outw(io(), 0); | ||
| 915 | outw(io() + USBCMD, CMD_HCRESET); | ||
| 916 | ms_spin(5); | ||
| 917 | let mut t = 0; | ||
| 918 | while t < 1_000_000 && inw(io() + USBCMD) & CMD_HCRESET != 0 { | ||
| 919 | t += 1; | ||
| 920 | core::hint::spin_loop(); | ||
| 921 | } | ||
| 922 | outw(io() + USBINTR, 0); | ||
| 923 | |||
| 924 | // DMA: 4 KiB frame list (1024 entries) + struct page. | ||
| 925 | FRAMELIST = dma_alloc(4096) as *mut u32; | ||
| 926 | P = dma_alloc(4096) as *mut u32; | ||
| 927 | if FRAMELIST.is_null() || P.is_null() { | ||
| 928 | crate::serial_println!("uhci: DMA alloc failed"); | ||
| 929 | return false; | ||
| 930 | } | ||
| 931 | |||
| 932 | // Wire the schedule: every frame entry -> CTRL_QH -> KBD_QH -> | ||
| 933 | // MOUSE_QH -> terminate. Each QH's element is set when its device | ||
| 934 | // arms; the control QH element is set per transfer. | ||
| 935 | let fl = FRAMELIST; | ||
| 936 | for i in 0..1024 { | ||
| 937 | wr(fl.add(i), (td_addr(OFF_CTRL_QH) as u32) | PTR_QH); | ||
| 938 | } | ||
| 939 | wr( | ||
| 940 | td_addr(OFF_CTRL_QH).add(0), | ||
| 941 | (td_addr(OFF_KBD_QH) as u32) | PTR_QH, | ||
| 942 | ); | ||
| 943 | wr(td_addr(OFF_CTRL_QH).add(1), PTR_TERM); | ||
| 944 | wr( | ||
| 945 | td_addr(OFF_KBD_QH).add(0), | ||
| 946 | (td_addr(OFF_MOUSE_QH) as u32) | PTR_QH, | ||
| 947 | ); | ||
| 948 | wr(td_addr(OFF_KBD_QH).add(1), PTR_TERM); | ||
| 949 | wr(td_addr(OFF_MOUSE_QH).add(0), PTR_TERM); | ||
| 950 | wr(td_addr(OFF_MOUSE_QH).add(1), PTR_TERM); | ||
| 951 | core::sync::atomic::fence(Ordering::SeqCst); | ||
| 952 | |||
| 953 | // Frame list base, configure, run. | ||
| 954 | outl(io() + FRBASEADD, fl as u32); | ||
| 955 | outw(io() + SOFMOD, 64); | ||
| 956 | outw(io() + FRNUM, 0); | ||
| 957 | outw(io() + USBCMD, CMD_CF); | ||
| 958 | outw(io() + USBCMD, CMD_CF | CMD_RUN); | ||
| 959 | ms_spin(2); | ||
| 960 | if inw(io() + USBSTS) & STS_HCHALTED != 0 { | ||
| 961 | crate::serial_println!("uhci: controller failed to run"); | ||
| 962 | return false; | ||
| 963 | } | ||
| 964 | |||
| 965 | // Port count: UHCI has 2 ports at 0x10/0x12; a few have more. | ||
| 966 | let mut np = 0; | ||
| 967 | while np < 4 { | ||
| 968 | let psc = inw(prtsc(np)); | ||
| 969 | if psc == 0xFFFF || psc == 0xff7f { | ||
| 970 | break; | ||
| 971 | } | ||
| 972 | np += 1; | ||
| 973 | } | ||
| 974 | N_PORTS.store(np as u32, Ordering::Relaxed); | ||
| 975 | CTRL_UP.store(true, Ordering::Relaxed); | ||
| 976 | crate::serial_println!("uhci: controller up (io={:#x} ports={})", base, np); | ||
| 977 | |||
| 978 | // Enumerate a boot keyboard first (input priority), then a mouse. | ||
| 979 | crate::serial_println!( | ||
| 980 | "uhci: dbg P={:#x} FL={:#x} ctrlqh={:#x} el={:#x} fl0={:#x} setup_td={:#x} setup_link={:#x}", | ||
| 981 | P as usize, | ||
| 982 | FRAMELIST as usize, | ||
| 983 | td_addr(OFF_CTRL_QH) as usize, | ||
| 984 | rd(td_addr(OFF_CTRL_QH).add(1)), | ||
| 985 | rd(FRAMELIST.add(0)), | ||
| 986 | td_addr(OFF_SETUP_TD) as usize, | ||
| 987 | rd(td_addr(OFF_SETUP_TD).add(0)) | ||
| 988 | ); | ||
| 989 | let _ = arm_device(1); | ||
| 990 | let _ = arm_device(2); | ||
| 991 | true | ||
| 992 | } | ||
| 993 | } | ||
| 994 | |||
| 995 | /// Non-blocking keyboard poll: drains the scancode FIFO (fed by completed | ||
| 996 | /// HID reports) through the shared PS/2 decoder. Lazy re-enumeration is | ||
| 997 | /// throttled to one attempt per second. | ||
| 998 | pub fn try_poll_key() -> Option<crate::ps2::KeyEvent> { | ||
| 999 | unsafe { | ||
| 1000 | // Fast path: pending scancodes first. | ||
| 1001 | if let Some(b) = kbdq_pop() { | ||
| 1002 | return crate::ps2::feed_scancode(b); | ||
| 1003 | } | ||
| 1004 | if KBD_ARMED.load(Ordering::Relaxed) { | ||
| 1005 | let td = td_addr(OFF_KBD_TD); | ||
| 1006 | let c = rd(td.add(1)); | ||
| 1007 | if c & TD_ACTIVE == 0 { | ||
| 1008 | if c & (TD_STALLED | TD_BABBLE | TD_TIMEO) != 0 { | ||
| 1009 | // Transfer error: drop the arm; bounded re-enum follows. | ||
| 1010 | KBD_ARMED.store(false, Ordering::Relaxed); | ||
| 1011 | return None; | ||
| 1012 | } | ||
| 1013 | // Completed: diff the report, re-arm, then drain the FIFO. | ||
| 1014 | let rep = buf_addr(OFF_KBD_REPORT); | ||
| 1015 | let mut report = [0u8; 8]; | ||
| 1016 | for (i, b) in report.iter_mut().enumerate() { | ||
| 1017 | *b = core::ptr::read_volatile(rep.add(i)); | ||
| 1018 | } | ||
| 1019 | diff_report(&report); | ||
| 1020 | let tog = KBD_TOGGLE.load(Ordering::Relaxed) ^ 1; | ||
| 1021 | KBD_TOGGLE.store(tog, Ordering::Relaxed); | ||
| 1022 | rearm_td( | ||
| 1023 | OFF_KBD_QH, | ||
| 1024 | OFF_KBD_TD, | ||
| 1025 | tog, | ||
| 1026 | KBD_LS.load(Ordering::Relaxed), | ||
| 1027 | KBD_MPS.load(Ordering::Relaxed) as usize, | ||
| 1028 | 8, | ||
| 1029 | ); | ||
| 1030 | return kbdq_pop().and_then(crate::ps2::feed_scancode); | ||
| 1031 | } | ||
| 1032 | return None; | ||
| 1033 | } | ||
| 1034 | // Not armed: bring the keyboard up lazily, never hot-loop. | ||
| 1035 | if CTRL_UP.load(Ordering::Relaxed) { | ||
| 1036 | let now = crate::interrupts::uptime_ms() as u32; | ||
| 1037 | if now.wrapping_sub(LAST_KBD_ENUM.load(Ordering::Relaxed)) >= 1000 { | ||
| 1038 | LAST_KBD_ENUM.store(now, Ordering::Relaxed); | ||
| 1039 | let _ = arm_device(1); | ||
| 1040 | } | ||
| 1041 | } | ||
| 1042 | kbdq_pop().and_then(crate::ps2::feed_scancode) | ||
| 1043 | } | ||
| 1044 | } | ||
| 1045 | |||
| 1046 | /// Poll for a mouse report (mirrors ehci::poll_mouse): hot path reads the | ||
| 1047 | /// armed TD's ACTIVE bit in DMA memory; completion decodes the report and | ||
| 1048 | /// re-arms; a stalled/TD-error transfer drops the arm for a bounded | ||
| 1049 | /// re-enumeration. | ||
| 1050 | pub fn poll_mouse() -> Option<MouseEvent> { | ||
| 1051 | unsafe { | ||
| 1052 | if MOUSE_ARMED.load(Ordering::Relaxed) { | ||
| 1053 | let td = td_addr(OFF_MOUSE_TD); | ||
| 1054 | let c = rd(td.add(1)); | ||
| 1055 | if c & TD_ACTIVE == 0 { | ||
| 1056 | if c & (TD_STALLED | TD_BABBLE | TD_TIMEO) != 0 { | ||
| 1057 | MOUSE_ARMED.store(false, Ordering::Relaxed); | ||
| 1058 | return None; | ||
| 1059 | } | ||
| 1060 | let hb = buf_addr(OFF_MOUSE_REPORT); | ||
| 1061 | let b0 = core::ptr::read_volatile(hb); | ||
| 1062 | let b1 = core::ptr::read_volatile(hb.add(1)); | ||
| 1063 | let b2 = core::ptr::read_volatile(hb.add(2)); | ||
| 1064 | let ev = Some(MouseEvent { | ||
| 1065 | dx: (b1 as i8) as i16, | ||
| 1066 | dy: (b2 as i8) as i16, | ||
| 1067 | left: b0 & 0x01 != 0, | ||
| 1068 | right: b0 & 0x02 != 0, | ||
| 1069 | middle: b0 & 0x04 != 0, | ||
| 1070 | }); | ||
| 1071 | let tog = MOUSE_TOGGLE.load(Ordering::Relaxed) ^ 1; | ||
| 1072 | MOUSE_TOGGLE.store(tog, Ordering::Relaxed); | ||
| 1073 | rearm_td( | ||
| 1074 | OFF_MOUSE_QH, | ||
| 1075 | OFF_MOUSE_TD, | ||
| 1076 | tog, | ||
| 1077 | MOUSE_LS.load(Ordering::Relaxed), | ||
| 1078 | MOUSE_MPS.load(Ordering::Relaxed) as usize, | ||
| 1079 | 3, | ||
| 1080 | ); | ||
| 1081 | return ev; | ||
| 1082 | } | ||
| 1083 | return None; | ||
| 1084 | } | ||
| 1085 | if CTRL_UP.load(Ordering::Relaxed) { | ||
| 1086 | let now = crate::interrupts::uptime_ms() as u32; | ||
| 1087 | if now.wrapping_sub(LAST_MOUSE_ENUM.load(Ordering::Relaxed)) >= 1000 { | ||
| 1088 | LAST_MOUSE_ENUM.store(now, Ordering::Relaxed); | ||
| 1089 | let _ = arm_device(2); | ||
| 1090 | } | ||
| 1091 | } | ||
| 1092 | None | ||
| 1093 | } | ||
| 1094 | } | ||
| 1095 | |||
| 1096 | /// One-line status for the shell `usb` command. | ||
| 1097 | pub fn diagnose() -> alloc::string::String { | ||
| 1098 | let mut s = alloc::string::String::new(); | ||
| 1099 | s.push_str(&alloc::format!( | ||
| 1100 | "uhci io={:#x} ports={} kbd={} mouse={} kbd_dev={} mouse_dev={}\n", | ||
| 1101 | IO_BASE.load(Ordering::Relaxed), | ||
| 1102 | N_PORTS.load(Ordering::Relaxed), | ||
| 1103 | if KBD_ARMED.load(Ordering::Relaxed) { | ||
| 1104 | "armed" | ||
| 1105 | } else { | ||
| 1106 | "-" | ||
| 1107 | }, | ||
| 1108 | if MOUSE_ARMED.load(Ordering::Relaxed) { | ||
| 1109 | "armed" | ||
| 1110 | } else { | ||
| 1111 | "-" | ||
| 1112 | }, | ||
| 1113 | DEV_KBD.load(Ordering::Relaxed), | ||
| 1114 | DEV_MOUSE.load(Ordering::Relaxed) | ||
| 1115 | )); | ||
| 1116 | let np = N_PORTS.load(Ordering::Relaxed) as usize; | ||
| 1117 | for p in 0..np.min(4) { | ||
| 1118 | let v = unsafe { inw(prtsc(p)) }; | ||
| 1119 | s.push_str(&alloc::format!( | ||
| 1120 | " uport{}: {}{}{}{:#x}\n", | ||
| 1121 | p, | ||
| 1122 | if v & PSC_CCS != 0 { "conn " } else { "none " }, | ||
| 1123 | if v & PSC_PE != 0 { "en " } else { "" }, | ||
| 1124 | if v & PSC_LSDA != 0 { "low " } else { "" }, | ||
| 1125 | v | ||
| 1126 | )); | ||
| 1127 | } | ||
| 1128 | s | ||
| 1129 | } | ||
diff --git a/rsnix/src/usb.rs b/rsnix/src/usb.rs index 71df781..3ed3a0c 100644 --- a/rsnix/src/usb.rs +++ b/rsnix/src/usb.rs | |||
| @@ -229,6 +229,15 @@ unsafe fn wr64(a: usize, v: u64) { | |||
| 229 | fn op(off: usize) -> usize { | 229 | fn op(off: usize) -> usize { |
| 230 | unsafe { OPB + off } | 230 | unsafe { OPB + off } |
| 231 | } | 231 | } |
| 232 | |||
| 233 | /// 16-bit I/O read (UHCI port status/control registers live in I/O space). | ||
| 234 | unsafe fn io_iow16(port: u16) -> u16 { | ||
| 235 | let v: u16; | ||
| 236 | unsafe { | ||
| 237 | core::arch::asm!("in ax, dx", out("ax") v, in("dx") port, options(nomem, nostack, preserves_flags)); | ||
| 238 | } | ||
| 239 | v | ||
| 240 | } | ||
| 232 | fn db(slot: u32) -> usize { | 241 | fn db(slot: u32) -> usize { |
| 233 | unsafe { DBB }.wrapping_add(slot as usize * 4) | 242 | unsafe { DBB }.wrapping_add(slot as usize * 4) |
| 234 | } | 243 | } |
| @@ -727,7 +736,13 @@ pub fn init() -> bool { | |||
| 727 | dev, | 736 | dev, |
| 728 | func, | 737 | func, |
| 729 | 0x04, | 738 | 0x04, |
| 730 | (cmd & 0xFFFF0000) | ((cmd & 0xFFFF) | 0x6), | 739 | // Memory space + bus master + INTx disable (bit 10). The |
| 740 | // driver is polled (event ring drained from the cursor task), | ||
| 741 | // so the controller must never assert INTx: an unhandled | ||
| 742 | // xHCI interrupt would hit the kernel's catch-all handler | ||
| 743 | // and (being level-triggered) re-fire forever on real | ||
| 744 | // hardware. | ||
| 745 | (cmd & 0xFFFF_0000) | ((cmd & 0xFFFF) | 0x6 | 0x400), | ||
| 731 | ); | 746 | ); |
| 732 | let bar = crate::pci::bar64(b, dev, func, 0); | 747 | let bar = crate::pci::bar64(b, dev, func, 0); |
| 733 | let base = bar.unwrap() as usize; | 748 | let base = bar.unwrap() as usize; |
| @@ -745,6 +760,15 @@ pub fn init() -> bool { | |||
| 745 | MAXPORTS = (hcs1 >> 24) & 0xff; // MaxPorts is bits 31:24 (bits 23:16 are MaxIntrs) | 760 | MAXPORTS = (hcs1 >> 24) & 0xff; // MaxPorts is bits 31:24 (bits 23:16 are MaxIntrs) |
| 746 | let _ = hcc; | 761 | let _ = hcc; |
| 747 | CTRL_CS = if hcc & (1 << 2) != 0 { 64 } else { 32 }; | 762 | CTRL_CS = if hcc & (1 << 2) != 0 { 64 } else { 32 }; |
| 763 | |||
| 764 | // Claim USB Legacy ownership from the BIOS before resetting the | ||
| 765 | // controller (same SMM-legacy rationale as the EHCI driver). | ||
| 766 | match crate::pci::claim_usb_legacy(b, dev, func, base, 0x04, true) { | ||
| 767 | 0 => crate::serial_println!("usb: xHCI USB legacy: already released (no claim needed)"), | ||
| 768 | 1 => crate::serial_println!("usb: xHCI USB legacy handed over by BIOS"), | ||
| 769 | 2 => crate::serial_println!("usb: xHCI USB legacy claim timed out (BIOS sem stuck)"), | ||
| 770 | _ => crate::serial_println!("usb: xHCI USB legacy: no capability found"), | ||
| 771 | } | ||
| 748 | } | 772 | } |
| 749 | 773 | ||
| 750 | // Reset the controller (stop, then reset). Wait for reset completion | 774 | // Reset the controller (stop, then reset). Wait for reset completion |
| @@ -802,8 +826,10 @@ pub fn init() -> bool { | |||
| 802 | wr(rt(RT_ERSTSZ), 1); | 826 | wr(rt(RT_ERSTSZ), 1); |
| 803 | wr64(rt(RT_ERSTBA), ERST as u64); | 827 | wr64(rt(RT_ERSTBA), ERST as u64); |
| 804 | wr64(rt(RT_ERDP), EV.raw as u64); | 828 | wr64(rt(RT_ERDP), EV.raw as u64); |
| 805 | // Interrupter 0: clear pending, enable. | 829 | // Interrupter 0: keep the interrupt DISABLED (IE=0, clear any |
| 806 | wr(rt(RT_IMAN), rd(rt(RT_IMAN)) | 0x2); | 830 | // pending IP) β the driver is polled and the controller must not |
| 831 | // assert INTx (see the PCI command-register comment above). | ||
| 832 | wr(rt(RT_IMAN), (rd(rt(RT_IMAN)) & !0x2) | 0x1); | ||
| 807 | wr(rt(RT_IMOD), 0); | 833 | wr(rt(RT_IMOD), 0); |
| 808 | // Run. | 834 | // Run. |
| 809 | wr(op(OP_USBCMD), rd(op(OP_USBCMD)) | CMD_RUN); | 835 | wr(op(OP_USBCMD), rd(op(OP_USBCMD)) | CMD_RUN); |
| @@ -973,5 +999,120 @@ pub fn diagnose() -> alloc::string::String { | |||
| 973 | unsafe { QUEUED } | 999 | unsafe { QUEUED } |
| 974 | )); | 1000 | )); |
| 975 | } | 1001 | } |
| 1002 | |||
| 1003 | // Cross-controller attach scan: report which PCI USB controller(s) | ||
| 1004 | // currently have a device with bit0 (connect) set, across xHCI / EHCI / | ||
| 1005 | // UHCI / OHCI. Read-only diagnostic to locate the mouse on real hw. | ||
| 1006 | s.push_str("usbctl: scanning all USB controllers:\n"); | ||
| 1007 | unsafe { | ||
| 1008 | for d in crate::pci::probe() { | ||
| 1009 | if d.class != 0x0C || d.subclass != 0x03 { | ||
| 1010 | continue; | ||
| 1011 | } | ||
| 1012 | let lbl = match d.prog_if { | ||
| 1013 | 0x00 => "UHCI", | ||
| 1014 | 0x10 => "OHCI", | ||
| 1015 | 0x20 => "EHCI", | ||
| 1016 | 0x30 => "xHCI", | ||
| 1017 | _ => "USB?", | ||
| 1018 | }; | ||
| 1019 | let bar = crate::pci::bar64(d.bus, d.device, d.func, 0); | ||
| 1020 | let base = bar.unwrap_or(0); | ||
| 1021 | s.push_str(&alloc::format!( | ||
| 1022 | " {} {:02x}:{:02x}.{} bar={:#x}\n", | ||
| 1023 | lbl, d.bus, d.device, d.func, base | ||
| 1024 | )); | ||
| 1025 | if base == 0 || base == u64::MAX { | ||
| 1026 | // BAR not decoded / not enabled β nothing to probe. | ||
| 1027 | continue; | ||
| 1028 | } | ||
| 1029 | match d.prog_if { | ||
| 1030 | 0x30 => { | ||
| 1031 | if base == 0 { | ||
| 1032 | continue; | ||
| 1033 | } | ||
| 1034 | let caplen = rd(base as usize); | ||
| 1035 | let hcs1 = rd(base as usize + 0x04); | ||
| 1036 | let np = ((hcs1 >> 24) & 0xff) as usize; | ||
| 1037 | let opb = base as usize + (caplen & 0xff) as usize; | ||
| 1038 | s.push_str(&alloc::format!( | ||
| 1039 | " xhci ports={} sc=", np | ||
| 1040 | )); | ||
| 1041 | for p in 0..np.min(16) { | ||
| 1042 | let v = rd(opb + 0x400 + p * 0x10); | ||
| 1043 | s.push_str(&alloc::format!( | ||
| 1044 | "{}{:#x}", | ||
| 1045 | if v & 1 != 0 { "*" } else { "" }, | ||
| 1046 | v & 0x3ff | ||
| 1047 | )); | ||
| 1048 | if p + 1 < (np.min(16)) { | ||
| 1049 | s.push(' '); | ||
| 1050 | } | ||
| 1051 | } | ||
| 1052 | s.push('\n'); | ||
| 1053 | } | ||
| 1054 | 0x20 => { | ||
| 1055 | // EHCI: MMIO BAR. CAPLENGTH byte0, HcSParams (nports) @0x04. | ||
| 1056 | let cl = rd(base as usize) & 0xff; | ||
| 1057 | let sp = rd(base as usize + 0x04); | ||
| 1058 | let hc = rd(base as usize + 0x08); | ||
| 1059 | let np = (sp & 0xf) as usize; | ||
| 1060 | let opb = base as usize + cl as usize; | ||
| 1061 | let cmd = crate::pci::config_read(d.bus, d.device, d.func, 0x04); | ||
| 1062 | let eccp = crate::pci::config_read(d.bus, d.device, d.func, 0x34); | ||
| 1063 | s.push_str(&alloc::format!( | ||
| 1064 | " ehci nports={} hrs={} hcc={:#x} cmd={:#x} eccp={:#x}\n", | ||
| 1065 | np, sp, hc, cmd, (eccp >> 8) & 0xff | ||
| 1066 | )); | ||
| 1067 | let cfg = rd(opb + 0x40); | ||
| 1068 | s.push_str(&alloc::format!( | ||
| 1069 | " ehci cfgflag={:#x} ", cfg | ||
| 1070 | )); | ||
| 1071 | for p in 0..np.min(16) { | ||
| 1072 | let v = rd(opb + 0x44 + p * 0x04); | ||
| 1073 | s.push_str(&alloc::format!( | ||
| 1074 | "{}{:#x},", | ||
| 1075 | if v & 1 != 0 { "*" } else { "" }, | ||
| 1076 | v & 0xffff | ||
| 1077 | )); | ||
| 1078 | } | ||
| 1079 | s.push('\n'); | ||
| 1080 | // Legacy BIOS handoff status (USBLEGSUP at EECP). | ||
| 1081 | let eo = ((eccp >> 8) & 0xff) as usize; | ||
| 1082 | if eo != 0 && (eccp & 0xff) != 0 { | ||
| 1083 | let hs = crate::pci::config_read( | ||
| 1084 | d.bus, | ||
| 1085 | d.device, | ||
| 1086 | d.func, | ||
| 1087 | (eo & 0xfc) as u8, | ||
| 1088 | ); | ||
| 1089 | s.push_str(&alloc::format!( | ||
| 1090 | " ehci legacy=usblegsup={:#x} os-owner={} bios-owned={}\n", | ||
| 1091 | hs, | ||
| 1092 | (hs & (1 << 24)) != 0, | ||
| 1093 | (hs & (1 << 16)) != 0 || (hs & 0x100) != 0 | ||
| 1094 | )); | ||
| 1095 | } | ||
| 1096 | } | ||
| 1097 | 0x00 => { | ||
| 1098 | // UHCI: I/O BAR (bar64 refuses I/O bars β read raw). | ||
| 1099 | let lo = crate::pci::config_read(d.bus, d.device, d.func, 0x10); | ||
| 1100 | let io = (lo & 0xFFF0) as u16; | ||
| 1101 | if io == 0 { | ||
| 1102 | continue; | ||
| 1103 | } | ||
| 1104 | for p in 0..2 { | ||
| 1105 | let pr = io_iow16(io + 0x10 + (p as u16) * 2); | ||
| 1106 | s.push_str(&alloc::format!( | ||
| 1107 | "{}{:x}:", if pr & 1 != 0 { "*" } else { "" }, | ||
| 1108 | (pr & 0xff) | ||
| 1109 | )); | ||
| 1110 | } | ||
| 1111 | s.push('\n'); | ||
| 1112 | } | ||
| 1113 | _ => {} | ||
| 1114 | } | ||
| 1115 | } | ||
| 1116 | } | ||
| 976 | s | 1117 | s |
| 977 | } | 1118 | } |
diff --git a/rsnix/src/vt.rs b/rsnix/src/vt.rs index 4ffb44d..9444063 100644 --- a/rsnix/src/vt.rs +++ b/rsnix/src/vt.rs | |||
| @@ -121,9 +121,11 @@ pub fn init() { | |||
| 121 | COLS = cols; | 121 | COLS = cols; |
| 122 | CELLS = Box::into_raw(Box::new(vec![BLANK; rows * cols])); | 122 | CELLS = Box::into_raw(Box::new(vec![BLANK; rows * cols])); |
| 123 | RENDERED = Box::into_raw(Box::new(vec![BLANK; rows * cols])); | 123 | RENDERED = Box::into_raw(Box::new(vec![BLANK; rows * cols])); |
| 124 | CURSOR_R = 0; | 124 | // Home below row 0: row 0 is the boot-marker status line, so the |
| 125 | // terminal (shell + klog screen echo) must never write to it. | ||
| 126 | CURSOR_R = 1; | ||
| 125 | CURSOR_C = 0; | 127 | CURSOR_C = 0; |
| 126 | LAST_CURSOR = (0, 0); | 128 | LAST_CURSOR = (1, 0); |
| 127 | CURSOR_VISIBLE = true; | 129 | CURSOR_VISIBLE = true; |
| 128 | SGR_FG = 7; | 130 | SGR_FG = 7; |
| 129 | SGR_BG = 0; | 131 | SGR_BG = 0; |
| @@ -133,6 +135,13 @@ pub fn init() { | |||
| 133 | } | 135 | } |
| 134 | } | 136 | } |
| 135 | 137 | ||
| 138 | /// True once the terminal is initialised (grids live). The klog screen | ||
| 139 | /// echo checks this so serial lines printed before vt::init (very early | ||
| 140 | /// boot) are skipped instead of hitting a null grid. | ||
| 141 | pub fn ready() -> bool { | ||
| 142 | unsafe { ROWS != 0 && !CELLS.is_null() } | ||
| 143 | } | ||
| 144 | |||
| 136 | /// (rows, cols) β matches the editor's expected (rows, cols) ordering. | 145 | /// (rows, cols) β matches the editor's expected (rows, cols) ordering. |
| 137 | pub fn size() -> (usize, usize) { | 146 | pub fn size() -> (usize, usize) { |
| 138 | unsafe { (ROWS, COLS) } | 147 | unsafe { (ROWS, COLS) } |
| @@ -667,10 +676,41 @@ pub fn sync_scroll() { | |||
| 667 | // public writing API | 676 | // public writing API |
| 668 | // --------------------------------------------------------------------------- | 677 | // --------------------------------------------------------------------------- |
| 669 | 678 | ||
| 679 | /// Disable interrupts unless they are already off (nested-safe). Returns | ||
| 680 | /// whether they were on, so `irq_restore` re-enables exactly when needed. | ||
| 681 | /// The terminal write path uses this so a klog screen-echo (which can run | ||
| 682 | /// in ISR context) can never tear a shell write, and a task-context write | ||
| 683 | /// can never be split by a tick. | ||
| 684 | fn irq_off() -> bool { | ||
| 685 | let rflags: u64; | ||
| 686 | unsafe { | ||
| 687 | core::arch::asm!( | ||
| 688 | "pushfq; pop {}", | ||
| 689 | out(reg) rflags, | ||
| 690 | options(nostack, preserves_flags) | ||
| 691 | ); | ||
| 692 | if rflags & (1 << 9) != 0 { | ||
| 693 | core::arch::asm!("cli", options(nostack, preserves_flags)); | ||
| 694 | true | ||
| 695 | } else { | ||
| 696 | false | ||
| 697 | } | ||
| 698 | } | ||
| 699 | } | ||
| 700 | |||
| 701 | fn irq_restore(was_on: bool) { | ||
| 702 | if was_on { | ||
| 703 | unsafe { | ||
| 704 | core::arch::asm!("sti", options(nostack, preserves_flags)); | ||
| 705 | } | ||
| 706 | } | ||
| 707 | } | ||
| 708 | |||
| 670 | /// Write raw bytes (ANSI-capable). Used by full-screen apps. UTF-8 | 709 | /// Write raw bytes (ANSI-capable). Used by full-screen apps. UTF-8 |
| 671 | /// sequences are decoded so Unicode glyphs (box-drawing, arrows) render; | 710 | /// sequences are decoded so Unicode glyphs (box-drawing, arrows) render; |
| 672 | /// undecodable bytes become '?'. | 711 | /// undecodable bytes become '?'. |
| 673 | pub fn write_bytes(b: &[u8]) { | 712 | pub fn write_bytes(b: &[u8]) { |
| 713 | let on = irq_off(); | ||
| 674 | let mut i = 0; | 714 | let mut i = 0; |
| 675 | while i < b.len() { | 715 | while i < b.len() { |
| 676 | let byte = b[i]; | 716 | let byte = b[i]; |
| @@ -701,20 +741,27 @@ pub fn write_bytes(b: &[u8]) { | |||
| 701 | } | 741 | } |
| 702 | } | 742 | } |
| 703 | flush(); | 743 | flush(); |
| 744 | irq_restore(on); | ||
| 704 | } | 745 | } |
| 705 | 746 | ||
| 706 | /// Write a UTF-8 string (plain text; Unicode glyphs render, others '?'). | 747 | /// Write a UTF-8 string (plain text; Unicode glyphs render, others '?'). |
| 748 | /// Atomic with respect to interrupts: a klog screen-echo in ISR context | ||
| 749 | /// cannot interleave with a shell write, and a tick cannot split a write. | ||
| 707 | pub fn write_str(s: &str) { | 750 | pub fn write_str(s: &str) { |
| 751 | let on = irq_off(); | ||
| 708 | for c in s.chars() { | 752 | for c in s.chars() { |
| 709 | write_char(c); | 753 | write_char(c); |
| 710 | } | 754 | } |
| 711 | flush(); | 755 | flush(); |
| 756 | irq_restore(on); | ||
| 712 | } | 757 | } |
| 713 | 758 | ||
| 714 | /// Write a single ASCII byte / char. | 759 | /// Write a single ASCII byte / char. |
| 715 | pub fn put_char(b: u8) { | 760 | pub fn put_char(b: u8) { |
| 761 | let on = irq_off(); | ||
| 716 | put_byte(b); | 762 | put_byte(b); |
| 717 | flush(); | 763 | flush(); |
| 764 | irq_restore(on); | ||
| 718 | } | 765 | } |
| 719 | 766 | ||
| 720 | /// Clear the whole screen, reset colours, home the cursor. | 767 | /// Clear the whole screen, reset colours, home the cursor. |
diff --git a/shell/src/lib.rs b/shell/src/lib.rs index a54f338..bb048d8 100644 --- a/shell/src/lib.rs +++ b/shell/src/lib.rs | |||
| @@ -2239,9 +2239,10 @@ fn cmd_pci(p: &Parsed) { | |||
| 2239 | } | 2239 | } |
| 2240 | 2240 | ||
| 2241 | fn cmd_usb(p: &Parsed) { | 2241 | fn cmd_usb(p: &Parsed) { |
| 2242 | // `usb rescan` clears the failed-port mask so discovery retries. | 2242 | // `usb rescan` clears the failed-port masks so discovery retries. |
| 2243 | if p.args.first().is_some_and(|w| w == "rescan") { | 2243 | if p.args.first().is_some_and(|w| w == "rescan") { |
| 2244 | rsnix::usb::rescan(); | 2244 | rsnix::usb::rescan(); |
| 2245 | rsnix::ehci::rescan(); | ||
| 2245 | let mut pipe = Pipeline::new(&p.filters, &p.output); | 2246 | let mut pipe = Pipeline::new(&p.filters, &p.output); |
| 2246 | let _ = pipe.line("usb: rescanning ports"); | 2247 | let _ = pipe.line("usb: rescanning ports"); |
| 2247 | pipe.end(); | 2248 | pipe.end(); |
| @@ -2262,6 +2263,14 @@ fn cmd_usb(p: &Parsed) { | |||
| 2262 | } | 2263 | } |
| 2263 | } | 2264 | } |
| 2264 | pipe.end(); | 2265 | pipe.end(); |
| 2266 | // UHCI (USB1.1 companion) β full/low-speed keyboards and mice. | ||
| 2267 | let mut pipe = Pipeline::new(&p.filters, &p.output); | ||
| 2268 | for l in rsnix::uhci::diagnose().lines() { | ||
| 2269 | if !pipe.line(l) { | ||
| 2270 | break; | ||
| 2271 | } | ||
| 2272 | } | ||
| 2273 | pipe.end(); | ||
| 2265 | } | 2274 | } |
| 2266 | 2275 | ||
| 2267 | fn cmd_poweroff() -> ! { | 2276 | fn cmd_poweroff() -> ! { |
