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 --- rsnix/src/ahci.rs | 7 +- rsnix/src/cursor.rs | 6 +- rsnix/src/ehci.rs | 1274 +++++++++++++++++++++++++++++++++++++++++++++++ rsnix/src/gop.rs | 18 +- rsnix/src/interrupts.rs | 36 +- rsnix/src/klog.rs | 10 + rsnix/src/lib.rs | 2 + rsnix/src/memory.rs | 44 ++ rsnix/src/pci.rs | 125 ++++- rsnix/src/ps2.rs | 78 ++- rsnix/src/sched.rs | 103 +++- rsnix/src/tty.rs | 28 +- rsnix/src/uhci.rs | 1129 +++++++++++++++++++++++++++++++++++++++++ rsnix/src/usb.rs | 147 +++++- rsnix/src/vt.rs | 51 +- 15 files changed, 3020 insertions(+), 38 deletions(-) create mode 100644 rsnix/src/ehci.rs create mode 100644 rsnix/src/uhci.rs (limited to 'rsnix') diff --git a/rsnix/src/ahci.rs b/rsnix/src/ahci.rs index a77b341..543bb21 100644 --- a/rsnix/src/ahci.rs +++ b/rsnix/src/ahci.rs @@ -230,9 +230,12 @@ pub fn init() -> bool { let Some((dev, func)) = find_ahci() else { return false; }; - // Enable memory space + bus mastering so MMIO and DMA work. + // Enable memory space + bus mastering so MMIO and DMA work, plus INTx + // disable (bit 10): the driver polls command completion (no ISR, no + // MSI), so the HBA must not assert INTx — a stray SATA interrupt would + // re-fire forever on real hardware. let cmd_reg = pci::config_read(0, dev, func, 0x04); - pci::config_write(0, dev, func, 0x04, cmd_reg | 0x7); + pci::config_write(0, dev, func, 0x04, cmd_reg | 0x7 | 0x400); let Some(abar) = pci::bar64(0, dev, func, 5) else { return false; diff --git a/rsnix/src/cursor.rs b/rsnix/src/cursor.rs index d9d97bc..31b919e 100644 --- a/rsnix/src/cursor.rs +++ b/rsnix/src/cursor.rs @@ -50,7 +50,11 @@ fn forward_target() -> Option { fn task_main() { loop { - while let Some(ev) = crate::usb::poll_mouse().or_else(crate::ps2::try_poll_mouse) { + while let Some(ev) = crate::usb::poll_mouse() + .or_else(crate::ehci::poll_mouse) + .or_else(crate::uhci::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/ehci.rs b/rsnix/src/ehci.rs new file mode 100644 index 0000000..6209155 --- /dev/null +++ b/rsnix/src/ehci.rs @@ -0,0 +1,1274 @@ +// --------------------------------------------------------------------------- +// rsdos EHCI (USB2.0 host controller) driver — mouse only. +// +// Enumerates devices connected to an Intel-class EHCI (direct root-port +// connect), discovers a HID boot-mouse interrupt-IN endpoint, and drains its +// reports into ps2::MouseEvent. Uses the EHCI async schedule for control +// transfers and the periodic (frame-list) schedule for the interrupt IN +// endpoint. Polled: no IRQ/MSI — keep exactly one report outstanding and +// drain it from the cursor task. +// +// Why EHCI: on real hardware (e.g. a Q67/Q77-era OptiPlex) a USB mouse is a +// USB2 (full/low-speed) device attached to the EHCI USB2 controllers, not to +// the xHCI's USB3/SS ports. QEMU only exercises the xHCI path, so this class +// of real hardware requires its own driver. +// +// Identity map (phys==virt) means any kernel-heap pointer is a valid DMA +// address; data structures must be 32-byte aligned for the EHCI. +// --------------------------------------------------------------------------- + +use crate::ps2::MouseEvent; +use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; + +// ---- Registers (host controller registers; opbase = base + CAPLENGTH) ---- +const OPCMD: usize = 0x00; +const OPSTS: usize = 0x04; +const OPINTR: usize = 0x08; +const OPFRIDX: usize = 0x0C; +const OPCFGFLAG: usize = 0x40; +const OP_PRTSC_BASE: usize = 0x44; // PORTSC #1 +const OP_PRTSC_STRIDE: usize = 0x04; + +const CMD_RUN: u32 = 1 << 0; +const CMD_HCRESET: u32 = 1 << 1; +const CMD_PSE: u32 = 1 << 4; +const CMD_ASE: u32 = 1 << 5; + +const STS_HCH: u32 = 1 << 12; +const STS_SE: u32 = 1 << 4; +const STS_RECL: u32 = 1 << 14; + +const PRT_CONNECT: u32 = 1 << 0; +const PRT_PED: u32 = 1 << 1; +const PRT_RESET: u32 = 1 << 8; +const PRT_POWER: u32 = 1 << 12; +const PRT_RWC: u32 = (1 << 17) | (1 << 18) | (1 << 16) | (1 << 20); // CSC|PEC|CRC|OCC + +// ---- QH / QTD fields ---- +const Q_TYPE_QH: u32 = 1 << 1; +const QT_TERMINATE: u32 = 1; // terminate = 1 in a link pointer +const QH_HEAD: u32 = 1 << 15; +const QH_CONTROL_EP: u32 = 1 << 27; +const QH_DTC: u32 = 1 << 14; // data toggle comes from the qTDs (control endpoints) +const QH_RL: u32 = 4 << 28; // NAK reload count (Linux EHCI_TUNE_RL_{HS,TT}) +const QH_EPS_HIGH: u32 = 2 << 12; +const QH_EPS_FULL: u32 = 0 << 12; +const QH_EPS_LOW: u32 = 1 << 12; +const QH_MULT: u32 = 1 << 30; // MULT=1: one split transaction per uframe + +const TOK_IOC: u32 = 1 << 15; +const TOK_ACTIVE: u32 = 1 << 7; +const TOK_HALT: u32 = 1 << 6; +const TOK_XACT: u32 = 1 << 3; +const TOK_MMF: u32 = 1 << 2; // missed microframe: split did not complete in its window +const PID_IN: u32 = 1; +const PID_SETUP: u32 = 2; + +const CCERR_3: u32 = 3 << 10; + +// ---- Static control state ---- +static EHCI_BASE: AtomicU32 = AtomicU32::new(0); +static N_PORTS: AtomicU32 = AtomicU32::new(0); +static CTRL_UP: AtomicBool = AtomicBool::new(false); +static MOUSE_PRES: AtomicBool = AtomicBool::new(false); +static DEV_ADDR: AtomicU32 = AtomicU32::new(0); +static HID_EP: AtomicU32 = AtomicU32::new(0); // EP number (1..15), IN +static HID_MPS: AtomicU32 = AtomicU32::new(0); +static ARMED: AtomicBool = AtomicBool::new(false); +// When the last enumerate attempt ran (ms): a failed/glitched mouse must +// never trigger a re-enumeration hot loop. +static LAST_ENUM: AtomicU32 = AtomicU32::new(0); +// Root ports observed in full/low-speed mode with a UHCI companion present. +// Those ports are owned by the companion controller — the EHCI can never +// talk to them, so they are skipped instead of failing a 5 s descriptor +// timeout every second (the "stuck in a loop" the OptiPlex showed). +static FS_LS_PORTS: AtomicU32 = AtomicU32::new(0); +// One-shot: the first transfer failure renders a diagnostic QR on screen +// (photograph it); retries keep dumping serial but must not re-render. +static QR_SHOWN: AtomicBool = AtomicBool::new(false); +// DEBUG: run the one-boot transfer variant matrix (real-hardware bisection +// of the EHCI System Error trigger). MATRIX_DONE gates it to one pass; +// XFER_TWEAK flips construction variables inside ctl_xfer. +static DEBUG_MATRIX: bool = true; +static MATRIX_DONE: AtomicBool = AtomicBool::new(false); +static XFER_TWEAK: AtomicU32 = AtomicU32::new(0); +// Low-region (<1 MB) control QH for the matrix's low-region variant. +static mut LOW_XFER: *mut u32 = core::ptr::null_mut(); + +// DMA buffers. Allocated once at init; base pointers recorded here. +static mut ASYNC_HEAD: *mut u32 = core::ptr::null_mut(); // QH (32 bytes min, align 32) +static mut XFER_QH: *mut u32 = core::ptr::null_mut(); // control QH +static mut FRAMELIST: *mut u32 = core::ptr::null_mut(); // 1024 entries, 4K aligned +static mut HID_QH: *mut u32 = core::ptr::null_mut(); + +// One dedicated 4 KiB DMA page for every transfer buffer (device descriptor, +// config descriptor, HID report). All DMA targets live in controller-visible +// pages — never in .bss adjacent to kernel statics — so an overrun (report +// longer than MPS, descriptor longer than requested, short-packet arithmetic +// slip) can only clobber spare DMA space, on QEMU or on real silicon. +static mut BUF_PAGE: *mut u8 = core::ptr::null_mut(); +const BUF_DEV_DESC: usize = 0x000; // 64 B incl. red zone +const BUF_CFG: usize = 0x100; // 320 B incl. red zone +const BUF_HID_REPORT: usize = 0x400; // 64 B incl. red zone + +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) } +} + +fn opbase() -> usize { + unsafe { + EHCI_BASE.load(Ordering::Relaxed) as usize + + (rd(EHCI_BASE.load(Ordering::Relaxed) as usize) & 0xff) as usize + } +} +fn op(off: usize) -> usize { + opbase() + off +} +fn prtsc(p: usize) -> usize { + opbase() + OP_PRTSC_BASE + p * OP_PRTSC_STRIDE +} + +// ---- QH/QTD helpers: all writes go through raw dword pointers ---- +fn qh_next(qh: *mut u32) -> *mut u32 { + qh.wrapping_add(0) +} +fn qh_info1(qh: *mut u32) -> *mut u32 { + qh.wrapping_add(1) +} +fn qh_info2(qh: *mut u32) -> *mut u32 { + qh.wrapping_add(2) +} +fn qh_current(qh: *mut u32) -> *mut u32 { + qh.wrapping_add(3) +} +fn qh_next_qtd(qh: *mut u32) -> *mut u32 { + qh.wrapping_add(4) +} +fn qh_alt_qtd(qh: *mut u32) -> *mut u32 { + qh.wrapping_add(5) +} +fn qh_token(qh: *mut u32) -> *mut u32 { + qh.wrapping_add(6) +} +fn qh_buf(qh: *mut u32) -> *mut u32 { + qh.wrapping_add(7) +} + +fn link_of(ptr: usize) -> u32 { + (ptr as u32) | Q_TYPE_QH +} + +unsafe fn wait_recl(timeout: usize) { + unsafe { + let t0 = crate::interrupts::uptime_ms(); + while t0 + timeout as u64 > crate::interrupts::uptime_ms() + && (rd(op(OPSTS)) & STS_RECL) == 0 + { + core::hint::spin_loop(); + } + } +} + +/// Issue a single control transfer on the async schedule. `dev` is the USB +/// device address (0 for the first GET_DESCRIPTOR), `mps` the EP0 max packet, +/// `speed` the device speed (0=full, 1=low, 2=high), `port` the root port +/// index (0-based; used only for full/low-speed split QHs), `req` the 8-byte +/// setup (bmRequestType,bRequest,..), `dir_in` true for a device->host data +/// stage, `buf/len` the data buffer. Returns bytes transferred, or a +/// negative error code (-1 timeout, -2 HALT, -3 XactErr, -4 missed uframe). +unsafe fn ctl_xfer( + dev: u32, + mps: u32, + speed: u32, + port: usize, + req: &[u8; 8], + dir_in: bool, + buf: *mut u8, + len: u32, +) -> i32 { + // DEBUG matrix hook: XFER_TWEAK bits flip construction variables. + // bit0: no-ASE arm; bit1: no-split fields; bit2: no qtd chain; + // bit3: zero-length (no DATA stage); bit6: short timeout (1.2 s). + let len = if XFER_TWEAK.load(Ordering::Relaxed) & 8 != 0 { + 0 + } else { + len + }; + unsafe { + let head = ASYNC_HEAD; + let tw0 = XFER_TWEAK.load(Ordering::Relaxed); + // DEBUG matrix: execute the same transfer from the low (<1 MB) page. + let xfer = if tw0 & 0x80 != 0 && !LOW_XFER.is_null() { + LOW_XFER + } else { + XFER_QH + }; + + // QH occupies dwords 0..11 (48 bytes) incl. the 5-dword buffer list. + // QEMU's NLPTR_GET masks qtd addresses to 32-byte alignment + // (& 0xffffffe0), so every qtd must sit on a 32-byte boundary. + // Place them at +0x40 / +0x60 / +0x80 (each 8 dwords = 32 bytes). + let st = xfer.add(0x40 / 4); // SETUP qtd at QH+0x40 (32-aligned) + let dt = st.add(0x20 / 4); // DATA qtd at QH+0x60 (32-aligned) + let sp = dt.add(0x20 / 4); // STATUS qtd at QH+0x80 (32-aligned) + + // SETUP token: toggle 0, len 8, PID_SETUP, active, IOC off. + core::ptr::write_volatile(st.add(0), 0u32); // next (chained below) + core::ptr::write_volatile(st.add(1), QT_TERMINATE); // alt: terminate + core::ptr::write_volatile( + st.add(2), + (8 << 16) | CCERR_3 | (PID_SETUP << 8) | TOK_ACTIVE, + ); + for i in 0..5 { + core::ptr::write_volatile(st.add(3 + i), 0u32); + } + // Setup packet buffer: 8 bytes at +0x110 (clear of the QH + qtds). + let sbuf = xfer.add(0x110 / 4); + let sb = core::slice::from_raw_parts_mut(sbuf.cast::(), 8); + sb.copy_from_slice(req); + core::ptr::write_volatile(st.add(3), sbuf as u32); + + // DATA qtd. EHCI token PID field (bits 9:8): OUT=0, IN=1, SETUP=2. + let data_pid_w = (if dir_in { 1u32 } else { 0u32 }) << 8; + if len == 0 { + // no data stage + core::ptr::write_volatile(dt.add(2), 0u32); + } else { + core::ptr::write_volatile(dt.add(0), sp as u32); + // altnext MUST be terminate (1): QEMU's ADVANCEQUEUE follows altnext + // whenever TBYTES != 0 after an IN (e.g. a short packet). altnext=0 + // is treated as *valid* (bit0==0) so the controller would jump to + // qtd address 0 and hang. Terminate keeps short transfers chained + // to the STATUS qtd, as required for control transfers. + core::ptr::write_volatile(dt.add(1), QT_TERMINATE); + core::ptr::write_volatile( + dt.add(2), + ((len & 0x7fff) << 16) | (1 << 31) | CCERR_3 | (data_pid_w) | TOK_ACTIVE, + ); + for i in 0..5 { + core::ptr::write_volatile(dt.add(3 + i), 0u32); + } + // data buffer + core::ptr::write_volatile(dt.add(3), buf as u32); + } + + // STATUS qtd: toggle 1, len 0, PID opposite of the data stage; for a + // zero-length control the status is opposite of the setup direction. + let setup_in = (req[0] & 0x80) != 0; + let status_pid_w = if len > 0 { + if dir_in { + 0u32 + } else { + 1u32 + } // opposite of data + } else if setup_in { + 0u32 + } else { + 1u32 + } << 8; + core::ptr::write_volatile(sp.add(0), 1u32); // terminate + core::ptr::write_volatile(sp.add(1), QT_TERMINATE); + core::ptr::write_volatile( + sp.add(2), + (1 << 31) | CCERR_3 | (status_pid_w) | TOK_ACTIVE | TOK_IOC, + ); + for i in 0..5 { + core::ptr::write_volatile(sp.add(3 + i), 0u32); + } + + // Chain the qtd list. + if len == 0 { + core::ptr::write_volatile(st.add(0), sp as u32); + } else { + core::ptr::write_volatile(st.add(0), dt as u32); + core::ptr::write_volatile(dt.add(0), sp as u32); + } + + // Configure the control QH (EP0). + let eps = match speed { + 2 => QH_EPS_HIGH, + 1 => QH_EPS_LOW, + _ => QH_EPS_FULL, + }; + let control_flag = if speed == 2 { 0 } else { QH_CONTROL_EP }; + // FS/LS split QH: DTC (toggles come from the qTDs), NAK reload, MULT=1, + // hub addr 0 (integrated root-hub TT) and the 1-based root port number + // in bits 29:23. The controller runs SSPLIT/CSPLIT pairs for every qTD + // on this QH; no S/C masks are needed on the async list (Linux and + // FreeBSD both leave them 0 for control/bulk). + let info1 = (mps & 0x7ff) << 16 | QH_RL | QH_DTC | control_flag | eps | dev; // devaddr in low 7 bits + *qh_info1(xfer) = info1; + *qh_info2(xfer) = if speed == 2 || XFER_TWEAK.load(Ordering::Relaxed) & 2 != 0 { + 0 + } else { + QH_MULT | (((port as u32) + 1) << 23) + }; + *qh_current(xfer) = 0; + *qh_next_qtd(xfer) = st as u32; + *qh_alt_qtd(xfer) = QT_TERMINATE; + *qh_token(xfer) = 0; + for i in 0..5 { + *qh_buf(xfer).add(i) = 0; + } + + // Link: head -> xfer -> head (reclamation cycle). + *qh_next(xfer) = link_of(head as usize); + *qh_next(head) = link_of(xfer as usize); + if XFER_TWEAK.load(Ordering::Relaxed) & 4 != 0 { + *qh_next_qtd(xfer) = 1; // no qtd chain: HC treats the QH as idle + } + + // Ensure the controller is running and both schedules enabled. + // Re-assert every transfer: QEMU may drop RUNSTOP, and re-setting the + // bits is what makes it (re)schedule the async walk. + // + // Clear the W1C status bits first so a failure dump reflects only + // this transfer's window (USBINT|USBERR|PCD|FR|SE). + wr(op(OPSTS), 0x1F); + // Fence: make all DMA writes globally visible before the controller reads. + core::sync::atomic::fence(core::sync::atomic::Ordering::SeqCst); + let tw = XFER_TWEAK.load(Ordering::Relaxed); + if tw & 0x20 != 0 { + // wire-off: disable the port so no transaction can reach the wire. + wr(prtsc(port), rd(prtsc(port)) & !(1 << 1)); // PED=0 + } + wr( + op(OPCMD), + rd(op(OPCMD)) | CMD_RUN | (if tw & 1 == 0 { CMD_ASE } else { 0 }), + ); // no PSE — periodic not needed for control + let ctl_timeout = if tw & 0x40 != 0 { 1200u64 } else { 5000u64 }; + // Poll for completion. Watch the whole chain: a HALT on the SETUP + // or DATA qtd stops the queue and the STATUS qtd would never clear + // (which previously read as a plain timeout). Exit early naming the + // failing stage, and on any failure dump the controller + queue state + // for real-hardware iteration. + let t0 = crate::interrupts::uptime_ms(); + let mut stage: i32 = 0; // 0=ok, 1=setup halted, 2=data halted, 3=timeout, 4=hc halted/SE + loop { + let stok = core::ptr::read_volatile(st.add(2).cast::()); + let ptok = core::ptr::read_volatile(sp.add(2).cast::()); + if ptok & TOK_ACTIVE == 0 { + break; // status completed + } + if stok & TOK_HALT != 0 { + stage = 1; + break; + } + if len > 0 { + let dtok = core::ptr::read_volatile(dt.add(2).cast::()); + if dtok & TOK_HALT != 0 { + stage = 2; + break; + } + } + // Controller-level fault: the HC halted itself (HCH) or threw a + // system error (SE). Polling further is pointless — the qtds + // would sit ACTIVE forever and read back as a plain timeout. + // Catching it here names the window precisely. + if rd(op(OPSTS)) & (STS_HCH | STS_SE) != 0 { + stage = 4; + break; + } + if t0 + ctl_timeout < crate::interrupts::uptime_ms() { + stage = 3; + break; + } + core::hint::spin_loop(); + } + if stage != 0 { + let stok = core::ptr::read_volatile(st.add(2).cast::()); + let dtok = if len > 0 { + core::ptr::read_volatile(dt.add(2).cast::()) + } else { + 0 + }; + let ptok = core::ptr::read_volatile(sp.add(2).cast::()); + crate::serial_println!( + "ehci: xfer fail stage={} (1=setup 2=data 3=timeout) speed={} dev={} dir_in={} len={}", + stage, + speed, + dev, + dir_in, + len + ); + let base = EHCI_BASE.load(Ordering::Relaxed) as usize; + crate::serial_println!( + "ehci: cmd={:#x} sts={:#x} prtsc{}={:#x} hostpc={:#x}", + rd(op(OPCMD)), + rd(op(OPSTS)), + port, + rd(prtsc(port)), + rd(base + 0x84 + port * 4) + ); + crate::serial_println!( + "ehci: qh i1={:#x} i2={:#x} cur={:#x} nqtd={:#x}", + core::ptr::read_volatile(qh_info1(xfer)), + core::ptr::read_volatile(qh_info2(xfer)), + core::ptr::read_volatile(qh_current(xfer)), + core::ptr::read_volatile(qh_next_qtd(xfer)) + ); + crate::serial_println!( + "ehci: tok setup={:#x} data={:#x} stat={:#x}", + stok, + dtok, + ptok + ); + if stage == 4 { + crate::serial_println!( + "ehci: HC halted/SE after {} ms since arm (frindex={:#x} asyncbase={:#x})", + crate::interrupts::uptime_ms().wrapping_sub(t0), + rd(op(OPFRIDX)), + rd(op(0x18)) + ); + } + // One-shot on-screen QR of the whole diagnostic: band text plus a + // scannable matrix below. Used on hardware with no serial pipe — + // point a phone at the screen. Lock the klog screen echo so the + // retry loop cannot overwrite it (serial keeps logging). + if !QR_SHOWN.swap(true, Ordering::Relaxed) { + let l1 = alloc::format!( + "EHCI ctl fail stage={} t={}ms speed={} dev={} in={} len={}", + stage, + crate::interrupts::uptime_ms().wrapping_sub(t0), + speed, + dev, + dir_in, + len + ); + let l2 = alloc::format!("cmd={:08x} sts={:08x}", rd(op(OPCMD)), rd(op(OPSTS))); + let l3 = alloc::format!( + "prtsc{}=0x{:08x} hp=0x{:08x}", + port, + rd(prtsc(port)), + rd(base + 0x84 + port * 4) + ); + let l4 = alloc::format!( + "qh1={:08x} qh2={:08x} cur={:08x} nqtd={:08x}", + core::ptr::read_volatile(qh_info1(xfer)), + core::ptr::read_volatile(qh_info2(xfer)), + core::ptr::read_volatile(qh_current(xfer)), + core::ptr::read_volatile(qh_next_qtd(xfer)) + ); + let l5 = alloc::format!("setup={:08x} data={:08x} stat={:08x}", stok, dtok, ptok); + let l6 = alloc::format!( + "dma a={:08x} x={:08x} f={:08x} h={:08x} b={:08x}", + ASYNC_HEAD as usize, + XFER_QH as usize, + FRAMELIST as usize, + HID_QH as usize, + BUF_PAGE as usize + ); + let all = [l1, l2, l3, l4, l5, l6]; + let refs: alloc::vec::Vec<&str> = all.iter().map(|s| s.as_str()).collect(); + crate::gop::dump_qr(&refs); + crate::gop::qr_lock(); + } + *qh_next(head) = link_of(head as usize); + wait_recl(100); + return match stage { + 1 => -3, // setup halted + 2 => -4, // data halted + _ => -1, // timeout + }; + } + // Status completed — check its error bits. + let tok = core::ptr::read_volatile(sp.add(2).cast::()); + if tok & TOK_HALT != 0 { + *qh_next(head) = link_of(head as usize); + wait_recl(100); + return -2; // HALT — qtd halted on error (babble/DBE/...) + } + if tok & TOK_XACT != 0 { + *qh_next(head) = link_of(head as usize); + wait_recl(100); + return -5; // XactErr — CRC/timeout on the transaction + } + if tok & TOK_MMF != 0 { + *qh_next(head) = link_of(head as usize); + wait_recl(100); + return -6; // missed microframe — split window missed (retryable) + } + // Bytes transferred = requested - remaining on the DATA qtd. + let mut got = len; + if len > 0 { + let dtok = core::ptr::read_volatile(dt.add(2).cast::()); + let rem = ((dtok >> 16) & 0x7fff) as u32; + got = len - rem.min(len); + } + // Unlink: idle self-loop must keep the QH type bit, otherwise QEMU + // parses it as an iTD and resets the HC ("non queue head request"). + *qh_next(head) = link_of(head as usize); + wait_recl(100); + let _ = qh_next_qtd; + got as i32 + } +} + +// Control-transfer convenience using the standard control request layout. +fn ctrl_req(bmreq: u8, breq: u8, wval: u16, widx: u16, wlen: u16) -> [u8; 8] { + [ + bmreq, + breq, + (wval & 0xff) as u8, + (wval >> 8) as u8, + (widx & 0xff) as u8, + (widx >> 8) as u8, + (wlen & 0xff) as u8, + (wlen >> 8) as u8, + ] +} + +// --------------------------------------------------------------------------- +// Controller bring-up +// --------------------------------------------------------------------------- + +/// Find an EHCI controller (class 0x0C class 0x03, prog_if 0x20). +fn find_ehci() -> Option<(u8, u8, u8)> { + for d in crate::pci::probe() { + if d.class == 0x0C && d.subclass == 0x03 && d.prog_if == 0x20 { + return Some((d.bus, d.device, d.func)); + } + } + None +} + +/// Return the number of the first root port with a device attached, or usize::MAX. +fn find_connected_port() -> usize { + let np = N_PORTS.load(Ordering::Relaxed) as usize; + let skip = FS_LS_PORTS.load(Ordering::Relaxed); + for p in 0..np { + if skip & (1 << p) != 0 { + continue; // full/low-speed — owned by the companion UHCI + } + let v = unsafe { rd(prtsc(p)) }; + if v & PRT_CONNECT != 0 { + return p; + } + } + usize::MAX +} + +/// Clear the FS/LS port mask so discovery retries (shell `usb rescan`). +pub fn rescan() { + FS_LS_PORTS.store(0, Ordering::Relaxed); +} + +pub fn init() -> bool { + let Some((b, d, f)) = find_ehci() else { + crate::serial_println!("ehci: no EHCI controller"); + return false; + }; + crate::serial_println!("ehci: USB2 EHCI at {:02x}:{:02x}.{}", b, d, f); + + unsafe { + // Enable Memory Space + Bus Master, and disable INTx assertion + // (bit 10) — the driver is polled (OPINTR=0), and an unhandled + // EHCI interrupt on real hardware would fire a stray IRQ that the + // kernel's catch-all handler EOI's but cannot service. + let cmd = crate::pci::config_read(b, d, f, 0x04); + crate::pci::config_write( + b, + d, + f, + 0x04, + (cmd & 0xFFFF_0000) | (cmd & 0xFFFF) | 0x6 | 0x400, + ); + let bar = crate::pci::bar64(b, d, f, 0); + let base = bar.unwrap_or(0) as usize; + EHCI_BASE.store(base as u32, Ordering::Relaxed); + let cap = rd(base); + let sp = rd(base + 4); + let hc = rd(base + 8); + let np = sp & 0xf; + N_PORTS.store(np, Ordering::Relaxed); + crate::serial_println!( + "ehci: mmio={:#x} caplen={:#x} ports={} 64-bit-caps={}", + base, + cap & 0xff, + np, + (hc & 1) != 0 + ); + if base == 0 { + return false; + } + + // Claim USB Legacy ownership from the BIOS before resetting the + // controller. The PCH BIOS drives USB keyboards through SMM + // legacy emulation while it owns the EHCI; resetting/reprogramming + // the controller underneath it silently kills the legacy keyboard + // (observed on the 7010) and the still-active SMI poll can collide + // with our async DMA — a Host System Error. The capability lives + // in PCI config space (EHCI spec §5.1); the claim walks it exactly + // like Linux/FreeBSD. Bounded, no-panic; reports what happened. + match crate::pci::claim_usb_legacy(b, d, f, base, 0x08, false) { + 0 => crate::serial_println!("ehci: USB legacy: already released (no claim needed)"), + 1 => crate::serial_println!("ehci: USB legacy handed over by BIOS"), + 2 => crate::serial_println!("ehci: USB legacy claim timed out (BIOS sem stuck)"), + _ => crate::serial_println!("ehci: USB legacy: no capability found"), + } + + // Halt the controller. + wr(op(OPCMD), 0); + let mut t = 0; + while t < 1_000_000 && (rd(op(OPSTS)) & STS_HCH) == 0 { + t += 1; + core::hint::spin_loop(); + } + crate::serial_println!( + "ehci: halted {}", + if rd(op(OPSTS)) & STS_HCH != 0 { + "ok" + } else { + "TIMEOUT" + } + ); + // Ensure not in reset already; then issue HC reset. + wr(op(OPCMD), CMD_HCRESET); + t = 0; + while t < 10_000_000 && (rd(op(OPCMD)) & CMD_HCRESET) != 0 { + t += 1; + core::hint::spin_loop(); + } + crate::serial_println!( + "ehci: hcreset {}", + if rd(op(OPCMD)) & CMD_HCRESET == 0 { + "ok" + } else { + "TIMEOUT" + } + ); + t = 0; + while t < 10_000_000 && (rd(op(OPSTS)) & STS_HCH) == 0 { + t += 1; + core::hint::spin_loop(); + } + crate::serial_println!( + "ehci: post-reset halt {}", + if rd(op(OPSTS)) & STS_HCH != 0 { + "ok" + } else { + "TIMEOUT" + } + ); + // Disable interrupts (polled). + wr(op(OPINTR), 0); + // Clear HCHalted? After reset the HC is halted; running is set later. + + // Allocate the async head QH + control QH + frame list + HID structures. + ASYNC_HEAD = dma_alloc(0x400, 0x20) as *mut u32; // 1KB region + XFER_QH = ASYNC_HEAD.add(0x80 / 4); // control QH at +0x80 + // qtds live after XFER_QH's 11 dwords (44 bytes); space reserved. + FRAMELIST = dma_alloc(0x1000, 0x1000) as *mut u32; + HID_QH = dma_alloc(0x400, 0x20) as *mut u32; + // DEBUG matrix: low-region control QH (sub-1MB page) to test whether + // the SE is tied to the 16 MB DMA region (write protection) or not. + if let Some(lp) = crate::memory::alloc_pages_above(2, 0) { + LOW_XFER = lp as *mut u32; + crate::serial_println!("ehci: low-xfer debug page at {:#x}", lp); + } + BUF_PAGE = dma_alloc(0x1000, 0x20); + if BUF_PAGE.is_null() { + crate::serial_println!("ehci: buffer page allocation failed"); + return false; + } + crate::serial_println!( + "ehci: dma async={:#x} xfer={:#x} framelist={:#x} hid={:#x} buf={:#x}", + ASYNC_HEAD as usize, + XFER_QH as usize, + FRAMELIST as usize, + HID_QH as usize, + BUF_PAGE as usize + ); + + // Init async head QH: self-linked, marked as head of reclamation. + *qh_next(ASYNC_HEAD) = link_of(XFER_QH as usize); // circular: head → control QH → head + *qh_info1(ASYNC_HEAD) = QH_HEAD; + *qh_info2(ASYNC_HEAD) = 0; + *qh_current(ASYNC_HEAD) = 0; + *qh_next_qtd(ASYNC_HEAD) = 0; + + // Configure CONTROL QH as an idle EP0 (filled per transfer). + *qh_next(XFER_QH) = link_of(ASYNC_HEAD as usize); + *qh_info1(XFER_QH) = QH_EPS_FULL | (64 << 16); // HS mps 64 + *qh_info2(XFER_QH) = 0; + *qh_current(XFER_QH) = 0; + *qh_next_qtd(XFER_QH) = 0; // idle + *qh_alt_qtd(XFER_QH) = QT_TERMINATE; + *qh_token(XFER_QH) = 0; + + // Async schedule address. + wr(op(OPFRIDX), 0); + // Set ASYNCLISTADDR (32-bit). Controller supports 64-bit (hcc&1) but we + // keep everything below 4 GB; CTRLDSSEGMENT=0 suffices. + // ASYNCLISTADDR low 32 bits: address of HEAD QH (lower 5 bits zero). + // With hcc 64-bit set, the controller reads the 64-bit address + // CTRLDSSEGMENT:ASYNCLISTADDR. Zero DSEG + low 32-bit pointer is fine + // for addresses < 4 GB. + let head_addr = ASYNC_HEAD as usize; + wr(op(0x10), 0); // CTRLDSSEGMENT + // ASYNCLISTADDR is at op+0x18 + wr(op(0x18), (head_addr as u32) & !0x1F); + // Periodic frame list address. + // Zero the frame list to avoid stale QH/ITD pointers causing + // QEMU to reset the controller when PSE is later enabled. + let fl = FRAMELIST as usize; + for i in 0..1024 { + core::ptr::write_volatile(FRAMELIST.add(i), 0u32); + } + wr(op(0x14), (fl as u32) & !0x3FF); + // Frame list size: FLS field (bits 2-3) stays 0 → 1024 entries. + // Intel/QEMU layout: PSE=bit4, ASE=bit5. + // NOTE: do NOT enable PSE here — the frame list is empty (all zeros). + // Enabling PSE on an empty frame list is fine (QEMU skips zero entries), + // but we want to avoid any race with stale OVMF entries. PSE will be + // enabled later in poll_mouse when the HID QH is linked into the list. + wr(op(OPCMD), 0x01u32); // CMD_RUN only, no PSE + wr(op(OPCMD), rd(op(OPCMD)) | CMD_RUN); + // Wait to leave halt. + let mut t = 0; + while t < 1_000_000 && (rd(op(OPSTS)) & STS_HCH) != 0 { + t += 1; + core::hint::spin_loop(); + } + crate::serial_println!( + "ehci: running {}", + if rd(op(OPSTS)) & STS_HCH == 0 { + "ok" + } else { + "STUCK HALTED" + } + ); + // Configure flag: route all ports to the host (CF=1) and power them. + wr(op(OPCFGFLAG), 1); + for p in 0..np as usize { + let psc = prtsc(p); + let v = rd(psc); + if v & PRT_POWER == 0 { + wr(psc, v | PRT_POWER); + } + } + CTRL_UP.store(true, Ordering::Relaxed); + crate::serial_println!("ehci: controller up ({} ports)", np); + + // DIAGNOSTIC: run the async schedule with an idle head (self-loop, + // no qtds) for 150 ms. If this faults (HCH/SE), the SE happens on + // schedule/DMA mechanics alone — nothing to do with the split + // transfer. If clean, the fault is specific to transfer execution. + *qh_next(ASYNC_HEAD) = link_of(ASYNC_HEAD as usize); + wr(op(OPSTS), 0x1F); + core::sync::atomic::fence(core::sync::atomic::Ordering::SeqCst); + wr(op(OPCMD), rd(op(OPCMD)) | CMD_RUN | CMD_ASE); + let pt0 = crate::interrupts::uptime_ms(); + loop { + let s = rd(op(OPSTS)); + if s & (STS_HCH | STS_SE) != 0 { + crate::serial_println!( + "ehci: idle-probe FAULTED {}ms: cmd={:#x} sts={:#x} frindex={:#x} asyncbase={:#x} prtsc0={:#x}", + crate::interrupts::uptime_ms().wrapping_sub(pt0), + rd(op(OPCMD)), + s, + rd(op(OPFRIDX)), + rd(op(0x18)), + rd(prtsc(0)) + ); + break; + } + if crate::interrupts::uptime_ms().wrapping_sub(pt0) >= 150 { + crate::serial_println!("ehci: idle-probe ok (150ms, no fault)"); + break; + } + core::hint::spin_loop(); + } + } + true +} + +/// Port reset on USB2 EHCI. Assert PR, hold it briefly (real EHCI clears +/// PR itself once reset signaling finishes; QEMU needs the driver to +/// deassert), then wait for the port to enable. Returns true if PED set +/// (high-speed device). Full/low-speed stay PED=0 and need a TT path. +unsafe fn reset_port(p: usize) -> bool { + unsafe { + let psc = prtsc(p); + let v = rd(psc); + // Put port in reset. + wr(psc, (v | PRT_RESET) & !PRT_RWC); + // Hold ~50 ms, bailing early if the HC cleared PR on its own. + let t0 = crate::interrupts::uptime_ms(); + while crate::interrupts::uptime_ms().saturating_sub(t0) < 50 { + core::hint::spin_loop(); + if rd(psc) & PRT_RESET == 0 { + break; + } + } + // Deassert explicitly (QEMU only completes the reset on deassert). + wr(psc, (rd(psc) & !PRT_RESET) & !PRT_RWC); + // Wait briefly for the port to enable (PED). High-speed devices set + // PED on reset; full/low-speed stay PED=0 (they need split/TT). Do + // not spin forever — decide on the line state instead. + let mut t = 0; + while t < 2_000_000 && (rd(psc) & PRT_PED) == 0 { + t += 1; + core::hint::spin_loop(); + } + // A high-speed device gains PED. Full/low-speed won't (needs companion/TT). + rd(psc) & PRT_PED != 0 + } +} + +/// Distinguish full- vs low-speed on a connected root port (for the split QH +/// EPS field). Intel PCH EHCIs expose the speed in HOSTPC (base+0x84+4p, +/// bits 27:26: 0=full, 1=low, 2=high); other controllers fall back to the +/// PORTSC bits 27:26. Only meaningful after the port has completed reset. +unsafe fn port_speed(p: usize) -> u32 { + unsafe { + let base = EHCI_BASE.load(Ordering::Relaxed) as usize; + let hp = rd(base + 0x84 + p * 4); + if hp != u32::MAX && hp != 0 { + let f = (hp >> 25) & 3; + if f != 3 { + return f; + } + } + (rd(prtsc(p)) >> 26) & 3 + } +} + +// --------------------------------------------------------------------------- +// Enumeration + HID discovery +// --------------------------------------------------------------------------- + +const HID_CLASS: u8 = 0x03; + +fn find_hid_ep(cfg: &[u8]) -> Option<(u8, u32, u32, u8)> { + // returns (epaddr, mps, interval_bint, epnum) + 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 bt = cfg[i + 1]; + if bt == 4 && i + 9 <= cfg.len() { + in_iface = cfg[i + 5] == HID_CLASS; + } else if bt == 5 && in_iface && i + 7 <= cfg.len() { + let addr = cfg[i + 2]; + let attrs = cfg[i + 3]; + let mps = (cfg[i + 4] as u32) | ((cfg[i + 5] as u32) << 8); + let bint = cfg[i + 6] as u32; + if (addr & 0x80) != 0 && (attrs & 0x03) == 0x03 { + return Some((addr, mps, bint, addr & 0x0f)); + } + } + i += blen; + } + None +} + +fn enumerate() -> bool { + let port = find_connected_port(); + if port == usize::MAX { + return false; + } + // DEBUG: one-boot transfer variant matrix replaces normal enumeration. + if DEBUG_MATRIX && !MATRIX_DONE.swap(true, Ordering::Relaxed) { + unsafe { + debug_xfer_matrix(port); + } + return false; // diagnostic build + } + unsafe { + // If a periodic schedule is live (a previous arm), stop it cleanly + // before touching the controller: point every frame at terminate, + // clear PSE and wait for the reclaim handshake. Re-enumerating while + // the old HID QH is still being walked interleaves the periodic walk + // with control transfers — fragile on any HC. + if rd(op(OPCMD)) & CMD_PSE != 0 { + let fl = FRAMELIST; + for f in 0..1024 { + core::ptr::write_volatile(fl.add(f), QT_TERMINATE); + } + core::sync::atomic::fence(core::sync::atomic::Ordering::SeqCst); + wr(op(OPCMD), rd(op(OPCMD)) & !CMD_PSE); + wait_recl(1000); + } + // Discovery state: nothing armed/present until the arm at the end. + ARMED.store(false, Ordering::Relaxed); + MOUSE_PRES.store(false, Ordering::Relaxed); + let hs = reset_port(port); + if !hs && crate::uhci::controller_up() { + // A real (non-QEMU) EHCI can never transfer to a full/low-speed + // device: the port is owned by the companion UHCI. Mark the + // port and stop — a descriptor timeout here costs 5 s and + // repeats every second, which is exactly the "stuck in a loop" + // the OptiPlex showed. (QEMU's usb-ehci accepts PED=0 with a + // high-speed device behind it, so this skip only triggers when + // a real UHCI companion is actually present.) + FS_LS_PORTS.store( + FS_LS_PORTS.load(Ordering::Relaxed) | (1 << port), + Ordering::Relaxed, + ); + crate::serial_println!( + "ehci: port {} full/low-speed — owned by companion UHCI (uhci driver handles it)", + port + ); + return false; + } + crate::serial_println!( + "ehci: port {} resolved{}", + port, + if hs { + " (high-speed)" + } else { + " (full/low — TT path)" + } + ); + // Speed for the QH EPS field: 2=HS, 1=LS, 0=FS. FS/LS on an EHCI + // root port must be serviced with split transactions (integrated TT). + let speed = if hs { 2 } else { port_speed(port) }; + let speed_name = match speed { + 2 => "high-speed", + 1 => "low-speed (split)", + _ => "full-speed (split)", + }; + let hostpc = unsafe { rd(EHCI_BASE.load(Ordering::Relaxed) as usize + 0x84 + port * 4) }; + crate::serial_println!( + "ehci: port {} speed: {} (hostpc={:#x})", + port, + speed_name, + hostpc + ); + let mps = if hs { 64u32 } else { 8u32 }; + + // 1. GET_DESCRIPTOR (device) — 18 bytes to learn idVendor etc. + let req = ctrl_req(0x80, 6, 0x0100, 0, 18); + let dd = BUF_PAGE.add(BUF_DEV_DESC); + let mut got = ctl_xfer(0, mps, speed, port, &req, true, dd, 18); + if got < 0 { + crate::serial_println!("ehci: get-dev-desc failed code={}", got); + return false; + } + if got < 8 { + crate::serial_println!("ehci: short dev-desc ({})", got); + return false; + } + let dd = dd as *const u8; + let mps0 = core::ptr::read_volatile(dd.add(7)) as u32; // bMaxPacketSize0 + let _ = mps0; + crate::serial_println!( + "ehci: dev {:04x}:{:04x} class={:#x} mps0={}", + (core::ptr::read_volatile(dd.add(9)) as u16) + | ((core::ptr::read_volatile(dd.add(8)) as u16) << 8), + (core::ptr::read_volatile(dd.add(11)) as u16) + | ((core::ptr::read_volatile(dd.add(10)) as u16) << 8), + core::ptr::read_volatile(dd.add(4)), + core::ptr::read_volatile(dd.add(7)) + ); + + // 2. SET_ADDRESS to 1. + let req = ctrl_req(0x00, 5, 1, 0, 0); + if ctl_xfer(0, mps, speed, port, &req, false, core::ptr::null_mut(), 0) < 0 { + crate::serial_println!("ehci: set-address failed"); + return false; + } + DEV_ADDR.store(1, Ordering::Relaxed); + + // 3. GET_CONFIG(0) — 256 bytes, enough for a HID descriptor. + let cfg = BUF_PAGE.add(BUF_CFG); + core::ptr::write_bytes(cfg, 0, 256); + let req = ctrl_req(0x80, 6, 0x0200, 0, 256); + got = ctl_xfer(1, mps, speed, port, &req, true, cfg, 256); + if got < 0 { + crate::serial_println!("ehci: get-config failed code={}", got); + return false; + } + let Some((epaddr, epmps, bint, epnum)) = + find_hid_ep(core::slice::from_raw_parts(cfg as *const u8, 256)) + else { + crate::serial_println!("ehci: no HID mouse endpoint in config"); + return false; + }; + crate::serial_println!( + "ehci: HID mouse IN ep{}({:#x}) mps={} interval={}", + epnum, + epaddr, + epmps, + bint + ); + + // 4. SET_CONFIGURATION(1). + let req = ctrl_req(0x00, 9, 1, 0, 0); + if ctl_xfer(1, mps, speed, port, &req, false, core::ptr::null_mut(), 0) < 0 { + crate::serial_println!("ehci: set-config failed"); + return false; + } + + // 5. Arm the HID interrupt-IN endpoint on the periodic schedule. + HID_EP.store(epnum as u32, Ordering::Relaxed); + HID_MPS.store(epmps, Ordering::Relaxed); + // Build HID QH for EP IN on device 1. + let hq = HID_QH; + // eps: speed (0/1/2) maps directly onto the QH EPS field (bits 13:12). + let info1 = QH_RL + | (speed << 12) + | ((epmps.max(4) & 0x7ff) << 16) + | (DEV_ADDR.load(Ordering::Relaxed) & 0x7f) + | (((epnum as u32) & 0x0f) << 8); + // Poll every frame. HS: SMASK covers every uframe. FS/LS: split — + // SSPLIT at uframe 0, CSPLIT at uframes 2..4 (Linux NEWSCHED), hub + // addr 0 (integrated root TT), 1-based root port in bits 29:23. + let (smask, cmask, split) = if speed == 2 { + (0xFFu32, 0u32, 0u32) + } else { + (0x01u32, 0x1Cu32 << 8, QH_MULT | (((port as u32) + 1) << 23)) + }; + *qh_next(hq) = QT_TERMINATE; // end of this frame's periodic list + *qh_info1(hq) = info1; + *qh_info2(hq) = smask | cmask | split; + *qh_current(hq) = 0; + let iqtd = hq.add(0x40 / 4); // qtd at +0x40 (must be 32-byte aligned) + // HID report qtd: len = epmps, PID IN. + core::ptr::write_volatile(iqtd.wrapping_add(0), QT_TERMINATE); + core::ptr::write_volatile(iqtd.wrapping_add(1), QT_TERMINATE); + core::ptr::write_volatile( + iqtd.wrapping_add(2), + ((epmps.max(4) & 0x7fff) << 16) | (PID_IN << 8) | TOK_ACTIVE | TOK_IOC, + ); + for i in 0..5 { + core::ptr::write_volatile(iqtd.add(3 + i), 0u32); + } + core::ptr::write_volatile(iqtd.wrapping_add(3), BUF_PAGE.add(BUF_HID_REPORT) as u32); + *qh_next_qtd(hq) = iqtd as u32; + *qh_alt_qtd(hq) = QT_TERMINATE; + *qh_token(hq) = 0; + // QEMU reads the report buffer from the qtd's bufptr (ehci_init_transfer), + // so the QH overlay's buffer list need not be pre-populated. + // Point every period frame entry at the HID QH. + let fl = FRAMELIST; + for f in 0..1024 { + core::ptr::write_volatile(fl.add(f), link_of(hq as usize)); + } + // Enable the periodic schedule so the frame list is actually walked. + // PSE was deliberately off until now (empty frame list during init). + core::sync::atomic::fence(core::sync::atomic::Ordering::SeqCst); + wr(op(OPCMD), rd(op(OPCMD)) | CMD_RUN | CMD_PSE); + ARMED.store(true, Ordering::Relaxed); + MOUSE_PRES.store(true, Ordering::Relaxed); + crate::serial_println!("ehci: HID boot mouse armed (device 1)"); + true + } +} + +// --------------------------------------------------------------------------- +// DEBUG: one-boot transfer variant matrix (real-hardware SE bisection). +// Each variant does a full HC reset + port reset, then one ctl_xfer with a +// single construction variable flipped (see XFER_TWEAK in ctl_xfer), and +// logs the outcome. A wedged port from an earlier SE cannot confound the +// comparisons because every variant restarts the controller. +// --------------------------------------------------------------------------- +unsafe fn hc_restart() { + unsafe { + wr(op(OPCMD), 0); + let mut t = 0; + while t < 1_000_000 && (rd(op(OPSTS)) & STS_HCH) == 0 { + t += 1; + core::hint::spin_loop(); + } + wr(op(OPCMD), CMD_HCRESET); + t = 0; + while t < 10_000_000 && (rd(op(OPCMD)) & CMD_HCRESET) != 0 { + t += 1; + core::hint::spin_loop(); + } + t = 0; + while t < 10_000_000 && (rd(op(OPSTS)) & STS_HCH) == 0 { + t += 1; + core::hint::spin_loop(); + } + wr(op(0x18), (ASYNC_HEAD as u32) & !0x1F); + wr(op(0x14), (FRAMELIST as u32) & !0x3FF); + wr(op(OPCFGFLAG), 1); + let np = N_PORTS.load(Ordering::Relaxed); + for p in 0..np as usize { + let psc = prtsc(p); + let v = rd(psc); + if v & PRT_POWER == 0 { + wr(psc, v | PRT_POWER); + } + } + wr(op(OPSTS), 0x1F); + wr(op(OPCMD), rd(op(OPCMD)) | CMD_RUN); + t = 0; + while t < 1_000_000 && (rd(op(OPSTS)) & STS_HCH) != 0 { + t += 1; + core::hint::spin_loop(); + } + } +} + +unsafe fn debug_xfer_matrix(port: usize) { + unsafe { + QR_SHOWN.store(true, Ordering::Relaxed); // serial-only diagnostics + crate::serial_println!( + "ehci: ===== DEBUG xfer variant matrix (port {}) =====", + port + ); + let tweaks: [u32; 7] = [0, 1, 2, 4, 8, 0x20, 0x80]; + let names = [ + "base", + "no-ASE", + "no-split", + "no-qtd", + "len0", + "wire-off", + "low-region", + ]; + let ebase = EHCI_BASE.load(Ordering::Relaxed) as usize; + for (i, &tw) in tweaks.iter().enumerate() { + hc_restart(); + let _hs = reset_port(port); + let req: [u8; 8] = [0x80, 0x06, 0x00, 0x01, 0, 0, 0x12, 0]; // get_desc dev, 18B + let mut buf = [0u8; 18]; + XFER_TWEAK.store(tw | 0x40, Ordering::Relaxed); // + short timeout + let t0 = crate::interrupts::uptime_ms(); + let r = ctl_xfer(0, 8, 0, port, &req, true, buf.as_mut_ptr(), 18); + let dt = crate::interrupts::uptime_ms().wrapping_sub(t0); + crate::serial_println!( + "ehci: v{} {:<10}: ret={} t={}ms sts={:#x} cmd={:#x} prtsc0={:#x} hp={:#x}", + i, + names[i], + r, + dt, + rd(op(OPSTS)), + rd(op(OPCMD)), + rd(prtsc(0)), + rd(ebase + 0x84) + ); + XFER_TWEAK.store(0, Ordering::Relaxed); + let w0 = crate::interrupts::uptime_ms(); + while crate::interrupts::uptime_ms().wrapping_sub(w0) < 600 { + core::hint::spin_loop(); + } + } + crate::serial_println!("ehci: ===== matrix done ====="); + } +} + +// --------------------------------------------------------------------------- +// Active interface (called by cursor task / shell) +// --------------------------------------------------------------------------- + +pub fn controller_up() -> bool { + CTRL_UP.load(Ordering::Relaxed) +} +pub fn mouse_present() -> bool { + MOUSE_PRES.load(Ordering::Relaxed) +} + +pub fn poll_mouse() -> Option { + unsafe { + // Hot path first. The qtd's ACTIVE bit (in controller-visible DMA + // memory) is the ground truth for "report outstanding"; a kernel + // static must never gate report delivery. + if ARMED.load(Ordering::Relaxed) { + let hq = HID_QH; + let iqtd = hq.add(0x40 / 4); // same 32-byte-aligned location as arming + let tok = core::ptr::read_volatile(iqtd.wrapping_add(2).cast::()); + if tok & TOK_ACTIVE == 0 { + if tok & (TOK_HALT | TOK_XACT) != 0 { + // Transfer error: drop the arm; a bounded re-enum follows. + ARMED.store(false, Ordering::Relaxed); + return None; + } + // Completed: decode the report from the dedicated DMA page. + let hb = BUF_PAGE.add(BUF_HID_REPORT) as *const u8; + let b0 = core::ptr::read_volatile(hb); + let b1 = core::ptr::read_volatile(hb.add(1)); + let b2 = core::ptr::read_volatile(hb.add(2)); + let ev = Some(MouseEvent { + dx: (b1 as i8) as i16, + dy: (b2 as i8) as i16, + left: b0 & 0x01 != 0, + right: b0 & 0x02 != 0, + middle: b0 & 0x04 != 0, + }); + // Re-arm the periodic interrupt-IN qtd: restore its token to + // ACTIVE and re-link it as the QH's next qtd so the HC fetches + // it again next frame (canonical one-qtd periodic re-arm). + core::ptr::write_volatile( + iqtd.wrapping_add(2), + ((HID_MPS.load(Ordering::Relaxed).max(4) & 0x7fff) << 16) + | (PID_IN << 8) + | TOK_ACTIVE + | TOK_IOC, + ); + *qh_next_qtd(hq) = iqtd as u32; + return ev; + } + return None; + } + // Not armed: bring the mouse up lazily, but never hot-loop — at most + // one enumerate attempt per second, and only while the controller is + // up. A transient glitch can delay a report; it must not wedge the + // system in a re-enumeration loop. + if CTRL_UP.load(Ordering::Relaxed) { + let now = crate::interrupts::uptime_ms() as u32; + if now.wrapping_sub(LAST_ENUM.load(Ordering::Relaxed)) >= 1000 { + LAST_ENUM.store(now, Ordering::Relaxed); + enumerate(); + } + } + } + None +} + +/// Allocate DMA-safe memory above 16 MB. Uses the page allocator +/// (physical, identity-mapped addresses) instead of the heap allocator +/// (virtual addresses) because the EHCI controller's DMA engine needs +/// physical addresses that match what the CPU sees. The 16 MB floor +/// keeps the schedule structures out of the first 640 K, which some real +/// chipsets reject from PCI masters (controller System Error halt). +unsafe fn dma_alloc(bytes: usize, _align: usize) -> *mut u8 { + unsafe { + let pages = bytes.div_ceil(4096); + let Some(phys) = crate::memory::alloc_pages_above(pages, 0x100_0000) else { + return core::ptr::null_mut(); + }; + let p = phys as *mut u8; + core::ptr::write_bytes(p, 0, pages * 4096); + p + } +} + +/// One-line status for the shell `usb` command. +pub fn diagnose() -> alloc::string::String { + let mut s = alloc::string::String::new(); + s.push_str(&alloc::format!( + "ehci mmio={:#x} ports={} mouse={} dev={} hid_ep={} armed={}\n", + EHCI_BASE.load(Ordering::Relaxed), + N_PORTS.load(Ordering::Relaxed), + if MOUSE_PRES.load(Ordering::Relaxed) { + "yes" + } else { + "no" + }, + DEV_ADDR.load(Ordering::Relaxed), + HID_EP.load(Ordering::Relaxed), + ARMED.load(Ordering::Relaxed) + )); + let np = N_PORTS.load(Ordering::Relaxed) as usize; + for p in 0..np.min(8) { + let v = unsafe { rd(prtsc(p)) }; + s.push_str(&alloc::format!( + " eport{}: {}{:#x}\n", + p, + if v & PRT_CONNECT != 0 { "*" } else { "" }, + v & 0xffff + )); + } + s +} diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs index ccc6509..676814e 100644 --- a/rsnix/src/gop.rs +++ b/rsnix/src/gop.rs @@ -7,7 +7,7 @@ use crate::font; use crate::uefi::{GopModeInfo, GraphicsOutputProtocol, EFI_SUCCESS, PIXEL_BGR, PIXEL_RGB}; use core::fmt; use core::ptr; -use core::sync::atomic::{AtomicU32, AtomicU64, AtomicU8, Ordering}; +use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicU8, Ordering}; /// Framebuffer state captured before ExitBootServices. pub struct Framebuffer { @@ -91,6 +91,19 @@ pub fn last_boot_stage() -> alloc::string::String { /// the preemptive scheduler. Set via the shell `stall` command. pub static DISPLAY_STALL: AtomicU32 = AtomicU32::new(0); +/// Set once a diagnostic QR has been rendered on screen. While locked, the +/// klog screen echo is suppressed so subsequent log lines cannot overwrite +/// the QR; the serial console keeps the full stream. Never released (the +/// QR is the "photograph this" surface for hardware with no serial pipe). +pub static QR_LOCKED: AtomicBool = AtomicBool::new(false); + +pub fn qr_lock() { + QR_LOCKED.store(true, Ordering::Relaxed); +} +pub fn qr_locked() -> bool { + QR_LOCKED.load(Ordering::Relaxed) +} + #[inline] fn stall_display() { let n = DISPLAY_STALL.load(Ordering::Relaxed); @@ -308,7 +321,8 @@ pub fn shadow_late_init() { /// boot stage is written straight to the framebuffer — the last visible /// marker before a hang identifies where the kernel died. Uses the /// direct-display glyph path (the shadow may not exist yet in early -/// boot); the shell banner overwrites row 0 at first paint. +/// boot). Row 0 stays a persistent status line: the shell starts its +/// output at row 1 so the marker survives a post-boot freeze. pub fn boot_marker(msg: &str) { boot_marker_at(msg, 0); } diff --git a/rsnix/src/interrupts.rs b/rsnix/src/interrupts.rs index 927e4f1..9872389 100644 --- a/rsnix/src/interrupts.rs +++ b/rsnix/src/interrupts.rs @@ -210,6 +210,10 @@ impl ByteRing { static KEYBUF: ByteRing = ByteRing::new(); static MOUSEBUF: ByteRing = ByteRing::new(); +/// One-shot flag: the first unhandled interrupt logs its vector, so real +/// hardware shows which device raised a stray IRQ (before the LAPIC EOI +/// drops it). +static STRAY_LOGGED: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false); // --------------------------------------------------------------------------- // Interrupt stubs (assembly) and dispatch @@ -412,7 +416,7 @@ pub fn enter_dump() { } } -fn in_dump() -> bool { +pub(crate) fn in_dump() -> bool { unsafe { IN_DUMP } } @@ -488,7 +492,35 @@ unsafe extern "C" fn isr_dispatch(frame: &IntFrame) -> usize { } crate::sched::fault_dump("exception", 0, frame) } - _ => fp, + _ => { + // Unhandled/stray interrupt. On real hardware an + // IO-APIC-delivered IRQ (USB/SATA/NIC INTx) that is never + // EOI'd stays pending in the local APIC and re-fires the + // instant we iretq — an interrupt storm that starves the + // timer tick (higher vector priority) and freezes the whole + // machine with no panic. EOI the LAPIC so an + // edge-triggered stray drops (a genuinely-spurious LAPIC + // IRQ needs no EOI, so that case is a benign no-op). + // Level-triggered strays are additionally stopped by the + // drivers masking their PCI INTx / interrupt enables. + if !STRAY_LOGGED.swap(true, Ordering::Relaxed) { + crate::serial_println!("intr: stray vector {:#x} — LAPIC EOI'd", frame.vector); + } + // EOI the local APIC when it is enabled (MSR 0x1B, bit 11). + let (lo, hi): (u32, u32); + core::arch::asm!( + "rdmsr", + in("ecx") 0x1Bu32, + out("eax") lo, + out("edx") hi, + options(nostack, preserves_flags) + ); + let apic = ((hi as u64) << 32) | lo as u64; + if apic & (1 << 11) != 0 { + core::ptr::write_volatile(0xFEE0_00B0usize as *mut u32, 0); + } + fp + } } }; crate::sched::set_in_isr(false); diff --git a/rsnix/src/klog.rs b/rsnix/src/klog.rs index 2e1fc82..300a597 100644 --- a/rsnix/src/klog.rs +++ b/rsnix/src/klog.rs @@ -95,6 +95,16 @@ pub fn emit(args: core::fmt::Arguments<'_>) { crate::serial::write_raw(line.as_str()); crate::serial::write_raw("\r\n"); append(line.as_str(), ms); + // Screen echo: mirror the serial line to the VGA console, so the whole + // boot log is visible on real hardware without a serial console. The + // last line on screen when the machine freezes is exactly where boot + // stopped (far more precise than the stage marker). Skipped while a + // fault dump is rendering its own band (row 0 = boot marker, terminal + // lives below it). + if crate::vt::ready() && !crate::interrupts::in_dump() && !crate::gop::qr_locked() { + crate::vt::write_str(line.as_str()); + crate::vt::write_str("\n"); + } crate::sched::unlock(); } diff --git a/rsnix/src/lib.rs b/rsnix/src/lib.rs index bc2553d..f5c3419 100644 --- a/rsnix/src/lib.rs +++ b/rsnix/src/lib.rs @@ -17,6 +17,8 @@ pub mod pci; pub mod qr; pub mod power; pub mod ps2; +pub mod uhci; +pub mod ehci; pub mod usb; pub mod rtc; pub mod sched; diff --git a/rsnix/src/memory.rs b/rsnix/src/memory.rs index 099f607..2a32da8 100644 --- a/rsnix/src/memory.rs +++ b/rsnix/src/memory.rs @@ -1194,6 +1194,50 @@ pub fn alloc_pages(count: usize) -> Option { alloc_pages_aligned(count, 1) } +/// Allocate `count` contiguous physical pages at or above `min_addr` +/// (first-fit upward from there). Used for controller DMA buffers that +/// must avoid low conventional memory — the first 640 K on some real +/// chipsets is hostile to PCI masters and shows up as a controller +/// "System Error" (SE) halt. Page-aligned like `alloc_pages`. +pub fn alloc_pages_above(count: usize, min_addr: usize) -> Option { + if count == 0 { + return None; + } + unsafe { + if BITMAP.is_null() { + return None; + } + let span = SPAN_PAGES; + let min_page = min_addr.div_ceil(PAGE_SIZE).max(BASE_PAGE); + if min_page >= BASE_PAGE + span { + return None; + } + let start_idx = min_page - BASE_PAGE; + let mut run_start = 0usize; + let mut run_len = 0usize; + for idx in start_idx..span { + let free = (*BITMAP.add(idx / 8) >> (idx % 8)) & 1 == 0; + if free { + if run_len == 0 { + run_start = idx; + } + run_len += 1; + if run_len >= count { + let p = (BASE_PAGE + run_start) * PAGE_SIZE; + for k in run_start..run_start + count { + *BITMAP.add(k / 8) |= 1 << (k % 8); + } + FREE_PAGES -= count; + return Some(p); + } + } else { + run_len = 0; + } + } + None + } +} + /// Allocate `count` contiguous physical pages whose start address is /// aligned to `align_pages` (a power of two, in page units). pub fn alloc_pages_aligned(count: usize, align_pages: usize) -> Option { diff --git a/rsnix/src/pci.rs b/rsnix/src/pci.rs index b9c2265..738c5e9 100644 --- a/rsnix/src/pci.rs +++ b/rsnix/src/pci.rs @@ -61,13 +61,32 @@ pub fn config_read(bus: u8, device: u8, func: u8, reg: u8) -> u32 { outl(CONFIG_ADDRESS, config_addr(bus, device, func, reg)); inl(CONFIG_DATA) } - /// Write a dword to a device's config space (`reg` must be dword-aligned). pub fn config_write(bus: u8, device: u8, func: u8, reg: u8, val: u32) { outl(CONFIG_ADDRESS, config_addr(bus, device, func, reg)); outl(CONFIG_DATA, val) } +fn outb(port: u16, val: u8) { + unsafe { + core::arch::asm!( + "out dx, al", + in("dx") port, + in("al") val, + options(nomem, nostack, preserves_flags) + ); + } +} + +/// Write a single byte to config space (any byte offset). The PCI +/// byte-enable cycle goes out on port 0xCFC + lane, exactly like `pci_ +/// write_config_byte` everywhere else; QEMU and real chipsets both decode +/// the low two address bits of the config-data port. +pub fn config_write_byte(bus: u8, device: u8, func: u8, reg: u8, val: u8) { + outl(CONFIG_ADDRESS, config_addr(bus, device, func, reg)); + outb(CONFIG_DATA + ((reg & 3) as u16), val) +} + /// Read a memory BAR (index 0-5) as a 64-bit address, handling 32/64-bit /// BARs. Returns None for I/O BARs or invalid values. pub fn bar64(bus: u8, device: u8, func: u8, bar_index: u8) -> Option { @@ -90,6 +109,110 @@ pub fn bar64(bus: u8, device: u8, func: u8, bar_index: u8) -> Option { } } +/// Claim USB Legacy ownership from the BIOS on a controller whose MMIO base +/// exposes the USB Legacy Support extended capability (USBLEGSUP, cap id 1). +/// +/// EHCI/xHCI “USB Legacy Support” ownership handoff (EHCI spec §5.1). +/// +/// The 2012-era PCH BIOS drives USB keyboards through SMM USB-legacy +/// emulation while it owns the controller: its SMI handler polls the EHCI +/// over USB-error/port-change events. If the OS reprograms the controller +/// and runs async DMA underneath the still-active BIOS module, the SMM poll +/// collides with the OS transfer at an unpredictable moment — a classic +/// real-silicon Host System Error (SE) that QEMU cannot reproduce (no SMM). +/// +/// CRITICAL: the extended-capability pointer is a PCI *config-space* +/// offset, not an MMIO offset. Read it from an MMIO register (`eccp_src` +/// = MMIO dword offset; EHCI: HCCPARAMS @+0x08 bits 15:8; xHCI: HCSPARAMS1 +/// @+0x04 bits 31:16), then walk config space — exactly Linux's +/// `quirk_usb_handoff_ehci` (pci-quirks.c) and FreeBSD's `ehci_pci_attach`. +/// Our previous implementation treated EECP as an MMIO offset, read a +/// reserved register (0), and silently never claimed ownership — the +/// observed “claim skipped” on the 7010. +/// +/// Handshake (Linux/FreeBSD-identical): if the BIOS-owned semaphore +/// (bit 16 of the cap dword) is set, write the OS-owned semaphore byte +/// (cap+3) = 1 and spin (bounded ~1 s) for bit 16 to clear. USBLEGCTLSTS +/// (cap+4) is deliberately left alone — Linux notes some systems “get +/// upset” if it's touched. +/// +/// Returns: 0 = already released/owned (no claim needed), 1 = handed over, +/// 2 = BIOS never released, 3 = no legacy capability / chain out of range. +pub fn claim_usb_legacy( + bus: u8, + dev: u8, + func: u8, + mmio_base: usize, + eccp_src: usize, + eccp_bits_high: bool, +) -> u8 { + let src = unsafe { (mmio_base as *const u32).add(eccp_src).read_volatile() }; + crate::serial_println!( + "usb-legacy: probe mmio=+{:#x} hcc={:#010x} high={}", + eccp_src, + src, + eccp_bits_high + ); + let mut offset = if eccp_bits_high { + ((src >> 16) & 0xFFFF) as u8 // xHCI: XECP (u8 caps the walk at 0xFF config) + } else { + ((src >> 8) & 0xFF) as u8 // EHCI: EECP + }; + if offset == 0 { + return 3; + } + let mut guard = 32; // bounded walk, no panics + while offset != 0 && guard > 0 { + guard -= 1; + let cap = config_read(bus, dev, func, offset); + crate::serial_println!( + "usb-legacy: cap@{:#x} id={:#04x} next={:#04x} d={:#010x}", + offset, + cap & 0xFF, + (cap >> 8) & 0xFF, + cap + ); + let id = cap & 0xFF; + let next = ((cap >> 8) & 0xFF) as u8; + match id { + 0 => return 3, // illegal reserved cap terminates the chain + 1 => { + // USB Legacy Support capability. + let ctlsts = config_read(bus, dev, func, offset + 4); + crate::serial_println!( + "usb-legacy: cap@{:#x} bios_sem={} os_sem={} ctlsts={:#x}", + offset, + (cap >> 16) & 1, + (cap >> 24) & 1, + ctlsts + ); + if cap & (1 << 24) != 0 { + return 0; // OS already owns + } + if cap & (1 << 16) == 0 { + return 0; // BIOS not claiming it + } + crate::serial_println!("usb-legacy: BIOS owns controller - claiming (OS sem)"); + config_write_byte(bus, dev, func, offset + 3, 1); + let t0 = crate::interrupts::uptime_ms(); + let mut handed = false; + while crate::interrupts::uptime_ms().wrapping_sub(t0) < 1000 { + if config_read(bus, dev, func, offset) & (1 << 16) == 0 { + handed = true; + break; + } + core::hint::spin_loop(); + } + return if handed { 1 } else { 2 }; + } + _ => { + offset = next; + } + } + } + 3 +} + /// Enumerate every device/function present on all buses. pub fn probe() -> alloc::vec::Vec { let mut out = alloc::vec::Vec::new(); diff --git a/rsnix/src/ps2.rs b/rsnix/src/ps2.rs index 5f4a804..965e4a0 100644 --- a/rsnix/src/ps2.rs +++ b/rsnix/src/ps2.rs @@ -169,14 +169,19 @@ pub fn init() { // Enable the keyboard interrupt on the PS/2 controller. // Read Configuration Byte (command 0x20), set bit 0 (IRQ1 enable), - // write back (command 0x60). + // write back (command 0x60). Every wait is bounded; a wedged + // controller bails out instead of hanging the boot. unsafe { // Tell the keyboard controller we want to read the config byte. // Need to wait for the controller to be ready first. - wait_write(); + if !wait_write() { + return; + } core::arch::asm!("out dx, al", in("dx") 0x64u16, in("al") 0x20u8, options(nostack, preserves_flags)); - wait_read(); + if !wait_read() { + return; + } let mut cfg: u8; core::arch::asm!("in al, dx", out("al") cfg, in("dx") 0x60u16, options(nostack, preserves_flags)); @@ -184,48 +189,60 @@ pub fn init() { cfg |= 0x01; // enable keyboard interrupt // Write config byte back - wait_write(); + if !wait_write() { + return; + } core::arch::asm!("out dx, al", in("dx") 0x64u16, in("al") 0x60u8, options(nostack, preserves_flags)); - wait_write(); + if !wait_write() { + return; + } core::arch::asm!("out dx, al", in("dx") 0x60u16, in("al") cfg, options(nostack, preserves_flags)); } } /// Wait until the PS/2 controller is ready to receive a command. -fn wait_write() { +/// Bounded: a wedged or missing 8042 must not hang the boot (the classic +/// real-hardware freeze — the controller reports busy forever on some +/// machines). Returns false on timeout. +fn wait_write() -> bool { unsafe { - loop { + for _ in 0..1_000_000 { let status: u8; core::arch::asm!("in al, dx", out("al") status, in("dx") 0x64u16, options(nostack, preserves_flags)); if status & 0x02 == 0 { // Input buffer empty — controller ready to receive - break; + return true; } } } + false } /// Wait until the PS/2 controller has data available to read. -fn wait_read() { +/// Bounded like `wait_write`. +fn wait_read() -> bool { unsafe { - loop { + for _ in 0..1_000_000 { let status: u8; core::arch::asm!("in al, dx", out("al") status, in("dx") 0x64u16, options(nostack, preserves_flags)); if status & 0x01 != 0 { // Output buffer full — data ready to read - break; + return true; } } } + false } +/// Drain any pending bytes. Bounded so a stuck output-full flag (or a +/// controller that keeps raising it with no data) cannot hang the boot. fn drain_input() { unsafe { - loop { + for _ in 0..32 { let status: u8; core::arch::asm!("in al, dx", out("al") status, in("dx") STATUS_PORT, options(nostack, preserves_flags)); @@ -268,6 +285,15 @@ fn kbd() -> &'static mut KbdState { unsafe { &mut *core::ptr::addr_of_mut!(KBD) } } +/// Feed one scancode-set-1 byte into the shared decoder state machine. +/// Used by the PS/2 IRQ path (raw bytes) AND the USB HID keyboard driver +/// (which translates HID usage codes to scancode-set-1 make/break bytes so +/// the same shift/ctrl/caps decoding applies). Returns a KeyEvent on a +/// visible press; releases and modifier-only changes return None. +pub(crate) fn feed_scancode(byte: u8) -> Option { + process_byte(byte) +} + fn process_byte(byte: u8) -> Option { let state = kbd(); @@ -466,25 +492,35 @@ pub fn mouse_init() -> bool { drain_input(); unsafe { // 1. Enable the auxiliary port itself. - wait_write(); + if !wait_write() { + return false; + } core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_AUX_ENABLE, options(nostack, preserves_flags)); // 2. Configuration Byte: enable IRQ12 + the aux port clock. Read, // OR the two bits, write back (preserves the keyboard IRQ bit // that ps2::init set). - wait_write(); + if !wait_write() { + return false; + } core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_READ_CFG, options(nostack, preserves_flags)); - wait_read(); + if !wait_read() { + return false; + } let mut cfg: u8; core::arch::asm!("in al, dx", out("al") cfg, in("dx") DATA_PORT, options(nostack, preserves_flags)); cfg |= CFG_AUX_IRQ | CFG_AUX_CLOCK; - wait_write(); + if !wait_write() { + return false; + } core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_WRITE_CFG, options(nostack, preserves_flags)); - wait_write(); + if !wait_write() { + return false; + } core::arch::asm!("out dx, al", in("dx") DATA_PORT, in("al") cfg, options(nostack, preserves_flags)); @@ -504,10 +540,14 @@ pub fn mouse_init() -> bool { /// The wait is bounded so a missing device cannot hang the boot. fn aux_command(cmd: u8) -> bool { unsafe { - wait_write(); + if !wait_write() { + return false; + } core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_SEND_TO_AUX, options(nostack, preserves_flags)); - wait_write(); + if !wait_write() { + return false; + } core::arch::asm!("out dx, al", in("dx") DATA_PORT, in("al") cmd, options(nostack, preserves_flags)); for _ in 0..100_000 { diff --git a/rsnix/src/sched.rs b/rsnix/src/sched.rs index dd1409e..259ddf6 100644 --- a/rsnix/src/sched.rs +++ b/rsnix/src/sched.rs @@ -157,6 +157,47 @@ static SCHED_READY: AtomicBool = AtomicBool::new(false); static IN_ISR: AtomicBool = AtomicBool::new(false); static PREEMPT_DEPTH: AtomicU64 = AtomicU64::new(0); +// --------------------------------------------------------------------------- +// Watchdog — turn wedged states into visible panics instead of silent +// freezes. Real hardware has no serial console, so the panic handler +// renders to the framebuffer and writes the crash log; a freeze that would +// otherwise look like a dead machine becomes a reported panic. +// +// The watchdog only runs when the PIT tick is firing (interrupts on). A +// hang with interrupts disabled is invisible to it, so driver waits must +// stay bounded (timeout loops) — enforced at the driver level. +// --------------------------------------------------------------------------- + +/// Consecutive ticks where the preemption lock was held (depth > 0). The +/// tick cannot switch tasks while the lock is held, so a hold this long is +/// a deadlock (a driver wedged inside the serial/klog lock, ...). +static STALL_TICKS: AtomicU64 = AtomicU64::new(0); +const WATCHDOG_LOCK_TICKS: u64 = 500; // 5 s at 100 Hz + +/// Per-task "same interrupted frame" counter for spin detection. +struct SpinWatch { + last_rsp: usize, + same: u32, +} +const SPIN_WATCH_INIT: SpinWatch = SpinWatch { last_rsp: 0, same: 0 }; +static mut SPIN_WATCH: [SpinWatch; MAX_TASKS] = [SPIN_WATCH_INIT; MAX_TASKS]; +/// A spinning background task is interrupted at roughly half the tick rate +/// (it alternates with idle), so 1500 hits is ~30 s of wall time. +const WATCHDOG_SPIN_TICKS: u32 = 1500; +/// Boot-phase threshold for task 0 (the shell): before the REPL is live, +/// task 0 runs init code where a fixed frame means a driver/controller +/// hang — panic after ~15 s wall. The boot must finish well inside this. +const WATCHDOG_BOOT_SPIN_TICKS: u32 = 750; + +/// True once the shell REPL is live (set by main.rs before shell::run()). +/// The shell legitimately parks in poll_key with a fixed frame when it is; +/// before that, task 0 running boot code must not sit in a fixed frame. +static SHELL_RUNNING: AtomicBool = AtomicBool::new(false); + +pub fn mark_shell_running() { + SHELL_RUNNING.store(true, Ordering::Relaxed); +} + // --------------------------------------------------------------------------- // Preemption lock // --------------------------------------------------------------------------- @@ -193,12 +234,49 @@ pub fn current() -> usize { /// Receives the interrupted task's frame pointer; returns the frame /// pointer the asm should switch to (the same one when no switch). pub fn on_tick(cur: usize) -> usize { - if !is_ready() || PREEMPT_DEPTH.load(Ordering::Relaxed) > 0 { + if !is_ready() { return cur; } unsafe { + // Watchdog: the tick fired, so the CPU is alive. A state we cannot + // leave is a hang, not a sleep — surface it as a visible panic. + if PREEMPT_DEPTH.load(Ordering::Relaxed) > 0 { + let n = STALL_TICKS.fetch_add(1, Ordering::Relaxed) + 1; + if n >= WATCHDOG_LOCK_TICKS { + watchdog_panic(cur, "preemption lock held"); + } + return cur; + } + STALL_TICKS.store(0, Ordering::Relaxed); + TASKS[CURRENT].saved_rsp = cur; TASKS[CURRENT].ticks += 1; + // Spin detection: a task whose interrupted frame never moves + // (identical RSP on every tick) is stuck in a hot loop — no yield, + // no sleep, no block. The shell (id 0) legitimately parks in + // poll_key with a fixed frame, and idle (id 1) hlt's; both exempt. + let id = CURRENT; + let w = &mut SPIN_WATCH[id]; + if w.last_rsp == cur { + w.same += 1; + // Background tasks (id >= 2): spinning is never legitimate. + // Task 0 (shell): exempt once the REPL is live (poll_key parks + // with a fixed frame); during the boot phase a fixed frame + // means usb/ehci init is wedged on real hardware. + let threshold = if id == SHELL_TID && !SHELL_RUNNING.load(Ordering::Relaxed) { + WATCHDOG_BOOT_SPIN_TICKS + } else if id >= 2 { + WATCHDOG_SPIN_TICKS + } else { + u32::MAX // idle hlt's with a fixed frame forever — fine + }; + if w.same >= threshold { + watchdog_panic(cur, "task spinning without progress"); + } + } else { + w.last_rsp = cur; + w.same = 1; + } let now = crate::interrupts::uptime_ms(); for t in 0..MAX_TASKS { match TASKS[t].state { @@ -219,6 +297,29 @@ pub fn on_tick(cur: usize) -> usize { } } +/// Render a watchdog verdict through the normal panic path (framebuffer + +/// serial + crash log) and halt. Runs in tick/ISR context: `cur` is the +/// interrupted task's IntFrame, so the reported RIP pinpoints the hang. +fn watchdog_panic(cur: usize, why: &str) -> ! { + let id = unsafe { CURRENT }; + let name = name_str(id); + let rip = unsafe { (cur as *const IntFrame).read().rip }; + let up = crate::interrupts::uptime_ms(); + if id == SHELL_TID { + // Boot-phase hang: report the last completed boot stage so the + // wedged init step is identifiable without serial. + let stage = crate::gop::last_boot_stage(); + panic!( + "watchdog: {} — task {} \"{}\" rip={:#x} (uptime {} ms) boot-stage: {}", + why, id, name, rip, up, stage + ); + } + panic!( + "watchdog: {} — task {} \"{}\" rip={:#x} (uptime {} ms)", + why, id, name, rip, up + ); +} + /// Software yield (vector 0x60). pub fn on_yield(cur: usize) -> usize { if !is_ready() || PREEMPT_DEPTH.load(Ordering::Relaxed) > 0 { diff --git a/rsnix/src/tty.rs b/rsnix/src/tty.rs index 7304925..0783521 100644 --- a/rsnix/src/tty.rs +++ b/rsnix/src/tty.rs @@ -1,4 +1,8 @@ -//! Line discipline over the PS/2 keyboard — the kernel's tty layer. +//! Line discipline over the keyboard — the kernel's tty layer. +//! +//! Keys come from either the PS/2 controller (IRQ-fed scancode ring) or the +//! USB HID boot keyboard (polled UHCI driver). Both are translated to the +//! same `KeyEvent` space, so the shell is input-source agnostic. //! //! Two modes, like a real UNIX terminal: //! - **canonical** (`read_line`): input is echoed, buffered until Enter, @@ -34,6 +38,20 @@ pub enum LineInput { static mut RAW_QUEUE: [u8; 8] = [0; 8]; static mut RAW_LEN: usize = 0; +/// Combined blocking key poll: USB HID keyboard first (polled UHCI), then +/// the PS/2 path (which parks via `hlt` until an IRQ-fed scancode lands). +fn poll_key() -> KeyEvent { + if let Some(ev) = crate::uhci::try_poll_key() { + return ev; + } + ps2::poll_key() +} + +/// Combined non-blocking key poll (USB first, then PS/2). +fn try_poll_key() -> Option { + crate::uhci::try_poll_key().or_else(ps2::try_poll_key) +} + // --------------------------------------------------------------------------- // canonical mode // --------------------------------------------------------------------------- @@ -46,7 +64,7 @@ pub fn read_line() -> LineInput { crate::vt::sync_scroll(); let mut buf = strbuf::String::::new(); loop { - match ps2::poll_key() { + match poll_key() { KeyEvent::Enter => { gop::put_char('\n'); serial::write_str("\r\n"); @@ -104,7 +122,7 @@ pub fn read_line() -> LineInput { /// streaming commands (cat/hexdump) so a long dump can be interrupted. pub fn interrupt_pressed() -> bool { let mut pressed = false; - while let Some(ev) = ps2::try_poll_key() { + while let Some(ev) = try_poll_key() { if matches!(ev, KeyEvent::Ctrl('c')) { pressed = true; } @@ -126,7 +144,7 @@ pub fn read_byte() -> u8 { if let Some(b) = pop_raw() { return b; } - let ev = ps2::poll_key(); + let ev = poll_key(); enqueue_key(ev); } } @@ -136,7 +154,7 @@ pub fn try_read_byte() -> Option { if let Some(b) = pop_raw() { return Some(b); } - match ps2::try_poll_key() { + match try_poll_key() { Some(ev) => { enqueue_key(ev); pop_raw() diff --git a/rsnix/src/uhci.rs b/rsnix/src/uhci.rs new file mode 100644 index 0000000..87ec9c6 --- /dev/null +++ b/rsnix/src/uhci.rs @@ -0,0 +1,1129 @@ +// --------------------------------------------------------------------------- +// rsdos UHCI (USB 1.1) companion driver — HID boot keyboard + mouse. +// +// Real Intel PCH (e.g. OptiPlex 7010) USB2 ports are handled by an EHCI +// (high-speed devices) plus its companion UHCI pair (full/low-speed +// devices). A real USB keyboard/mouse is full- or low-speed, so it lives +// on the UHCI — the EHCI cannot talk to it without a root-hub TT (Intel +// PCH EHCI has none; the ports belong to the companion UHCI). QEMU's +// virtual usb-mouse/usb-kbd are high-speed when attached to usb-ehci, so +// this class of real hardware was never exercised by the xHCI/EHCI paths. +// +// This driver is polled (no IRQ), exactly like ehci.rs: the frame list is +// walked every 1 ms by the controller; we keep one armed interrupt-IN TD +// per device and drain reports from the shell task (keyboard) and cursor +// task (mouse). Both devices share one control pipeline, serialized by a +// spin lock so the shell and cursor tasks cannot corrupt each other's +// transfers. +// +// Data structures follow the UHCI spec as implemented by QEMU's hcd-uhci +// and Linux's uhci-hcd (they agree field-for-field): +// TD: link(0x00) ctrl(0x04) token(0x08) buffer(0x0C) +// QH: link(0x00) el_link(0x04) +// ctrl: ACTIVE=1<<23 IOC=1<<24 IOS=1<<25 LS=1<<26 C_ERR=3<<27 SPD=1<<29 +// ACTLEN=bits 10:0 (n-1, written by the HC) +// token: PID=bits 7:0 (full byte: SETUP=0x2D IN=0x69 OUT=0xE1) +// DEVADDR=bits 14:8, EP=bits 18:15, TOGGLE=bit 19, +// EXPLEN=bits 31:21 (n-1) +// link ptrs: bit0=terminate, bit1=QH type, bit2=depth-first +// --------------------------------------------------------------------------- + +use crate::ps2::MouseEvent; +use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; + +// ---- Registers (I/O space; 16-bit unless noted) ---- +const USBCMD: u16 = 0x00; +const USBSTS: u16 = 0x02; +const USBINTR: u16 = 0x04; +const FRNUM: u16 = 0x06; +const FRBASEADD: u16 = 0x08; // 32-bit +const SOFMOD: u16 = 0x0C; +const PORTSC_BASE: u16 = 0x10; // PORTSC1, PORTSC2 (16-bit each) + +const CMD_RUN: u16 = 1 << 0; +const CMD_HCRESET: u16 = 1 << 1; +const CMD_CF: u16 = 1 << 6; + +const STS_HCHALTED: u16 = 1 << 5; + +const PSC_CCS: u16 = 1 << 0; // current connect status +const PSC_CSC: u16 = 1 << 1; // connect change (write 1 to clear) +const PSC_PE: u16 = 1 << 2; // port enabled +const PSC_PEDC: u16 = 1 << 3; // enable change (write 1 to clear) +const PSC_LSDA: u16 = 1 << 8; // low-speed device attached +const PSC_R: u16 = 1 << 9; // reset +const PSC_SUSP: u16 = 1 << 12; // suspend + +// ---- TD / QH fields ---- +const TD_ACTIVE: u32 = 1 << 23; +const TD_IOC: u32 = 1 << 24; +const TD_LS: u32 = 1 << 26; +const TD_ERR: u32 = 3 << 27; // C_ERR = 3 retries +const TD_STALLED: u32 = 1 << 22; +const TD_BABBLE: u32 = 1 << 20; +const TD_TIMEO: u32 = 1 << 18; +const TD_ACTLEN: u32 = 0x7FF; // actual length, n-1 + +const TOK_DEV_SHIFT: u32 = 8; +const TOK_EP_SHIFT: u32 = 15; +const TOK_TOGGLE: u32 = 1 << 19; +const TOK_EXPLEN_SHIFT: u32 = 21; + +const PID_SETUP: u32 = 0x2D; +const PID_IN: u32 = 0x69; +const PID_OUT: u32 = 0xE1; + +const PTR_TERM: u32 = 0x1; +const PTR_QH: u32 = 0x2; + +// ---- HID boot requests ---- +const RT_CLASS_IFACE: u8 = 0x21; +const REQ_SET_IDLE: u8 = 0x0A; +const REQ_SET_PROTOCOL: u8 = 0x0B; + +// ---- Static state ---- +static IO_BASE: AtomicU32 = AtomicU32::new(0); +static N_PORTS: AtomicU32 = AtomicU32::new(0); +static CTRL_UP: AtomicBool = AtomicBool::new(false); +static MOUSE_PRES: AtomicBool = AtomicBool::new(false); +static KBD_ARMED: AtomicBool = AtomicBool::new(false); +static MOUSE_ARMED: AtomicBool = AtomicBool::new(false); +// Port index each device was found on (usize::MAX = none). Used so the +// keyboard scan skips the mouse's port and vice versa. +static KBD_PORT: AtomicU32 = AtomicU32::new(u32::MAX); +static MOUSE_PORT: AtomicU32 = AtomicU32::new(u32::MAX); +static DEV_KBD: AtomicU32 = AtomicU32::new(0); +static DEV_MOUSE: AtomicU32 = AtomicU32::new(0); +static KBD_EP: AtomicU32 = AtomicU32::new(1); +static KBD_MPS: AtomicU32 = AtomicU32::new(8); +static MOUSE_EP: AtomicU32 = AtomicU32::new(1); +static MOUSE_MPS: AtomicU32 = AtomicU32::new(4); +// Low-speed flag per device (TD_LS needed for 1.5 Mb/s). +static KBD_LS: AtomicBool = AtomicBool::new(false); +static MOUSE_LS: AtomicBool = AtomicBool::new(false); +// Next USB device address to assign (1, 2, ...). +static DEV_SEQ: AtomicU32 = AtomicU32::new(1); +// Throttles for the lazy re-enumeration paths (ms). +static LAST_KBD_ENUM: AtomicU32 = AtomicU32::new(0); +static LAST_MOUSE_ENUM: AtomicU32 = AtomicU32::new(0); +// Serializes control transfers between the shell task (keyboard) and the +// cursor task (mouse). Bounded spin; on timeout the transfer is skipped +// and retried on the next throttled pass. +static CTRL_LOCK: AtomicBool = AtomicBool::new(false); +// Toggle bit the device expects next (alternates after each report). +static KBD_TOGGLE: AtomicU32 = AtomicU32::new(0); +static MOUSE_TOGGLE: AtomicU32 = AtomicU32::new(0); + +// ---- DMA: frame list + one struct page ---- +static mut FRAMELIST: *mut u32 = core::ptr::null_mut(); +static mut P: *mut u32 = core::ptr::null_mut(); // QHs, TDs, buffers + +// Offsets within the struct page (16-byte-aligned QHs/TDs). +const OFF_CTRL_QH: usize = 0x000; +const OFF_KBD_QH: usize = 0x010; +const OFF_MOUSE_QH: usize = 0x020; +const OFF_SETUP_TD: usize = 0x030; +const OFF_DATA_TD: usize = 0x040; +const OFF_STAT_TD: usize = 0x050; +const OFF_KBD_TD: usize = 0x060; +const OFF_MOUSE_TD: usize = 0x070; +const OFF_REQ: usize = 0x100; // 8-byte setup packet +const OFF_DESC: usize = 0x200; // device descriptor (64 B) +const OFF_CFG: usize = 0x300; // config descriptor (256 B) +const OFF_KBD_REPORT: usize = 0x500; // 8 B +const OFF_MOUSE_REPORT: usize = 0x520; // 8 B + +// Keyboard: previous report + pending scancode FIFO (fed to ps2 decoder). +static mut KBD_PREV: [u8; 8] = [0; 8]; +const KBDQ_SIZE: usize = 64; +static mut KBDQ: [u8; KBDQ_SIZE] = [0; KBDQ_SIZE]; +static KBDQ_HEAD: AtomicU32 = AtomicU32::new(0); +static KBDQ_LEN: AtomicU32 = AtomicU32::new(0); + +// ---- port I/O ---- +#[inline] +unsafe fn inw(port: u16) -> u16 { + let v: u16; + unsafe { + core::arch::asm!("in ax, dx", out("ax") v, in("dx") port, options(nostack, preserves_flags)); + } + v +} +#[inline] +unsafe fn outw(port: u16, v: u16) { + unsafe { + core::arch::asm!("out dx, ax", in("dx") port, in("ax") v, options(nostack, preserves_flags)); + } +} +#[inline] +unsafe fn outl(port: u16, v: u32) { + unsafe { + core::arch::asm!("out dx, eax", in("dx") port, in("eax") v, options(nostack, preserves_flags)); + } +} + +fn io() -> u16 { + IO_BASE.load(Ordering::Relaxed) as u16 +} +/// Absolute I/O port of root port `p`'s PORTSC register. +fn prtsc(p: usize) -> u16 { + io() + PORTSC_BASE + (p as u16) * 2 +} + +// ---- DMA helpers (mirror ehci.rs) ---- +unsafe fn dma_alloc(bytes: usize) -> *mut u8 { + unsafe { + let pages = bytes.div_ceil(4096); + let Some(phys) = crate::memory::alloc_pages(pages) else { + return core::ptr::null_mut(); + }; + let p = phys as *mut u8; + core::ptr::write_bytes(p, 0, pages * 4096); + p + } +} + +unsafe fn rd(addr: *const u32) -> u32 { + unsafe { addr.read_volatile() } +} +unsafe fn wr(addr: *mut u32, v: u32) { + unsafe { addr.write_volatile(v) } +} + +fn td_addr(o: usize) -> *mut u32 { + unsafe { P.add(o / 4) } +} +fn buf_addr(o: usize) -> *mut u8 { + unsafe { P.add(o / 4) as *mut u8 } +} + +#[inline] +fn ms_now() -> u64 { + crate::interrupts::uptime_ms() +} + +/// Busy-wait for `ms` milliseconds (interrupts stay enabled; the tick keeps +/// the scheduler alive). +fn ms_spin(ms: u64) { + let t0 = ms_now(); + while ms_now().wrapping_sub(t0) < ms { + core::hint::spin_loop(); + } +} + +/// Build an 8-byte USB setup packet. +fn ctrl_req(bm: u8, breq: u8, val: u16, idx: u16, len: u16) -> [u8; 8] { + let mut r = [0u8; 8]; + r[0] = bm; + r[1] = breq; + r[2] = val as u8; + r[3] = (val >> 8) as u8; + r[4] = idx as u8; + r[5] = (idx >> 8) as u8; + r[6] = len as u8; + r[7] = (len >> 8) as u8; + r +} + +// --------------------------------------------------------------------------- +// Control transfer pipeline (shared, lock-protected) +// --------------------------------------------------------------------------- + +fn lock_ctrl() -> bool { + for _ in 0..200_000 { + if !CTRL_LOCK.swap(true, Ordering::Acquire) { + return true; + } + core::hint::spin_loop(); + } + false +} +fn unlock_ctrl() { + CTRL_LOCK.store(false, Ordering::Release); +} + +/// Issue one control transfer on EP0 of `dev`. `mps` is the device's EP0 +/// max packet, `low_speed` selects the TD_CTRL_LS bit, `req` the setup +/// packet, `dir_in` a device->host data phase, `buf/len` the data buffer. +/// Returns bytes received (IN) or 0 (OUT/setup-only), or a negative error. +/// Completion is polled (STATUS TD active bit) with a 5 s timeout. +unsafe fn ctl_xfer( + dev: u32, + _mps: u32, + low_speed: bool, + req: &[u8; 8], + dir_in: bool, + buf: *mut u8, + len: usize, +) -> i32 { + unsafe { + // The controller DMA's the setup packet straight from the request + // buffer — copy it there first. + core::ptr::copy_nonoverlapping(req.as_ptr(), buf_addr(OFF_REQ), 8); + core::sync::atomic::fence(Ordering::SeqCst); + let ls = if low_speed { TD_LS } else { 0 }; + + // SETUP TD (toggle 0, 8 bytes, always from the request buffer). + let st = td_addr(OFF_SETUP_TD); + wr(st.add(0), 0); // link: filled below + wr(st.add(1), TD_ACTIVE | TD_ERR | ls); + wr( + st.add(2), + PID_SETUP | (dev << TOK_DEV_SHIFT) | (7 << TOK_EXPLEN_SHIFT), // 8 bytes + ); + wr(st.add(3), buf_addr(OFF_REQ) as u32); + + // STATUS TD (toggle 1, opposite direction of the data phase). + let sp = td_addr(OFF_STAT_TD); + let status_pid = if dir_in { PID_OUT } else { PID_IN }; + wr(sp.add(0), PTR_TERM); + wr(sp.add(1), TD_ACTIVE | TD_ERR | ls); + wr( + sp.add(2), + status_pid | (dev << TOK_DEV_SHIFT) | TOK_TOGGLE | (0 << TOK_EXPLEN_SHIFT), + ); + wr(sp.add(3), buf_addr(OFF_REQ) as u32); + + if len == 0 { + // Setup-only transfer: SETUP -> STATUS. + wr(st.add(0), td_addr(OFF_STAT_TD) as u32); + } else { + // DATA TD (toggle 1). + let dt = td_addr(OFF_DATA_TD); + let dpid = if dir_in { PID_IN } else { PID_OUT }; + wr(dt.add(0), td_addr(OFF_STAT_TD) as u32); + wr(dt.add(1), TD_ACTIVE | TD_ERR | ls); + wr( + dt.add(2), + dpid | (dev << TOK_DEV_SHIFT) + | TOK_TOGGLE + | (((len as u32).saturating_sub(1) & 0x7FF) << TOK_EXPLEN_SHIFT), + ); + wr(dt.add(3), buf as u32); + wr(st.add(0), dt as u32); + } + + // Point the control QH at the new chain and let the HC run it. + core::sync::atomic::fence(Ordering::SeqCst); + wr(td_addr(OFF_CTRL_QH).add(1), td_addr(OFF_SETUP_TD) as u32); + core::sync::atomic::fence(Ordering::SeqCst); + + // Wait for completion (STATUS TD clears ACTIVE). + let t0 = ms_now(); + loop { + let c = rd(sp.add(1)); + if c & TD_ACTIVE == 0 { + // Error checks: the status TD mirrors the chain's fate. + if c & (TD_STALLED | TD_BABBLE | TD_TIMEO) != 0 { + return -2; + } + let got = if len > 0 && dir_in { + ((rd(td_addr(OFF_DATA_TD).add(1)) & TD_ACTLEN) + 1) as i32 + } else { + 0 + }; + return got.min(len as i32); + } + if ms_now().wrapping_sub(t0) >= 5000 { + return -1; // timeout + } + core::hint::spin_loop(); + } + } +} + +// --------------------------------------------------------------------------- +// Port management +// --------------------------------------------------------------------------- + +/// Reset a connected root port and enable it. Returns the low-speed flag +/// (false = full-speed) on success. +unsafe fn reset_port(p: usize) -> Option { + unsafe { + let r = prtsc(p); + // Clear connect/enable change bits (write 1s). + outw(r, PSC_CSC | PSC_PEDC); + // Start reset. + outw(r, PSC_R); + ms_spin(50); + // End reset. + outw(r, 0); + // Enable the port (device must be connected). + outw(r, PSC_PE); + let t0 = ms_now(); + while ms_now().wrapping_sub(t0) < 100 { + let psc = inw(r); + if psc & PSC_PE != 0 { + return Some(psc & PSC_LSDA != 0); + } + ms_spin(1); + } + None + } +} + +// --------------------------------------------------------------------------- +// Config-descriptor parsing: find a HID boot interface with the wanted +// protocol and its first interrupt IN endpoint. +// --------------------------------------------------------------------------- + +struct HidEp { + epnum: u8, + mps: u16, + iface: u8, +} + +/// Walk a config descriptor for a boot-protocol HID interface (class 3, +/// subclass 1) whose protocol matches `want` (1 = keyboard, 2 = mouse), +/// returning its first interrupt IN endpoint. +fn find_hid_ep(cfg: &[u8], want: u8) -> Option { + let mut i = 0; + while i + 1 < cfg.len() && cfg[i] != 0 { + let blen = cfg[i] as usize; + let btype = cfg[i + 1]; + if blen < 2 || i + blen > cfg.len() { + return None; + } + if btype == 4 { + // Interface descriptor. + if blen >= 9 + && cfg[i + 5] == 3 // bInterfaceClass = HID + && cfg[i + 6] == 1 // bInterfaceSubClass = boot + && cfg[i + 7] == want + // bInterfaceProtocol + { + let iface = cfg[i + 2]; + // Scan this interface's endpoints. + let mut j = i + blen; + while j + 1 < cfg.len() && cfg[j] != 0 { + let elen = cfg[j] as usize; + let etype = cfg[j + 1]; + if etype == 4 { + break; // next interface + } + if etype == 5 && elen >= 7 { + let epa = cfg[j + 2]; + let attrs = cfg[j + 3]; + if epa & 0x80 != 0 && attrs & 0x03 == 0x03 { + let mps = (cfg[j + 4] as u16) | ((cfg[j + 5] as u16) << 8); + return Some(HidEp { + epnum: epa & 0x0F, + mps: mps.max(1), + iface, + }); + } + } + if elen < 2 { + return None; + } + j += elen; + } + } + } + i += blen; + } + None +} + +// --------------------------------------------------------------------------- +// Device enumeration (one root port) +// --------------------------------------------------------------------------- + +/// Enumerate the device on root port `p` and arm the interrupt-IN endpoint +/// if it is a HID boot device of protocol `want` (1 = keyboard, 2 = mouse). +/// Returns true when armed. +unsafe fn enumerate_port(p: usize, want: u8) -> bool { + unsafe { + let Some(low_speed) = reset_port(p) else { + return false; + }; + + // 1. GET_DESCRIPTOR(device) at address 0 — 8 bytes for EP0 MPS. + let dd = buf_addr(OFF_DESC); + core::ptr::write_bytes(dd, 0, 64); + let req = ctrl_req(0x80, 6, 0x0100, 0, 8); + if ctl_xfer(0, 8, low_speed, &req, true, dd, 8) < 0 { + crate::serial_println!("uhci: port {} get-dev-desc failed (want {})", p, want); + return false; + } + let mps0 = core::ptr::read_volatile(dd.add(7)).max(8) as u32; + + // 2. SET_ADDRESS. + let dev = DEV_SEQ.fetch_add(1, Ordering::Relaxed); + let req = ctrl_req(0x00, 5, dev as u16, 0, 0); + if ctl_xfer(0, mps0, low_speed, &req, false, core::ptr::null_mut(), 0) < 0 { + crate::serial_println!("uhci: set-address failed on port {}", p); + return false; + } + + // 3. Full device descriptor at the new address. + core::ptr::write_bytes(dd, 0, 64); + let req = ctrl_req(0x80, 6, 0x0100, 0, 18); + let got = ctl_xfer(dev, mps0, low_speed, &req, true, dd, 18); + if got < 0 { + crate::serial_println!("uhci: re-get dev-desc failed on port {}", p); + return false; + } + let class = core::ptr::read_volatile(dd.add(4)); + let _ = class; // HID devices have device class 0x00; the HID class + // lives in the interface descriptor (checked below). + + // 4. Full config descriptor. + let cfg = buf_addr(OFF_CFG); + core::ptr::write_bytes(cfg, 0, 256); + let req = ctrl_req(0x80, 6, 0x0200, 0, 256); + let got = ctl_xfer(dev, mps0, low_speed, &req, true, cfg, 256); + if got < 0 { + crate::serial_println!( + "uhci: get-config failed on port {} code={} (data ctrl={:#x} stat ctrl={:#x})", + p, + got, + rd(td_addr(OFF_DATA_TD).add(1)), + rd(td_addr(OFF_STAT_TD).add(1)) + ); + return false; + } + let Some(ep) = find_hid_ep(core::slice::from_raw_parts(cfg, got as usize), want) else { + return false; // not the device we're looking for + }; + + // 5. SET_CONFIGURATION(1). + let req = ctrl_req(0x00, 9, 1, 0, 0); + if ctl_xfer(dev, mps0, low_speed, &req, false, core::ptr::null_mut(), 0) < 0 { + crate::serial_println!("uhci: set-config failed on port {}", p); + return false; + } + + // 6. Boot protocol + no idle. Some devices stall these; tolerate. + let req = ctrl_req(RT_CLASS_IFACE, REQ_SET_PROTOCOL, 1, ep.iface as u16, 0); + if ctl_xfer(dev, mps0, low_speed, &req, false, core::ptr::null_mut(), 0) < 0 { + crate::serial_println!("uhci: set-protocol stalled on port {}", p); + } + let req = ctrl_req(RT_CLASS_IFACE, REQ_SET_IDLE, 0, ep.iface as u16, 0); + if ctl_xfer(dev, mps0, low_speed, &req, false, core::ptr::null_mut(), 0) < 0 { + crate::serial_println!("uhci: set-idle stalled on port {}", p); + } + + // 7. Arm the interrupt-IN TD. + arm_in_td(want, dev, &ep, low_speed, p); + crate::serial_println!( + "uhci: HID boot {} armed (port {} dev {} ep{}({:#x}) mps={} {})", + if want == 1 { "keyboard" } else { "mouse" }, + p, + dev, + ep.epnum, + if want == 1 { 0x81 } else { 0x81 }, + ep.mps, + if low_speed { "low-speed" } else { "full-speed" } + ); + true + } +} + +/// Build the interrupt-IN TD for a device and point its QH element at it. +unsafe fn arm_in_td(want: u8, dev: u32, ep: &HidEp, low_speed: bool, port: usize) { + unsafe { + let (qho, tdo, buf_off, mps, is_kbd) = if want == 1 { + ( + OFF_KBD_QH, + OFF_KBD_TD, + OFF_KBD_REPORT, + (ep.mps as u32).max(8), + true, + ) + } else { + ( + OFF_MOUSE_QH, + OFF_MOUSE_TD, + OFF_MOUSE_REPORT, + (ep.mps as u32).max(4), + false, + ) + }; + let qh = td_addr(qho); + let td = td_addr(tdo); + let ls = if low_speed { TD_LS } else { 0 }; + let explen = if is_kbd { 8 } else { 3 }; + let toggle = 0; // endpoint toggles start at 0 after SET_CONFIG + + wr(td.add(0), PTR_TERM); + wr(td.add(1), TD_ACTIVE | TD_ERR | TD_IOC | ls); + wr( + td.add(2), + PID_IN + | (dev << TOK_DEV_SHIFT) + | ((ep.epnum as u32) << TOK_EP_SHIFT) + | (toggle << 19) + | (explen << TOK_EXPLEN_SHIFT), + ); + wr(td.add(3), buf_addr(buf_off) as u32); + // QH: el_link points at the TD (link/el_link written last). + wr(qh.add(0), PTR_TERM); // no next QH (per-device QH at chain end) + core::sync::atomic::fence(Ordering::SeqCst); + wr(qh.add(1), td as u32); + core::sync::atomic::fence(Ordering::SeqCst); + + match is_kbd { + true => { + KBD_ARMED.store(true, Ordering::Relaxed); + KBD_PORT.store(port as u32, Ordering::Relaxed); + DEV_KBD.store(dev, Ordering::Relaxed); + KBD_EP.store(ep.epnum as u32, Ordering::Relaxed); + KBD_MPS.store(mps, Ordering::Relaxed); + KBD_LS.store(low_speed, Ordering::Relaxed); + KBD_TOGGLE.store(0, Ordering::Relaxed); + } + false => { + MOUSE_ARMED.store(true, Ordering::Relaxed); + MOUSE_PORT.store(port as u32, Ordering::Relaxed); + DEV_MOUSE.store(dev, Ordering::Relaxed); + MOUSE_EP.store(ep.epnum as u32, Ordering::Relaxed); + MOUSE_MPS.store(mps, Ordering::Relaxed); + MOUSE_LS.store(low_speed, Ordering::Relaxed); + MOUSE_TOGGLE.store(0, Ordering::Relaxed); + MOUSE_PRES.store(true, Ordering::Relaxed); + } + } + } +} + +/// Scan all root ports and arm the first device of the wanted protocol. +unsafe fn arm_device(want: u8) -> bool { + unsafe { + if !lock_ctrl() { + return false; + } + let np = N_PORTS.load(Ordering::Relaxed) as usize; + let other = if want == 1 { + MOUSE_PORT.load(Ordering::Relaxed) + } else { + KBD_PORT.load(Ordering::Relaxed) + }; + for p in 0..np { + if p as u32 == other { + continue; // claimed by the other device + } + let psc = inw(prtsc(p)); + if psc & PSC_CCS == 0 { + continue; + } + if enumerate_port(p, want) { + unlock_ctrl(); + return true; + } + } + unlock_ctrl(); + false + } +} + +// --------------------------------------------------------------------------- +// Keyboard: HID report -> scancode-set-1 make/break bytes -> ps2 decoder +// --------------------------------------------------------------------------- + +/// HID usage -> scancode set 1 (make code). High byte 0xE0 = E0-prefixed. +/// 0 = unmapped. Table covers the common keyboard usages (0x04..0x63). +static HID_TO_SCAN: [u16; 0x64] = { + let mut t = [0u16; 0x64]; + let mut i = 0; + while i < 0x64 { + t[i] = 0; + i += 1; + } + // Letters a-z + t[0x04] = 0x1E; + t[0x05] = 0x30; + t[0x06] = 0x2E; + t[0x07] = 0x20; + t[0x08] = 0x12; + t[0x09] = 0x21; + t[0x0A] = 0x22; + t[0x0B] = 0x23; + t[0x0C] = 0x17; + t[0x0D] = 0x24; + t[0x0E] = 0x25; + t[0x0F] = 0x26; + t[0x10] = 0x32; + t[0x11] = 0x31; + t[0x12] = 0x18; + t[0x13] = 0x19; + t[0x14] = 0x10; + t[0x15] = 0x13; + t[0x16] = 0x1F; + t[0x17] = 0x14; + t[0x18] = 0x16; + t[0x19] = 0x2F; + t[0x1A] = 0x11; + t[0x1B] = 0x2D; + t[0x1C] = 0x15; + t[0x1D] = 0x2C; + // Digits + punctuation + t[0x1E] = 0x02; + t[0x1F] = 0x03; + t[0x20] = 0x04; + t[0x21] = 0x05; + t[0x22] = 0x06; + t[0x23] = 0x07; + t[0x24] = 0x08; + t[0x25] = 0x09; + t[0x26] = 0x0A; + t[0x27] = 0x0B; + t[0x28] = 0x1C; // Enter + t[0x29] = 0x01; // Esc + t[0x2A] = 0x0E; // Backspace + t[0x2B] = 0x0F; // Tab + t[0x2C] = 0x39; // Space + t[0x2D] = 0x0C; // - + t[0x2E] = 0x0D; // = + t[0x2F] = 0x1A; // [ + t[0x30] = 0x1B; // ] + t[0x31] = 0x2B; // backslash + t[0x33] = 0x27; // ; + t[0x34] = 0x28; // ' + t[0x35] = 0x29; // ` + t[0x36] = 0x33; // , + t[0x37] = 0x34; // . + t[0x38] = 0x35; // / + t[0x39] = 0x3A; // CapsLock + // F-keys + t[0x3A] = 0x3B; // F1 + t[0x3B] = 0x3C; + t[0x3C] = 0x3D; + t[0x3D] = 0x3E; + t[0x3E] = 0x3F; + t[0x3F] = 0x40; + t[0x40] = 0x41; + t[0x41] = 0x42; + t[0x42] = 0x43; + t[0x43] = 0x44; // F10 + t[0x44] = 0xE057; // F11 + t[0x45] = 0xE058; // F12 + // Nav cluster + t[0x49] = 0xE052; // Insert + t[0x4A] = 0xE047; // Home + t[0x4B] = 0xE049; // PageUp + t[0x4C] = 0xE053; // Delete + t[0x4D] = 0xE04F; // End + t[0x4E] = 0xE051; // PageDown + t[0x4F] = 0xE04D; // Right + t[0x50] = 0xE04B; // Left + t[0x51] = 0xE050; // Down + t[0x52] = 0xE048; // Up + // Keypad + t[0x53] = 0x45; // NumLock + t[0x54] = 0xE035; // / + t[0x55] = 0x37; // * + t[0x56] = 0x4A; // - + t[0x57] = 0x4E; // + + t[0x58] = 0xE01C; // Enter + t[0x59] = 0x4F; // 1 + t[0x5A] = 0x50; // 2 + t[0x5B] = 0x51; // 3 + t[0x5C] = 0x4B; // 4 + t[0x5D] = 0x4C; // 5 + t[0x5E] = 0x4D; // 6 + t[0x5F] = 0x47; // 7 + t[0x60] = 0x48; // 8 + t[0x61] = 0x49; // 9 + t[0x62] = 0x52; // 0 + t[0x63] = 0x53; // . + t +}; + +/// Modifier bit -> scancode (set 1, high byte 0xE0 = E0-prefixed). +static MOD_TO_SCAN: [u16; 8] = [ + 0x1D, // 0 LCtrl + 0x2A, // 1 LShift + 0x38, // 2 LAlt + 0xE05B, // 3 LGui + 0xE01D, // 4 RCtrl + 0x36, // 5 RShift + 0xE038, // 6 RAlt + 0xE05C, // 7 RGui +]; + +fn kbdq_push(b: u8) { + unsafe { + let h = KBDQ_HEAD.load(Ordering::Relaxed) as usize; + let l = KBDQ_LEN.load(Ordering::Relaxed) as usize; + if l >= KBDQ_SIZE { + return; + } + KBDQ[(h + l) % KBDQ_SIZE] = b; + KBDQ_LEN.store((l + 1) as u32, Ordering::Relaxed); + } +} +fn kbdq_pop() -> Option { + unsafe { + let l = KBDQ_LEN.load(Ordering::Relaxed) as usize; + if l == 0 { + return None; + } + let h = KBDQ_HEAD.load(Ordering::Relaxed) as usize; + let b = KBDQ[h]; + KBDQ_HEAD.store(((h + 1) % KBDQ_SIZE) as u32, Ordering::Relaxed); + KBDQ_LEN.store((l - 1) as u32, Ordering::Relaxed); + Some(b) + } +} + +/// Push the make (press) or break (release) scancode sequence for a +/// scancode-map entry (0 = unmapped, 0x00XX = plain, 0xE0XX = E0-prefixed). +fn push_scancode(scan: u16, press: bool) { + if scan == 0 { + return; + } + if scan & 0xFF00 == 0xE000 { + kbdq_push(0xE0); + let code = (scan & 0xFF) as u8; + kbdq_push(if press { code } else { code | 0x80 }); + } else { + let code = scan as u8; + kbdq_push(if press { code } else { code | 0x80 }); + } +} + +/// Diff the completed HID report against the previous one and queue the +/// scancode transitions (make/break) for the shared PS/2 decoder. +unsafe fn diff_report(report: &[u8]) { + unsafe { + let prev = &mut *core::ptr::addr_of_mut!(KBD_PREV); + let new_mods = report[0]; + let old_mods = prev[0]; + for bit in 0..8 { + let new_set = new_mods & (1 << bit) != 0; + let old_set = old_mods & (1 << bit) != 0; + if new_set != old_set { + push_scancode(MOD_TO_SCAN[bit], new_set); + } + } + let new_keys = &report[2..8]; + let old_keys = &prev[2..8]; + // Releases first (old keys not in new set). + for &k in old_keys { + if k == 0 || new_keys.contains(&k) { + continue; + } + if k as usize >= HID_TO_SCAN.len() { + continue; + } + push_scancode(HID_TO_SCAN[k as usize], false); + } + // Then presses (new keys not in the old set). + for &k in new_keys { + if k == 0 || old_keys.contains(&k) { + continue; + } + if k as usize >= HID_TO_SCAN.len() { + continue; + } + push_scancode(HID_TO_SCAN[k as usize], true); + } + prev.copy_from_slice(report); + } +} + +/// Re-arm a completed interrupt-IN TD (one-outstanding report pattern). +unsafe fn rearm_td(qho: usize, tdo: usize, toggle: u32, ls: bool, mps: usize, explen: u32) { + unsafe { + let qh = td_addr(qho); + let td = td_addr(tdo); + let t = rd(td.add(2)); + let dev = (t >> TOK_DEV_SHIFT) & 0x7F; + let ep = (t >> TOK_EP_SHIFT) & 0xF; + wr( + td.add(1), + TD_ACTIVE | TD_ERR | TD_IOC | if ls { TD_LS } else { 0 }, + ); + wr( + td.add(2), + PID_IN + | (dev << TOK_DEV_SHIFT) + | (ep << TOK_EP_SHIFT) + | (toggle << 19) + | (explen << TOK_EXPLEN_SHIFT), + ); + core::sync::atomic::fence(Ordering::SeqCst); + wr(qh.add(1), td as u32); + core::sync::atomic::fence(Ordering::SeqCst); + let _ = mps; + } +} + +// --------------------------------------------------------------------------- +// Public interface +// --------------------------------------------------------------------------- + +pub fn controller_up() -> bool { + CTRL_UP.load(Ordering::Relaxed) +} +pub fn mouse_present() -> bool { + MOUSE_PRES.load(Ordering::Relaxed) +} + +/// Find the first UHCI companion controller (class 0x0C, subclass 0x03, +/// prog_if 0x00). +fn find_uhci() -> Option<(u8, u8, u8)> { + for d in crate::pci::probe() { + if d.class == 0x0C && d.subclass == 0x03 && d.prog_if == 0x00 { + return Some((d.bus, d.device, d.func)); + } + } + None +} + +/// Bring up the first UHCI controller: claim it, reset it, run the frame +/// list, and enumerate any connected boot HID devices. +pub fn init() -> bool { + let Some((b, d, f)) = find_uhci() else { + crate::serial_println!("uhci: no UHCI controller"); + return false; + }; + crate::serial_println!("uhci: USB1.1 UHCI at {:02x}:{:02x}.{}", b, d, f); + + unsafe { + // I/O space + bus master (polled: no INTx assertion needed, but + // clear it anyway so a stray legacy IRQ can never storm). + let cmd = crate::pci::config_read(b, d, f, 0x04); + crate::pci::config_write( + b, + d, + f, + 0x04, + (cmd & 0xFFFF_0000) | (cmd & 0xFFFF) | 0x5 | 0x400, + ); + let mut base: u32 = 0; + // BAR0 first (Intel ICH), then BAR4 (PIIX3/PIIX4-style). Take the + // first nonzero I/O BAR. + for bar_idx in [0u8, 4] { + let bar = crate::pci::config_read(b, d, f, 0x10 + bar_idx * 4); + let cand = bar & !0x3; + if cand != 0 { + base = cand; + break; + } + } + if base == 0 { + crate::serial_println!("uhci: no I/O BAR"); + return false; + } + IO_BASE.store(base, Ordering::Relaxed); + + // Controller reset: stop, then HCRESET (UHCI clears HCRESET itself + // within microseconds). + outw(io(), 0); + outw(io() + USBCMD, CMD_HCRESET); + ms_spin(5); + let mut t = 0; + while t < 1_000_000 && inw(io() + USBCMD) & CMD_HCRESET != 0 { + t += 1; + core::hint::spin_loop(); + } + outw(io() + USBINTR, 0); + + // DMA: 4 KiB frame list (1024 entries) + struct page. + FRAMELIST = dma_alloc(4096) as *mut u32; + P = dma_alloc(4096) as *mut u32; + if FRAMELIST.is_null() || P.is_null() { + crate::serial_println!("uhci: DMA alloc failed"); + return false; + } + + // Wire the schedule: every frame entry -> CTRL_QH -> KBD_QH -> + // MOUSE_QH -> terminate. Each QH's element is set when its device + // arms; the control QH element is set per transfer. + let fl = FRAMELIST; + for i in 0..1024 { + wr(fl.add(i), (td_addr(OFF_CTRL_QH) as u32) | PTR_QH); + } + wr( + td_addr(OFF_CTRL_QH).add(0), + (td_addr(OFF_KBD_QH) as u32) | PTR_QH, + ); + wr(td_addr(OFF_CTRL_QH).add(1), PTR_TERM); + wr( + td_addr(OFF_KBD_QH).add(0), + (td_addr(OFF_MOUSE_QH) as u32) | PTR_QH, + ); + wr(td_addr(OFF_KBD_QH).add(1), PTR_TERM); + wr(td_addr(OFF_MOUSE_QH).add(0), PTR_TERM); + wr(td_addr(OFF_MOUSE_QH).add(1), PTR_TERM); + core::sync::atomic::fence(Ordering::SeqCst); + + // Frame list base, configure, run. + outl(io() + FRBASEADD, fl as u32); + outw(io() + SOFMOD, 64); + outw(io() + FRNUM, 0); + outw(io() + USBCMD, CMD_CF); + outw(io() + USBCMD, CMD_CF | CMD_RUN); + ms_spin(2); + if inw(io() + USBSTS) & STS_HCHALTED != 0 { + crate::serial_println!("uhci: controller failed to run"); + return false; + } + + // Port count: UHCI has 2 ports at 0x10/0x12; a few have more. + let mut np = 0; + while np < 4 { + let psc = inw(prtsc(np)); + if psc == 0xFFFF || psc == 0xff7f { + break; + } + np += 1; + } + N_PORTS.store(np as u32, Ordering::Relaxed); + CTRL_UP.store(true, Ordering::Relaxed); + crate::serial_println!("uhci: controller up (io={:#x} ports={})", base, np); + + // Enumerate a boot keyboard first (input priority), then a mouse. + crate::serial_println!( + "uhci: dbg P={:#x} FL={:#x} ctrlqh={:#x} el={:#x} fl0={:#x} setup_td={:#x} setup_link={:#x}", + P as usize, + FRAMELIST as usize, + td_addr(OFF_CTRL_QH) as usize, + rd(td_addr(OFF_CTRL_QH).add(1)), + rd(FRAMELIST.add(0)), + td_addr(OFF_SETUP_TD) as usize, + rd(td_addr(OFF_SETUP_TD).add(0)) + ); + let _ = arm_device(1); + let _ = arm_device(2); + true + } +} + +/// Non-blocking keyboard poll: drains the scancode FIFO (fed by completed +/// HID reports) through the shared PS/2 decoder. Lazy re-enumeration is +/// throttled to one attempt per second. +pub fn try_poll_key() -> Option { + unsafe { + // Fast path: pending scancodes first. + if let Some(b) = kbdq_pop() { + return crate::ps2::feed_scancode(b); + } + if KBD_ARMED.load(Ordering::Relaxed) { + let td = td_addr(OFF_KBD_TD); + let c = rd(td.add(1)); + if c & TD_ACTIVE == 0 { + if c & (TD_STALLED | TD_BABBLE | TD_TIMEO) != 0 { + // Transfer error: drop the arm; bounded re-enum follows. + KBD_ARMED.store(false, Ordering::Relaxed); + return None; + } + // Completed: diff the report, re-arm, then drain the FIFO. + let rep = buf_addr(OFF_KBD_REPORT); + let mut report = [0u8; 8]; + for (i, b) in report.iter_mut().enumerate() { + *b = core::ptr::read_volatile(rep.add(i)); + } + diff_report(&report); + let tog = KBD_TOGGLE.load(Ordering::Relaxed) ^ 1; + KBD_TOGGLE.store(tog, Ordering::Relaxed); + rearm_td( + OFF_KBD_QH, + OFF_KBD_TD, + tog, + KBD_LS.load(Ordering::Relaxed), + KBD_MPS.load(Ordering::Relaxed) as usize, + 8, + ); + return kbdq_pop().and_then(crate::ps2::feed_scancode); + } + return None; + } + // Not armed: bring the keyboard up lazily, never hot-loop. + if CTRL_UP.load(Ordering::Relaxed) { + let now = crate::interrupts::uptime_ms() as u32; + if now.wrapping_sub(LAST_KBD_ENUM.load(Ordering::Relaxed)) >= 1000 { + LAST_KBD_ENUM.store(now, Ordering::Relaxed); + let _ = arm_device(1); + } + } + kbdq_pop().and_then(crate::ps2::feed_scancode) + } +} + +/// Poll for a mouse report (mirrors ehci::poll_mouse): hot path reads the +/// armed TD's ACTIVE bit in DMA memory; completion decodes the report and +/// re-arms; a stalled/TD-error transfer drops the arm for a bounded +/// re-enumeration. +pub fn poll_mouse() -> Option { + unsafe { + if MOUSE_ARMED.load(Ordering::Relaxed) { + let td = td_addr(OFF_MOUSE_TD); + let c = rd(td.add(1)); + if c & TD_ACTIVE == 0 { + if c & (TD_STALLED | TD_BABBLE | TD_TIMEO) != 0 { + MOUSE_ARMED.store(false, Ordering::Relaxed); + return None; + } + let hb = buf_addr(OFF_MOUSE_REPORT); + let b0 = core::ptr::read_volatile(hb); + let b1 = core::ptr::read_volatile(hb.add(1)); + let b2 = core::ptr::read_volatile(hb.add(2)); + let ev = Some(MouseEvent { + dx: (b1 as i8) as i16, + dy: (b2 as i8) as i16, + left: b0 & 0x01 != 0, + right: b0 & 0x02 != 0, + middle: b0 & 0x04 != 0, + }); + let tog = MOUSE_TOGGLE.load(Ordering::Relaxed) ^ 1; + MOUSE_TOGGLE.store(tog, Ordering::Relaxed); + rearm_td( + OFF_MOUSE_QH, + OFF_MOUSE_TD, + tog, + MOUSE_LS.load(Ordering::Relaxed), + MOUSE_MPS.load(Ordering::Relaxed) as usize, + 3, + ); + return ev; + } + return None; + } + if CTRL_UP.load(Ordering::Relaxed) { + let now = crate::interrupts::uptime_ms() as u32; + if now.wrapping_sub(LAST_MOUSE_ENUM.load(Ordering::Relaxed)) >= 1000 { + LAST_MOUSE_ENUM.store(now, Ordering::Relaxed); + let _ = arm_device(2); + } + } + None + } +} + +/// One-line status for the shell `usb` command. +pub fn diagnose() -> alloc::string::String { + let mut s = alloc::string::String::new(); + s.push_str(&alloc::format!( + "uhci io={:#x} ports={} kbd={} mouse={} kbd_dev={} mouse_dev={}\n", + IO_BASE.load(Ordering::Relaxed), + N_PORTS.load(Ordering::Relaxed), + if KBD_ARMED.load(Ordering::Relaxed) { + "armed" + } else { + "-" + }, + if MOUSE_ARMED.load(Ordering::Relaxed) { + "armed" + } else { + "-" + }, + DEV_KBD.load(Ordering::Relaxed), + DEV_MOUSE.load(Ordering::Relaxed) + )); + let np = N_PORTS.load(Ordering::Relaxed) as usize; + for p in 0..np.min(4) { + let v = unsafe { inw(prtsc(p)) }; + s.push_str(&alloc::format!( + " uport{}: {}{}{}{:#x}\n", + p, + if v & PSC_CCS != 0 { "conn " } else { "none " }, + if v & PSC_PE != 0 { "en " } else { "" }, + if v & PSC_LSDA != 0 { "low " } else { "" }, + v + )); + } + s +} diff --git a/rsnix/src/usb.rs b/rsnix/src/usb.rs index 71df781..3ed3a0c 100644 --- a/rsnix/src/usb.rs +++ b/rsnix/src/usb.rs @@ -229,6 +229,15 @@ unsafe fn wr64(a: usize, v: u64) { fn op(off: usize) -> usize { unsafe { OPB + off } } + +/// 16-bit I/O read (UHCI port status/control registers live in I/O space). +unsafe fn io_iow16(port: u16) -> u16 { + let v: u16; + unsafe { + core::arch::asm!("in ax, dx", out("ax") v, in("dx") port, options(nomem, nostack, preserves_flags)); + } + v +} fn db(slot: u32) -> usize { unsafe { DBB }.wrapping_add(slot as usize * 4) } @@ -727,7 +736,13 @@ pub fn init() -> bool { dev, func, 0x04, - (cmd & 0xFFFF0000) | ((cmd & 0xFFFF) | 0x6), + // Memory space + bus master + INTx disable (bit 10). The + // driver is polled (event ring drained from the cursor task), + // so the controller must never assert INTx: an unhandled + // xHCI interrupt would hit the kernel's catch-all handler + // and (being level-triggered) re-fire forever on real + // hardware. + (cmd & 0xFFFF_0000) | ((cmd & 0xFFFF) | 0x6 | 0x400), ); let bar = crate::pci::bar64(b, dev, func, 0); let base = bar.unwrap() as usize; @@ -745,6 +760,15 @@ pub fn init() -> bool { 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 }; + + // Claim USB Legacy ownership from the BIOS before resetting the + // controller (same SMM-legacy rationale as the EHCI driver). + match crate::pci::claim_usb_legacy(b, dev, func, base, 0x04, true) { + 0 => crate::serial_println!("usb: xHCI USB legacy: already released (no claim needed)"), + 1 => crate::serial_println!("usb: xHCI USB legacy handed over by BIOS"), + 2 => crate::serial_println!("usb: xHCI USB legacy claim timed out (BIOS sem stuck)"), + _ => crate::serial_println!("usb: xHCI USB legacy: no capability found"), + } } // Reset the controller (stop, then reset). Wait for reset completion @@ -802,8 +826,10 @@ pub fn init() -> bool { 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); + // Interrupter 0: keep the interrupt DISABLED (IE=0, clear any + // pending IP) — the driver is polled and the controller must not + // assert INTx (see the PCI command-register comment above). + wr(rt(RT_IMAN), (rd(rt(RT_IMAN)) & !0x2) | 0x1); wr(rt(RT_IMOD), 0); // Run. wr(op(OP_USBCMD), rd(op(OP_USBCMD)) | CMD_RUN); @@ -973,5 +999,120 @@ pub fn diagnose() -> alloc::string::String { unsafe { QUEUED } )); } + + // Cross-controller attach scan: report which PCI USB controller(s) + // currently have a device with bit0 (connect) set, across xHCI / EHCI / + // UHCI / OHCI. Read-only diagnostic to locate the mouse on real hw. + s.push_str("usbctl: scanning all USB controllers:\n"); + unsafe { + for d in crate::pci::probe() { + if d.class != 0x0C || d.subclass != 0x03 { + continue; + } + let lbl = match d.prog_if { + 0x00 => "UHCI", + 0x10 => "OHCI", + 0x20 => "EHCI", + 0x30 => "xHCI", + _ => "USB?", + }; + let bar = crate::pci::bar64(d.bus, d.device, d.func, 0); + let base = bar.unwrap_or(0); + s.push_str(&alloc::format!( + " {} {:02x}:{:02x}.{} bar={:#x}\n", + lbl, d.bus, d.device, d.func, base + )); + if base == 0 || base == u64::MAX { + // BAR not decoded / not enabled — nothing to probe. + continue; + } + match d.prog_if { + 0x30 => { + if base == 0 { + continue; + } + let caplen = rd(base as usize); + let hcs1 = rd(base as usize + 0x04); + let np = ((hcs1 >> 24) & 0xff) as usize; + let opb = base as usize + (caplen & 0xff) as usize; + s.push_str(&alloc::format!( + " xhci ports={} sc=", np + )); + for p in 0..np.min(16) { + let v = rd(opb + 0x400 + p * 0x10); + s.push_str(&alloc::format!( + "{}{:#x}", + if v & 1 != 0 { "*" } else { "" }, + v & 0x3ff + )); + if p + 1 < (np.min(16)) { + s.push(' '); + } + } + s.push('\n'); + } + 0x20 => { + // EHCI: MMIO BAR. CAPLENGTH byte0, HcSParams (nports) @0x04. + let cl = rd(base as usize) & 0xff; + let sp = rd(base as usize + 0x04); + let hc = rd(base as usize + 0x08); + let np = (sp & 0xf) as usize; + let opb = base as usize + cl as usize; + let cmd = crate::pci::config_read(d.bus, d.device, d.func, 0x04); + let eccp = crate::pci::config_read(d.bus, d.device, d.func, 0x34); + s.push_str(&alloc::format!( + " ehci nports={} hrs={} hcc={:#x} cmd={:#x} eccp={:#x}\n", + np, sp, hc, cmd, (eccp >> 8) & 0xff + )); + let cfg = rd(opb + 0x40); + s.push_str(&alloc::format!( + " ehci cfgflag={:#x} ", cfg + )); + for p in 0..np.min(16) { + let v = rd(opb + 0x44 + p * 0x04); + s.push_str(&alloc::format!( + "{}{:#x},", + if v & 1 != 0 { "*" } else { "" }, + v & 0xffff + )); + } + s.push('\n'); + // Legacy BIOS handoff status (USBLEGSUP at EECP). + let eo = ((eccp >> 8) & 0xff) as usize; + if eo != 0 && (eccp & 0xff) != 0 { + let hs = crate::pci::config_read( + d.bus, + d.device, + d.func, + (eo & 0xfc) as u8, + ); + s.push_str(&alloc::format!( + " ehci legacy=usblegsup={:#x} os-owner={} bios-owned={}\n", + hs, + (hs & (1 << 24)) != 0, + (hs & (1 << 16)) != 0 || (hs & 0x100) != 0 + )); + } + } + 0x00 => { + // UHCI: I/O BAR (bar64 refuses I/O bars — read raw). + let lo = crate::pci::config_read(d.bus, d.device, d.func, 0x10); + let io = (lo & 0xFFF0) as u16; + if io == 0 { + continue; + } + for p in 0..2 { + let pr = io_iow16(io + 0x10 + (p as u16) * 2); + s.push_str(&alloc::format!( + "{}{:x}:", if pr & 1 != 0 { "*" } else { "" }, + (pr & 0xff) + )); + } + s.push('\n'); + } + _ => {} + } + } + } s } diff --git a/rsnix/src/vt.rs b/rsnix/src/vt.rs index 4ffb44d..9444063 100644 --- a/rsnix/src/vt.rs +++ b/rsnix/src/vt.rs @@ -121,9 +121,11 @@ pub fn init() { COLS = cols; CELLS = Box::into_raw(Box::new(vec![BLANK; rows * cols])); RENDERED = Box::into_raw(Box::new(vec![BLANK; rows * cols])); - CURSOR_R = 0; + // Home below row 0: row 0 is the boot-marker status line, so the + // terminal (shell + klog screen echo) must never write to it. + CURSOR_R = 1; CURSOR_C = 0; - LAST_CURSOR = (0, 0); + LAST_CURSOR = (1, 0); CURSOR_VISIBLE = true; SGR_FG = 7; SGR_BG = 0; @@ -133,6 +135,13 @@ pub fn init() { } } +/// True once the terminal is initialised (grids live). The klog screen +/// echo checks this so serial lines printed before vt::init (very early +/// boot) are skipped instead of hitting a null grid. +pub fn ready() -> bool { + unsafe { ROWS != 0 && !CELLS.is_null() } +} + /// (rows, cols) — matches the editor's expected (rows, cols) ordering. pub fn size() -> (usize, usize) { unsafe { (ROWS, COLS) } @@ -667,10 +676,41 @@ pub fn sync_scroll() { // public writing API // --------------------------------------------------------------------------- +/// Disable interrupts unless they are already off (nested-safe). Returns +/// whether they were on, so `irq_restore` re-enables exactly when needed. +/// The terminal write path uses this so a klog screen-echo (which can run +/// in ISR context) can never tear a shell write, and a task-context write +/// can never be split by a tick. +fn irq_off() -> bool { + let rflags: u64; + unsafe { + core::arch::asm!( + "pushfq; pop {}", + out(reg) rflags, + options(nostack, preserves_flags) + ); + if rflags & (1 << 9) != 0 { + core::arch::asm!("cli", options(nostack, preserves_flags)); + true + } else { + false + } + } +} + +fn irq_restore(was_on: bool) { + if was_on { + unsafe { + core::arch::asm!("sti", options(nostack, preserves_flags)); + } + } +} + /// Write raw bytes (ANSI-capable). Used by full-screen apps. UTF-8 /// sequences are decoded so Unicode glyphs (box-drawing, arrows) render; /// undecodable bytes become '?'. pub fn write_bytes(b: &[u8]) { + let on = irq_off(); let mut i = 0; while i < b.len() { let byte = b[i]; @@ -701,20 +741,27 @@ pub fn write_bytes(b: &[u8]) { } } flush(); + irq_restore(on); } /// Write a UTF-8 string (plain text; Unicode glyphs render, others '?'). +/// Atomic with respect to interrupts: a klog screen-echo in ISR context +/// cannot interleave with a shell write, and a tick cannot split a write. pub fn write_str(s: &str) { + let on = irq_off(); for c in s.chars() { write_char(c); } flush(); + irq_restore(on); } /// Write a single ASCII byte / char. pub fn put_char(b: u8) { + let on = irq_off(); put_byte(b); flush(); + irq_restore(on); } /// Clear the whole screen, reset colours, home the cursor. -- cgit v1.2.3