From be868a43185826b02bf6d091019bb779678525bb Mon Sep 17 00:00:00 2001 From: salmonik Date: Mon, 17 Aug 2026 14:53:52 +0200 Subject: 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 --- DEBUG_NOTES.md | 207 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 206 insertions(+), 1 deletion(-) (limited to 'DEBUG_NOTES.md') 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.) --- +## πŸ”„ EHCI driver debug β€” in progress (`rsnix/src/ehci.rs`) + +### Background + +The USB mouse attaches to the **EHCI** (USB2) controllers on the real +OptiPlex (`00:1a.0`, `00:1d.0`), NOT the xHCI (`00:14.0` = USB3 only). +A full EHCI driver was written in `rsnix/src/ehci.rs` (~26 KB), wired +into `lib.rs`, `main.rs`, `cursor.rs`, `shell/src/lib.rs` (`usb` cmd). + +### What works + +- PCI discovery, MMIO probe, HC reset, async+periodic list init. +- Port reset + device detection: FS device found on port 0. +- `ctl_xfer` builds SETUP/DATA/STATUS qtd chain; async list is + circular (head β†’ xfer_qh β†’ head). +- QEMU EHCI bench: async schedule processes our control QH correctly. + +### The bug: QTD DMA reads return all-zeros + +The async schedule fetches our XFER_QH at `0x01b6e7c0` and reads its +`next_qtd` field correctly (`0x01b6e7f0`). Then `ADVANCEQUEUE` sets +`q->qtdaddr = 0x01b6e7f0`. But `FETCHQTD` reads from `0x01b6e7e0` +(**16 bytes BEFORE** the expected QTD address) and gets all-zeros. + +``` +FETCHQH @ 0x01b6e7c0: next_qtd=0x01b6e7f0 ← correct +ADVANCEQUEUE β†’ qtdaddr=0x01b6e7f0 ← correct +FETCHQTD @ 0x01b6e7e0: all zeros ← WRONG address + zeros +``` + +The QH's buffer pointer area (offset 28–44) is at `0x01b6e7dc–0x01b6e7ec`. +`0x01b6e7e0` falls inside the QH's `bufptr[1]` field β€” which we zeroed. +So the DMA is reading from inside our own QH structure, not the QTD that +follows it. + +### Root cause hypothesis: DMA address β‰  CPU address + +`dma_alloc` uses `alloc::alloc::alloc()` (the Rust global heap allocator). +This returns a **virtual address**. On UEFI, the page tables from +`adopt_tables()` copy the firmware's identity map β€” so virt == phys for +most RAM. BUT: + +1. The heap pages allocated by the page allocator may have been remapped + or the identity map may not cover all heap pages. +2. The EHCI controller does DMA to/from the address we write to + `ASYNCLISTADDR`. If the heap allocation's physical address differs + from the virtual address the CPU sees, DMA reads will fetch zeros + (unmapped or wrong page). + +**Evidence**: CPU can read back `*st.add(2) = 0x80e80` (the SETUP token) +via `read_volatile`, but QEMU's EHCI DMA engine reads 0 from the same +address. The CPU sees the data; the DMA engine does not. + +### What's NOT the problem + +- **USBCMD bits**: Fixed (PSE=bit4, ASE=bit5, RUN=bit0). +- **Async list linkage**: Fixed (headβ†’xferβ†’head circular, Q_TYPE_QH tags). +- **QH/QTD struct layout**: Matches QEMU's `EHCIqh`/`EHCIqtd` exactly. +- **Periodic schedule interference**: Disabled (no PSE). OVMF's stale + periodic entries caused crashes; now avoided. +- **Build cache**: Full `cargo clean` done; binary matches source. +- **Identity mapping**: `adopt_tables` copies UEFI page tables β€” should + be identity-mapped. But the evidence says otherwise for our DMA buffers. + +### Debug QEMU command + +```bash +qemu-system-x86_64 \ + -bios /usr/share/edk2/x64/OVMF.4m.fd \ + -drive file=fat:rw:esp,format=raw \ + -serial file:/tmp/rsdos_q.log \ + -monitor unix:/tmp/rsdos_mon.sock,server,nowait \ + -m 256M -machine q35 -cpu qemu64 -no-reboot \ + -device ich9-usb-ehci1 -device usb-mouse \ + -trace "usb_ehci*" >/tmp/rsdos_trace.txt 2>&1 +``` + +### Key QEMU EHCI facts + +- **Register layout**: `CAPLEN +0, HCSPARAMS +4, HCCPARAMS +8`; + operational base = `base + CAPLEN`. `USBCMD=+0, USBSTS=+4, + ASYNCLISTADDR=+0x18, CONFIGFLAG=+0x40, PORTSC#1=+0x44`. +- **USBCMD bits**: `RUNSTOP=bit0, HCRESET=bit1, FLS=bits2-3 (2-bit), + PSE=bit4, ASE=bit5, IAAD=bit6, ITC=bits16-23`. +- **QEMU async BH**: triggered by USBCMD writes (bit-change detection) + and frame timer. `ehci_work_bh` processes periodic frames first, then + advances async schedule. +- **QEMU strips FLS** if guest sets it (FCR register). +- **Port reset**: deassert PR; PED stays 0 for FS (no HS speed mask). +- **`ehci_find_queue_by_qh`** reuses queue objects; `q->seen > 1` stops + re-processing the same QH (reclamation detection). + +### Key QEMU EHCI structs (from `h110.h` / `hw/usb/hcd-ehci.h`) + +```c +// QH: 12 dwords (48 bytes) +typedef struct EHCIqh { + uint32_t next; // dword0: horizontal link pointer + uint32_t epchar; // dword1: dev(0-6), i(7), ep(8-10), eps(12-13), dtc(14), h(15), mps(16-26) + uint32_t epcap; // dword2 + uint32_t current_qtd; // dword3 + uint32_t next_qtd; // dword4 + uint32_t altnext_qtd; // dword5 + uint32_t token; // dword6: tbytes(16-30), cerr(10-11), pid(8-9), ioc(16), active(7), halt(6) + uint32_t bufptr[5]; // dword7-11 +} EHCIqh; + +// QTD: 8 dwords (32 bytes) +typedef struct EHCIqtd { + uint32_t next; // dword0 + uint32_t altnext; // dword1 + uint32_t token; // dword2: tbytes(16-30), cpage(12-15), cerr(10-11), pid(8-9), ioc(16), active(7) + uint32_t bufptr[5]; // dword3-7 +} EHCIqtd; + +#define NLPTR_GET(x) ((x) & ~0xf) +#define NLPTR_TBIT(x) ((x) & 1) +#define NLPTR_TYPE_GET(x) (((x) >> 1) & 3) +#define Q_TYPE_QH 1 // NLPTR_TYPE value for QH links +``` + +### What to try next + +1. **Allocate DMA buffers from a known-safe region**: Instead of + `alloc::alloc`, use a dedicated DMA pool at a known physical address + (e.g., the first page allocator pages, or a statically allocated + buffer). Verify the physical address matches the virtual address by + reading the PML4/PDPT/PD/PT entries for the buffer's page. + +2. **Add a page-table walk diagnostic**: Walk the 4-level page tables for + the DMA buffer address and print the physical address. If virt β‰  phys, + that's the bug. Add this to `ehci.rs` init or to `paging.rs`. + +3. **Use the page allocator directly** (which returns physical addresses + that are identity-mapped by construction) instead of the heap allocator. + `alloc_pages(n)` in `memory.rs` returns a `*mut u8` β€” verify this is + identity-mapped by checking a known page table entry. + +4. **Check if the heap allocator's pages lose identity mapping**: The heap + is built from the UEFI memory map. `adopt_tables` copies the firmware + page tables. But if the heap allocator later allocates pages that were + NOT in the firmware's map (e.g., from a free region), those pages + might not have identity-mapped PTEs. + +5. **Simpler test**: Allocate a small buffer (e.g., 64 bytes) with + `dma_alloc`, write a known pattern, and check if the EHCI controller + can DMA-read it. If even a small buffer fails, the problem is + fundamental (identity mapping broken for heap pages). + +### Key code locations + +- `rsnix/src/ehci.rs:751` β€” `dma_alloc()` (uses `alloc::alloc`) +- `rsnix/src/ehci.rs:420` β€” `init()` (sets up ASYNCLISTADDR, head QH, XFER_QH) +- `rsnix/src/ehci.rs:230` β€” `ctl_xfer()` (builds QH + QTD chain, enables schedule) +- `rsnix/src/paging.rs:48` β€” `adopt_tables()` (copies UEFI page tables) +- `rsnix/src/memory.rs` β€” `alloc_pages()` (page allocator) + +--- + ## βœ… v0.5.10 β€” USB HID mouse driver (polled xHCI, `rsnix/src/usb.rs`) Handoff: HEAD = `v0.5.10` (workspace Cargo.toml + boot banner). Adds a @@ -292,6 +451,7 @@ polled xHCI controller driver (mouse-only) announced via `usb: xHCI at so a USB mouse supersedes PS/2. New shell cmd: `usb` (diagnose), `usb rescan`. ### How it works + - Bring up controller (`OPC=0`, set `RCS`+`CRR`, `CNR`β†’0), discover a mouse asynchronously from the cursor task (`maybe_discover`), reset the 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`. heap pointers; no IRQ β€” keep one IN report armed and drain on each poll. ### QEMU bring-up observations / gotchas that bit us + - **MaxPorts = bits 31:24 of `HCSPARAMS1`**, not 16:23 (`MaxIntrs`). Using the wrong nibble silently yielded 0 ports ("ports=0", every PORTSC read 0). - **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`. - A long-lived QEMU session sometimes stopped forwarding `mouse_move` after heavy use; a fresh relaunch always revived it β€” not a driver bug. - Diagnose cmd `usb` prints `xhci mmio=… mapped=… ports=… mouse=… slot=…` - plus per-port `PORTS` dwords and a ` hid ep-index=… mps=… armed=true` line. + plus per-port `PORTS` dwords and a `hid ep-index=… mps=… armed=true` line. ### Boot log (good path) + ``` usb: xHCI at 00:03.0 usb: controller up (8 ports) β€” mouse discovery deferred to cursor task @@ -334,5 +496,48 @@ usb: HID boot mouse armed (slot 1) ``` ### Known limitations + - Mouse-only, no HID descriptor protocol selection (`SET_PROTOCOL` boot), no high-rate intervals, no keyboard/tablet, no hot-unplug handling, no MSI. + +### EHCI (usb2) bring-up β€” QEMU quirks that bit us (all now fixed) + +- **QTDs must be 32-byte aligned.** QEMU's `NLPTR_GET(x) = x & 0xffffffe0` + masks the LOW 5 bits, so a qtd at QH+0x30 (e.g. `0x01b6e7f0`) gets rounded + down to `0x01b6e7e0` and the transfer executes at the wrong address + (seen as a 16-byte jump in the FETCHQTD trace). Control qtds now live at + QH+0x40/0x60/0x80; the HID report qtd at HID_QH+0x40. +- **Unlink must self-loop WITH the QH type bit**: `link_of(head)` (addr|0x02). + A bare `head as u32` self-loop is parsed by QEMU as an iTD β†’ + "non queue head request in async schedule" β†’ HC reset. +- **DATA qtd altnext must be `1` (terminate)**, not 0. On a short IN + (e.g. config descriptor < 256 bytes) TBYTES stays != 0 so ADVANCEQUEUE + follows altnext; QEMU treats altnext=0 as a *valid* address β†’ jump to 0. +- **Periodic HID QH must set the EP number (epchar bits 8-11).** The QH was + built with devaddr|EPS|MPS but EP=0, so every periodic IN went to the + control endpoint: the first transfer "completed" oddly, then the qtd + HALTed every frame (`usb_ehci_qtd_bits` showed `halt 1`, packet on ep 0). + Fix: `| ((epnum & 0x0f) << 8)`. +- **Idle periodic qtd with correct EP just NAKs** (device has no report) β€” + that is correct; don't mistake "no reports" for a broken schedule. +- Headless QEMU 11 **cannot inject input into `usb-mouse`**: it has no + `display=` property (only usb-tablet/usb-kbd do), so it is never bound to + a display and `input-send-event`/`send-key` events (traced as `con -1`) + never reach it. First-report delivery was verified with `usb-kbd` + + `send-key` instead (b2 carried the real HID keycode, e.g. `0x04` for 'a'). +- **Controller halt+reset before init** is required to clear OVMF's stale + async schedule left in the controller. + +### EHCI boot log (good path, usb-mouse) + +``` +ehci: USB2 EHCI at 00:04.0 +ehci: mmio=0x81060000 caplen=0x20 ports=6 64-bit-caps=false +ehci: controller up (6 ports) +> ehci: port 0 resolved (full/low β€” TT path) +ehci: dev 2706:0100 class=0x0 mps0=64 +ehci: HID mouse IN ep1(0x81) mps=4 interval=7 +ehci: HID boot mouse armed (device 1) +``` + +Periodic schedule: 2200+ EXECUTEs/frame with 0 qtd halts; idle mouse NAKs. -- cgit v1.2.3