diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-04 03:06:06 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-04 03:06:06 +0200 |
| commit | 5e1bdc12bd298a9751da4285b7e93c68b1314332 (patch) | |
| tree | cc4ae9f256dda100477784a47cbc53373fce79b1 | |
| parent | v0.5.9: fix OptiPlex heap crash — EBS retry must not rebuild the free list (diff) | |
| download | rs-dos-5e1bdc12bd298a9751da4285b7e93c68b1314332.tar.gz rs-dos-5e1bdc12bd298a9751da4285b7e93c68b1314332.zip | |
v0.5.10: USB HID mouse via polled xHCI (rsnix/src/usb.rs)
Polled xHCI driver (mouse-only): controller bring-up, port reset,
enable-slot -> address-device -> config-descriptor walk -> HID boot
mouse IN endpoint, one outstanding IN TRB drained on poll. Cursor
task polls usb first, PS/2 second. New 'usb'/'usb rescan' shell cmds.
Verified in QEMU (q35 + OVMF + qemu-xhci + usb-mouse): movement and
all three buttons tracked exactly; heap stays clean.
Fixes en route: MaxPorts=HCSPARAMS1[31:24]; operational regs CRCR=0x18
DCBAAP=0x30 CONFIG=0x38; pass input-context ptr in Address-Device;
Input Control Context DW0=Drop DW1=Add; HID walker bInterfaceClass at
+5; async discovery from cursor task.
| -rw-r--r-- | Cargo.lock | 6 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | DEBUG_NOTES.md | 55 | ||||
| -rw-r--r-- | rsdos/src/main.rs | 6 | ||||
| -rw-r--r-- | rsnix/src/cursor.rs | 2 | ||||
| -rw-r--r-- | rsnix/src/lib.rs | 1 | ||||
| -rw-r--r-- | rsnix/src/usb.rs | 977 | ||||
| -rw-r--r-- | shell/src/lib.rs | 19 |
8 files changed, 1061 insertions, 7 deletions
| @@ -4,7 +4,7 @@ version = 4 | |||
| 4 | 4 | ||
| 5 | [[package]] | 5 | [[package]] |
| 6 | name = "rsdos" | 6 | name = "rsdos" |
| 7 | version = "0.5.9" | 7 | version = "0.5.10" |
| 8 | dependencies = [ | 8 | dependencies = [ |
| 9 | "rsnix", | 9 | "rsnix", |
| 10 | "shell", | 10 | "shell", |
| @@ -19,11 +19,11 @@ dependencies = [ | |||
| 19 | 19 | ||
| 20 | [[package]] | 20 | [[package]] |
| 21 | name = "rsnix" | 21 | name = "rsnix" |
| 22 | version = "0.5.9" | 22 | version = "0.5.10" |
| 23 | 23 | ||
| 24 | [[package]] | 24 | [[package]] |
| 25 | name = "shell" | 25 | name = "shell" |
| 26 | version = "0.5.9" | 26 | version = "0.5.10" |
| 27 | dependencies = [ | 27 | dependencies = [ |
| 28 | "rsdosedit", | 28 | "rsdosedit", |
| 29 | "rsnix", | 29 | "rsnix", |
| @@ -3,7 +3,7 @@ resolver = "2" | |||
| 3 | members = ["rsnix", "shell", "rsdos", "rsdosedit"] | 3 | members = ["rsnix", "shell", "rsdos", "rsdosedit"] |
| 4 | 4 | ||
| 5 | [workspace.package] | 5 | [workspace.package] |
| 6 | version = "0.5.9" | 6 | version = "0.5.10" |
| 7 | edition = "2024" | 7 | edition = "2024" |
| 8 | 8 | ||
| 9 | # no_std UEFI kernel: no unwinder available, so panic must abort. | 9 | # no_std UEFI kernel: no unwinder available, so panic must abort. |
diff --git a/DEBUG_NOTES.md b/DEBUG_NOTES.md index a1f2d75..19fe5cf 100644 --- a/DEBUG_NOTES.md +++ b/DEBUG_NOTES.md | |||
| @@ -281,3 +281,58 @@ with the `klog` shell command. | |||
| 281 | (QEMU control arm is green with and without a forced EBS failure: | 281 | (QEMU control arm is green with and without a forced EBS failure: |
| 282 | `verify: ok`, probe NOT free, shadow base NOT in free list, full shell | 282 | `verify: ok`, probe NOT free, shadow base NOT in free list, full shell |
| 283 | sequence clean. `0x00AAAAAA` = gray text colour; shadow far rows stay 0.) | 283 | sequence clean. `0x00AAAAAA` = gray text colour; shadow far rows stay 0.) |
| 284 | |||
| 285 | --- | ||
| 286 | |||
| 287 | ## ✅ v0.5.10 — USB HID mouse driver (polled xHCI, `rsnix/src/usb.rs`) | ||
| 288 | |||
| 289 | Handoff: HEAD = `v0.5.10` (workspace Cargo.toml + boot banner). Adds a | ||
| 290 | polled xHCI controller driver (mouse-only) announced via `usb: xHCI at | ||
| 291 | <bus.dev.fn>` during boot; `cursor.rs` polls `usb::poll_mouse().or_else(ps2)` | ||
| 292 | so a USB mouse supersedes PS/2. New shell cmd: `usb` (diagnose), `usb rescan`. | ||
| 293 | |||
| 294 | ### How it works | ||
| 295 | - Bring up controller (`OPC=0`, set `RCS`+`CRR`, `CNR`→0), discover a | ||
| 296 | mouse asynchronously from the cursor task (`maybe_discover`), reset the | ||
| 297 | port, enable slot, address device, read device + config descriptors, | ||
| 298 | find the HID boot-mouse IN endpoint, SET_CONFIGURATION, configure EP, | ||
| 299 | arm an IN TRB and drain transfer-completions into `ps2::MouseEvent`. | ||
| 300 | - Kernel is identity-mapped + PIC-only (no MSI): DMA is plain kernel | ||
| 301 | heap pointers; no IRQ — keep one IN report armed and drain on each poll. | ||
| 302 | |||
| 303 | ### QEMU bring-up observations / gotchas that bit us | ||
| 304 | - **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). | ||
| 306 | - **Operational registers** (xHCI >=1.1, matches QEMU/Linux): `CRCR=0x18`, | ||
| 307 | `DCBAAP=0x30`, `CONFIG=0x38`. Wrong offsets made Enable-Slot never | ||
| 308 | complete (only Port-Status-Change events → "Enable-Slot never completed"). | ||
| 309 | - **`issue_cmd` must pass the input-context pointer** in TRB parameter | ||
| 310 | (never drop it); Address-Device failed with code 5 (`CC_TRB_ERROR`) until | ||
| 311 | `code5issue_cmd_ptr(d3, ptr)` passed `INCTX`, and | ||
| 312 | - **Input Context is DW0=Drop, DW1=Add** (reverse of what looks natural). | ||
| 313 | QEMU validates `ictl[0]==0 && ictl[1]==0x3`, else `CC_TRB_ERROR`. | ||
| 314 | - **HID walker: `bInterfaceClass` is `+5` in the interface descriptor**, | ||
| 315 | not `+2` (`cfg[i+5]`). A reverse-engineering of the mouse's config dump | ||
| 316 | (`09 04 00 00 01 03 01 02 00 … 07 05 81 03 04 00 07`) made it obvious. | ||
| 317 | - QEMU monitor **`mouse_button` help ("1=L 2=M 4=R") is wrong for the | ||
| 318 | usb-mouse path**: the value is a direct HID bitmask — `1`=bit0 L, | ||
| 319 | `2`=bit1 R, `4`=bit2 M (matches a real boot-mouse report byte). Bump monitors | ||
| 320 | semantics: transport `mouse_move dx dy`; button via `mouse_button 1|2|4`. | ||
| 321 | - A long-lived QEMU session sometimes stopped forwarding `mouse_move` | ||
| 322 | after heavy use; a fresh relaunch always revived it — not a driver bug. | ||
| 323 | - 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. | ||
| 325 | |||
| 326 | ### Boot log (good path) | ||
| 327 | ``` | ||
| 328 | usb: xHCI at 00:03.0 | ||
| 329 | usb: controller up (8 ports) — mouse discovery deferred to cursor task | ||
| 330 | usb: port 4 connect (speed 3) | ||
| 331 | usb: dev 0627:0001 class=0x0 mps0=64 | ||
| 332 | usb: HID mouse IN ep mps=4 interval=7 euid=3 | ||
| 333 | usb: HID boot mouse armed (slot 1) | ||
| 334 | ``` | ||
| 335 | |||
| 336 | ### Known limitations | ||
| 337 | - Mouse-only, no HID descriptor protocol selection (`SET_PROTOCOL` boot), | ||
| 338 | no high-rate intervals, no keyboard/tablet, no hot-unplug handling, no MSI. | ||
diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs index e40098d..27dab06 100644 --- a/rsdos/src/main.rs +++ b/rsdos/src/main.rs | |||
| @@ -185,7 +185,7 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut | |||
| 185 | } else { | 185 | } else { |
| 186 | rsnix::serial_println!("gop: not found (0x{:x})", gop_result); | 186 | rsnix::serial_println!("gop: not found (0x{:x})", gop_result); |
| 187 | } | 187 | } |
| 188 | rsnix::gop::boot_marker("rsnix v0.5.9 booting.."); | 188 | rsnix::gop::boot_marker("rsnix v0.5.10 booting.."); |
| 189 | rsnix::vt::init(); | 189 | rsnix::vt::init(); |
| 190 | let (rows, cols) = rsnix::vt::size(); | 190 | let (rows, cols) = rsnix::vt::size(); |
| 191 | rsnix::serial_println!("vt: terminal ready ({}x{})", rows, cols); | 191 | rsnix::serial_println!("vt: terminal ready ({}x{})", rows, cols); |
| @@ -347,7 +347,9 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut | |||
| 347 | 347 | ||
| 348 | rsnix::serial_println!("boot: entering shell"); | 348 | rsnix::serial_println!("boot: entering shell"); |
| 349 | rsnix::gop::boot_marker("rsnix: boot done — shell"); | 349 | rsnix::gop::boot_marker("rsnix: boot done — shell"); |
| 350 | if rsnix::ps2::mouse_present() { | 350 | // USB (xHCI) mouse is polled; fall back to PS/2 if no USB mouse. |
| 351 | rsnix::usb::init(); | ||
| 352 | if rsnix::ps2::mouse_present() || rsnix::usb::controller_up() { | ||
| 351 | rsnix::cursor::spawn(); | 353 | rsnix::cursor::spawn(); |
| 352 | } else { | 354 | } else { |
| 353 | rsnix::serial_println!("cursor: no mouse — overlay disabled"); | 355 | rsnix::serial_println!("cursor: no mouse — overlay disabled"); |
diff --git a/rsnix/src/cursor.rs b/rsnix/src/cursor.rs index 18c9a1d..d9d97bc 100644 --- a/rsnix/src/cursor.rs +++ b/rsnix/src/cursor.rs | |||
| @@ -50,7 +50,7 @@ fn forward_target() -> Option<usize> { | |||
| 50 | 50 | ||
| 51 | fn task_main() { | 51 | fn task_main() { |
| 52 | loop { | 52 | loop { |
| 53 | while let Some(ev) = crate::ps2::try_poll_mouse() { | 53 | while let Some(ev) = crate::usb::poll_mouse().or_else(crate::ps2::try_poll_mouse) { |
| 54 | crate::gop::cursor_move(ev.dx, ev.dy); | 54 | crate::gop::cursor_move(ev.dx, ev.dy); |
| 55 | crate::gop::cursor_set_buttons(btn_bits(&ev)); | 55 | crate::gop::cursor_set_buttons(btn_bits(&ev)); |
| 56 | if let Some(id) = forward_target() | 56 | if let Some(id) = forward_target() |
diff --git a/rsnix/src/lib.rs b/rsnix/src/lib.rs index dbb327c..bc2553d 100644 --- a/rsnix/src/lib.rs +++ b/rsnix/src/lib.rs | |||
| @@ -17,6 +17,7 @@ pub mod pci; | |||
| 17 | pub mod qr; | 17 | pub mod qr; |
| 18 | pub mod power; | 18 | pub mod power; |
| 19 | pub mod ps2; | 19 | pub mod ps2; |
| 20 | pub mod usb; | ||
| 20 | pub mod rtc; | 21 | pub mod rtc; |
| 21 | pub mod sched; | 22 | pub mod sched; |
| 22 | pub mod serial; | 23 | pub mod serial; |
diff --git a/rsnix/src/usb.rs b/rsnix/src/usb.rs new file mode 100644 index 0000000..71df781 --- /dev/null +++ b/rsnix/src/usb.rs | |||
| @@ -0,0 +1,977 @@ | |||
| 1 | // --------------------------------------------------------------------------- | ||
| 2 | // USB host controller driver — xHCI (USB 2.0/3.x) + HID mouse support. | ||
| 3 | // | ||
| 4 | // **Polled** driver: it runs the xHCI event ring from the cursor task loop | ||
| 5 | // instead of wiring MSI/INTx into the PIC, matching how the kernel already | ||
| 6 | // polls the PS/2 mouse (see `cursor`). | ||
| 7 | // | ||
| 8 | // Minimal, mouse-only scope: | ||
| 9 | // • take the controller out of reset, set up the command + event rings | ||
| 10 | // • reset ports, run the slot/address/configure firmware sequence | ||
| 11 | // • find the HID mouse boot interface, open its interrupt-IN endpoint, | ||
| 12 | // and translate the standard report into `ps2::MouseEvent` (reusing | ||
| 13 | // the cursor task's consumption path). | ||
| 14 | // | ||
| 15 | // DMA / MMIO: the kernel is identity-mapped (physical == virtual), so bus | ||
| 16 | // addresses are plain kernel pointers and every heap allocation is DMA-able. | ||
| 17 | // Rings/contexts are allocated once at boot and handed to the controller | ||
| 18 | // permanently. | ||
| 19 | // | ||
| 20 | // Register/TRB/context layouts follow the Linux xHCI driver definitions | ||
| 21 | // (drivers/usb/host/xhci*.h) and the xHCI 1.2 spec. All exact field | ||
| 22 | // positions were cross-checked against the Linux sources: | ||
| 23 | // * TRB type field is bits 10-15 (TRB_TYPE(p) = p << 10) | ||
| 24 | // * transfer/command completion code is bits 24-31 of event dword2 | ||
| 25 | // * slot id = bits 24-31, ep id = bits 16-20 of event dword3 | ||
| 26 | // * endpoint context: type at bits 3-5, interval at 16-23, | ||
| 27 | // max packet size at bits 16-31 of dword1, TR dequeue is a 64-bit | ||
| 28 | // field at bytes 8-15 with DCS in bit 0 | ||
| 29 | // * slot context: route/LAST_CTX in dword0, root-hub-port at 16-23 of | ||
| 30 | // dword1, speed at bits 20-23 | ||
| 31 | // --------------------------------------------------------------------------- | ||
| 32 | #![allow(non_snake_case)] // macro-style helpers mirror the Linux xHCI headers | ||
| 33 | use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; | ||
| 34 | |||
| 35 | use crate::ps2::MouseEvent; | ||
| 36 | |||
| 37 | // ---- capability registers (offset from mmio base) ---- | ||
| 38 | const CAP_CAPLENGTH: usize = 0x00; | ||
| 39 | const HCSPARAMS1: usize = 0x04; // MaxSlots[7:0] MaxIntrs[15:8] MaxPorts[23:16] | ||
| 40 | const HCCPARAMS1: usize = 0x10; // bit0=64-bit addressing; bit2=64-byte contexts | ||
| 41 | const DBOFF: usize = 0x14; | ||
| 42 | const RTSOFF: usize = 0x18; | ||
| 43 | |||
| 44 | // ---- operational registers (relative to op = mmio + CAPLENGTH) ---- | ||
| 45 | const OP_USBCMD: usize = 0x00; | ||
| 46 | const OP_USBSTS: usize = 0x04; | ||
| 47 | const OP_CRCR: usize = 0x18; // 64-bit | ||
| 48 | const OP_DCBAAP: usize = 0x30; // 64-bit | ||
| 49 | const OP_CONFIG: usize = 0x38; | ||
| 50 | const OP_PORTSC: usize = 0x400; // PORTSC port 1 (port set stride 0x10) | ||
| 51 | |||
| 52 | // ---- runtime registers (relative to mmio + RTSOFF), interrupter 0 ---- | ||
| 53 | const RT_IMAN: usize = 0x20; | ||
| 54 | const RT_IMOD: usize = 0x24; | ||
| 55 | const RT_ERSTSZ: usize = 0x28; | ||
| 56 | const RT_ERSTBA: usize = 0x30; // 64-bit | ||
| 57 | const RT_ERDP: usize = 0x38; // 64-bit | ||
| 58 | |||
| 59 | const CMD_RUN: u32 = 1 << 0; | ||
| 60 | const CMD_RESET: u32 = 1 << 1; | ||
| 61 | const STS_HALT: u32 = 1 << 0; | ||
| 62 | const STS_CNR: u32 = 1 << 11; | ||
| 63 | |||
| 64 | const CMD_RING_CYCLE: u32 = 1 << 0; | ||
| 65 | |||
| 66 | // ---- TRB control-word bit layout ---- | ||
| 67 | const TRB_CYCLE: u32 = 1 << 0; | ||
| 68 | const TRB_IOC: u32 = 1 << 5; | ||
| 69 | const TRB_IDT: u32 = 1 << 6; | ||
| 70 | const TRB_TYPE_BITMASK: u32 = 0xfc00; // bits 10..15 | ||
| 71 | fn TYPE(t: u32) -> u32 { | ||
| 72 | t << 10 | ||
| 73 | } | ||
| 74 | fn SLOT_ID(p: u32) -> u32 { | ||
| 75 | (p & 0xff) << 24 | ||
| 76 | } | ||
| 77 | fn TRB_LEN(p: u32) -> u32 { | ||
| 78 | p & 0x1fffff | ||
| 79 | } | ||
| 80 | const TRB_DIR_IN: u32 = 1 << 16; | ||
| 81 | fn TRB_TX_TYPE(p: u32) -> u32 { | ||
| 82 | p << 16 | ||
| 83 | } | ||
| 84 | const TRB_DATA_OUT: u32 = 2; | ||
| 85 | const TRB_DATA_IN: u32 = 3; | ||
| 86 | |||
| 87 | const TRB_NORMAL: u32 = 1; | ||
| 88 | const TRB_SETUP: u32 = 2; | ||
| 89 | const TRB_DATA: u32 = 3; | ||
| 90 | const TRB_STATUS: u32 = 4; | ||
| 91 | const TRB_ENABLE_SLOT: u32 = 9; | ||
| 92 | const TRB_ADDR_DEV: u32 = 11; | ||
| 93 | const TRB_CONFIG_EP: u32 = 12; | ||
| 94 | const TRB_EV_TRANSFER: u32 = 0x20; | ||
| 95 | const TRB_EV_CMD_COMPLETE: u32 = 0x21; | ||
| 96 | |||
| 97 | const COMP_SUCCESS: u32 = 1; | ||
| 98 | const COMP_SHORT: u32 = 13; | ||
| 99 | |||
| 100 | fn GET_COMP_CODE(d2: u32) -> u32 { | ||
| 101 | (d2 >> 24) & 0xff | ||
| 102 | } | ||
| 103 | fn TRB_TO_SLOT_ID(d3: u32) -> u32 { | ||
| 104 | (d3 >> 24) & 0xff | ||
| 105 | } | ||
| 106 | fn TRB_TO_EP_ID(d3: u32) -> u32 { | ||
| 107 | (d3 >> 16) & 0x1f | ||
| 108 | } | ||
| 109 | fn TRB_FIELD_TO_TYPE(d3: u32) -> u32 { | ||
| 110 | (d3 & TRB_TYPE_BITMASK) >> 10 | ||
| 111 | } | ||
| 112 | |||
| 113 | // ---- context fields ---- | ||
| 114 | fn LAST_CTX(p: u32) -> u32 { | ||
| 115 | p << 27 | ||
| 116 | } | ||
| 117 | fn ROOTPORT(p: u32) -> u32 { | ||
| 118 | (p & 0xff) << 16 | ||
| 119 | } | ||
| 120 | fn SLOT_SPEED_X(p: u32) -> u32 { | ||
| 121 | (p & 0xf) << 20 | ||
| 122 | } | ||
| 123 | fn EP_TYPE(p: u32) -> u32 { | ||
| 124 | p << 3 | ||
| 125 | } | ||
| 126 | fn MAX_PACKET(p: u32) -> u32 { | ||
| 127 | (p & 0xffff) << 16 | ||
| 128 | } | ||
| 129 | fn ERROR_COUNT(p: u32) -> u32 { | ||
| 130 | (p & 0x3) << 1 | ||
| 131 | } | ||
| 132 | fn EP_INTERVAL(p: u32) -> u32 { | ||
| 133 | (p & 0xff) << 16 | ||
| 134 | } | ||
| 135 | fn EP_AVG(p: u32) -> u32 { | ||
| 136 | p & 0xffff | ||
| 137 | } | ||
| 138 | const CTRL_EP: u32 = 4; | ||
| 139 | const INT_IN_EP: u32 = 7; | ||
| 140 | |||
| 141 | // ---- PORTSC ---- | ||
| 142 | const PORT_CONNECT: u32 = 1 << 0; | ||
| 143 | const PORT_PE: u32 = 1 << 1; | ||
| 144 | const PORT_RESET: u32 = 1 << 4; | ||
| 145 | const PORT_POWER: u32 = 1 << 9; | ||
| 146 | fn DEV_SPEED(p: u32) -> u32 { | ||
| 147 | (p >> 10) & 0xf | ||
| 148 | } | ||
| 149 | |||
| 150 | // ---- USB meta protocol ---- | ||
| 151 | const USB_DIR_IN: u8 = 0x80; | ||
| 152 | const USB_REQ_GET_DESCRIPTOR: u8 = 0x06; | ||
| 153 | const USB_REQ_SET_CONFIGURATION: u8 = 0x09; | ||
| 154 | const DT_DEVICE: u8 = 0x01; | ||
| 155 | const DT_CONFIG: u8 = 0x02; | ||
| 156 | const DT_INTERFACE: u8 = 0x04; | ||
| 157 | const DT_ENDPOINT: u8 = 0x05; | ||
| 158 | const HID_CLASS: u8 = 0x03; | ||
| 159 | |||
| 160 | // --------------------------------------------------------------------------- | ||
| 161 | // State (cooperative single-task kernel → unsafe statics) | ||
| 162 | // --------------------------------------------------------------------------- | ||
| 163 | static mut M: usize = 0; // mmio base | ||
| 164 | static mut OPB: usize = 0; // operational base | ||
| 165 | static mut DBB: usize = 0; // doorbell base | ||
| 166 | static mut RTB: usize = 0; // runtime base | ||
| 167 | static mut CTRL_CS: usize = 32; // context size | ||
| 168 | static mut MAXPORTS: u32 = 0; | ||
| 169 | |||
| 170 | struct Ring { | ||
| 171 | raw: *mut u32, | ||
| 172 | count: usize, // TRBs | ||
| 173 | idx: usize, // producer (command/transfer) or consumer (event) index | ||
| 174 | cyc: u32, // software cycle | ||
| 175 | } | ||
| 176 | const fn ri_empty(count: usize) -> Ring { | ||
| 177 | Ring { | ||
| 178 | raw: core::ptr::null_mut(), | ||
| 179 | count, | ||
| 180 | idx: 0, | ||
| 181 | cyc: 1, | ||
| 182 | } | ||
| 183 | } | ||
| 184 | static mut CMD: Ring = ri_empty(64); | ||
| 185 | static mut EP0: Ring = ri_empty(8); | ||
| 186 | static mut EPIN: Ring = ri_empty(8); | ||
| 187 | static mut EV: Ring = ri_empty(32); | ||
| 188 | |||
| 189 | static mut DCBAA: *mut u64 = core::ptr::null_mut(); | ||
| 190 | static mut DEVCTX: *mut u8 = core::ptr::null_mut(); | ||
| 191 | static mut INCTX: *mut u8 = core::ptr::null_mut(); | ||
| 192 | static mut ERST: *mut u8 = core::ptr::null_mut(); | ||
| 193 | |||
| 194 | static SLOT: AtomicU32 = AtomicU32::new(0); | ||
| 195 | static HID_EPID: AtomicU32 = AtomicU32::new(0); // device-context index of the IN endpoint | ||
| 196 | static mut HID_BUF: [u8; 8] = [0; 8]; | ||
| 197 | static mut HID_LEN: usize = 4; | ||
| 198 | static mut MPS0: u32 = 8; | ||
| 199 | |||
| 200 | static mut GOT_REPORT: bool = false; | ||
| 201 | static mut QUEUED: bool = false; | ||
| 202 | static mut RBTN: u8 = 0; | ||
| 203 | static mut RDX: i8 = 0; | ||
| 204 | static mut RDY: i8 = 0; | ||
| 205 | |||
| 206 | static INITOK: AtomicBool = AtomicBool::new(false); | ||
| 207 | static MOUSEPRESENT: AtomicBool = AtomicBool::new(false); | ||
| 208 | |||
| 209 | fn cs() -> usize { | ||
| 210 | unsafe { CTRL_CS } | ||
| 211 | } | ||
| 212 | |||
| 213 | // --------------------------------------------------------------------------- | ||
| 214 | // MMIO accessors | ||
| 215 | // --------------------------------------------------------------------------- | ||
| 216 | unsafe fn rd(a: usize) -> u32 { | ||
| 217 | unsafe { (a as *const u32).read_volatile() } | ||
| 218 | } | ||
| 219 | unsafe fn wr(a: usize, v: u32) { | ||
| 220 | unsafe { (a as *mut u32).write_volatile(v) } | ||
| 221 | } | ||
| 222 | unsafe fn wr64(a: usize, v: u64) { | ||
| 223 | unsafe { | ||
| 224 | (a as *mut u32).write_volatile(v as u32); | ||
| 225 | (a as *mut u32).add(1).write_volatile((v >> 32) as u32); | ||
| 226 | let _ = rd(a); | ||
| 227 | } | ||
| 228 | } | ||
| 229 | fn op(off: usize) -> usize { | ||
| 230 | unsafe { OPB + off } | ||
| 231 | } | ||
| 232 | fn db(slot: u32) -> usize { | ||
| 233 | unsafe { DBB }.wrapping_add(slot as usize * 4) | ||
| 234 | } | ||
| 235 | fn rt(off: usize) -> usize { | ||
| 236 | unsafe { RTB + off } | ||
| 237 | } | ||
| 238 | |||
| 239 | fn dma_alloc(bytes: usize, align: usize) -> *mut u8 { | ||
| 240 | unsafe { | ||
| 241 | let layout = alloc::alloc::Layout::from_size_align(bytes.max(1), align).unwrap(); | ||
| 242 | let p = alloc::alloc::alloc(layout); | ||
| 243 | if !p.is_null() { | ||
| 244 | core::ptr::write_bytes(p, 0, bytes.max(1)); | ||
| 245 | } | ||
| 246 | p | ||
| 247 | } | ||
| 248 | } | ||
| 249 | |||
| 250 | // --------------------------------------------------------------------------- | ||
| 251 | // Ring primitives | ||
| 252 | // --------------------------------------------------------------------------- | ||
| 253 | unsafe fn trb_push(r: &mut Ring, d0: u32, d1: u32, d2: u32, d3: u32) { | ||
| 254 | unsafe { | ||
| 255 | let s = r.raw.add((r.idx % r.count) * 4); | ||
| 256 | *s.add(0) = d0; | ||
| 257 | *s.add(1) = d1; | ||
| 258 | *s.add(2) = d2; | ||
| 259 | *s.add(3) = (d3 & !TRB_CYCLE) | r.cyc; | ||
| 260 | r.idx += 1; | ||
| 261 | if r.idx.is_multiple_of(r.count) { | ||
| 262 | r.cyc ^= 1; | ||
| 263 | } | ||
| 264 | } | ||
| 265 | } | ||
| 266 | |||
| 267 | // --------------------------------------------------------------------------- | ||
| 268 | // Event ring: drain produced events, calling handle(d0,d1,d2,d3) per event, | ||
| 269 | // then advance ERDP to the consumption point so the controller reuses it. | ||
| 270 | // --------------------------------------------------------------------------- | ||
| 271 | unsafe fn drain_events(handle: &mut dyn FnMut(u32, u32, u32, u32)) { | ||
| 272 | unsafe { | ||
| 273 | let ev = &mut *core::ptr::addr_of_mut!(EV); | ||
| 274 | let mut consumed = 0usize; | ||
| 275 | loop { | ||
| 276 | let idx = ev.idx % ev.count; | ||
| 277 | let s = ev.raw.add(idx * 4); | ||
| 278 | let d3 = s.add(3).read(); | ||
| 279 | if (d3 & 1) != ev.cyc { | ||
| 280 | break; | ||
| 281 | } | ||
| 282 | let (d0, d1, d2) = (s.add(0).read(), s.add(1).read(), s.add(2).read()); | ||
| 283 | handle(d0, d1, d2, d3); | ||
| 284 | ev.idx += 1; | ||
| 285 | consumed += 1; | ||
| 286 | if ev.idx.is_multiple_of(ev.count) { | ||
| 287 | ev.cyc ^= 1; | ||
| 288 | } | ||
| 289 | } | ||
| 290 | if consumed > 0 { | ||
| 291 | let idx = ev.idx % ev.count; | ||
| 292 | wr64(rt(RT_ERDP), ev.raw.add(idx * 4) as u64); | ||
| 293 | } | ||
| 294 | } | ||
| 295 | } | ||
| 296 | |||
| 297 | /// Wait for a command-completion event; returns (code, slot). | ||
| 298 | fn wait_cmd() -> (u32, u32) { | ||
| 299 | let mut code = 0u32; | ||
| 300 | let mut slot = 0u32; | ||
| 301 | let mut done = false; | ||
| 302 | let mut seen = 0u32; | ||
| 303 | let mut raw: [u32; 4] = [0; 4]; | ||
| 304 | for _ in 0..4_000_000 { | ||
| 305 | if done { | ||
| 306 | break; | ||
| 307 | } | ||
| 308 | unsafe { | ||
| 309 | drain_events(&mut |d0, d1, d2, d3| { | ||
| 310 | seen += 1; | ||
| 311 | if TRB_FIELD_TO_TYPE(d3) == TRB_EV_CMD_COMPLETE { | ||
| 312 | code = GET_COMP_CODE(d2); | ||
| 313 | slot = TRB_TO_SLOT_ID(d3); | ||
| 314 | raw = [d0, d1, d2, d3]; | ||
| 315 | done = true; | ||
| 316 | } | ||
| 317 | }); | ||
| 318 | } | ||
| 319 | core::hint::spin_loop(); | ||
| 320 | } | ||
| 321 | if code != COMP_SUCCESS { | ||
| 322 | unsafe { | ||
| 323 | let evidx = core::ptr::addr_of!(EV).cast::<Ring>().read().idx; | ||
| 324 | crate::serial_println!( | ||
| 325 | "usb: cmd-complete code={} slot={} seen={} evidx={}", | ||
| 326 | code, slot, seen, evidx | ||
| 327 | ); | ||
| 328 | for i in 0..4 { | ||
| 329 | let s = EV.raw.add(i * 4); | ||
| 330 | crate::serial_println!( | ||
| 331 | " ev[{i}] {:08x} {:08x} {:08x} {:08x}", | ||
| 332 | s.add(0).read(), s.add(1).read(), s.add(2).read(), s.add(3).read() | ||
| 333 | ); | ||
| 334 | } | ||
| 335 | } | ||
| 336 | } | ||
| 337 | (code, slot) | ||
| 338 | } | ||
| 339 | |||
| 340 | /// Enqueue a command TRB and ring the host command doorbell. | ||
| 341 | fn issue_cmd(d3: u32) -> (u32, u32) { | ||
| 342 | issue_cmd_ptr(d3, 0) | ||
| 343 | } | ||
| 344 | |||
| 345 | /// Enqueue a command TRB that carries a 64-bit parameter (used by | ||
| 346 | /// Address-Device / Configure-Endpoint for the input-context pointer). | ||
| 347 | fn issue_cmd_ptr(d3: u32, ptr: usize) -> (u32, u32) { | ||
| 348 | unsafe { | ||
| 349 | trb_push( | ||
| 350 | &mut *core::ptr::addr_of_mut!(CMD), | ||
| 351 | ptr as u32, | ||
| 352 | (ptr as u64 >> 32) as u32, | ||
| 353 | 0, | ||
| 354 | d3, | ||
| 355 | ); | ||
| 356 | wr(db(0), 0); | ||
| 357 | let _ = rd(db(0)); | ||
| 358 | } | ||
| 359 | wait_cmd() | ||
| 360 | } | ||
| 361 | |||
| 362 | /// Wait for a transfer-complete event on the given device-context endpoint id. | ||
| 363 | fn wait_xfer(epid: u32) -> u32 { | ||
| 364 | let mut code = 0u32; | ||
| 365 | let mut done = false; | ||
| 366 | let sl = SLOT.load(Ordering::Relaxed); | ||
| 367 | for _ in 0..4_000_000 { | ||
| 368 | if done { | ||
| 369 | break; | ||
| 370 | } | ||
| 371 | unsafe { | ||
| 372 | drain_events(&mut |_, _, d2, d3| { | ||
| 373 | if TRB_FIELD_TO_TYPE(d3) == TRB_EV_TRANSFER | ||
| 374 | && TRB_TO_SLOT_ID(d3) == sl | ||
| 375 | && TRB_TO_EP_ID(d3) == epid | ||
| 376 | { | ||
| 377 | code = GET_COMP_CODE(d2); | ||
| 378 | done = true; | ||
| 379 | } | ||
| 380 | }); | ||
| 381 | } | ||
| 382 | core::hint::spin_loop(); | ||
| 383 | } | ||
| 384 | code | ||
| 385 | } | ||
| 386 | |||
| 387 | // --------------------------------------------------------------------------- | ||
| 388 | // Control transfer on EP0 (setup + optional data + status), per Linux | ||
| 389 | // xhci_queue_ctrl_tx. Returns OK(len) or the failure completion code. | ||
| 390 | // --------------------------------------------------------------------------- | ||
| 391 | fn ctrl_xfer( | ||
| 392 | bmreq: u8, | ||
| 393 | breq: u8, | ||
| 394 | wval: u16, | ||
| 395 | widx: u16, | ||
| 396 | buf: *mut u8, | ||
| 397 | buflen: u32, | ||
| 398 | dir_in: bool, | ||
| 399 | ) -> Result<usize, u32> { | ||
| 400 | let slot = SLOT.load(Ordering::Relaxed); | ||
| 401 | let e0 = unsafe { &mut *core::ptr::addr_of_mut!(EP0) }; | ||
| 402 | unsafe { | ||
| 403 | // Setup stage (immediate data, 8-byte setup packet in d0/d1). | ||
| 404 | let tx = if buflen > 0 { | ||
| 405 | TRB_TX_TYPE(if dir_in { TRB_DATA_IN } else { TRB_DATA_OUT }) | ||
| 406 | } else { | ||
| 407 | 0 | ||
| 408 | }; | ||
| 409 | trb_push( | ||
| 410 | e0, | ||
| 411 | (wval as u32) << 16 | (breq as u32) << 8 | bmreq as u32, | ||
| 412 | (widx as u32) | buflen << 16, | ||
| 413 | TRB_LEN(8), | ||
| 414 | TYPE(TRB_SETUP) | TRB_IDT | tx, | ||
| 415 | ); | ||
| 416 | if buflen > 0 { | ||
| 417 | trb_push( | ||
| 418 | e0, | ||
| 419 | buf as u32, | ||
| 420 | (buf as usize >> 32) as u32, | ||
| 421 | TRB_LEN(buflen), | ||
| 422 | TYPE(TRB_DATA) | if dir_in { TRB_DIR_IN } else { 0 }, | ||
| 423 | ); | ||
| 424 | } | ||
| 425 | // Status stage. | ||
| 426 | let stat_dir = if buflen > 0 && dir_in { 0 } else { TRB_DIR_IN }; | ||
| 427 | trb_push(e0, 0, 0, 0, TYPE(TRB_STATUS) | TRB_IOC | stat_dir); | ||
| 428 | wr(db(slot), 1); // ring EP0 (doorbell target value 1) | ||
| 429 | let _ = rd(db(slot)); | ||
| 430 | } | ||
| 431 | let code = wait_xfer(1); | ||
| 432 | if code == COMP_SUCCESS || code == COMP_SHORT { | ||
| 433 | Ok(buflen as usize) | ||
| 434 | } else { | ||
| 435 | Err(code) | ||
| 436 | } | ||
| 437 | } | ||
| 438 | |||
| 439 | // --------------------------------------------------------------------------- | ||
| 440 | // Context construction helpers | ||
| 441 | // --------------------------------------------------------------------------- | ||
| 442 | fn build_addr_ictx(port_speed: u32, port_num: u32) { | ||
| 443 | unsafe { | ||
| 444 | core::ptr::write_bytes(INCTX, 0, 32 + 8 * cs()); | ||
| 445 | let cc = INCTX as *mut u32; | ||
| 446 | *cc.add(0) = 0; // Drop flags | ||
| 447 | *cc.add(1) = (1 << 0) | (1 << 1); // Add slot + EP0 | ||
| 448 | let sc = INCTX.offset(32) as *mut u32; | ||
| 449 | *sc.add(0) = LAST_CTX(1) | SLOT_SPEED_X(port_speed); | ||
| 450 | *sc.add(1) = ROOTPORT(port_num + 1); | ||
| 451 | let e0 = INCTX.add(32 + cs()) as *mut u32; | ||
| 452 | *e0.add(0) = 0; | ||
| 453 | *e0.add(1) = EP_TYPE(CTRL_EP) | MAX_PACKET(MPS0) | ERROR_COUNT(3); | ||
| 454 | (INCTX.add(32 + cs() + 8) as *mut u64).write_volatile((EP0.raw as u64) | 1); | ||
| 455 | (INCTX.add(32 + cs() + 16) as *mut u32).write_volatile(EP_AVG(8)); | ||
| 456 | } | ||
| 457 | } | ||
| 458 | |||
| 459 | // --------------------------------------------------------------------------- | ||
| 460 | // Port reset | ||
| 461 | // --------------------------------------------------------------------------- | ||
| 462 | unsafe fn reset_port(port: u32) -> Result<u32, ()> { | ||
| 463 | unsafe { | ||
| 464 | let psc = op(OP_PORTSC) + (port as usize) * 0x10; | ||
| 465 | // Ensure powered. | ||
| 466 | wr(psc, rd(psc) | PORT_POWER); | ||
| 467 | for _ in 0..1000 { | ||
| 468 | core::hint::spin_loop(); | ||
| 469 | } | ||
| 470 | // Assert reset, wait for the hardware to clear it (reset done). | ||
| 471 | wr(psc, rd(psc) | PORT_RESET); | ||
| 472 | for _ in 0..4_000_000 { | ||
| 473 | if rd(psc) & PORT_RESET == 0 { | ||
| 474 | break; | ||
| 475 | } | ||
| 476 | core::hint::spin_loop(); | ||
| 477 | } | ||
| 478 | // Wait for the port to be enabled. | ||
| 479 | for _ in 0..4_000_000 { | ||
| 480 | if rd(psc) & PORT_PE != 0 { | ||
| 481 | break; | ||
| 482 | } | ||
| 483 | core::hint::spin_loop(); | ||
| 484 | } | ||
| 485 | if rd(psc) & PORT_PE == 0 { | ||
| 486 | return Err(()); | ||
| 487 | } | ||
| 488 | Ok(DEV_SPEED(rd(psc))) | ||
| 489 | } | ||
| 490 | } | ||
| 491 | |||
| 492 | // --------------------------------------------------------------------------- | ||
| 493 | // Enumeration + HID setup | ||
| 494 | // --------------------------------------------------------------------------- | ||
| 495 | fn read_device_descriptor() -> Result<[u8; 18], u32> { | ||
| 496 | let mut d = [0u8; 18]; | ||
| 497 | ctrl_xfer( | ||
| 498 | USB_DIR_IN, | ||
| 499 | USB_REQ_GET_DESCRIPTOR, | ||
| 500 | (DT_DEVICE as u16) << 8, | ||
| 501 | 0, | ||
| 502 | d.as_mut_ptr(), | ||
| 503 | 18, | ||
| 504 | true, | ||
| 505 | )?; | ||
| 506 | Ok(d) | ||
| 507 | } | ||
| 508 | |||
| 509 | fn read_config_descriptor(buf: &mut [u8]) -> Result<usize, u32> { | ||
| 510 | let total = ctrl_xfer( | ||
| 511 | USB_DIR_IN, | ||
| 512 | USB_REQ_GET_DESCRIPTOR, | ||
| 513 | (DT_CONFIG as u16) << 8, | ||
| 514 | 0, | ||
| 515 | buf.as_mut_ptr(), | ||
| 516 | buf.len() as u32, | ||
| 517 | true, | ||
| 518 | )?; | ||
| 519 | Ok(total) | ||
| 520 | } | ||
| 521 | |||
| 522 | /// Walk a config descriptor tree looking for a HID interrupt IN endpoint | ||
| 523 | /// with an interrupt (transfer type 3). Returns (mps, interval, eepid). | ||
| 524 | fn find_hid_mouse(cfg: &[u8]) -> Option<(u32, u32, u32)> { | ||
| 525 | let mut i = 0usize; | ||
| 526 | let mut in_iface = false; | ||
| 527 | while i + 2 <= cfg.len() { | ||
| 528 | let bLen = cfg[i] as usize; | ||
| 529 | if bLen == 0 { | ||
| 530 | break; | ||
| 531 | } | ||
| 532 | let btype = cfg[i + 1]; | ||
| 533 | if btype == DT_INTERFACE && i + 9 <= cfg.len() { | ||
| 534 | // interface descriptor: +5 = bInterfaceClass | ||
| 535 | in_iface = cfg[i + 5] == HID_CLASS; | ||
| 536 | } else if btype == DT_ENDPOINT && in_iface && i + 7 <= cfg.len() { | ||
| 537 | let addr = cfg[i + 2]; | ||
| 538 | let attrs = cfg[i + 3]; | ||
| 539 | let dir_in = addr & 0x80 != 0; | ||
| 540 | let xfer = attrs & 0x03; | ||
| 541 | if dir_in && xfer == 3 { | ||
| 542 | let mps = (cfg[i + 4] as u32) | ((cfg[i + 5] as u32) << 8); | ||
| 543 | let interval = cfg[i + 6] as u32; | ||
| 544 | let epnum = (addr & 0x0f) as u32; | ||
| 545 | let euid = 2 * epnum + 1; // device-context index (IN) | ||
| 546 | return Some((mps, interval, euid)); | ||
| 547 | } | ||
| 548 | } | ||
| 549 | i += bLen; | ||
| 550 | } | ||
| 551 | None | ||
| 552 | } | ||
| 553 | |||
| 554 | // --------------------------------------------------------------------------- | ||
| 555 | // Bring one port's device through address + configure to an HID mouse | ||
| 556 | // --------------------------------------------------------------------------- | ||
| 557 | fn enumerate(port: u32, speed: u32) -> bool { | ||
| 558 | let _ = DEV_SPEED; | ||
| 559 | // 1. Enable slot. | ||
| 560 | let (code, slotid) = issue_cmd(TYPE(TRB_ENABLE_SLOT)); | ||
| 561 | if code != COMP_SUCCESS || slotid == 0 { | ||
| 562 | crate::serial_println!("usb: enable-slot failed code={} slot={}", code, slotid); | ||
| 563 | return false; | ||
| 564 | } | ||
| 565 | SLOT.store(slotid, Ordering::Relaxed); | ||
| 566 | unsafe { | ||
| 567 | *DCBAA.add(slotid as usize) = DEVCTX as u64; | ||
| 568 | } | ||
| 569 | // 2. Address device. | ||
| 570 | build_addr_ictx(speed, port); | ||
| 571 | let (code, _) = issue_cmd_ptr(TYPE(TRB_ADDR_DEV) | SLOT_ID(slotid), unsafe { INCTX } as usize); | ||
| 572 | if code != COMP_SUCCESS { | ||
| 573 | crate::serial_println!("usb: address-device failed code={}", code); | ||
| 574 | return false; | ||
| 575 | } | ||
| 576 | // 3. Read the device descriptor (picks up bMaxPacketSize0). | ||
| 577 | match read_device_descriptor() { | ||
| 578 | Ok(d) => { | ||
| 579 | let mps = d[7]; | ||
| 580 | if mps != 0 { | ||
| 581 | unsafe { | ||
| 582 | MPS0 = mps as u32; | ||
| 583 | } | ||
| 584 | } | ||
| 585 | crate::serial_println!( | ||
| 586 | "usb: dev {:04x}:{:04x} class={:#x} mps0={}", | ||
| 587 | u16::from_le_bytes([d[8], d[9]]), | ||
| 588 | u16::from_le_bytes([d[10], d[11]]), | ||
| 589 | d[4], | ||
| 590 | d[7] | ||
| 591 | ); | ||
| 592 | } | ||
| 593 | Err(c) => crate::serial_println!("usb: get-device-desc failed code={}", c), | ||
| 594 | } | ||
| 595 | // 4. Read the whole config descriptor and find the HID mouse IN ep. | ||
| 596 | let mut cfg = [0u8; 256]; | ||
| 597 | let got = match read_config_descriptor(&mut cfg) { | ||
| 598 | Ok(n) => n, | ||
| 599 | Err(c) => { | ||
| 600 | crate::serial_println!("usb: get-config failed code={}", c); | ||
| 601 | return false; | ||
| 602 | } | ||
| 603 | }; | ||
| 604 | let _ = got; | ||
| 605 | match find_hid_mouse(&cfg) { | ||
| 606 | Some((mps, interval, euid)) => { | ||
| 607 | crate::serial_println!( | ||
| 608 | "usb: HID mouse IN ep mps={} interval={} euid={}", | ||
| 609 | mps, | ||
| 610 | interval, | ||
| 611 | euid | ||
| 612 | ); | ||
| 613 | // 5. Set configuration = 1 (first config value present). | ||
| 614 | let cfgval = cfg[5]; | ||
| 615 | if cfgval == 0 { | ||
| 616 | return false; | ||
| 617 | } | ||
| 618 | if read_configuration(cfgval).is_err() { | ||
| 619 | return false; | ||
| 620 | } | ||
| 621 | // 6. Configure the IN endpoint. | ||
| 622 | if !configure_hid_endpoint(euid, mps, interval) { | ||
| 623 | return false; | ||
| 624 | } | ||
| 625 | HID_EPID.store(euid, Ordering::Relaxed); | ||
| 626 | unsafe { | ||
| 627 | HID_LEN = mps as usize; | ||
| 628 | QUEUED = false; | ||
| 629 | } | ||
| 630 | // 7. Arm the first IN read. | ||
| 631 | queue_report(); | ||
| 632 | MOUSEPRESENT.store(true, Ordering::Relaxed); | ||
| 633 | crate::serial_println!( | ||
| 634 | "usb: HID boot mouse armed (slot {})", | ||
| 635 | SLOT.load(Ordering::Relaxed) | ||
| 636 | ); | ||
| 637 | true | ||
| 638 | } | ||
| 639 | None => { | ||
| 640 | crate::serial_println!("usb: no HID mouse endpoint in config"); | ||
| 641 | false | ||
| 642 | } | ||
| 643 | } | ||
| 644 | } | ||
| 645 | |||
| 646 | fn read_configuration(val: u8) -> Result<(), u32> { | ||
| 647 | ctrl_xfer( | ||
| 648 | 0, | ||
| 649 | USB_REQ_SET_CONFIGURATION, | ||
| 650 | val.into(), | ||
| 651 | 0, | ||
| 652 | core::ptr::null_mut(), | ||
| 653 | 0, | ||
| 654 | false, | ||
| 655 | ) | ||
| 656 | .map(|_| ()) | ||
| 657 | } | ||
| 658 | |||
| 659 | fn configure_hid_endpoint(euid: u32, mps: u32, interval: u32) -> bool { | ||
| 660 | unsafe { | ||
| 661 | let slot = SLOT.load(Ordering::Relaxed); | ||
| 662 | core::ptr::write_bytes(INCTX, 0, 32 + 8 * cs()); | ||
| 663 | let cc = INCTX as *mut u32; | ||
| 664 | *cc.add(0) = 0; // Drop flags | ||
| 665 | *cc.add(1) = (1 << 0) | (1 << euid); // Add slot + this EP | ||
| 666 | // Input slot context: copy the output (post-address) slot context so | ||
| 667 | // route/speed/rootport are preserved, then bump LAST_CTX to the ep. | ||
| 668 | let osc = DEVCTX.offset(0).cast::<u32>(); | ||
| 669 | let sc = INCTX.offset(32).cast::<u32>(); | ||
| 670 | *sc.add(0) = (*osc.add(0) & !LAST_CTX(31)) | LAST_CTX(euid); | ||
| 671 | *sc.add(1) = *osc.add(1); | ||
| 672 | // Endpoint IN context. | ||
| 673 | let ep = INCTX.add(32 + (euid as usize) * cs()).cast::<u32>(); | ||
| 674 | *ep.add(0) = EP_INTERVAL(interval); | ||
| 675 | *ep.add(1) = EP_TYPE(INT_IN_EP) | MAX_PACKET(mps) | ERROR_COUNT(3); | ||
| 676 | *ep.add(4) = EP_AVG(mps); | ||
| 677 | let deq = EPIN.raw as u64 | 1; | ||
| 678 | *ep.offset(2).cast::<u64>() = deq; | ||
| 679 | let (code, _) = issue_cmd_ptr(TYPE(TRB_CONFIG_EP) | SLOT_ID(slot), INCTX as usize); | ||
| 680 | code == COMP_SUCCESS | ||
| 681 | } | ||
| 682 | } | ||
| 683 | |||
| 684 | fn queue_report() { | ||
| 685 | let slot = SLOT.load(Ordering::Relaxed); | ||
| 686 | let epid = HID_EPID.load(Ordering::Relaxed); | ||
| 687 | let epin = unsafe { &mut *core::ptr::addr_of_mut!(EPIN) }; | ||
| 688 | unsafe { | ||
| 689 | let b = core::ptr::addr_of_mut!(HID_BUF) as *mut u8; | ||
| 690 | trb_push( | ||
| 691 | epin, | ||
| 692 | b as u32, | ||
| 693 | (b as usize >> 32) as u32, | ||
| 694 | TRB_LEN(HID_LEN as u32), | ||
| 695 | TYPE(TRB_NORMAL) | TRB_IOC, | ||
| 696 | ); | ||
| 697 | wr(db(slot), epid); | ||
| 698 | let _ = rd(db(slot)); | ||
| 699 | QUEUED = true; | ||
| 700 | } | ||
| 701 | } | ||
| 702 | |||
| 703 | // --------------------------------------------------------------------------- | ||
| 704 | // Controller bring-up | ||
| 705 | // --------------------------------------------------------------------------- | ||
| 706 | fn find_xhci() -> Option<(u8, u8, u8)> { | ||
| 707 | for d in crate::pci::probe() { | ||
| 708 | if d.class == 0x0C && d.subclass == 0x03 && d.prog_if == 0x30 { | ||
| 709 | return Some((d.bus, d.device, d.func)); | ||
| 710 | } | ||
| 711 | } | ||
| 712 | None | ||
| 713 | } | ||
| 714 | |||
| 715 | pub fn init() -> bool { | ||
| 716 | let Some((b, dev, func)) = find_xhci() else { | ||
| 717 | crate::serial_println!("usb: no xHCI controller"); | ||
| 718 | return false; | ||
| 719 | }; | ||
| 720 | crate::serial_println!("usb: xHCI at {:02x}:{:02x}.{}", b, dev, func); | ||
| 721 | |||
| 722 | // Enable MMIO + bus-mastering on the PCI command register. | ||
| 723 | unsafe { | ||
| 724 | let cmd = crate::pci::config_read(b, dev, func, 0x04); | ||
| 725 | crate::pci::config_write( | ||
| 726 | b, | ||
| 727 | dev, | ||
| 728 | func, | ||
| 729 | 0x04, | ||
| 730 | (cmd & 0xFFFF0000) | ((cmd & 0xFFFF) | 0x6), | ||
| 731 | ); | ||
| 732 | let bar = crate::pci::bar64(b, dev, func, 0); | ||
| 733 | let base = bar.unwrap() as usize; | ||
| 734 | M = base; | ||
| 735 | let caplen = rd(base + CAP_CAPLENGTH); | ||
| 736 | let hcs1 = rd(base + HCSPARAMS1); | ||
| 737 | let hcc = rd(base + HCCPARAMS1); | ||
| 738 | crate::serial_println!( | ||
| 739 | "usb: mmio={:#x} caplen={:#x} hcs1={:#x} hcc1={:#x}", | ||
| 740 | base, caplen, hcs1, hcc | ||
| 741 | ); | ||
| 742 | OPB = base + (caplen & 0xff) as usize; | ||
| 743 | DBB = base + rd(base + DBOFF) as usize; | ||
| 744 | RTB = base + rd(base + RTSOFF) as usize; | ||
| 745 | MAXPORTS = (hcs1 >> 24) & 0xff; // MaxPorts is bits 31:24 (bits 23:16 are MaxIntrs) | ||
| 746 | let _ = hcc; | ||
| 747 | CTRL_CS = if hcc & (1 << 2) != 0 { 64 } else { 32 }; | ||
| 748 | } | ||
| 749 | |||
| 750 | // Reset the controller (stop, then reset). Wait for reset completion | ||
| 751 | // on the CMD_RESET bit clearing (per Linux xhci_reset), then for the | ||
| 752 | // Controller-Not-Ready flag to clear before any doorbell/op-register | ||
| 753 | // activity. | ||
| 754 | unsafe { | ||
| 755 | wr(op(OP_USBCMD), rd(op(OP_USBCMD)) & !CMD_RUN); | ||
| 756 | // Wait for halt. | ||
| 757 | let mut t = 0; | ||
| 758 | while t < 1_000_000 && rd(op(OP_USBSTS)) & STS_HALT == 0 { | ||
| 759 | t += 1; | ||
| 760 | core::hint::spin_loop(); | ||
| 761 | } | ||
| 762 | wr(op(OP_USBCMD), rd(op(OP_USBCMD)) | CMD_RESET); | ||
| 763 | t = 0; | ||
| 764 | while t < 10_000_000 && rd(op(OP_USBCMD)) & CMD_RESET != 0 { | ||
| 765 | t += 1; | ||
| 766 | core::hint::spin_loop(); | ||
| 767 | } | ||
| 768 | t = 0; | ||
| 769 | while t < 10_000_000 && rd(op(OP_USBSTS)) & STS_CNR != 0 { | ||
| 770 | t += 1; | ||
| 771 | core::hint::spin_loop(); | ||
| 772 | } | ||
| 773 | } | ||
| 774 | |||
| 775 | // Allocate rings / contexts. | ||
| 776 | unsafe { | ||
| 777 | CMD.raw = dma_alloc(64 * 16, 64) as *mut u32; | ||
| 778 | EV.raw = dma_alloc(32 * 16, 64) as *mut u32; | ||
| 779 | EP0.raw = dma_alloc(8 * 16, 64) as *mut u32; | ||
| 780 | EPIN.raw = dma_alloc(8 * 16, 64) as *mut u32; | ||
| 781 | ERST = dma_alloc(16, 64); | ||
| 782 | DCBAA = dma_alloc(64 * 8, 64) as *mut u64; | ||
| 783 | DEVCTX = dma_alloc((1 + 8) * cs(), 64); | ||
| 784 | INCTX = dma_alloc(32 + 8 * cs(), 64); | ||
| 785 | // ERST segment 0 -> event ring. | ||
| 786 | let e = ERST as *mut u32; | ||
| 787 | *e.add(0) = EV.raw as u32; | ||
| 788 | *e.add(1) = (EV.raw as usize >> 32) as u32; | ||
| 789 | *e.add(2) = 32; // segment size (TRBs) | ||
| 790 | } | ||
| 791 | |||
| 792 | // Program registers. | ||
| 793 | unsafe { | ||
| 794 | wr64(op(OP_DCBAAP), DCBAA as u64); | ||
| 795 | wr64( | ||
| 796 | op(OP_CRCR), | ||
| 797 | ((CMD.raw as u64) & !0x3f) | CMD_RING_CYCLE as u64, | ||
| 798 | ); | ||
| 799 | // MaxSlotsEn = 1 (we use a single slot). | ||
| 800 | wr(op(OP_CONFIG), (rd(op(OP_CONFIG)) & !0xff) | 1); | ||
| 801 | // Event ring. | ||
| 802 | wr(rt(RT_ERSTSZ), 1); | ||
| 803 | wr64(rt(RT_ERSTBA), ERST as u64); | ||
| 804 | wr64(rt(RT_ERDP), EV.raw as u64); | ||
| 805 | // Interrupter 0: clear pending, enable. | ||
| 806 | wr(rt(RT_IMAN), rd(rt(RT_IMAN)) | 0x2); | ||
| 807 | wr(rt(RT_IMOD), 0); | ||
| 808 | // Run. | ||
| 809 | wr(op(OP_USBCMD), rd(op(OP_USBCMD)) | CMD_RUN); | ||
| 810 | let mut t = 0; | ||
| 811 | while t < 4_000_000 && rd(op(OP_USBSTS)) & STS_HALT != 0 { | ||
| 812 | t += 1; | ||
| 813 | core::hint::spin_loop(); | ||
| 814 | } | ||
| 815 | if rd(op(OP_USBSTS)) & STS_HALT != 0 { | ||
| 816 | crate::serial_println!("usb: controller failed to start"); | ||
| 817 | return false; | ||
| 818 | } | ||
| 819 | } | ||
| 820 | INITOK.store(true, Ordering::Relaxed); | ||
| 821 | crate::serial_println!( | ||
| 822 | "usb: controller up ({} ports) — mouse discovery deferred to cursor task", | ||
| 823 | unsafe { MAXPORTS } | ||
| 824 | ); | ||
| 825 | true | ||
| 826 | } | ||
| 827 | |||
| 828 | /// Bitmask of root ports whose attempt already failed; skip them so we don't | ||
| 829 | /// re-reset the port on every poll. | ||
| 830 | static DISCOVERED_TRIED: AtomicU32 = AtomicU32::new(0); | ||
| 831 | |||
| 832 | /// Periodic, cheap scan called from the cursor task until a HID mouse is | ||
| 833 | /// found. A USB device can finish connecting long after the controller | ||
| 834 | /// starts, so discovery is driven asynchronously rather than blocking boot. | ||
| 835 | fn maybe_discover() { | ||
| 836 | if MOUSEPRESENT.load(Ordering::Relaxed) { | ||
| 837 | return; | ||
| 838 | } | ||
| 839 | let maxports = unsafe { MAXPORTS }; | ||
| 840 | for p in 0..maxports { | ||
| 841 | if DISCOVERED_TRIED.load(Ordering::Relaxed) & (1 << p) != 0 { | ||
| 842 | continue; | ||
| 843 | } | ||
| 844 | unsafe { | ||
| 845 | let psc = op(OP_PORTSC) + (p as usize) * 0x10; | ||
| 846 | // Only touch ports that are both connected and not yet enabled. | ||
| 847 | if rd(psc) & (PORT_CONNECT | PORT_PE) == PORT_CONNECT { | ||
| 848 | crate::serial_println!("usb: port {} connect (speed {})", p, DEV_SPEED(rd(psc))); | ||
| 849 | if let Ok(speed) = reset_port(p) | ||
| 850 | && enumerate(p, speed) { | ||
| 851 | return; | ||
| 852 | } | ||
| 853 | DISCOVERED_TRIED.fetch_or(1 << p, Ordering::Relaxed); | ||
| 854 | } | ||
| 855 | } | ||
| 856 | } | ||
| 857 | } | ||
| 858 | |||
| 859 | pub fn mouse_present() -> bool { | ||
| 860 | MOUSEPRESENT.load(Ordering::Relaxed) | ||
| 861 | } | ||
| 862 | |||
| 863 | /// True once the xHCI controller is brought up (regardless of whether a | ||
| 864 | /// device has attached yet). The cursor task is spawned as long as this is | ||
| 865 | /// set so async mouse discovery can run. | ||
| 866 | pub fn controller_up() -> bool { | ||
| 867 | INITOK.load(Ordering::Relaxed) | ||
| 868 | } | ||
| 869 | |||
| 870 | /// Reset discovery state (used by the `usb` shell command to rescan). | ||
| 871 | pub fn rescan() { | ||
| 872 | DISCOVERED_TRIED.store(0, Ordering::Relaxed); | ||
| 873 | } | ||
| 874 | |||
| 875 | /// Non-blocking poll: drain the event ring, then decode one mouse report if | ||
| 876 | /// one arrived (re-arming the IN transfer so reports keep flowing). | ||
| 877 | /// Returns `None` when no report is pending (or no mouse). | ||
| 878 | pub fn poll_mouse() -> Option<MouseEvent> { | ||
| 879 | // Drive discovery while no device is attached, then drain reports. | ||
| 880 | if !MOUSEPRESENT.load(Ordering::Relaxed) { | ||
| 881 | if INITOK.load(Ordering::Relaxed) { | ||
| 882 | maybe_discover(); | ||
| 883 | } | ||
| 884 | return None; | ||
| 885 | } | ||
| 886 | let epid = HID_EPID.load(Ordering::Relaxed); | ||
| 887 | let sl = SLOT.load(Ordering::Relaxed); | ||
| 888 | unsafe { | ||
| 889 | drain_events(&mut |_, _, d2, d3| { | ||
| 890 | if TRB_FIELD_TO_TYPE(d3) == TRB_EV_TRANSFER | ||
| 891 | && TRB_TO_SLOT_ID(d3) == sl | ||
| 892 | && TRB_TO_EP_ID(d3) == epid | ||
| 893 | { | ||
| 894 | let c = GET_COMP_CODE(d2); | ||
| 895 | if c == COMP_SUCCESS || c == COMP_SHORT { | ||
| 896 | // A fresh report sits in HID_BUF. | ||
| 897 | let p = core::ptr::addr_of!(HID_BUF) as *const u8; | ||
| 898 | RBTN = core::ptr::read_volatile(p); | ||
| 899 | RDX = core::ptr::read_volatile(p.add(1)) as i8; | ||
| 900 | RDY = core::ptr::read_volatile(p.add(2)) as i8; | ||
| 901 | GOT_REPORT = true; | ||
| 902 | QUEUED = false; | ||
| 903 | } | ||
| 904 | } | ||
| 905 | }); | ||
| 906 | if GOT_REPORT { | ||
| 907 | GOT_REPORT = false; | ||
| 908 | let ev = Some(MouseEvent { | ||
| 909 | dx: RDX as i16, | ||
| 910 | dy: RDY as i16, | ||
| 911 | left: RBTN & 0x01 != 0, | ||
| 912 | right: RBTN & 0x02 != 0, | ||
| 913 | middle: RBTN & 0x04 != 0, | ||
| 914 | }); | ||
| 915 | queue_report(); // re-arm for the next report | ||
| 916 | return ev; | ||
| 917 | } | ||
| 918 | if !QUEUED { | ||
| 919 | queue_report(); // keep exactly one IN read outstanding | ||
| 920 | } | ||
| 921 | } | ||
| 922 | None | ||
| 923 | } | ||
| 924 | |||
| 925 | /// One-line status for the shell's `usb` command. | ||
| 926 | pub fn diagnose() -> alloc::string::String { | ||
| 927 | let mut s = alloc::string::String::new(); | ||
| 928 | s.push_str(&alloc::format!( | ||
| 929 | "xhci mmio={:#x} mapped={} ports={} mouse={} slot={}\n", | ||
| 930 | unsafe { M }, | ||
| 931 | if crate::paging::is_mapped(unsafe { M }) { "yes" } else { "no" }, | ||
| 932 | unsafe { MAXPORTS }, | ||
| 933 | if MOUSEPRESENT.load(Ordering::Relaxed) { | ||
| 934 | "yes" | ||
| 935 | } else { | ||
| 936 | "no" | ||
| 937 | }, | ||
| 938 | SLOT.load(Ordering::Relaxed) | ||
| 939 | )); | ||
| 940 | unsafe { | ||
| 941 | // Page-table walk for the MMIO base (physical == virtual here). | ||
| 942 | let phys = M; | ||
| 943 | let pml4 = crate::paging::cr3() as *const u64; | ||
| 944 | let l4 = (phys >> 39) & 0x1FF; | ||
| 945 | let l3 = (phys >> 30) & 0x1FF; | ||
| 946 | let l2 = (phys >> 21) & 0x1FF; | ||
| 947 | let l1 = (phys >> 12) & 0x1FF; | ||
| 948 | let pml4e = pml4.add(l4).read_volatile(); | ||
| 949 | let pdpte = if pml4e & 1 != 0 { | ||
| 950 | ((pml4e & 0x000f_ffff_ffff_f000) as *const u64).add(l3).read_volatile() | ||
| 951 | } else { 0 }; | ||
| 952 | let pde = if pdpte & 1 != 0 && pdpte & (1 << 7) == 0 { | ||
| 953 | ((pdpte & 0x000f_ffff_ffff_f000) as *const u64).add(l2).read_volatile() | ||
| 954 | } else { 0 }; | ||
| 955 | let pte = if pde & 1 != 0 && pde & (1 << 7) == 0 { | ||
| 956 | ((pde & 0x000f_ffff_ffff_f000) as *const u64).add(l1).read_volatile() | ||
| 957 | } else { 0 }; | ||
| 958 | s.push_str(&alloc::format!( | ||
| 959 | " pt: pml4e={:#018x} pdpte={:#018x} pde={:#018x} pte={:#018x}\n", | ||
| 960 | pml4e, pdpte, pde, pte | ||
| 961 | )); | ||
| 962 | for p in 0..8 { | ||
| 963 | let psc = op(OP_PORTSC) + (p as usize) * 0x10; | ||
| 964 | let v = rd(psc); | ||
| 965 | s.push_str(&alloc::format!(" port{}: {:#010x}\n", p, v)); | ||
| 966 | } | ||
| 967 | } | ||
| 968 | if MOUSEPRESENT.load(Ordering::Relaxed) { | ||
| 969 | s.push_str(&alloc::format!( | ||
| 970 | " hid ep-index={} mps={} armed={}\n", | ||
| 971 | HID_EPID.load(Ordering::Relaxed), | ||
| 972 | unsafe { HID_LEN }, | ||
| 973 | unsafe { QUEUED } | ||
| 974 | )); | ||
| 975 | } | ||
| 976 | s | ||
| 977 | } | ||
diff --git a/shell/src/lib.rs b/shell/src/lib.rs index 0f5ac4b..c9b7cdd 100644 --- a/shell/src/lib.rs +++ b/shell/src/lib.rs | |||
| @@ -721,6 +721,7 @@ write `{} <path> TIL <marker>`", | |||
| 721 | "stress" => cmd_stress(&parsed), | 721 | "stress" => cmd_stress(&parsed), |
| 722 | "date" => cmd_date(&parsed), | 722 | "date" => cmd_date(&parsed), |
| 723 | "pci" => cmd_pci(&parsed), | 723 | "pci" => cmd_pci(&parsed), |
| 724 | "usb" => cmd_usb(&parsed), | ||
| 724 | "rescan" => cmd_rescan(&parsed), | 725 | "rescan" => cmd_rescan(&parsed), |
| 725 | "mouse" => cmd_mouse(&parsed), | 726 | "mouse" => cmd_mouse(&parsed), |
| 726 | "cursor" => cmd_cursor(&parsed), | 727 | "cursor" => cmd_cursor(&parsed), |
| @@ -2217,6 +2218,24 @@ fn cmd_pci(p: &Parsed) { | |||
| 2217 | pipe.end(); | 2218 | pipe.end(); |
| 2218 | } | 2219 | } |
| 2219 | 2220 | ||
| 2221 | fn cmd_usb(p: &Parsed) { | ||
| 2222 | // `usb rescan` clears the failed-port mask so discovery retries. | ||
| 2223 | if p.args.first().is_some_and(|w| w == "rescan") { | ||
| 2224 | rsnix::usb::rescan(); | ||
| 2225 | let mut pipe = Pipeline::new(&p.filters, &p.output); | ||
| 2226 | let _ = pipe.line("usb: rescanning ports"); | ||
| 2227 | pipe.end(); | ||
| 2228 | return; | ||
| 2229 | } | ||
| 2230 | let mut pipe = Pipeline::new(&p.filters, &p.output); | ||
| 2231 | for l in rsnix::usb::diagnose().lines() { | ||
| 2232 | if !pipe.line(l) { | ||
| 2233 | break; | ||
| 2234 | } | ||
| 2235 | } | ||
| 2236 | pipe.end(); | ||
| 2237 | } | ||
| 2238 | |||
| 2220 | fn cmd_poweroff() -> ! { | 2239 | fn cmd_poweroff() -> ! { |
| 2221 | term_println("shutting down..."); | 2240 | term_println("shutting down..."); |
| 2222 | rsnix::power::poweroff() | 2241 | rsnix::power::poweroff() |
