summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-17 14:53:52 +0200
committersalmonik <salmonik@tuta.io>2026-08-17 14:53:52 +0200
commitbe868a43185826b02bf6d091019bb779678525bb (patch)
tree586988c15f1d700b1621c7f0208584b30878a56c
parentv0.5.10: change em-dashes to dashes (diff)
downloadrs-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.md207
-rw-r--r--rsdos/src/main.rs62
-rw-r--r--rsnix/src/ahci.rs7
-rw-r--r--rsnix/src/cursor.rs6
-rw-r--r--rsnix/src/ehci.rs1274
-rw-r--r--rsnix/src/gop.rs18
-rw-r--r--rsnix/src/interrupts.rs36
-rw-r--r--rsnix/src/klog.rs10
-rw-r--r--rsnix/src/lib.rs2
-rw-r--r--rsnix/src/memory.rs44
-rw-r--r--rsnix/src/pci.rs125
-rw-r--r--rsnix/src/ps2.rs78
-rw-r--r--rsnix/src/sched.rs103
-rw-r--r--rsnix/src/tty.rs28
-rw-r--r--rsnix/src/uhci.rs1129
-rw-r--r--rsnix/src/usb.rs147
-rw-r--r--rsnix/src/vt.rs51
-rw-r--r--shell/src/lib.rs11
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
291The USB mouse attaches to the **EHCI** (USB2) controllers on the real
292OptiPlex (`00:1a.0`, `00:1d.0`), NOT the xHCI (`00:14.0` = USB3 only).
293A full EHCI driver was written in `rsnix/src/ehci.rs` (~26 KB), wired
294into `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
306The 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```
312FETCHQH @ 0x01b6e7c0: next_qtd=0x01b6e7f0 ← correct
313ADVANCEQUEUE β†’ qtdaddr=0x01b6e7f0 ← correct
314FETCHQTD @ 0x01b6e7e0: all zeros ← WRONG address + zeros
315```
316
317The 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.
319So the DMA is reading from inside our own QH structure, not the QTD that
320follows it.
321
322### Root cause hypothesis: DMA address β‰  CPU address
323
324`dma_alloc` uses `alloc::alloc::alloc()` (the Rust global heap allocator).
325This returns a **virtual address**. On UEFI, the page tables from
326`adopt_tables()` copy the firmware's identity map β€” so virt == phys for
327most RAM. BUT:
328
3291. The heap pages allocated by the page allocator may have been remapped
330 or the identity map may not cover all heap pages.
3312. 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)
337via `read_volatile`, but QEMU's EHCI DMA engine reads 0 from the same
338address. 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
354qemu-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)
383typedef 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)
395typedef 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
4101. **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
4162. **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
4203. **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
4254. **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
4315. **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
289Handoff: HEAD = `v0.5.10` (workspace Cargo.toml + boot banner). Adds a 448Handoff: 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
292so a USB mouse supersedes PS/2. New shell cmd: `usb` (diagnose), `usb rescan`. 451so 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```
328usb: xHCI at 00:03.0 490usb: xHCI at 00:03.0
329usb: controller up (8 ports) β€” mouse discovery deferred to cursor task 491usb: 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```
534ehci: USB2 EHCI at 00:04.0
535ehci: mmio=0x81060000 caplen=0x20 ports=6 64-bit-caps=false
536ehci: controller up (6 ports)
537> ehci: port 0 resolved (full/low β€” TT path)
538ehci: dev 2706:0100 class=0x0 mps0=64
539ehci: HID mouse IN ep1(0x81) mps=4 interval=7
540ehci: HID boot mouse armed (device 1)
541```
542
543Periodic 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
51fn task_main() { 51fn 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
20use crate::ps2::MouseEvent;
21use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
22
23// ---- Registers (host controller registers; opbase = base + CAPLENGTH) ----
24const OPCMD: usize = 0x00;
25const OPSTS: usize = 0x04;
26const OPINTR: usize = 0x08;
27const OPFRIDX: usize = 0x0C;
28const OPCFGFLAG: usize = 0x40;
29const OP_PRTSC_BASE: usize = 0x44; // PORTSC #1
30const OP_PRTSC_STRIDE: usize = 0x04;
31
32const CMD_RUN: u32 = 1 << 0;
33const CMD_HCRESET: u32 = 1 << 1;
34const CMD_PSE: u32 = 1 << 4;
35const CMD_ASE: u32 = 1 << 5;
36
37const STS_HCH: u32 = 1 << 12;
38const STS_SE: u32 = 1 << 4;
39const STS_RECL: u32 = 1 << 14;
40
41const PRT_CONNECT: u32 = 1 << 0;
42const PRT_PED: u32 = 1 << 1;
43const PRT_RESET: u32 = 1 << 8;
44const PRT_POWER: u32 = 1 << 12;
45const PRT_RWC: u32 = (1 << 17) | (1 << 18) | (1 << 16) | (1 << 20); // CSC|PEC|CRC|OCC
46
47// ---- QH / QTD fields ----
48const Q_TYPE_QH: u32 = 1 << 1;
49const QT_TERMINATE: u32 = 1; // terminate = 1 in a link pointer
50const QH_HEAD: u32 = 1 << 15;
51const QH_CONTROL_EP: u32 = 1 << 27;
52const QH_DTC: u32 = 1 << 14; // data toggle comes from the qTDs (control endpoints)
53const QH_RL: u32 = 4 << 28; // NAK reload count (Linux EHCI_TUNE_RL_{HS,TT})
54const QH_EPS_HIGH: u32 = 2 << 12;
55const QH_EPS_FULL: u32 = 0 << 12;
56const QH_EPS_LOW: u32 = 1 << 12;
57const QH_MULT: u32 = 1 << 30; // MULT=1: one split transaction per uframe
58
59const TOK_IOC: u32 = 1 << 15;
60const TOK_ACTIVE: u32 = 1 << 7;
61const TOK_HALT: u32 = 1 << 6;
62const TOK_XACT: u32 = 1 << 3;
63const TOK_MMF: u32 = 1 << 2; // missed microframe: split did not complete in its window
64const PID_IN: u32 = 1;
65const PID_SETUP: u32 = 2;
66
67const CCERR_3: u32 = 3 << 10;
68
69// ---- Static control state ----
70static EHCI_BASE: AtomicU32 = AtomicU32::new(0);
71static N_PORTS: AtomicU32 = AtomicU32::new(0);
72static CTRL_UP: AtomicBool = AtomicBool::new(false);
73static MOUSE_PRES: AtomicBool = AtomicBool::new(false);
74static DEV_ADDR: AtomicU32 = AtomicU32::new(0);
75static HID_EP: AtomicU32 = AtomicU32::new(0); // EP number (1..15), IN
76static HID_MPS: AtomicU32 = AtomicU32::new(0);
77static 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.
80static 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).
85static 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.
88static 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.
92static DEBUG_MATRIX: bool = true;
93static MATRIX_DONE: AtomicBool = AtomicBool::new(false);
94static XFER_TWEAK: AtomicU32 = AtomicU32::new(0);
95// Low-region (<1 MB) control QH for the matrix's low-region variant.
96static mut LOW_XFER: *mut u32 = core::ptr::null_mut();
97
98// DMA buffers. Allocated once at init; base pointers recorded here.
99static mut ASYNC_HEAD: *mut u32 = core::ptr::null_mut(); // QH (32 bytes min, align 32)
100static mut XFER_QH: *mut u32 = core::ptr::null_mut(); // control QH
101static mut FRAMELIST: *mut u32 = core::ptr::null_mut(); // 1024 entries, 4K aligned
102static 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.
109static mut BUF_PAGE: *mut u8 = core::ptr::null_mut();
110const BUF_DEV_DESC: usize = 0x000; // 64 B incl. red zone
111const BUF_CFG: usize = 0x100; // 320 B incl. red zone
112const BUF_HID_REPORT: usize = 0x400; // 64 B incl. red zone
113
114unsafe fn rd(a: usize) -> u32 {
115 unsafe { (a as *const u32).read_volatile() }
116}
117unsafe fn wr(a: usize, v: u32) {
118 unsafe { (a as *mut u32).write_volatile(v) }
119}
120
121fn 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}
127fn op(off: usize) -> usize {
128 opbase() + off
129}
130fn 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 ----
135fn qh_next(qh: *mut u32) -> *mut u32 {
136 qh.wrapping_add(0)
137}
138fn qh_info1(qh: *mut u32) -> *mut u32 {
139 qh.wrapping_add(1)
140}
141fn qh_info2(qh: *mut u32) -> *mut u32 {
142 qh.wrapping_add(2)
143}
144fn qh_current(qh: *mut u32) -> *mut u32 {
145 qh.wrapping_add(3)
146}
147fn qh_next_qtd(qh: *mut u32) -> *mut u32 {
148 qh.wrapping_add(4)
149}
150fn qh_alt_qtd(qh: *mut u32) -> *mut u32 {
151 qh.wrapping_add(5)
152}
153fn qh_token(qh: *mut u32) -> *mut u32 {
154 qh.wrapping_add(6)
155}
156fn qh_buf(qh: *mut u32) -> *mut u32 {
157 qh.wrapping_add(7)
158}
159
160fn link_of(ptr: usize) -> u32 {
161 (ptr as u32) | Q_TYPE_QH
162}
163
164unsafe 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).
182unsafe 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.
511fn 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).
529fn 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.
539fn 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`).
555pub fn rescan() {
556 FS_LS_PORTS.store(0, Ordering::Relaxed);
557}
558
559pub 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.
793unsafe 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.
826unsafe 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
844const HID_CLASS: u8 = 0x03;
845
846fn 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
872fn 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// ---------------------------------------------------------------------------
1073unsafe 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
1113unsafe 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
1165pub fn controller_up() -> bool {
1166 CTRL_UP.load(Ordering::Relaxed)
1167}
1168pub fn mouse_present() -> bool {
1169 MOUSE_PRES.load(Ordering::Relaxed)
1170}
1171
1172pub 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).
1235unsafe 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.
1248pub 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;
7use crate::uefi::{GopModeInfo, GraphicsOutputProtocol, EFI_SUCCESS, PIXEL_BGR, PIXEL_RGB}; 7use crate::uefi::{GopModeInfo, GraphicsOutputProtocol, EFI_SUCCESS, PIXEL_BGR, PIXEL_RGB};
8use core::fmt; 8use core::fmt;
9use core::ptr; 9use core::ptr;
10use core::sync::atomic::{AtomicU32, AtomicU64, AtomicU8, Ordering}; 10use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicU8, Ordering};
11 11
12/// Framebuffer state captured before ExitBootServices. 12/// Framebuffer state captured before ExitBootServices.
13pub struct Framebuffer { 13pub 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.
92pub static DISPLAY_STALL: AtomicU32 = AtomicU32::new(0); 92pub 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).
98pub static QR_LOCKED: AtomicBool = AtomicBool::new(false);
99
100pub fn qr_lock() {
101 QR_LOCKED.store(true, Ordering::Relaxed);
102}
103pub fn qr_locked() -> bool {
104 QR_LOCKED.load(Ordering::Relaxed)
105}
106
94#[inline] 107#[inline]
95fn stall_display() { 108fn 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.
312pub fn boot_marker(msg: &str) { 326pub 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
211static KEYBUF: ByteRing = ByteRing::new(); 211static KEYBUF: ByteRing = ByteRing::new();
212static MOUSEBUF: ByteRing = ByteRing::new(); 212static 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).
216static 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
415fn in_dump() -> bool { 419pub(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;
17pub mod qr; 17pub mod qr;
18pub mod power; 18pub mod power;
19pub mod ps2; 19pub mod ps2;
20pub mod uhci;
21pub mod ehci;
20pub mod usb; 22pub mod usb;
21pub mod rtc; 23pub mod rtc;
22pub mod sched; 24pub 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`.
1202pub 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).
1199pub fn alloc_pages_aligned(count: usize, align_pages: usize) -> Option<usize> { 1243pub 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).
66pub fn config_write(bus: u8, device: u8, func: u8, reg: u8, val: u32) { 65pub 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
70fn 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.
85pub 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.
73pub fn bar64(bus: u8, device: u8, func: u8, bar_index: u8) -> Option<u64> { 92pub 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.
141pub 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.
94pub fn probe() -> alloc::vec::Vec<DeviceInfo> { 217pub 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.
197fn 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.
209fn 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.
212fn wait_read() { 225/// Bounded like `wait_write`.
226fn 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.
226fn drain_input() { 243fn 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.
293pub(crate) fn feed_scancode(byte: u8) -> Option<KeyEvent> {
294 process_byte(byte)
295}
296
271fn process_byte(byte: u8) -> Option<KeyEvent> { 297fn 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.
505fn aux_command(cmd: u8) -> bool { 541fn 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);
158static PREEMPT_DEPTH: AtomicU64 = AtomicU64::new(0); 158static 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, ...).
174static STALL_TICKS: AtomicU64 = AtomicU64::new(0);
175const WATCHDOG_LOCK_TICKS: u64 = 500; // 5 s at 100 Hz
176
177/// Per-task "same interrupted frame" counter for spin detection.
178struct SpinWatch {
179 last_rsp: usize,
180 same: u32,
181}
182const SPIN_WATCH_INIT: SpinWatch = SpinWatch { last_rsp: 0, same: 0 };
183static 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.
186const 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.
190const 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.
195static SHELL_RUNNING: AtomicBool = AtomicBool::new(false);
196
197pub 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).
195pub fn on_tick(cur: usize) -> usize { 236pub 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.
303fn 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).
223pub fn on_yield(cur: usize) -> usize { 324pub 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 {
34static mut RAW_QUEUE: [u8; 8] = [0; 8]; 38static mut RAW_QUEUE: [u8; 8] = [0; 8];
35static mut RAW_LEN: usize = 0; 39static 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).
43fn 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).
51fn 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.
105pub fn interrupt_pressed() -> bool { 123pub 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
31use crate::ps2::MouseEvent;
32use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
33
34// ---- Registers (I/O space; 16-bit unless noted) ----
35const USBCMD: u16 = 0x00;
36const USBSTS: u16 = 0x02;
37const USBINTR: u16 = 0x04;
38const FRNUM: u16 = 0x06;
39const FRBASEADD: u16 = 0x08; // 32-bit
40const SOFMOD: u16 = 0x0C;
41const PORTSC_BASE: u16 = 0x10; // PORTSC1, PORTSC2 (16-bit each)
42
43const CMD_RUN: u16 = 1 << 0;
44const CMD_HCRESET: u16 = 1 << 1;
45const CMD_CF: u16 = 1 << 6;
46
47const STS_HCHALTED: u16 = 1 << 5;
48
49const PSC_CCS: u16 = 1 << 0; // current connect status
50const PSC_CSC: u16 = 1 << 1; // connect change (write 1 to clear)
51const PSC_PE: u16 = 1 << 2; // port enabled
52const PSC_PEDC: u16 = 1 << 3; // enable change (write 1 to clear)
53const PSC_LSDA: u16 = 1 << 8; // low-speed device attached
54const PSC_R: u16 = 1 << 9; // reset
55const PSC_SUSP: u16 = 1 << 12; // suspend
56
57// ---- TD / QH fields ----
58const TD_ACTIVE: u32 = 1 << 23;
59const TD_IOC: u32 = 1 << 24;
60const TD_LS: u32 = 1 << 26;
61const TD_ERR: u32 = 3 << 27; // C_ERR = 3 retries
62const TD_STALLED: u32 = 1 << 22;
63const TD_BABBLE: u32 = 1 << 20;
64const TD_TIMEO: u32 = 1 << 18;
65const TD_ACTLEN: u32 = 0x7FF; // actual length, n-1
66
67const TOK_DEV_SHIFT: u32 = 8;
68const TOK_EP_SHIFT: u32 = 15;
69const TOK_TOGGLE: u32 = 1 << 19;
70const TOK_EXPLEN_SHIFT: u32 = 21;
71
72const PID_SETUP: u32 = 0x2D;
73const PID_IN: u32 = 0x69;
74const PID_OUT: u32 = 0xE1;
75
76const PTR_TERM: u32 = 0x1;
77const PTR_QH: u32 = 0x2;
78
79// ---- HID boot requests ----
80const RT_CLASS_IFACE: u8 = 0x21;
81const REQ_SET_IDLE: u8 = 0x0A;
82const REQ_SET_PROTOCOL: u8 = 0x0B;
83
84// ---- Static state ----
85static IO_BASE: AtomicU32 = AtomicU32::new(0);
86static N_PORTS: AtomicU32 = AtomicU32::new(0);
87static CTRL_UP: AtomicBool = AtomicBool::new(false);
88static MOUSE_PRES: AtomicBool = AtomicBool::new(false);
89static KBD_ARMED: AtomicBool = AtomicBool::new(false);
90static 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.
93static KBD_PORT: AtomicU32 = AtomicU32::new(u32::MAX);
94static MOUSE_PORT: AtomicU32 = AtomicU32::new(u32::MAX);
95static DEV_KBD: AtomicU32 = AtomicU32::new(0);
96static DEV_MOUSE: AtomicU32 = AtomicU32::new(0);
97static KBD_EP: AtomicU32 = AtomicU32::new(1);
98static KBD_MPS: AtomicU32 = AtomicU32::new(8);
99static MOUSE_EP: AtomicU32 = AtomicU32::new(1);
100static MOUSE_MPS: AtomicU32 = AtomicU32::new(4);
101// Low-speed flag per device (TD_LS needed for 1.5 Mb/s).
102static KBD_LS: AtomicBool = AtomicBool::new(false);
103static MOUSE_LS: AtomicBool = AtomicBool::new(false);
104// Next USB device address to assign (1, 2, ...).
105static DEV_SEQ: AtomicU32 = AtomicU32::new(1);
106// Throttles for the lazy re-enumeration paths (ms).
107static LAST_KBD_ENUM: AtomicU32 = AtomicU32::new(0);
108static 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.
112static CTRL_LOCK: AtomicBool = AtomicBool::new(false);
113// Toggle bit the device expects next (alternates after each report).
114static KBD_TOGGLE: AtomicU32 = AtomicU32::new(0);
115static MOUSE_TOGGLE: AtomicU32 = AtomicU32::new(0);
116
117// ---- DMA: frame list + one struct page ----
118static mut FRAMELIST: *mut u32 = core::ptr::null_mut();
119static mut P: *mut u32 = core::ptr::null_mut(); // QHs, TDs, buffers
120
121// Offsets within the struct page (16-byte-aligned QHs/TDs).
122const OFF_CTRL_QH: usize = 0x000;
123const OFF_KBD_QH: usize = 0x010;
124const OFF_MOUSE_QH: usize = 0x020;
125const OFF_SETUP_TD: usize = 0x030;
126const OFF_DATA_TD: usize = 0x040;
127const OFF_STAT_TD: usize = 0x050;
128const OFF_KBD_TD: usize = 0x060;
129const OFF_MOUSE_TD: usize = 0x070;
130const OFF_REQ: usize = 0x100; // 8-byte setup packet
131const OFF_DESC: usize = 0x200; // device descriptor (64 B)
132const OFF_CFG: usize = 0x300; // config descriptor (256 B)
133const OFF_KBD_REPORT: usize = 0x500; // 8 B
134const OFF_MOUSE_REPORT: usize = 0x520; // 8 B
135
136// Keyboard: previous report + pending scancode FIFO (fed to ps2 decoder).
137static mut KBD_PREV: [u8; 8] = [0; 8];
138const KBDQ_SIZE: usize = 64;
139static mut KBDQ: [u8; KBDQ_SIZE] = [0; KBDQ_SIZE];
140static KBDQ_HEAD: AtomicU32 = AtomicU32::new(0);
141static KBDQ_LEN: AtomicU32 = AtomicU32::new(0);
142
143// ---- port I/O ----
144#[inline]
145unsafe 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]
153unsafe 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]
159unsafe 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
165fn io() -> u16 {
166 IO_BASE.load(Ordering::Relaxed) as u16
167}
168/// Absolute I/O port of root port `p`'s PORTSC register.
169fn prtsc(p: usize) -> u16 {
170 io() + PORTSC_BASE + (p as u16) * 2
171}
172
173// ---- DMA helpers (mirror ehci.rs) ----
174unsafe 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
186unsafe fn rd(addr: *const u32) -> u32 {
187 unsafe { addr.read_volatile() }
188}
189unsafe fn wr(addr: *mut u32, v: u32) {
190 unsafe { addr.write_volatile(v) }
191}
192
193fn td_addr(o: usize) -> *mut u32 {
194 unsafe { P.add(o / 4) }
195}
196fn buf_addr(o: usize) -> *mut u8 {
197 unsafe { P.add(o / 4) as *mut u8 }
198}
199
200#[inline]
201fn 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).
207fn 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.
215fn 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
232fn 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}
241fn 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.
250unsafe 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.
341unsafe 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
370struct 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.
379fn 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.
435unsafe 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.
524unsafe 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.
591unsafe 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).
626static 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).
734static 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
745fn 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}
756fn 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).
772fn 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.
788unsafe 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).
827unsafe 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
857pub fn controller_up() -> bool {
858 CTRL_UP.load(Ordering::Relaxed)
859}
860pub 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).
866fn 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.
877pub 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.
998pub 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.
1050pub 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.
1097pub 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) {
229fn op(off: usize) -> usize { 229fn 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).
234unsafe 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}
232fn db(slot: u32) -> usize { 241fn 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.
141pub 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.
137pub fn size() -> (usize, usize) { 146pub 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.
684fn 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
701fn 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 '?'.
673pub fn write_bytes(b: &[u8]) { 712pub 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.
707pub fn write_str(s: &str) { 750pub 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.
715pub fn put_char(b: u8) { 760pub 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
2241fn cmd_usb(p: &Parsed) { 2241fn 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
2267fn cmd_poweroff() -> ! { 2276fn cmd_poweroff() -> ! {