From 5e1bdc12bd298a9751da4285b7e93c68b1314332 Mon Sep 17 00:00:00 2001 From: salmonik Date: Tue, 4 Aug 2026 03:06:06 +0200 Subject: 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. --- rsnix/src/cursor.rs | 2 +- rsnix/src/lib.rs | 1 + rsnix/src/usb.rs | 977 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 979 insertions(+), 1 deletion(-) create mode 100644 rsnix/src/usb.rs (limited to 'rsnix') 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 { fn task_main() { loop { - while let Some(ev) = crate::ps2::try_poll_mouse() { + while let Some(ev) = crate::usb::poll_mouse().or_else(crate::ps2::try_poll_mouse) { crate::gop::cursor_move(ev.dx, ev.dy); crate::gop::cursor_set_buttons(btn_bits(&ev)); 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; pub mod qr; pub mod power; pub mod ps2; +pub mod usb; pub mod rtc; pub mod sched; 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 @@ +// --------------------------------------------------------------------------- +// USB host controller driver — xHCI (USB 2.0/3.x) + HID mouse support. +// +// **Polled** driver: it runs the xHCI event ring from the cursor task loop +// instead of wiring MSI/INTx into the PIC, matching how the kernel already +// polls the PS/2 mouse (see `cursor`). +// +// Minimal, mouse-only scope: +// • take the controller out of reset, set up the command + event rings +// • reset ports, run the slot/address/configure firmware sequence +// • find the HID mouse boot interface, open its interrupt-IN endpoint, +// and translate the standard report into `ps2::MouseEvent` (reusing +// the cursor task's consumption path). +// +// DMA / MMIO: the kernel is identity-mapped (physical == virtual), so bus +// addresses are plain kernel pointers and every heap allocation is DMA-able. +// Rings/contexts are allocated once at boot and handed to the controller +// permanently. +// +// Register/TRB/context layouts follow the Linux xHCI driver definitions +// (drivers/usb/host/xhci*.h) and the xHCI 1.2 spec. All exact field +// positions were cross-checked against the Linux sources: +// * TRB type field is bits 10-15 (TRB_TYPE(p) = p << 10) +// * transfer/command completion code is bits 24-31 of event dword2 +// * slot id = bits 24-31, ep id = bits 16-20 of event dword3 +// * endpoint context: type at bits 3-5, interval at 16-23, +// max packet size at bits 16-31 of dword1, TR dequeue is a 64-bit +// field at bytes 8-15 with DCS in bit 0 +// * slot context: route/LAST_CTX in dword0, root-hub-port at 16-23 of +// dword1, speed at bits 20-23 +// --------------------------------------------------------------------------- +#![allow(non_snake_case)] // macro-style helpers mirror the Linux xHCI headers +use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; + +use crate::ps2::MouseEvent; + +// ---- capability registers (offset from mmio base) ---- +const CAP_CAPLENGTH: usize = 0x00; +const HCSPARAMS1: usize = 0x04; // MaxSlots[7:0] MaxIntrs[15:8] MaxPorts[23:16] +const HCCPARAMS1: usize = 0x10; // bit0=64-bit addressing; bit2=64-byte contexts +const DBOFF: usize = 0x14; +const RTSOFF: usize = 0x18; + +// ---- operational registers (relative to op = mmio + CAPLENGTH) ---- +const OP_USBCMD: usize = 0x00; +const OP_USBSTS: usize = 0x04; +const OP_CRCR: usize = 0x18; // 64-bit +const OP_DCBAAP: usize = 0x30; // 64-bit +const OP_CONFIG: usize = 0x38; +const OP_PORTSC: usize = 0x400; // PORTSC port 1 (port set stride 0x10) + +// ---- runtime registers (relative to mmio + RTSOFF), interrupter 0 ---- +const RT_IMAN: usize = 0x20; +const RT_IMOD: usize = 0x24; +const RT_ERSTSZ: usize = 0x28; +const RT_ERSTBA: usize = 0x30; // 64-bit +const RT_ERDP: usize = 0x38; // 64-bit + +const CMD_RUN: u32 = 1 << 0; +const CMD_RESET: u32 = 1 << 1; +const STS_HALT: u32 = 1 << 0; +const STS_CNR: u32 = 1 << 11; + +const CMD_RING_CYCLE: u32 = 1 << 0; + +// ---- TRB control-word bit layout ---- +const TRB_CYCLE: u32 = 1 << 0; +const TRB_IOC: u32 = 1 << 5; +const TRB_IDT: u32 = 1 << 6; +const TRB_TYPE_BITMASK: u32 = 0xfc00; // bits 10..15 +fn TYPE(t: u32) -> u32 { + t << 10 +} +fn SLOT_ID(p: u32) -> u32 { + (p & 0xff) << 24 +} +fn TRB_LEN(p: u32) -> u32 { + p & 0x1fffff +} +const TRB_DIR_IN: u32 = 1 << 16; +fn TRB_TX_TYPE(p: u32) -> u32 { + p << 16 +} +const TRB_DATA_OUT: u32 = 2; +const TRB_DATA_IN: u32 = 3; + +const TRB_NORMAL: u32 = 1; +const TRB_SETUP: u32 = 2; +const TRB_DATA: u32 = 3; +const TRB_STATUS: u32 = 4; +const TRB_ENABLE_SLOT: u32 = 9; +const TRB_ADDR_DEV: u32 = 11; +const TRB_CONFIG_EP: u32 = 12; +const TRB_EV_TRANSFER: u32 = 0x20; +const TRB_EV_CMD_COMPLETE: u32 = 0x21; + +const COMP_SUCCESS: u32 = 1; +const COMP_SHORT: u32 = 13; + +fn GET_COMP_CODE(d2: u32) -> u32 { + (d2 >> 24) & 0xff +} +fn TRB_TO_SLOT_ID(d3: u32) -> u32 { + (d3 >> 24) & 0xff +} +fn TRB_TO_EP_ID(d3: u32) -> u32 { + (d3 >> 16) & 0x1f +} +fn TRB_FIELD_TO_TYPE(d3: u32) -> u32 { + (d3 & TRB_TYPE_BITMASK) >> 10 +} + +// ---- context fields ---- +fn LAST_CTX(p: u32) -> u32 { + p << 27 +} +fn ROOTPORT(p: u32) -> u32 { + (p & 0xff) << 16 +} +fn SLOT_SPEED_X(p: u32) -> u32 { + (p & 0xf) << 20 +} +fn EP_TYPE(p: u32) -> u32 { + p << 3 +} +fn MAX_PACKET(p: u32) -> u32 { + (p & 0xffff) << 16 +} +fn ERROR_COUNT(p: u32) -> u32 { + (p & 0x3) << 1 +} +fn EP_INTERVAL(p: u32) -> u32 { + (p & 0xff) << 16 +} +fn EP_AVG(p: u32) -> u32 { + p & 0xffff +} +const CTRL_EP: u32 = 4; +const INT_IN_EP: u32 = 7; + +// ---- PORTSC ---- +const PORT_CONNECT: u32 = 1 << 0; +const PORT_PE: u32 = 1 << 1; +const PORT_RESET: u32 = 1 << 4; +const PORT_POWER: u32 = 1 << 9; +fn DEV_SPEED(p: u32) -> u32 { + (p >> 10) & 0xf +} + +// ---- USB meta protocol ---- +const USB_DIR_IN: u8 = 0x80; +const USB_REQ_GET_DESCRIPTOR: u8 = 0x06; +const USB_REQ_SET_CONFIGURATION: u8 = 0x09; +const DT_DEVICE: u8 = 0x01; +const DT_CONFIG: u8 = 0x02; +const DT_INTERFACE: u8 = 0x04; +const DT_ENDPOINT: u8 = 0x05; +const HID_CLASS: u8 = 0x03; + +// --------------------------------------------------------------------------- +// State (cooperative single-task kernel → unsafe statics) +// --------------------------------------------------------------------------- +static mut M: usize = 0; // mmio base +static mut OPB: usize = 0; // operational base +static mut DBB: usize = 0; // doorbell base +static mut RTB: usize = 0; // runtime base +static mut CTRL_CS: usize = 32; // context size +static mut MAXPORTS: u32 = 0; + +struct Ring { + raw: *mut u32, + count: usize, // TRBs + idx: usize, // producer (command/transfer) or consumer (event) index + cyc: u32, // software cycle +} +const fn ri_empty(count: usize) -> Ring { + Ring { + raw: core::ptr::null_mut(), + count, + idx: 0, + cyc: 1, + } +} +static mut CMD: Ring = ri_empty(64); +static mut EP0: Ring = ri_empty(8); +static mut EPIN: Ring = ri_empty(8); +static mut EV: Ring = ri_empty(32); + +static mut DCBAA: *mut u64 = core::ptr::null_mut(); +static mut DEVCTX: *mut u8 = core::ptr::null_mut(); +static mut INCTX: *mut u8 = core::ptr::null_mut(); +static mut ERST: *mut u8 = core::ptr::null_mut(); + +static SLOT: AtomicU32 = AtomicU32::new(0); +static HID_EPID: AtomicU32 = AtomicU32::new(0); // device-context index of the IN endpoint +static mut HID_BUF: [u8; 8] = [0; 8]; +static mut HID_LEN: usize = 4; +static mut MPS0: u32 = 8; + +static mut GOT_REPORT: bool = false; +static mut QUEUED: bool = false; +static mut RBTN: u8 = 0; +static mut RDX: i8 = 0; +static mut RDY: i8 = 0; + +static INITOK: AtomicBool = AtomicBool::new(false); +static MOUSEPRESENT: AtomicBool = AtomicBool::new(false); + +fn cs() -> usize { + unsafe { CTRL_CS } +} + +// --------------------------------------------------------------------------- +// MMIO accessors +// --------------------------------------------------------------------------- +unsafe fn rd(a: usize) -> u32 { + unsafe { (a as *const u32).read_volatile() } +} +unsafe fn wr(a: usize, v: u32) { + unsafe { (a as *mut u32).write_volatile(v) } +} +unsafe fn wr64(a: usize, v: u64) { + unsafe { + (a as *mut u32).write_volatile(v as u32); + (a as *mut u32).add(1).write_volatile((v >> 32) as u32); + let _ = rd(a); + } +} +fn op(off: usize) -> usize { + unsafe { OPB + off } +} +fn db(slot: u32) -> usize { + unsafe { DBB }.wrapping_add(slot as usize * 4) +} +fn rt(off: usize) -> usize { + unsafe { RTB + off } +} + +fn dma_alloc(bytes: usize, align: usize) -> *mut u8 { + unsafe { + let layout = alloc::alloc::Layout::from_size_align(bytes.max(1), align).unwrap(); + let p = alloc::alloc::alloc(layout); + if !p.is_null() { + core::ptr::write_bytes(p, 0, bytes.max(1)); + } + p + } +} + +// --------------------------------------------------------------------------- +// Ring primitives +// --------------------------------------------------------------------------- +unsafe fn trb_push(r: &mut Ring, d0: u32, d1: u32, d2: u32, d3: u32) { + unsafe { + let s = r.raw.add((r.idx % r.count) * 4); + *s.add(0) = d0; + *s.add(1) = d1; + *s.add(2) = d2; + *s.add(3) = (d3 & !TRB_CYCLE) | r.cyc; + r.idx += 1; + if r.idx.is_multiple_of(r.count) { + r.cyc ^= 1; + } + } +} + +// --------------------------------------------------------------------------- +// Event ring: drain produced events, calling handle(d0,d1,d2,d3) per event, +// then advance ERDP to the consumption point so the controller reuses it. +// --------------------------------------------------------------------------- +unsafe fn drain_events(handle: &mut dyn FnMut(u32, u32, u32, u32)) { + unsafe { + let ev = &mut *core::ptr::addr_of_mut!(EV); + let mut consumed = 0usize; + loop { + let idx = ev.idx % ev.count; + let s = ev.raw.add(idx * 4); + let d3 = s.add(3).read(); + if (d3 & 1) != ev.cyc { + break; + } + let (d0, d1, d2) = (s.add(0).read(), s.add(1).read(), s.add(2).read()); + handle(d0, d1, d2, d3); + ev.idx += 1; + consumed += 1; + if ev.idx.is_multiple_of(ev.count) { + ev.cyc ^= 1; + } + } + if consumed > 0 { + let idx = ev.idx % ev.count; + wr64(rt(RT_ERDP), ev.raw.add(idx * 4) as u64); + } + } +} + +/// Wait for a command-completion event; returns (code, slot). +fn wait_cmd() -> (u32, u32) { + let mut code = 0u32; + let mut slot = 0u32; + let mut done = false; + let mut seen = 0u32; + let mut raw: [u32; 4] = [0; 4]; + for _ in 0..4_000_000 { + if done { + break; + } + unsafe { + drain_events(&mut |d0, d1, d2, d3| { + seen += 1; + if TRB_FIELD_TO_TYPE(d3) == TRB_EV_CMD_COMPLETE { + code = GET_COMP_CODE(d2); + slot = TRB_TO_SLOT_ID(d3); + raw = [d0, d1, d2, d3]; + done = true; + } + }); + } + core::hint::spin_loop(); + } + if code != COMP_SUCCESS { + unsafe { + let evidx = core::ptr::addr_of!(EV).cast::().read().idx; + crate::serial_println!( + "usb: cmd-complete code={} slot={} seen={} evidx={}", + code, slot, seen, evidx + ); + for i in 0..4 { + let s = EV.raw.add(i * 4); + crate::serial_println!( + " ev[{i}] {:08x} {:08x} {:08x} {:08x}", + s.add(0).read(), s.add(1).read(), s.add(2).read(), s.add(3).read() + ); + } + } + } + (code, slot) +} + +/// Enqueue a command TRB and ring the host command doorbell. +fn issue_cmd(d3: u32) -> (u32, u32) { + issue_cmd_ptr(d3, 0) +} + +/// Enqueue a command TRB that carries a 64-bit parameter (used by +/// Address-Device / Configure-Endpoint for the input-context pointer). +fn issue_cmd_ptr(d3: u32, ptr: usize) -> (u32, u32) { + unsafe { + trb_push( + &mut *core::ptr::addr_of_mut!(CMD), + ptr as u32, + (ptr as u64 >> 32) as u32, + 0, + d3, + ); + wr(db(0), 0); + let _ = rd(db(0)); + } + wait_cmd() +} + +/// Wait for a transfer-complete event on the given device-context endpoint id. +fn wait_xfer(epid: u32) -> u32 { + let mut code = 0u32; + let mut done = false; + let sl = SLOT.load(Ordering::Relaxed); + for _ in 0..4_000_000 { + if done { + break; + } + unsafe { + drain_events(&mut |_, _, d2, d3| { + if TRB_FIELD_TO_TYPE(d3) == TRB_EV_TRANSFER + && TRB_TO_SLOT_ID(d3) == sl + && TRB_TO_EP_ID(d3) == epid + { + code = GET_COMP_CODE(d2); + done = true; + } + }); + } + core::hint::spin_loop(); + } + code +} + +// --------------------------------------------------------------------------- +// Control transfer on EP0 (setup + optional data + status), per Linux +// xhci_queue_ctrl_tx. Returns OK(len) or the failure completion code. +// --------------------------------------------------------------------------- +fn ctrl_xfer( + bmreq: u8, + breq: u8, + wval: u16, + widx: u16, + buf: *mut u8, + buflen: u32, + dir_in: bool, +) -> Result { + let slot = SLOT.load(Ordering::Relaxed); + let e0 = unsafe { &mut *core::ptr::addr_of_mut!(EP0) }; + unsafe { + // Setup stage (immediate data, 8-byte setup packet in d0/d1). + let tx = if buflen > 0 { + TRB_TX_TYPE(if dir_in { TRB_DATA_IN } else { TRB_DATA_OUT }) + } else { + 0 + }; + trb_push( + e0, + (wval as u32) << 16 | (breq as u32) << 8 | bmreq as u32, + (widx as u32) | buflen << 16, + TRB_LEN(8), + TYPE(TRB_SETUP) | TRB_IDT | tx, + ); + if buflen > 0 { + trb_push( + e0, + buf as u32, + (buf as usize >> 32) as u32, + TRB_LEN(buflen), + TYPE(TRB_DATA) | if dir_in { TRB_DIR_IN } else { 0 }, + ); + } + // Status stage. + let stat_dir = if buflen > 0 && dir_in { 0 } else { TRB_DIR_IN }; + trb_push(e0, 0, 0, 0, TYPE(TRB_STATUS) | TRB_IOC | stat_dir); + wr(db(slot), 1); // ring EP0 (doorbell target value 1) + let _ = rd(db(slot)); + } + let code = wait_xfer(1); + if code == COMP_SUCCESS || code == COMP_SHORT { + Ok(buflen as usize) + } else { + Err(code) + } +} + +// --------------------------------------------------------------------------- +// Context construction helpers +// --------------------------------------------------------------------------- +fn build_addr_ictx(port_speed: u32, port_num: u32) { + unsafe { + core::ptr::write_bytes(INCTX, 0, 32 + 8 * cs()); + let cc = INCTX as *mut u32; + *cc.add(0) = 0; // Drop flags + *cc.add(1) = (1 << 0) | (1 << 1); // Add slot + EP0 + let sc = INCTX.offset(32) as *mut u32; + *sc.add(0) = LAST_CTX(1) | SLOT_SPEED_X(port_speed); + *sc.add(1) = ROOTPORT(port_num + 1); + let e0 = INCTX.add(32 + cs()) as *mut u32; + *e0.add(0) = 0; + *e0.add(1) = EP_TYPE(CTRL_EP) | MAX_PACKET(MPS0) | ERROR_COUNT(3); + (INCTX.add(32 + cs() + 8) as *mut u64).write_volatile((EP0.raw as u64) | 1); + (INCTX.add(32 + cs() + 16) as *mut u32).write_volatile(EP_AVG(8)); + } +} + +// --------------------------------------------------------------------------- +// Port reset +// --------------------------------------------------------------------------- +unsafe fn reset_port(port: u32) -> Result { + unsafe { + let psc = op(OP_PORTSC) + (port as usize) * 0x10; + // Ensure powered. + wr(psc, rd(psc) | PORT_POWER); + for _ in 0..1000 { + core::hint::spin_loop(); + } + // Assert reset, wait for the hardware to clear it (reset done). + wr(psc, rd(psc) | PORT_RESET); + for _ in 0..4_000_000 { + if rd(psc) & PORT_RESET == 0 { + break; + } + core::hint::spin_loop(); + } + // Wait for the port to be enabled. + for _ in 0..4_000_000 { + if rd(psc) & PORT_PE != 0 { + break; + } + core::hint::spin_loop(); + } + if rd(psc) & PORT_PE == 0 { + return Err(()); + } + Ok(DEV_SPEED(rd(psc))) + } +} + +// --------------------------------------------------------------------------- +// Enumeration + HID setup +// --------------------------------------------------------------------------- +fn read_device_descriptor() -> Result<[u8; 18], u32> { + let mut d = [0u8; 18]; + ctrl_xfer( + USB_DIR_IN, + USB_REQ_GET_DESCRIPTOR, + (DT_DEVICE as u16) << 8, + 0, + d.as_mut_ptr(), + 18, + true, + )?; + Ok(d) +} + +fn read_config_descriptor(buf: &mut [u8]) -> Result { + let total = ctrl_xfer( + USB_DIR_IN, + USB_REQ_GET_DESCRIPTOR, + (DT_CONFIG as u16) << 8, + 0, + buf.as_mut_ptr(), + buf.len() as u32, + true, + )?; + Ok(total) +} + +/// Walk a config descriptor tree looking for a HID interrupt IN endpoint +/// with an interrupt (transfer type 3). Returns (mps, interval, eepid). +fn find_hid_mouse(cfg: &[u8]) -> Option<(u32, u32, u32)> { + let mut i = 0usize; + let mut in_iface = false; + while i + 2 <= cfg.len() { + let bLen = cfg[i] as usize; + if bLen == 0 { + break; + } + let btype = cfg[i + 1]; + if btype == DT_INTERFACE && i + 9 <= cfg.len() { + // interface descriptor: +5 = bInterfaceClass + in_iface = cfg[i + 5] == HID_CLASS; + } else if btype == DT_ENDPOINT && in_iface && i + 7 <= cfg.len() { + let addr = cfg[i + 2]; + let attrs = cfg[i + 3]; + let dir_in = addr & 0x80 != 0; + let xfer = attrs & 0x03; + if dir_in && xfer == 3 { + let mps = (cfg[i + 4] as u32) | ((cfg[i + 5] as u32) << 8); + let interval = cfg[i + 6] as u32; + let epnum = (addr & 0x0f) as u32; + let euid = 2 * epnum + 1; // device-context index (IN) + return Some((mps, interval, euid)); + } + } + i += bLen; + } + None +} + +// --------------------------------------------------------------------------- +// Bring one port's device through address + configure to an HID mouse +// --------------------------------------------------------------------------- +fn enumerate(port: u32, speed: u32) -> bool { + let _ = DEV_SPEED; + // 1. Enable slot. + let (code, slotid) = issue_cmd(TYPE(TRB_ENABLE_SLOT)); + if code != COMP_SUCCESS || slotid == 0 { + crate::serial_println!("usb: enable-slot failed code={} slot={}", code, slotid); + return false; + } + SLOT.store(slotid, Ordering::Relaxed); + unsafe { + *DCBAA.add(slotid as usize) = DEVCTX as u64; + } + // 2. Address device. + build_addr_ictx(speed, port); + let (code, _) = issue_cmd_ptr(TYPE(TRB_ADDR_DEV) | SLOT_ID(slotid), unsafe { INCTX } as usize); + if code != COMP_SUCCESS { + crate::serial_println!("usb: address-device failed code={}", code); + return false; + } + // 3. Read the device descriptor (picks up bMaxPacketSize0). + match read_device_descriptor() { + Ok(d) => { + let mps = d[7]; + if mps != 0 { + unsafe { + MPS0 = mps as u32; + } + } + crate::serial_println!( + "usb: dev {:04x}:{:04x} class={:#x} mps0={}", + u16::from_le_bytes([d[8], d[9]]), + u16::from_le_bytes([d[10], d[11]]), + d[4], + d[7] + ); + } + Err(c) => crate::serial_println!("usb: get-device-desc failed code={}", c), + } + // 4. Read the whole config descriptor and find the HID mouse IN ep. + let mut cfg = [0u8; 256]; + let got = match read_config_descriptor(&mut cfg) { + Ok(n) => n, + Err(c) => { + crate::serial_println!("usb: get-config failed code={}", c); + return false; + } + }; + let _ = got; + match find_hid_mouse(&cfg) { + Some((mps, interval, euid)) => { + crate::serial_println!( + "usb: HID mouse IN ep mps={} interval={} euid={}", + mps, + interval, + euid + ); + // 5. Set configuration = 1 (first config value present). + let cfgval = cfg[5]; + if cfgval == 0 { + return false; + } + if read_configuration(cfgval).is_err() { + return false; + } + // 6. Configure the IN endpoint. + if !configure_hid_endpoint(euid, mps, interval) { + return false; + } + HID_EPID.store(euid, Ordering::Relaxed); + unsafe { + HID_LEN = mps as usize; + QUEUED = false; + } + // 7. Arm the first IN read. + queue_report(); + MOUSEPRESENT.store(true, Ordering::Relaxed); + crate::serial_println!( + "usb: HID boot mouse armed (slot {})", + SLOT.load(Ordering::Relaxed) + ); + true + } + None => { + crate::serial_println!("usb: no HID mouse endpoint in config"); + false + } + } +} + +fn read_configuration(val: u8) -> Result<(), u32> { + ctrl_xfer( + 0, + USB_REQ_SET_CONFIGURATION, + val.into(), + 0, + core::ptr::null_mut(), + 0, + false, + ) + .map(|_| ()) +} + +fn configure_hid_endpoint(euid: u32, mps: u32, interval: u32) -> bool { + unsafe { + let slot = SLOT.load(Ordering::Relaxed); + core::ptr::write_bytes(INCTX, 0, 32 + 8 * cs()); + let cc = INCTX as *mut u32; + *cc.add(0) = 0; // Drop flags + *cc.add(1) = (1 << 0) | (1 << euid); // Add slot + this EP + // Input slot context: copy the output (post-address) slot context so + // route/speed/rootport are preserved, then bump LAST_CTX to the ep. + let osc = DEVCTX.offset(0).cast::(); + let sc = INCTX.offset(32).cast::(); + *sc.add(0) = (*osc.add(0) & !LAST_CTX(31)) | LAST_CTX(euid); + *sc.add(1) = *osc.add(1); + // Endpoint IN context. + let ep = INCTX.add(32 + (euid as usize) * cs()).cast::(); + *ep.add(0) = EP_INTERVAL(interval); + *ep.add(1) = EP_TYPE(INT_IN_EP) | MAX_PACKET(mps) | ERROR_COUNT(3); + *ep.add(4) = EP_AVG(mps); + let deq = EPIN.raw as u64 | 1; + *ep.offset(2).cast::() = deq; + let (code, _) = issue_cmd_ptr(TYPE(TRB_CONFIG_EP) | SLOT_ID(slot), INCTX as usize); + code == COMP_SUCCESS + } +} + +fn queue_report() { + let slot = SLOT.load(Ordering::Relaxed); + let epid = HID_EPID.load(Ordering::Relaxed); + let epin = unsafe { &mut *core::ptr::addr_of_mut!(EPIN) }; + unsafe { + let b = core::ptr::addr_of_mut!(HID_BUF) as *mut u8; + trb_push( + epin, + b as u32, + (b as usize >> 32) as u32, + TRB_LEN(HID_LEN as u32), + TYPE(TRB_NORMAL) | TRB_IOC, + ); + wr(db(slot), epid); + let _ = rd(db(slot)); + QUEUED = true; + } +} + +// --------------------------------------------------------------------------- +// Controller bring-up +// --------------------------------------------------------------------------- +fn find_xhci() -> Option<(u8, u8, u8)> { + for d in crate::pci::probe() { + if d.class == 0x0C && d.subclass == 0x03 && d.prog_if == 0x30 { + return Some((d.bus, d.device, d.func)); + } + } + None +} + +pub fn init() -> bool { + let Some((b, dev, func)) = find_xhci() else { + crate::serial_println!("usb: no xHCI controller"); + return false; + }; + crate::serial_println!("usb: xHCI at {:02x}:{:02x}.{}", b, dev, func); + + // Enable MMIO + bus-mastering on the PCI command register. + unsafe { + let cmd = crate::pci::config_read(b, dev, func, 0x04); + crate::pci::config_write( + b, + dev, + func, + 0x04, + (cmd & 0xFFFF0000) | ((cmd & 0xFFFF) | 0x6), + ); + let bar = crate::pci::bar64(b, dev, func, 0); + let base = bar.unwrap() as usize; + M = base; + let caplen = rd(base + CAP_CAPLENGTH); + let hcs1 = rd(base + HCSPARAMS1); + let hcc = rd(base + HCCPARAMS1); + crate::serial_println!( + "usb: mmio={:#x} caplen={:#x} hcs1={:#x} hcc1={:#x}", + base, caplen, hcs1, hcc + ); + OPB = base + (caplen & 0xff) as usize; + DBB = base + rd(base + DBOFF) as usize; + RTB = base + rd(base + RTSOFF) as usize; + MAXPORTS = (hcs1 >> 24) & 0xff; // MaxPorts is bits 31:24 (bits 23:16 are MaxIntrs) + let _ = hcc; + CTRL_CS = if hcc & (1 << 2) != 0 { 64 } else { 32 }; + } + + // Reset the controller (stop, then reset). Wait for reset completion + // on the CMD_RESET bit clearing (per Linux xhci_reset), then for the + // Controller-Not-Ready flag to clear before any doorbell/op-register + // activity. + unsafe { + wr(op(OP_USBCMD), rd(op(OP_USBCMD)) & !CMD_RUN); + // Wait for halt. + let mut t = 0; + while t < 1_000_000 && rd(op(OP_USBSTS)) & STS_HALT == 0 { + t += 1; + core::hint::spin_loop(); + } + wr(op(OP_USBCMD), rd(op(OP_USBCMD)) | CMD_RESET); + t = 0; + while t < 10_000_000 && rd(op(OP_USBCMD)) & CMD_RESET != 0 { + t += 1; + core::hint::spin_loop(); + } + t = 0; + while t < 10_000_000 && rd(op(OP_USBSTS)) & STS_CNR != 0 { + t += 1; + core::hint::spin_loop(); + } + } + + // Allocate rings / contexts. + unsafe { + CMD.raw = dma_alloc(64 * 16, 64) as *mut u32; + EV.raw = dma_alloc(32 * 16, 64) as *mut u32; + EP0.raw = dma_alloc(8 * 16, 64) as *mut u32; + EPIN.raw = dma_alloc(8 * 16, 64) as *mut u32; + ERST = dma_alloc(16, 64); + DCBAA = dma_alloc(64 * 8, 64) as *mut u64; + DEVCTX = dma_alloc((1 + 8) * cs(), 64); + INCTX = dma_alloc(32 + 8 * cs(), 64); + // ERST segment 0 -> event ring. + let e = ERST as *mut u32; + *e.add(0) = EV.raw as u32; + *e.add(1) = (EV.raw as usize >> 32) as u32; + *e.add(2) = 32; // segment size (TRBs) + } + + // Program registers. + unsafe { + wr64(op(OP_DCBAAP), DCBAA as u64); + wr64( + op(OP_CRCR), + ((CMD.raw as u64) & !0x3f) | CMD_RING_CYCLE as u64, + ); + // MaxSlotsEn = 1 (we use a single slot). + wr(op(OP_CONFIG), (rd(op(OP_CONFIG)) & !0xff) | 1); + // Event ring. + wr(rt(RT_ERSTSZ), 1); + wr64(rt(RT_ERSTBA), ERST as u64); + wr64(rt(RT_ERDP), EV.raw as u64); + // Interrupter 0: clear pending, enable. + wr(rt(RT_IMAN), rd(rt(RT_IMAN)) | 0x2); + wr(rt(RT_IMOD), 0); + // Run. + wr(op(OP_USBCMD), rd(op(OP_USBCMD)) | CMD_RUN); + let mut t = 0; + while t < 4_000_000 && rd(op(OP_USBSTS)) & STS_HALT != 0 { + t += 1; + core::hint::spin_loop(); + } + if rd(op(OP_USBSTS)) & STS_HALT != 0 { + crate::serial_println!("usb: controller failed to start"); + return false; + } + } + INITOK.store(true, Ordering::Relaxed); + crate::serial_println!( + "usb: controller up ({} ports) — mouse discovery deferred to cursor task", + unsafe { MAXPORTS } + ); + true +} + +/// Bitmask of root ports whose attempt already failed; skip them so we don't +/// re-reset the port on every poll. +static DISCOVERED_TRIED: AtomicU32 = AtomicU32::new(0); + +/// Periodic, cheap scan called from the cursor task until a HID mouse is +/// found. A USB device can finish connecting long after the controller +/// starts, so discovery is driven asynchronously rather than blocking boot. +fn maybe_discover() { + if MOUSEPRESENT.load(Ordering::Relaxed) { + return; + } + let maxports = unsafe { MAXPORTS }; + for p in 0..maxports { + if DISCOVERED_TRIED.load(Ordering::Relaxed) & (1 << p) != 0 { + continue; + } + unsafe { + let psc = op(OP_PORTSC) + (p as usize) * 0x10; + // Only touch ports that are both connected and not yet enabled. + if rd(psc) & (PORT_CONNECT | PORT_PE) == PORT_CONNECT { + crate::serial_println!("usb: port {} connect (speed {})", p, DEV_SPEED(rd(psc))); + if let Ok(speed) = reset_port(p) + && enumerate(p, speed) { + return; + } + DISCOVERED_TRIED.fetch_or(1 << p, Ordering::Relaxed); + } + } + } +} + +pub fn mouse_present() -> bool { + MOUSEPRESENT.load(Ordering::Relaxed) +} + +/// True once the xHCI controller is brought up (regardless of whether a +/// device has attached yet). The cursor task is spawned as long as this is +/// set so async mouse discovery can run. +pub fn controller_up() -> bool { + INITOK.load(Ordering::Relaxed) +} + +/// Reset discovery state (used by the `usb` shell command to rescan). +pub fn rescan() { + DISCOVERED_TRIED.store(0, Ordering::Relaxed); +} + +/// Non-blocking poll: drain the event ring, then decode one mouse report if +/// one arrived (re-arming the IN transfer so reports keep flowing). +/// Returns `None` when no report is pending (or no mouse). +pub fn poll_mouse() -> Option { + // Drive discovery while no device is attached, then drain reports. + if !MOUSEPRESENT.load(Ordering::Relaxed) { + if INITOK.load(Ordering::Relaxed) { + maybe_discover(); + } + return None; + } + let epid = HID_EPID.load(Ordering::Relaxed); + let sl = SLOT.load(Ordering::Relaxed); + unsafe { + drain_events(&mut |_, _, d2, d3| { + if TRB_FIELD_TO_TYPE(d3) == TRB_EV_TRANSFER + && TRB_TO_SLOT_ID(d3) == sl + && TRB_TO_EP_ID(d3) == epid + { + let c = GET_COMP_CODE(d2); + if c == COMP_SUCCESS || c == COMP_SHORT { + // A fresh report sits in HID_BUF. + let p = core::ptr::addr_of!(HID_BUF) as *const u8; + RBTN = core::ptr::read_volatile(p); + RDX = core::ptr::read_volatile(p.add(1)) as i8; + RDY = core::ptr::read_volatile(p.add(2)) as i8; + GOT_REPORT = true; + QUEUED = false; + } + } + }); + if GOT_REPORT { + GOT_REPORT = false; + let ev = Some(MouseEvent { + dx: RDX as i16, + dy: RDY as i16, + left: RBTN & 0x01 != 0, + right: RBTN & 0x02 != 0, + middle: RBTN & 0x04 != 0, + }); + queue_report(); // re-arm for the next report + return ev; + } + if !QUEUED { + queue_report(); // keep exactly one IN read outstanding + } + } + None +} + +/// One-line status for the shell's `usb` command. +pub fn diagnose() -> alloc::string::String { + let mut s = alloc::string::String::new(); + s.push_str(&alloc::format!( + "xhci mmio={:#x} mapped={} ports={} mouse={} slot={}\n", + unsafe { M }, + if crate::paging::is_mapped(unsafe { M }) { "yes" } else { "no" }, + unsafe { MAXPORTS }, + if MOUSEPRESENT.load(Ordering::Relaxed) { + "yes" + } else { + "no" + }, + SLOT.load(Ordering::Relaxed) + )); + unsafe { + // Page-table walk for the MMIO base (physical == virtual here). + let phys = M; + let pml4 = crate::paging::cr3() as *const u64; + let l4 = (phys >> 39) & 0x1FF; + let l3 = (phys >> 30) & 0x1FF; + let l2 = (phys >> 21) & 0x1FF; + let l1 = (phys >> 12) & 0x1FF; + let pml4e = pml4.add(l4).read_volatile(); + let pdpte = if pml4e & 1 != 0 { + ((pml4e & 0x000f_ffff_ffff_f000) as *const u64).add(l3).read_volatile() + } else { 0 }; + let pde = if pdpte & 1 != 0 && pdpte & (1 << 7) == 0 { + ((pdpte & 0x000f_ffff_ffff_f000) as *const u64).add(l2).read_volatile() + } else { 0 }; + let pte = if pde & 1 != 0 && pde & (1 << 7) == 0 { + ((pde & 0x000f_ffff_ffff_f000) as *const u64).add(l1).read_volatile() + } else { 0 }; + s.push_str(&alloc::format!( + " pt: pml4e={:#018x} pdpte={:#018x} pde={:#018x} pte={:#018x}\n", + pml4e, pdpte, pde, pte + )); + for p in 0..8 { + let psc = op(OP_PORTSC) + (p as usize) * 0x10; + let v = rd(psc); + s.push_str(&alloc::format!(" port{}: {:#010x}\n", p, v)); + } + } + if MOUSEPRESENT.load(Ordering::Relaxed) { + s.push_str(&alloc::format!( + " hid ep-index={} mps={} armed={}\n", + HID_EPID.load(Ordering::Relaxed), + unsafe { HID_LEN }, + unsafe { QUEUED } + )); + } + s +} -- cgit v1.2.3