summaryrefslogtreecommitdiffstats
path: root/rsnix
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-17 14:53:52 +0200
committersalmonik <salmonik@tuta.io>2026-08-17 14:53:52 +0200
commitbe868a43185826b02bf6d091019bb779678525bb (patch)
tree586988c15f1d700b1621c7f0208584b30878a56c /rsnix
parentv0.5.10: change em-dashes to dashes (diff)
downloadrs-dos-be868a43185826b02bf6d091019bb779678525bb.tar.gz
rs-dos-be868a43185826b02bf6d091019bb779678525bb.zip
USB2/1.1 (EHCI/UHCI) drivers + real-hardware bring-up hardening
- Add polled EHCI (USB2) and UHCI (USB1.1) host-controller drivers with HID boot mouse support, wired into cursor polling and the 'usb' shell cmd - Add USB Legacy ownership handoff (claim_usb_legacy) plus config_write_byte - Harden boot: verify the PIT/timer ticks before shell entry, EOI stray interrupts in the LAPIC to stop interrupt storms, bound PS/2 controller waits, and INTx-disable AHCI (polled driver) - Add boot stage markers, klog screen echo, and QR lock; scheduler mark_shell_running; NO_USB stopgap to keep BIOS USB-Legacy input alive - alloc_pages_above for DMA buffers that must avoid low conventional memory - Expand DEBUG_NOTES with EHCI/QEMU bring-up findings
Diffstat (limited to 'rsnix')
-rw-r--r--rsnix/src/ahci.rs7
-rw-r--r--rsnix/src/cursor.rs6
-rw-r--r--rsnix/src/ehci.rs1274
-rw-r--r--rsnix/src/gop.rs18
-rw-r--r--rsnix/src/interrupts.rs36
-rw-r--r--rsnix/src/klog.rs10
-rw-r--r--rsnix/src/lib.rs2
-rw-r--r--rsnix/src/memory.rs44
-rw-r--r--rsnix/src/pci.rs125
-rw-r--r--rsnix/src/ps2.rs78
-rw-r--r--rsnix/src/sched.rs103
-rw-r--r--rsnix/src/tty.rs28
-rw-r--r--rsnix/src/uhci.rs1129
-rw-r--r--rsnix/src/usb.rs147
-rw-r--r--rsnix/src/vt.rs51
15 files changed, 3020 insertions, 38 deletions
diff --git a/rsnix/src/ahci.rs b/rsnix/src/ahci.rs
index a77b341..543bb21 100644
--- a/rsnix/src/ahci.rs
+++ b/rsnix/src/ahci.rs
@@ -230,9 +230,12 @@ pub fn init() -> bool {
230 let Some((dev, func)) = find_ahci() else { 230 let Some((dev, func)) = find_ahci() else {
231 return false; 231 return false;
232 }; 232 };
233 // Enable memory space + bus mastering so MMIO and DMA work. 233 // Enable memory space + bus mastering so MMIO and DMA work, plus INTx
234 // disable (bit 10): the driver polls command completion (no ISR, no
235 // MSI), so the HBA must not assert INTx — a stray SATA interrupt would
236 // re-fire forever on real hardware.
234 let cmd_reg = pci::config_read(0, dev, func, 0x04); 237 let cmd_reg = pci::config_read(0, dev, func, 0x04);
235 pci::config_write(0, dev, func, 0x04, cmd_reg | 0x7); 238 pci::config_write(0, dev, func, 0x04, cmd_reg | 0x7 | 0x400);
236 239
237 let Some(abar) = pci::bar64(0, dev, func, 5) else { 240 let Some(abar) = pci::bar64(0, dev, func, 5) else {
238 return false; 241 return false;
diff --git a/rsnix/src/cursor.rs b/rsnix/src/cursor.rs
index d9d97bc..31b919e 100644
--- a/rsnix/src/cursor.rs
+++ b/rsnix/src/cursor.rs
@@ -50,7 +50,11 @@ fn forward_target() -> Option<usize> {
50 50
51fn task_main() { 51fn task_main() {
52 loop { 52 loop {
53 while let Some(ev) = crate::usb::poll_mouse().or_else(crate::ps2::try_poll_mouse) { 53 while let Some(ev) = crate::usb::poll_mouse()
54 .or_else(crate::ehci::poll_mouse)
55 .or_else(crate::uhci::poll_mouse)
56 .or_else(crate::ps2::try_poll_mouse)
57 {
54 crate::gop::cursor_move(ev.dx, ev.dy); 58 crate::gop::cursor_move(ev.dx, ev.dy);
55 crate::gop::cursor_set_buttons(btn_bits(&ev)); 59 crate::gop::cursor_set_buttons(btn_bits(&ev));
56 if let Some(id) = forward_target() 60 if let Some(id) = forward_target()
diff --git a/rsnix/src/ehci.rs b/rsnix/src/ehci.rs
new file mode 100644
index 0000000..6209155
--- /dev/null
+++ b/rsnix/src/ehci.rs
@@ -0,0 +1,1274 @@
1// ---------------------------------------------------------------------------
2// rsdos EHCI (USB2.0 host controller) driver — mouse only.
3//
4// Enumerates devices connected to an Intel-class EHCI (direct root-port
5// connect), discovers a HID boot-mouse interrupt-IN endpoint, and drains its
6// reports into ps2::MouseEvent. Uses the EHCI async schedule for control
7// transfers and the periodic (frame-list) schedule for the interrupt IN
8// endpoint. Polled: no IRQ/MSI — keep exactly one report outstanding and
9// drain it from the cursor task.
10//
11// Why EHCI: on real hardware (e.g. a Q67/Q77-era OptiPlex) a USB mouse is a
12// USB2 (full/low-speed) device attached to the EHCI USB2 controllers, not to
13// the xHCI's USB3/SS ports. QEMU only exercises the xHCI path, so this class
14// of real hardware requires its own driver.
15//
16// Identity map (phys==virt) means any kernel-heap pointer is a valid DMA
17// address; data structures must be 32-byte aligned for the EHCI.
18// ---------------------------------------------------------------------------
19
20use crate::ps2::MouseEvent;
21use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
22
23// ---- Registers (host controller registers; opbase = base + CAPLENGTH) ----
24const OPCMD: usize = 0x00;
25const OPSTS: usize = 0x04;
26const OPINTR: usize = 0x08;
27const OPFRIDX: usize = 0x0C;
28const OPCFGFLAG: usize = 0x40;
29const OP_PRTSC_BASE: usize = 0x44; // PORTSC #1
30const OP_PRTSC_STRIDE: usize = 0x04;
31
32const CMD_RUN: u32 = 1 << 0;
33const CMD_HCRESET: u32 = 1 << 1;
34const CMD_PSE: u32 = 1 << 4;
35const CMD_ASE: u32 = 1 << 5;
36
37const STS_HCH: u32 = 1 << 12;
38const STS_SE: u32 = 1 << 4;
39const STS_RECL: u32 = 1 << 14;
40
41const PRT_CONNECT: u32 = 1 << 0;
42const PRT_PED: u32 = 1 << 1;
43const PRT_RESET: u32 = 1 << 8;
44const PRT_POWER: u32 = 1 << 12;
45const PRT_RWC: u32 = (1 << 17) | (1 << 18) | (1 << 16) | (1 << 20); // CSC|PEC|CRC|OCC
46
47// ---- QH / QTD fields ----
48const Q_TYPE_QH: u32 = 1 << 1;
49const QT_TERMINATE: u32 = 1; // terminate = 1 in a link pointer
50const QH_HEAD: u32 = 1 << 15;
51const QH_CONTROL_EP: u32 = 1 << 27;
52const QH_DTC: u32 = 1 << 14; // data toggle comes from the qTDs (control endpoints)
53const QH_RL: u32 = 4 << 28; // NAK reload count (Linux EHCI_TUNE_RL_{HS,TT})
54const QH_EPS_HIGH: u32 = 2 << 12;
55const QH_EPS_FULL: u32 = 0 << 12;
56const QH_EPS_LOW: u32 = 1 << 12;
57const QH_MULT: u32 = 1 << 30; // MULT=1: one split transaction per uframe
58
59const TOK_IOC: u32 = 1 << 15;
60const TOK_ACTIVE: u32 = 1 << 7;
61const TOK_HALT: u32 = 1 << 6;
62const TOK_XACT: u32 = 1 << 3;
63const TOK_MMF: u32 = 1 << 2; // missed microframe: split did not complete in its window
64const PID_IN: u32 = 1;
65const PID_SETUP: u32 = 2;
66
67const CCERR_3: u32 = 3 << 10;
68
69// ---- Static control state ----
70static EHCI_BASE: AtomicU32 = AtomicU32::new(0);
71static N_PORTS: AtomicU32 = AtomicU32::new(0);
72static CTRL_UP: AtomicBool = AtomicBool::new(false);
73static MOUSE_PRES: AtomicBool = AtomicBool::new(false);
74static DEV_ADDR: AtomicU32 = AtomicU32::new(0);
75static HID_EP: AtomicU32 = AtomicU32::new(0); // EP number (1..15), IN
76static HID_MPS: AtomicU32 = AtomicU32::new(0);
77static ARMED: AtomicBool = AtomicBool::new(false);
78// When the last enumerate attempt ran (ms): a failed/glitched mouse must
79// never trigger a re-enumeration hot loop.
80static LAST_ENUM: AtomicU32 = AtomicU32::new(0);
81// Root ports observed in full/low-speed mode with a UHCI companion present.
82// Those ports are owned by the companion controller — the EHCI can never
83// talk to them, so they are skipped instead of failing a 5 s descriptor
84// timeout every second (the "stuck in a loop" the OptiPlex showed).
85static FS_LS_PORTS: AtomicU32 = AtomicU32::new(0);
86// One-shot: the first transfer failure renders a diagnostic QR on screen
87// (photograph it); retries keep dumping serial but must not re-render.
88static QR_SHOWN: AtomicBool = AtomicBool::new(false);
89// DEBUG: run the one-boot transfer variant matrix (real-hardware bisection
90// of the EHCI System Error trigger). MATRIX_DONE gates it to one pass;
91// XFER_TWEAK flips construction variables inside ctl_xfer.
92static DEBUG_MATRIX: bool = true;
93static MATRIX_DONE: AtomicBool = AtomicBool::new(false);
94static XFER_TWEAK: AtomicU32 = AtomicU32::new(0);
95// Low-region (<1 MB) control QH for the matrix's low-region variant.
96static mut LOW_XFER: *mut u32 = core::ptr::null_mut();
97
98// DMA buffers. Allocated once at init; base pointers recorded here.
99static mut ASYNC_HEAD: *mut u32 = core::ptr::null_mut(); // QH (32 bytes min, align 32)
100static mut XFER_QH: *mut u32 = core::ptr::null_mut(); // control QH
101static mut FRAMELIST: *mut u32 = core::ptr::null_mut(); // 1024 entries, 4K aligned
102static mut HID_QH: *mut u32 = core::ptr::null_mut();
103
104// One dedicated 4 KiB DMA page for every transfer buffer (device descriptor,
105// config descriptor, HID report). All DMA targets live in controller-visible
106// pages — never in .bss adjacent to kernel statics — so an overrun (report
107// longer than MPS, descriptor longer than requested, short-packet arithmetic
108// slip) can only clobber spare DMA space, on QEMU or on real silicon.
109static mut BUF_PAGE: *mut u8 = core::ptr::null_mut();
110const BUF_DEV_DESC: usize = 0x000; // 64 B incl. red zone
111const BUF_CFG: usize = 0x100; // 320 B incl. red zone
112const BUF_HID_REPORT: usize = 0x400; // 64 B incl. red zone
113
114unsafe fn rd(a: usize) -> u32 {
115 unsafe { (a as *const u32).read_volatile() }
116}
117unsafe fn wr(a: usize, v: u32) {
118 unsafe { (a as *mut u32).write_volatile(v) }
119}
120
121fn opbase() -> usize {
122 unsafe {
123 EHCI_BASE.load(Ordering::Relaxed) as usize
124 + (rd(EHCI_BASE.load(Ordering::Relaxed) as usize) & 0xff) as usize
125 }
126}
127fn op(off: usize) -> usize {
128 opbase() + off
129}
130fn prtsc(p: usize) -> usize {
131 opbase() + OP_PRTSC_BASE + p * OP_PRTSC_STRIDE
132}
133
134// ---- QH/QTD helpers: all writes go through raw dword pointers ----
135fn qh_next(qh: *mut u32) -> *mut u32 {
136 qh.wrapping_add(0)
137}
138fn qh_info1(qh: *mut u32) -> *mut u32 {
139 qh.wrapping_add(1)
140}
141fn qh_info2(qh: *mut u32) -> *mut u32 {
142 qh.wrapping_add(2)
143}
144fn qh_current(qh: *mut u32) -> *mut u32 {
145 qh.wrapping_add(3)
146}
147fn qh_next_qtd(qh: *mut u32) -> *mut u32 {
148 qh.wrapping_add(4)
149}
150fn qh_alt_qtd(qh: *mut u32) -> *mut u32 {
151 qh.wrapping_add(5)
152}
153fn qh_token(qh: *mut u32) -> *mut u32 {
154 qh.wrapping_add(6)
155}
156fn qh_buf(qh: *mut u32) -> *mut u32 {
157 qh.wrapping_add(7)
158}
159
160fn link_of(ptr: usize) -> u32 {
161 (ptr as u32) | Q_TYPE_QH
162}
163
164unsafe fn wait_recl(timeout: usize) {
165 unsafe {
166 let t0 = crate::interrupts::uptime_ms();
167 while t0 + timeout as u64 > crate::interrupts::uptime_ms()
168 && (rd(op(OPSTS)) & STS_RECL) == 0
169 {
170 core::hint::spin_loop();
171 }
172 }
173}
174
175/// Issue a single control transfer on the async schedule. `dev` is the USB
176/// device address (0 for the first GET_DESCRIPTOR), `mps` the EP0 max packet,
177/// `speed` the device speed (0=full, 1=low, 2=high), `port` the root port
178/// index (0-based; used only for full/low-speed split QHs), `req` the 8-byte
179/// setup (bmRequestType,bRequest,..), `dir_in` true for a device->host data
180/// stage, `buf/len` the data buffer. Returns bytes transferred, or a
181/// negative error code (-1 timeout, -2 HALT, -3 XactErr, -4 missed uframe).
182unsafe fn ctl_xfer(
183 dev: u32,
184 mps: u32,
185 speed: u32,
186 port: usize,
187 req: &[u8; 8],
188 dir_in: bool,
189 buf: *mut u8,
190 len: u32,
191) -> i32 {
192 // DEBUG matrix hook: XFER_TWEAK bits flip construction variables.
193 // bit0: no-ASE arm; bit1: no-split fields; bit2: no qtd chain;
194 // bit3: zero-length (no DATA stage); bit6: short timeout (1.2 s).
195 let len = if XFER_TWEAK.load(Ordering::Relaxed) & 8 != 0 {
196 0
197 } else {
198 len
199 };
200 unsafe {
201 let head = ASYNC_HEAD;
202 let tw0 = XFER_TWEAK.load(Ordering::Relaxed);
203 // DEBUG matrix: execute the same transfer from the low (<1 MB) page.
204 let xfer = if tw0 & 0x80 != 0 && !LOW_XFER.is_null() {
205 LOW_XFER
206 } else {
207 XFER_QH
208 };
209
210 // QH occupies dwords 0..11 (48 bytes) incl. the 5-dword buffer list.
211 // QEMU's NLPTR_GET masks qtd addresses to 32-byte alignment
212 // (& 0xffffffe0), so every qtd must sit on a 32-byte boundary.
213 // Place them at +0x40 / +0x60 / +0x80 (each 8 dwords = 32 bytes).
214 let st = xfer.add(0x40 / 4); // SETUP qtd at QH+0x40 (32-aligned)
215 let dt = st.add(0x20 / 4); // DATA qtd at QH+0x60 (32-aligned)
216 let sp = dt.add(0x20 / 4); // STATUS qtd at QH+0x80 (32-aligned)
217
218 // SETUP token: toggle 0, len 8, PID_SETUP, active, IOC off.
219 core::ptr::write_volatile(st.add(0), 0u32); // next (chained below)
220 core::ptr::write_volatile(st.add(1), QT_TERMINATE); // alt: terminate
221 core::ptr::write_volatile(
222 st.add(2),
223 (8 << 16) | CCERR_3 | (PID_SETUP << 8) | TOK_ACTIVE,
224 );
225 for i in 0..5 {
226 core::ptr::write_volatile(st.add(3 + i), 0u32);
227 }
228 // Setup packet buffer: 8 bytes at +0x110 (clear of the QH + qtds).
229 let sbuf = xfer.add(0x110 / 4);
230 let sb = core::slice::from_raw_parts_mut(sbuf.cast::<u8>(), 8);
231 sb.copy_from_slice(req);
232 core::ptr::write_volatile(st.add(3), sbuf as u32);
233
234 // DATA qtd. EHCI token PID field (bits 9:8): OUT=0, IN=1, SETUP=2.
235 let data_pid_w = (if dir_in { 1u32 } else { 0u32 }) << 8;
236 if len == 0 {
237 // no data stage
238 core::ptr::write_volatile(dt.add(2), 0u32);
239 } else {
240 core::ptr::write_volatile(dt.add(0), sp as u32);
241 // altnext MUST be terminate (1): QEMU's ADVANCEQUEUE follows altnext
242 // whenever TBYTES != 0 after an IN (e.g. a short packet). altnext=0
243 // is treated as *valid* (bit0==0) so the controller would jump to
244 // qtd address 0 and hang. Terminate keeps short transfers chained
245 // to the STATUS qtd, as required for control transfers.
246 core::ptr::write_volatile(dt.add(1), QT_TERMINATE);
247 core::ptr::write_volatile(
248 dt.add(2),
249 ((len & 0x7fff) << 16) | (1 << 31) | CCERR_3 | (data_pid_w) | TOK_ACTIVE,
250 );
251 for i in 0..5 {
252 core::ptr::write_volatile(dt.add(3 + i), 0u32);
253 }
254 // data buffer
255 core::ptr::write_volatile(dt.add(3), buf as u32);
256 }
257
258 // STATUS qtd: toggle 1, len 0, PID opposite of the data stage; for a
259 // zero-length control the status is opposite of the setup direction.
260 let setup_in = (req[0] & 0x80) != 0;
261 let status_pid_w = if len > 0 {
262 if dir_in {
263 0u32
264 } else {
265 1u32
266 } // opposite of data
267 } else if setup_in {
268 0u32
269 } else {
270 1u32
271 } << 8;
272 core::ptr::write_volatile(sp.add(0), 1u32); // terminate
273 core::ptr::write_volatile(sp.add(1), QT_TERMINATE);
274 core::ptr::write_volatile(
275 sp.add(2),
276 (1 << 31) | CCERR_3 | (status_pid_w) | TOK_ACTIVE | TOK_IOC,
277 );
278 for i in 0..5 {
279 core::ptr::write_volatile(sp.add(3 + i), 0u32);
280 }
281
282 // Chain the qtd list.
283 if len == 0 {
284 core::ptr::write_volatile(st.add(0), sp as u32);
285 } else {
286 core::ptr::write_volatile(st.add(0), dt as u32);
287 core::ptr::write_volatile(dt.add(0), sp as u32);
288 }
289
290 // Configure the control QH (EP0).
291 let eps = match speed {
292 2 => QH_EPS_HIGH,
293 1 => QH_EPS_LOW,
294 _ => QH_EPS_FULL,
295 };
296 let control_flag = if speed == 2 { 0 } else { QH_CONTROL_EP };
297 // FS/LS split QH: DTC (toggles come from the qTDs), NAK reload, MULT=1,
298 // hub addr 0 (integrated root-hub TT) and the 1-based root port number
299 // in bits 29:23. The controller runs SSPLIT/CSPLIT pairs for every qTD
300 // on this QH; no S/C masks are needed on the async list (Linux and
301 // FreeBSD both leave them 0 for control/bulk).
302 let info1 = (mps & 0x7ff) << 16 | QH_RL | QH_DTC | control_flag | eps | dev; // devaddr in low 7 bits
303 *qh_info1(xfer) = info1;
304 *qh_info2(xfer) = if speed == 2 || XFER_TWEAK.load(Ordering::Relaxed) & 2 != 0 {
305 0
306 } else {
307 QH_MULT | (((port as u32) + 1) << 23)
308 };
309 *qh_current(xfer) = 0;
310 *qh_next_qtd(xfer) = st as u32;
311 *qh_alt_qtd(xfer) = QT_TERMINATE;
312 *qh_token(xfer) = 0;
313 for i in 0..5 {
314 *qh_buf(xfer).add(i) = 0;
315 }
316
317 // Link: head -> xfer -> head (reclamation cycle).
318 *qh_next(xfer) = link_of(head as usize);
319 *qh_next(head) = link_of(xfer as usize);
320 if XFER_TWEAK.load(Ordering::Relaxed) & 4 != 0 {
321 *qh_next_qtd(xfer) = 1; // no qtd chain: HC treats the QH as idle
322 }
323
324 // Ensure the controller is running and both schedules enabled.
325 // Re-assert every transfer: QEMU may drop RUNSTOP, and re-setting the
326 // bits is what makes it (re)schedule the async walk.
327 //
328 // Clear the W1C status bits first so a failure dump reflects only
329 // this transfer's window (USBINT|USBERR|PCD|FR|SE).
330 wr(op(OPSTS), 0x1F);
331 // Fence: make all DMA writes globally visible before the controller reads.
332 core::sync::atomic::fence(core::sync::atomic::Ordering::SeqCst);
333 let tw = XFER_TWEAK.load(Ordering::Relaxed);
334 if tw & 0x20 != 0 {
335 // wire-off: disable the port so no transaction can reach the wire.
336 wr(prtsc(port), rd(prtsc(port)) & !(1 << 1)); // PED=0
337 }
338 wr(
339 op(OPCMD),
340 rd(op(OPCMD)) | CMD_RUN | (if tw & 1 == 0 { CMD_ASE } else { 0 }),
341 ); // no PSE — periodic not needed for control
342 let ctl_timeout = if tw & 0x40 != 0 { 1200u64 } else { 5000u64 };
343 // Poll for completion. Watch the whole chain: a HALT on the SETUP
344 // or DATA qtd stops the queue and the STATUS qtd would never clear
345 // (which previously read as a plain timeout). Exit early naming the
346 // failing stage, and on any failure dump the controller + queue state
347 // for real-hardware iteration.
348 let t0 = crate::interrupts::uptime_ms();
349 let mut stage: i32 = 0; // 0=ok, 1=setup halted, 2=data halted, 3=timeout, 4=hc halted/SE
350 loop {
351 let stok = core::ptr::read_volatile(st.add(2).cast::<u32>());
352 let ptok = core::ptr::read_volatile(sp.add(2).cast::<u32>());
353 if ptok & TOK_ACTIVE == 0 {
354 break; // status completed
355 }
356 if stok & TOK_HALT != 0 {
357 stage = 1;
358 break;
359 }
360 if len > 0 {
361 let dtok = core::ptr::read_volatile(dt.add(2).cast::<u32>());
362 if dtok & TOK_HALT != 0 {
363 stage = 2;
364 break;
365 }
366 }
367 // Controller-level fault: the HC halted itself (HCH) or threw a
368 // system error (SE). Polling further is pointless — the qtds
369 // would sit ACTIVE forever and read back as a plain timeout.
370 // Catching it here names the window precisely.
371 if rd(op(OPSTS)) & (STS_HCH | STS_SE) != 0 {
372 stage = 4;
373 break;
374 }
375 if t0 + ctl_timeout < crate::interrupts::uptime_ms() {
376 stage = 3;
377 break;
378 }
379 core::hint::spin_loop();
380 }
381 if stage != 0 {
382 let stok = core::ptr::read_volatile(st.add(2).cast::<u32>());
383 let dtok = if len > 0 {
384 core::ptr::read_volatile(dt.add(2).cast::<u32>())
385 } else {
386 0
387 };
388 let ptok = core::ptr::read_volatile(sp.add(2).cast::<u32>());
389 crate::serial_println!(
390 "ehci: xfer fail stage={} (1=setup 2=data 3=timeout) speed={} dev={} dir_in={} len={}",
391 stage,
392 speed,
393 dev,
394 dir_in,
395 len
396 );
397 let base = EHCI_BASE.load(Ordering::Relaxed) as usize;
398 crate::serial_println!(
399 "ehci: cmd={:#x} sts={:#x} prtsc{}={:#x} hostpc={:#x}",
400 rd(op(OPCMD)),
401 rd(op(OPSTS)),
402 port,
403 rd(prtsc(port)),
404 rd(base + 0x84 + port * 4)
405 );
406 crate::serial_println!(
407 "ehci: qh i1={:#x} i2={:#x} cur={:#x} nqtd={:#x}",
408 core::ptr::read_volatile(qh_info1(xfer)),
409 core::ptr::read_volatile(qh_info2(xfer)),
410 core::ptr::read_volatile(qh_current(xfer)),
411 core::ptr::read_volatile(qh_next_qtd(xfer))
412 );
413 crate::serial_println!(
414 "ehci: tok setup={:#x} data={:#x} stat={:#x}",
415 stok,
416 dtok,
417 ptok
418 );
419 if stage == 4 {
420 crate::serial_println!(
421 "ehci: HC halted/SE after {} ms since arm (frindex={:#x} asyncbase={:#x})",
422 crate::interrupts::uptime_ms().wrapping_sub(t0),
423 rd(op(OPFRIDX)),
424 rd(op(0x18))
425 );
426 }
427 // One-shot on-screen QR of the whole diagnostic: band text plus a
428 // scannable matrix below. Used on hardware with no serial pipe —
429 // point a phone at the screen. Lock the klog screen echo so the
430 // retry loop cannot overwrite it (serial keeps logging).
431 if !QR_SHOWN.swap(true, Ordering::Relaxed) {
432 let l1 = alloc::format!(
433 "EHCI ctl fail stage={} t={}ms speed={} dev={} in={} len={}",
434 stage,
435 crate::interrupts::uptime_ms().wrapping_sub(t0),
436 speed,
437 dev,
438 dir_in,
439 len
440 );
441 let l2 = alloc::format!("cmd={:08x} sts={:08x}", rd(op(OPCMD)), rd(op(OPSTS)));
442 let l3 = alloc::format!(
443 "prtsc{}=0x{:08x} hp=0x{:08x}",
444 port,
445 rd(prtsc(port)),
446 rd(base + 0x84 + port * 4)
447 );
448 let l4 = alloc::format!(
449 "qh1={:08x} qh2={:08x} cur={:08x} nqtd={:08x}",
450 core::ptr::read_volatile(qh_info1(xfer)),
451 core::ptr::read_volatile(qh_info2(xfer)),
452 core::ptr::read_volatile(qh_current(xfer)),
453 core::ptr::read_volatile(qh_next_qtd(xfer))
454 );
455 let l5 = alloc::format!("setup={:08x} data={:08x} stat={:08x}", stok, dtok, ptok);
456 let l6 = alloc::format!(
457 "dma a={:08x} x={:08x} f={:08x} h={:08x} b={:08x}",
458 ASYNC_HEAD as usize,
459 XFER_QH as usize,
460 FRAMELIST as usize,
461 HID_QH as usize,
462 BUF_PAGE as usize
463 );
464 let all = [l1, l2, l3, l4, l5, l6];
465 let refs: alloc::vec::Vec<&str> = all.iter().map(|s| s.as_str()).collect();
466 crate::gop::dump_qr(&refs);
467 crate::gop::qr_lock();
468 }
469 *qh_next(head) = link_of(head as usize);
470 wait_recl(100);
471 return match stage {
472 1 => -3, // setup halted
473 2 => -4, // data halted
474 _ => -1, // timeout
475 };
476 }
477 // Status completed — check its error bits.
478 let tok = core::ptr::read_volatile(sp.add(2).cast::<u32>());
479 if tok & TOK_HALT != 0 {
480 *qh_next(head) = link_of(head as usize);
481 wait_recl(100);
482 return -2; // HALT — qtd halted on error (babble/DBE/...)
483 }
484 if tok & TOK_XACT != 0 {
485 *qh_next(head) = link_of(head as usize);
486 wait_recl(100);
487 return -5; // XactErr — CRC/timeout on the transaction
488 }
489 if tok & TOK_MMF != 0 {
490 *qh_next(head) = link_of(head as usize);
491 wait_recl(100);
492 return -6; // missed microframe — split window missed (retryable)
493 }
494 // Bytes transferred = requested - remaining on the DATA qtd.
495 let mut got = len;
496 if len > 0 {
497 let dtok = core::ptr::read_volatile(dt.add(2).cast::<u32>());
498 let rem = ((dtok >> 16) & 0x7fff) as u32;
499 got = len - rem.min(len);
500 }
501 // Unlink: idle self-loop must keep the QH type bit, otherwise QEMU
502 // parses it as an iTD and resets the HC ("non queue head request").
503 *qh_next(head) = link_of(head as usize);
504 wait_recl(100);
505 let _ = qh_next_qtd;
506 got as i32
507 }
508}
509
510// Control-transfer convenience using the standard control request layout.
511fn ctrl_req(bmreq: u8, breq: u8, wval: u16, widx: u16, wlen: u16) -> [u8; 8] {
512 [
513 bmreq,
514 breq,
515 (wval & 0xff) as u8,
516 (wval >> 8) as u8,
517 (widx & 0xff) as u8,
518 (widx >> 8) as u8,
519 (wlen & 0xff) as u8,
520 (wlen >> 8) as u8,
521 ]
522}
523
524// ---------------------------------------------------------------------------
525// Controller bring-up
526// ---------------------------------------------------------------------------
527
528/// Find an EHCI controller (class 0x0C class 0x03, prog_if 0x20).
529fn find_ehci() -> Option<(u8, u8, u8)> {
530 for d in crate::pci::probe() {
531 if d.class == 0x0C && d.subclass == 0x03 && d.prog_if == 0x20 {
532 return Some((d.bus, d.device, d.func));
533 }
534 }
535 None
536}
537
538/// Return the number of the first root port with a device attached, or usize::MAX.
539fn find_connected_port() -> usize {
540 let np = N_PORTS.load(Ordering::Relaxed) as usize;
541 let skip = FS_LS_PORTS.load(Ordering::Relaxed);
542 for p in 0..np {
543 if skip & (1 << p) != 0 {
544 continue; // full/low-speed — owned by the companion UHCI
545 }
546 let v = unsafe { rd(prtsc(p)) };
547 if v & PRT_CONNECT != 0 {
548 return p;
549 }
550 }
551 usize::MAX
552}
553
554/// Clear the FS/LS port mask so discovery retries (shell `usb rescan`).
555pub fn rescan() {
556 FS_LS_PORTS.store(0, Ordering::Relaxed);
557}
558
559pub fn init() -> bool {
560 let Some((b, d, f)) = find_ehci() else {
561 crate::serial_println!("ehci: no EHCI controller");
562 return false;
563 };
564 crate::serial_println!("ehci: USB2 EHCI at {:02x}:{:02x}.{}", b, d, f);
565
566 unsafe {
567 // Enable Memory Space + Bus Master, and disable INTx assertion
568 // (bit 10) — the driver is polled (OPINTR=0), and an unhandled
569 // EHCI interrupt on real hardware would fire a stray IRQ that the
570 // kernel's catch-all handler EOI's but cannot service.
571 let cmd = crate::pci::config_read(b, d, f, 0x04);
572 crate::pci::config_write(
573 b,
574 d,
575 f,
576 0x04,
577 (cmd & 0xFFFF_0000) | (cmd & 0xFFFF) | 0x6 | 0x400,
578 );
579 let bar = crate::pci::bar64(b, d, f, 0);
580 let base = bar.unwrap_or(0) as usize;
581 EHCI_BASE.store(base as u32, Ordering::Relaxed);
582 let cap = rd(base);
583 let sp = rd(base + 4);
584 let hc = rd(base + 8);
585 let np = sp & 0xf;
586 N_PORTS.store(np, Ordering::Relaxed);
587 crate::serial_println!(
588 "ehci: mmio={:#x} caplen={:#x} ports={} 64-bit-caps={}",
589 base,
590 cap & 0xff,
591 np,
592 (hc & 1) != 0
593 );
594 if base == 0 {
595 return false;
596 }
597
598 // Claim USB Legacy ownership from the BIOS before resetting the
599 // controller. The PCH BIOS drives USB keyboards through SMM
600 // legacy emulation while it owns the EHCI; resetting/reprogramming
601 // the controller underneath it silently kills the legacy keyboard
602 // (observed on the 7010) and the still-active SMI poll can collide
603 // with our async DMA — a Host System Error. The capability lives
604 // in PCI config space (EHCI spec §5.1); the claim walks it exactly
605 // like Linux/FreeBSD. Bounded, no-panic; reports what happened.
606 match crate::pci::claim_usb_legacy(b, d, f, base, 0x08, false) {
607 0 => crate::serial_println!("ehci: USB legacy: already released (no claim needed)"),
608 1 => crate::serial_println!("ehci: USB legacy handed over by BIOS"),
609 2 => crate::serial_println!("ehci: USB legacy claim timed out (BIOS sem stuck)"),
610 _ => crate::serial_println!("ehci: USB legacy: no capability found"),
611 }
612
613 // Halt the controller.
614 wr(op(OPCMD), 0);
615 let mut t = 0;
616 while t < 1_000_000 && (rd(op(OPSTS)) & STS_HCH) == 0 {
617 t += 1;
618 core::hint::spin_loop();
619 }
620 crate::serial_println!(
621 "ehci: halted {}",
622 if rd(op(OPSTS)) & STS_HCH != 0 {
623 "ok"
624 } else {
625 "TIMEOUT"
626 }
627 );
628 // Ensure not in reset already; then issue HC reset.
629 wr(op(OPCMD), CMD_HCRESET);
630 t = 0;
631 while t < 10_000_000 && (rd(op(OPCMD)) & CMD_HCRESET) != 0 {
632 t += 1;
633 core::hint::spin_loop();
634 }
635 crate::serial_println!(
636 "ehci: hcreset {}",
637 if rd(op(OPCMD)) & CMD_HCRESET == 0 {
638 "ok"
639 } else {
640 "TIMEOUT"
641 }
642 );
643 t = 0;
644 while t < 10_000_000 && (rd(op(OPSTS)) & STS_HCH) == 0 {
645 t += 1;
646 core::hint::spin_loop();
647 }
648 crate::serial_println!(
649 "ehci: post-reset halt {}",
650 if rd(op(OPSTS)) & STS_HCH != 0 {
651 "ok"
652 } else {
653 "TIMEOUT"
654 }
655 );
656 // Disable interrupts (polled).
657 wr(op(OPINTR), 0);
658 // Clear HCHalted? After reset the HC is halted; running is set later.
659
660 // Allocate the async head QH + control QH + frame list + HID structures.
661 ASYNC_HEAD = dma_alloc(0x400, 0x20) as *mut u32; // 1KB region
662 XFER_QH = ASYNC_HEAD.add(0x80 / 4); // control QH at +0x80
663 // qtds live after XFER_QH's 11 dwords (44 bytes); space reserved.
664 FRAMELIST = dma_alloc(0x1000, 0x1000) as *mut u32;
665 HID_QH = dma_alloc(0x400, 0x20) as *mut u32;
666 // DEBUG matrix: low-region control QH (sub-1MB page) to test whether
667 // the SE is tied to the 16 MB DMA region (write protection) or not.
668 if let Some(lp) = crate::memory::alloc_pages_above(2, 0) {
669 LOW_XFER = lp as *mut u32;
670 crate::serial_println!("ehci: low-xfer debug page at {:#x}", lp);
671 }
672 BUF_PAGE = dma_alloc(0x1000, 0x20);
673 if BUF_PAGE.is_null() {
674 crate::serial_println!("ehci: buffer page allocation failed");
675 return false;
676 }
677 crate::serial_println!(
678 "ehci: dma async={:#x} xfer={:#x} framelist={:#x} hid={:#x} buf={:#x}",
679 ASYNC_HEAD as usize,
680 XFER_QH as usize,
681 FRAMELIST as usize,
682 HID_QH as usize,
683 BUF_PAGE as usize
684 );
685
686 // Init async head QH: self-linked, marked as head of reclamation.
687 *qh_next(ASYNC_HEAD) = link_of(XFER_QH as usize); // circular: head → control QH → head
688 *qh_info1(ASYNC_HEAD) = QH_HEAD;
689 *qh_info2(ASYNC_HEAD) = 0;
690 *qh_current(ASYNC_HEAD) = 0;
691 *qh_next_qtd(ASYNC_HEAD) = 0;
692
693 // Configure CONTROL QH as an idle EP0 (filled per transfer).
694 *qh_next(XFER_QH) = link_of(ASYNC_HEAD as usize);
695 *qh_info1(XFER_QH) = QH_EPS_FULL | (64 << 16); // HS mps 64
696 *qh_info2(XFER_QH) = 0;
697 *qh_current(XFER_QH) = 0;
698 *qh_next_qtd(XFER_QH) = 0; // idle
699 *qh_alt_qtd(XFER_QH) = QT_TERMINATE;
700 *qh_token(XFER_QH) = 0;
701
702 // Async schedule address.
703 wr(op(OPFRIDX), 0);
704 // Set ASYNCLISTADDR (32-bit). Controller supports 64-bit (hcc&1) but we
705 // keep everything below 4 GB; CTRLDSSEGMENT=0 suffices.
706 // ASYNCLISTADDR low 32 bits: address of HEAD QH (lower 5 bits zero).
707 // With hcc 64-bit set, the controller reads the 64-bit address
708 // CTRLDSSEGMENT:ASYNCLISTADDR. Zero DSEG + low 32-bit pointer is fine
709 // for addresses < 4 GB.
710 let head_addr = ASYNC_HEAD as usize;
711 wr(op(0x10), 0); // CTRLDSSEGMENT
712 // ASYNCLISTADDR is at op+0x18
713 wr(op(0x18), (head_addr as u32) & !0x1F);
714 // Periodic frame list address.
715 // Zero the frame list to avoid stale QH/ITD pointers causing
716 // QEMU to reset the controller when PSE is later enabled.
717 let fl = FRAMELIST as usize;
718 for i in 0..1024 {
719 core::ptr::write_volatile(FRAMELIST.add(i), 0u32);
720 }
721 wr(op(0x14), (fl as u32) & !0x3FF);
722 // Frame list size: FLS field (bits 2-3) stays 0 → 1024 entries.
723 // Intel/QEMU layout: PSE=bit4, ASE=bit5.
724 // NOTE: do NOT enable PSE here — the frame list is empty (all zeros).
725 // Enabling PSE on an empty frame list is fine (QEMU skips zero entries),
726 // but we want to avoid any race with stale OVMF entries. PSE will be
727 // enabled later in poll_mouse when the HID QH is linked into the list.
728 wr(op(OPCMD), 0x01u32); // CMD_RUN only, no PSE
729 wr(op(OPCMD), rd(op(OPCMD)) | CMD_RUN);
730 // Wait to leave halt.
731 let mut t = 0;
732 while t < 1_000_000 && (rd(op(OPSTS)) & STS_HCH) != 0 {
733 t += 1;
734 core::hint::spin_loop();
735 }
736 crate::serial_println!(
737 "ehci: running {}",
738 if rd(op(OPSTS)) & STS_HCH == 0 {
739 "ok"
740 } else {
741 "STUCK HALTED"
742 }
743 );
744 // Configure flag: route all ports to the host (CF=1) and power them.
745 wr(op(OPCFGFLAG), 1);
746 for p in 0..np as usize {
747 let psc = prtsc(p);
748 let v = rd(psc);
749 if v & PRT_POWER == 0 {
750 wr(psc, v | PRT_POWER);
751 }
752 }
753 CTRL_UP.store(true, Ordering::Relaxed);
754 crate::serial_println!("ehci: controller up ({} ports)", np);
755
756 // DIAGNOSTIC: run the async schedule with an idle head (self-loop,
757 // no qtds) for 150 ms. If this faults (HCH/SE), the SE happens on
758 // schedule/DMA mechanics alone — nothing to do with the split
759 // transfer. If clean, the fault is specific to transfer execution.
760 *qh_next(ASYNC_HEAD) = link_of(ASYNC_HEAD as usize);
761 wr(op(OPSTS), 0x1F);
762 core::sync::atomic::fence(core::sync::atomic::Ordering::SeqCst);
763 wr(op(OPCMD), rd(op(OPCMD)) | CMD_RUN | CMD_ASE);
764 let pt0 = crate::interrupts::uptime_ms();
765 loop {
766 let s = rd(op(OPSTS));
767 if s & (STS_HCH | STS_SE) != 0 {
768 crate::serial_println!(
769 "ehci: idle-probe FAULTED {}ms: cmd={:#x} sts={:#x} frindex={:#x} asyncbase={:#x} prtsc0={:#x}",
770 crate::interrupts::uptime_ms().wrapping_sub(pt0),
771 rd(op(OPCMD)),
772 s,
773 rd(op(OPFRIDX)),
774 rd(op(0x18)),
775 rd(prtsc(0))
776 );
777 break;
778 }
779 if crate::interrupts::uptime_ms().wrapping_sub(pt0) >= 150 {
780 crate::serial_println!("ehci: idle-probe ok (150ms, no fault)");
781 break;
782 }
783 core::hint::spin_loop();
784 }
785 }
786 true
787}
788
789/// Port reset on USB2 EHCI. Assert PR, hold it briefly (real EHCI clears
790/// PR itself once reset signaling finishes; QEMU needs the driver to
791/// deassert), then wait for the port to enable. Returns true if PED set
792/// (high-speed device). Full/low-speed stay PED=0 and need a TT path.
793unsafe fn reset_port(p: usize) -> bool {
794 unsafe {
795 let psc = prtsc(p);
796 let v = rd(psc);
797 // Put port in reset.
798 wr(psc, (v | PRT_RESET) & !PRT_RWC);
799 // Hold ~50 ms, bailing early if the HC cleared PR on its own.
800 let t0 = crate::interrupts::uptime_ms();
801 while crate::interrupts::uptime_ms().saturating_sub(t0) < 50 {
802 core::hint::spin_loop();
803 if rd(psc) & PRT_RESET == 0 {
804 break;
805 }
806 }
807 // Deassert explicitly (QEMU only completes the reset on deassert).
808 wr(psc, (rd(psc) & !PRT_RESET) & !PRT_RWC);
809 // Wait briefly for the port to enable (PED). High-speed devices set
810 // PED on reset; full/low-speed stay PED=0 (they need split/TT). Do
811 // not spin forever — decide on the line state instead.
812 let mut t = 0;
813 while t < 2_000_000 && (rd(psc) & PRT_PED) == 0 {
814 t += 1;
815 core::hint::spin_loop();
816 }
817 // A high-speed device gains PED. Full/low-speed won't (needs companion/TT).
818 rd(psc) & PRT_PED != 0
819 }
820}
821
822/// Distinguish full- vs low-speed on a connected root port (for the split QH
823/// EPS field). Intel PCH EHCIs expose the speed in HOSTPC (base+0x84+4p,
824/// bits 27:26: 0=full, 1=low, 2=high); other controllers fall back to the
825/// PORTSC bits 27:26. Only meaningful after the port has completed reset.
826unsafe fn port_speed(p: usize) -> u32 {
827 unsafe {
828 let base = EHCI_BASE.load(Ordering::Relaxed) as usize;
829 let hp = rd(base + 0x84 + p * 4);
830 if hp != u32::MAX && hp != 0 {
831 let f = (hp >> 25) & 3;
832 if f != 3 {
833 return f;
834 }
835 }
836 (rd(prtsc(p)) >> 26) & 3
837 }
838}
839
840// ---------------------------------------------------------------------------
841// Enumeration + HID discovery
842// ---------------------------------------------------------------------------
843
844const HID_CLASS: u8 = 0x03;
845
846fn find_hid_ep(cfg: &[u8]) -> Option<(u8, u32, u32, u8)> {
847 // returns (epaddr, mps, interval_bint, epnum)
848 let mut i = 0usize;
849 let mut in_iface = false;
850 while i + 2 <= cfg.len() {
851 let blen = cfg[i] as usize;
852 if blen == 0 {
853 break;
854 }
855 let bt = cfg[i + 1];
856 if bt == 4 && i + 9 <= cfg.len() {
857 in_iface = cfg[i + 5] == HID_CLASS;
858 } else if bt == 5 && in_iface && i + 7 <= cfg.len() {
859 let addr = cfg[i + 2];
860 let attrs = cfg[i + 3];
861 let mps = (cfg[i + 4] as u32) | ((cfg[i + 5] as u32) << 8);
862 let bint = cfg[i + 6] as u32;
863 if (addr & 0x80) != 0 && (attrs & 0x03) == 0x03 {
864 return Some((addr, mps, bint, addr & 0x0f));
865 }
866 }
867 i += blen;
868 }
869 None
870}
871
872fn enumerate() -> bool {
873 let port = find_connected_port();
874 if port == usize::MAX {
875 return false;
876 }
877 // DEBUG: one-boot transfer variant matrix replaces normal enumeration.
878 if DEBUG_MATRIX && !MATRIX_DONE.swap(true, Ordering::Relaxed) {
879 unsafe {
880 debug_xfer_matrix(port);
881 }
882 return false; // diagnostic build
883 }
884 unsafe {
885 // If a periodic schedule is live (a previous arm), stop it cleanly
886 // before touching the controller: point every frame at terminate,
887 // clear PSE and wait for the reclaim handshake. Re-enumerating while
888 // the old HID QH is still being walked interleaves the periodic walk
889 // with control transfers — fragile on any HC.
890 if rd(op(OPCMD)) & CMD_PSE != 0 {
891 let fl = FRAMELIST;
892 for f in 0..1024 {
893 core::ptr::write_volatile(fl.add(f), QT_TERMINATE);
894 }
895 core::sync::atomic::fence(core::sync::atomic::Ordering::SeqCst);
896 wr(op(OPCMD), rd(op(OPCMD)) & !CMD_PSE);
897 wait_recl(1000);
898 }
899 // Discovery state: nothing armed/present until the arm at the end.
900 ARMED.store(false, Ordering::Relaxed);
901 MOUSE_PRES.store(false, Ordering::Relaxed);
902 let hs = reset_port(port);
903 if !hs && crate::uhci::controller_up() {
904 // A real (non-QEMU) EHCI can never transfer to a full/low-speed
905 // device: the port is owned by the companion UHCI. Mark the
906 // port and stop — a descriptor timeout here costs 5 s and
907 // repeats every second, which is exactly the "stuck in a loop"
908 // the OptiPlex showed. (QEMU's usb-ehci accepts PED=0 with a
909 // high-speed device behind it, so this skip only triggers when
910 // a real UHCI companion is actually present.)
911 FS_LS_PORTS.store(
912 FS_LS_PORTS.load(Ordering::Relaxed) | (1 << port),
913 Ordering::Relaxed,
914 );
915 crate::serial_println!(
916 "ehci: port {} full/low-speed — owned by companion UHCI (uhci driver handles it)",
917 port
918 );
919 return false;
920 }
921 crate::serial_println!(
922 "ehci: port {} resolved{}",
923 port,
924 if hs {
925 " (high-speed)"
926 } else {
927 " (full/low — TT path)"
928 }
929 );
930 // Speed for the QH EPS field: 2=HS, 1=LS, 0=FS. FS/LS on an EHCI
931 // root port must be serviced with split transactions (integrated TT).
932 let speed = if hs { 2 } else { port_speed(port) };
933 let speed_name = match speed {
934 2 => "high-speed",
935 1 => "low-speed (split)",
936 _ => "full-speed (split)",
937 };
938 let hostpc = unsafe { rd(EHCI_BASE.load(Ordering::Relaxed) as usize + 0x84 + port * 4) };
939 crate::serial_println!(
940 "ehci: port {} speed: {} (hostpc={:#x})",
941 port,
942 speed_name,
943 hostpc
944 );
945 let mps = if hs { 64u32 } else { 8u32 };
946
947 // 1. GET_DESCRIPTOR (device) — 18 bytes to learn idVendor etc.
948 let req = ctrl_req(0x80, 6, 0x0100, 0, 18);
949 let dd = BUF_PAGE.add(BUF_DEV_DESC);
950 let mut got = ctl_xfer(0, mps, speed, port, &req, true, dd, 18);
951 if got < 0 {
952 crate::serial_println!("ehci: get-dev-desc failed code={}", got);
953 return false;
954 }
955 if got < 8 {
956 crate::serial_println!("ehci: short dev-desc ({})", got);
957 return false;
958 }
959 let dd = dd as *const u8;
960 let mps0 = core::ptr::read_volatile(dd.add(7)) as u32; // bMaxPacketSize0
961 let _ = mps0;
962 crate::serial_println!(
963 "ehci: dev {:04x}:{:04x} class={:#x} mps0={}",
964 (core::ptr::read_volatile(dd.add(9)) as u16)
965 | ((core::ptr::read_volatile(dd.add(8)) as u16) << 8),
966 (core::ptr::read_volatile(dd.add(11)) as u16)
967 | ((core::ptr::read_volatile(dd.add(10)) as u16) << 8),
968 core::ptr::read_volatile(dd.add(4)),
969 core::ptr::read_volatile(dd.add(7))
970 );
971
972 // 2. SET_ADDRESS to 1.
973 let req = ctrl_req(0x00, 5, 1, 0, 0);
974 if ctl_xfer(0, mps, speed, port, &req, false, core::ptr::null_mut(), 0) < 0 {
975 crate::serial_println!("ehci: set-address failed");
976 return false;
977 }
978 DEV_ADDR.store(1, Ordering::Relaxed);
979
980 // 3. GET_CONFIG(0) — 256 bytes, enough for a HID descriptor.
981 let cfg = BUF_PAGE.add(BUF_CFG);
982 core::ptr::write_bytes(cfg, 0, 256);
983 let req = ctrl_req(0x80, 6, 0x0200, 0, 256);
984 got = ctl_xfer(1, mps, speed, port, &req, true, cfg, 256);
985 if got < 0 {
986 crate::serial_println!("ehci: get-config failed code={}", got);
987 return false;
988 }
989 let Some((epaddr, epmps, bint, epnum)) =
990 find_hid_ep(core::slice::from_raw_parts(cfg as *const u8, 256))
991 else {
992 crate::serial_println!("ehci: no HID mouse endpoint in config");
993 return false;
994 };
995 crate::serial_println!(
996 "ehci: HID mouse IN ep{}({:#x}) mps={} interval={}",
997 epnum,
998 epaddr,
999 epmps,
1000 bint
1001 );
1002
1003 // 4. SET_CONFIGURATION(1).
1004 let req = ctrl_req(0x00, 9, 1, 0, 0);
1005 if ctl_xfer(1, mps, speed, port, &req, false, core::ptr::null_mut(), 0) < 0 {
1006 crate::serial_println!("ehci: set-config failed");
1007 return false;
1008 }
1009
1010 // 5. Arm the HID interrupt-IN endpoint on the periodic schedule.
1011 HID_EP.store(epnum as u32, Ordering::Relaxed);
1012 HID_MPS.store(epmps, Ordering::Relaxed);
1013 // Build HID QH for EP<epnum> IN on device 1.
1014 let hq = HID_QH;
1015 // eps: speed (0/1/2) maps directly onto the QH EPS field (bits 13:12).
1016 let info1 = QH_RL
1017 | (speed << 12)
1018 | ((epmps.max(4) & 0x7ff) << 16)
1019 | (DEV_ADDR.load(Ordering::Relaxed) & 0x7f)
1020 | (((epnum as u32) & 0x0f) << 8);
1021 // Poll every frame. HS: SMASK covers every uframe. FS/LS: split —
1022 // SSPLIT at uframe 0, CSPLIT at uframes 2..4 (Linux NEWSCHED), hub
1023 // addr 0 (integrated root TT), 1-based root port in bits 29:23.
1024 let (smask, cmask, split) = if speed == 2 {
1025 (0xFFu32, 0u32, 0u32)
1026 } else {
1027 (0x01u32, 0x1Cu32 << 8, QH_MULT | (((port as u32) + 1) << 23))
1028 };
1029 *qh_next(hq) = QT_TERMINATE; // end of this frame's periodic list
1030 *qh_info1(hq) = info1;
1031 *qh_info2(hq) = smask | cmask | split;
1032 *qh_current(hq) = 0;
1033 let iqtd = hq.add(0x40 / 4); // qtd at +0x40 (must be 32-byte aligned)
1034 // HID report qtd: len = epmps, PID IN.
1035 core::ptr::write_volatile(iqtd.wrapping_add(0), QT_TERMINATE);
1036 core::ptr::write_volatile(iqtd.wrapping_add(1), QT_TERMINATE);
1037 core::ptr::write_volatile(
1038 iqtd.wrapping_add(2),
1039 ((epmps.max(4) & 0x7fff) << 16) | (PID_IN << 8) | TOK_ACTIVE | TOK_IOC,
1040 );
1041 for i in 0..5 {
1042 core::ptr::write_volatile(iqtd.add(3 + i), 0u32);
1043 }
1044 core::ptr::write_volatile(iqtd.wrapping_add(3), BUF_PAGE.add(BUF_HID_REPORT) as u32);
1045 *qh_next_qtd(hq) = iqtd as u32;
1046 *qh_alt_qtd(hq) = QT_TERMINATE;
1047 *qh_token(hq) = 0;
1048 // QEMU reads the report buffer from the qtd's bufptr (ehci_init_transfer),
1049 // so the QH overlay's buffer list need not be pre-populated.
1050 // Point every period frame entry at the HID QH.
1051 let fl = FRAMELIST;
1052 for f in 0..1024 {
1053 core::ptr::write_volatile(fl.add(f), link_of(hq as usize));
1054 }
1055 // Enable the periodic schedule so the frame list is actually walked.
1056 // PSE was deliberately off until now (empty frame list during init).
1057 core::sync::atomic::fence(core::sync::atomic::Ordering::SeqCst);
1058 wr(op(OPCMD), rd(op(OPCMD)) | CMD_RUN | CMD_PSE);
1059 ARMED.store(true, Ordering::Relaxed);
1060 MOUSE_PRES.store(true, Ordering::Relaxed);
1061 crate::serial_println!("ehci: HID boot mouse armed (device 1)");
1062 true
1063 }
1064}
1065
1066// ---------------------------------------------------------------------------
1067// DEBUG: one-boot transfer variant matrix (real-hardware SE bisection).
1068// Each variant does a full HC reset + port reset, then one ctl_xfer with a
1069// single construction variable flipped (see XFER_TWEAK in ctl_xfer), and
1070// logs the outcome. A wedged port from an earlier SE cannot confound the
1071// comparisons because every variant restarts the controller.
1072// ---------------------------------------------------------------------------
1073unsafe fn hc_restart() {
1074 unsafe {
1075 wr(op(OPCMD), 0);
1076 let mut t = 0;
1077 while t < 1_000_000 && (rd(op(OPSTS)) & STS_HCH) == 0 {
1078 t += 1;
1079 core::hint::spin_loop();
1080 }
1081 wr(op(OPCMD), CMD_HCRESET);
1082 t = 0;
1083 while t < 10_000_000 && (rd(op(OPCMD)) & CMD_HCRESET) != 0 {
1084 t += 1;
1085 core::hint::spin_loop();
1086 }
1087 t = 0;
1088 while t < 10_000_000 && (rd(op(OPSTS)) & STS_HCH) == 0 {
1089 t += 1;
1090 core::hint::spin_loop();
1091 }
1092 wr(op(0x18), (ASYNC_HEAD as u32) & !0x1F);
1093 wr(op(0x14), (FRAMELIST as u32) & !0x3FF);
1094 wr(op(OPCFGFLAG), 1);
1095 let np = N_PORTS.load(Ordering::Relaxed);
1096 for p in 0..np as usize {
1097 let psc = prtsc(p);
1098 let v = rd(psc);
1099 if v & PRT_POWER == 0 {
1100 wr(psc, v | PRT_POWER);
1101 }
1102 }
1103 wr(op(OPSTS), 0x1F);
1104 wr(op(OPCMD), rd(op(OPCMD)) | CMD_RUN);
1105 t = 0;
1106 while t < 1_000_000 && (rd(op(OPSTS)) & STS_HCH) != 0 {
1107 t += 1;
1108 core::hint::spin_loop();
1109 }
1110 }
1111}
1112
1113unsafe fn debug_xfer_matrix(port: usize) {
1114 unsafe {
1115 QR_SHOWN.store(true, Ordering::Relaxed); // serial-only diagnostics
1116 crate::serial_println!(
1117 "ehci: ===== DEBUG xfer variant matrix (port {}) =====",
1118 port
1119 );
1120 let tweaks: [u32; 7] = [0, 1, 2, 4, 8, 0x20, 0x80];
1121 let names = [
1122 "base",
1123 "no-ASE",
1124 "no-split",
1125 "no-qtd",
1126 "len0",
1127 "wire-off",
1128 "low-region",
1129 ];
1130 let ebase = EHCI_BASE.load(Ordering::Relaxed) as usize;
1131 for (i, &tw) in tweaks.iter().enumerate() {
1132 hc_restart();
1133 let _hs = reset_port(port);
1134 let req: [u8; 8] = [0x80, 0x06, 0x00, 0x01, 0, 0, 0x12, 0]; // get_desc dev, 18B
1135 let mut buf = [0u8; 18];
1136 XFER_TWEAK.store(tw | 0x40, Ordering::Relaxed); // + short timeout
1137 let t0 = crate::interrupts::uptime_ms();
1138 let r = ctl_xfer(0, 8, 0, port, &req, true, buf.as_mut_ptr(), 18);
1139 let dt = crate::interrupts::uptime_ms().wrapping_sub(t0);
1140 crate::serial_println!(
1141 "ehci: v{} {:<10}: ret={} t={}ms sts={:#x} cmd={:#x} prtsc0={:#x} hp={:#x}",
1142 i,
1143 names[i],
1144 r,
1145 dt,
1146 rd(op(OPSTS)),
1147 rd(op(OPCMD)),
1148 rd(prtsc(0)),
1149 rd(ebase + 0x84)
1150 );
1151 XFER_TWEAK.store(0, Ordering::Relaxed);
1152 let w0 = crate::interrupts::uptime_ms();
1153 while crate::interrupts::uptime_ms().wrapping_sub(w0) < 600 {
1154 core::hint::spin_loop();
1155 }
1156 }
1157 crate::serial_println!("ehci: ===== matrix done =====");
1158 }
1159}
1160
1161// ---------------------------------------------------------------------------
1162// Active interface (called by cursor task / shell)
1163// ---------------------------------------------------------------------------
1164
1165pub fn controller_up() -> bool {
1166 CTRL_UP.load(Ordering::Relaxed)
1167}
1168pub fn mouse_present() -> bool {
1169 MOUSE_PRES.load(Ordering::Relaxed)
1170}
1171
1172pub fn poll_mouse() -> Option<MouseEvent> {
1173 unsafe {
1174 // Hot path first. The qtd's ACTIVE bit (in controller-visible DMA
1175 // memory) is the ground truth for "report outstanding"; a kernel
1176 // static must never gate report delivery.
1177 if ARMED.load(Ordering::Relaxed) {
1178 let hq = HID_QH;
1179 let iqtd = hq.add(0x40 / 4); // same 32-byte-aligned location as arming
1180 let tok = core::ptr::read_volatile(iqtd.wrapping_add(2).cast::<u32>());
1181 if tok & TOK_ACTIVE == 0 {
1182 if tok & (TOK_HALT | TOK_XACT) != 0 {
1183 // Transfer error: drop the arm; a bounded re-enum follows.
1184 ARMED.store(false, Ordering::Relaxed);
1185 return None;
1186 }
1187 // Completed: decode the report from the dedicated DMA page.
1188 let hb = BUF_PAGE.add(BUF_HID_REPORT) as *const u8;
1189 let b0 = core::ptr::read_volatile(hb);
1190 let b1 = core::ptr::read_volatile(hb.add(1));
1191 let b2 = core::ptr::read_volatile(hb.add(2));
1192 let ev = Some(MouseEvent {
1193 dx: (b1 as i8) as i16,
1194 dy: (b2 as i8) as i16,
1195 left: b0 & 0x01 != 0,
1196 right: b0 & 0x02 != 0,
1197 middle: b0 & 0x04 != 0,
1198 });
1199 // Re-arm the periodic interrupt-IN qtd: restore its token to
1200 // ACTIVE and re-link it as the QH's next qtd so the HC fetches
1201 // it again next frame (canonical one-qtd periodic re-arm).
1202 core::ptr::write_volatile(
1203 iqtd.wrapping_add(2),
1204 ((HID_MPS.load(Ordering::Relaxed).max(4) & 0x7fff) << 16)
1205 | (PID_IN << 8)
1206 | TOK_ACTIVE
1207 | TOK_IOC,
1208 );
1209 *qh_next_qtd(hq) = iqtd as u32;
1210 return ev;
1211 }
1212 return None;
1213 }
1214 // Not armed: bring the mouse up lazily, but never hot-loop — at most
1215 // one enumerate attempt per second, and only while the controller is
1216 // up. A transient glitch can delay a report; it must not wedge the
1217 // system in a re-enumeration loop.
1218 if CTRL_UP.load(Ordering::Relaxed) {
1219 let now = crate::interrupts::uptime_ms() as u32;
1220 if now.wrapping_sub(LAST_ENUM.load(Ordering::Relaxed)) >= 1000 {
1221 LAST_ENUM.store(now, Ordering::Relaxed);
1222 enumerate();
1223 }
1224 }
1225 }
1226 None
1227}
1228
1229/// Allocate DMA-safe memory above 16 MB. Uses the page allocator
1230/// (physical, identity-mapped addresses) instead of the heap allocator
1231/// (virtual addresses) because the EHCI controller's DMA engine needs
1232/// physical addresses that match what the CPU sees. The 16 MB floor
1233/// keeps the schedule structures out of the first 640 K, which some real
1234/// chipsets reject from PCI masters (controller System Error halt).
1235unsafe fn dma_alloc(bytes: usize, _align: usize) -> *mut u8 {
1236 unsafe {
1237 let pages = bytes.div_ceil(4096);
1238 let Some(phys) = crate::memory::alloc_pages_above(pages, 0x100_0000) else {
1239 return core::ptr::null_mut();
1240 };
1241 let p = phys as *mut u8;
1242 core::ptr::write_bytes(p, 0, pages * 4096);
1243 p
1244 }
1245}
1246
1247/// One-line status for the shell `usb` command.
1248pub fn diagnose() -> alloc::string::String {
1249 let mut s = alloc::string::String::new();
1250 s.push_str(&alloc::format!(
1251 "ehci mmio={:#x} ports={} mouse={} dev={} hid_ep={} armed={}\n",
1252 EHCI_BASE.load(Ordering::Relaxed),
1253 N_PORTS.load(Ordering::Relaxed),
1254 if MOUSE_PRES.load(Ordering::Relaxed) {
1255 "yes"
1256 } else {
1257 "no"
1258 },
1259 DEV_ADDR.load(Ordering::Relaxed),
1260 HID_EP.load(Ordering::Relaxed),
1261 ARMED.load(Ordering::Relaxed)
1262 ));
1263 let np = N_PORTS.load(Ordering::Relaxed) as usize;
1264 for p in 0..np.min(8) {
1265 let v = unsafe { rd(prtsc(p)) };
1266 s.push_str(&alloc::format!(
1267 " eport{}: {}{:#x}\n",
1268 p,
1269 if v & PRT_CONNECT != 0 { "*" } else { "" },
1270 v & 0xffff
1271 ));
1272 }
1273 s
1274}
diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs
index ccc6509..676814e 100644
--- a/rsnix/src/gop.rs
+++ b/rsnix/src/gop.rs
@@ -7,7 +7,7 @@ use crate::font;
7use crate::uefi::{GopModeInfo, GraphicsOutputProtocol, EFI_SUCCESS, PIXEL_BGR, PIXEL_RGB}; 7use crate::uefi::{GopModeInfo, GraphicsOutputProtocol, EFI_SUCCESS, PIXEL_BGR, PIXEL_RGB};
8use core::fmt; 8use core::fmt;
9use core::ptr; 9use core::ptr;
10use core::sync::atomic::{AtomicU32, AtomicU64, AtomicU8, Ordering}; 10use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicU8, Ordering};
11 11
12/// Framebuffer state captured before ExitBootServices. 12/// Framebuffer state captured before ExitBootServices.
13pub struct Framebuffer { 13pub struct Framebuffer {
@@ -91,6 +91,19 @@ pub fn last_boot_stage() -> alloc::string::String {
91/// the preemptive scheduler. Set via the shell `stall` command. 91/// the preemptive scheduler. Set via the shell `stall` command.
92pub static DISPLAY_STALL: AtomicU32 = AtomicU32::new(0); 92pub static DISPLAY_STALL: AtomicU32 = AtomicU32::new(0);
93 93
94/// Set once a diagnostic QR has been rendered on screen. While locked, the
95/// klog screen echo is suppressed so subsequent log lines cannot overwrite
96/// the QR; the serial console keeps the full stream. Never released (the
97/// QR is the "photograph this" surface for hardware with no serial pipe).
98pub static QR_LOCKED: AtomicBool = AtomicBool::new(false);
99
100pub fn qr_lock() {
101 QR_LOCKED.store(true, Ordering::Relaxed);
102}
103pub fn qr_locked() -> bool {
104 QR_LOCKED.load(Ordering::Relaxed)
105}
106
94#[inline] 107#[inline]
95fn stall_display() { 108fn stall_display() {
96 let n = DISPLAY_STALL.load(Ordering::Relaxed); 109 let n = DISPLAY_STALL.load(Ordering::Relaxed);
@@ -308,7 +321,8 @@ pub fn shadow_late_init() {
308/// boot stage is written straight to the framebuffer — the last visible 321/// boot stage is written straight to the framebuffer — the last visible
309/// marker before a hang identifies where the kernel died. Uses the 322/// marker before a hang identifies where the kernel died. Uses the
310/// direct-display glyph path (the shadow may not exist yet in early 323/// direct-display glyph path (the shadow may not exist yet in early
311/// boot); the shell banner overwrites row 0 at first paint. 324/// boot). Row 0 stays a persistent status line: the shell starts its
325/// output at row 1 so the marker survives a post-boot freeze.
312pub fn boot_marker(msg: &str) { 326pub fn boot_marker(msg: &str) {
313 boot_marker_at(msg, 0); 327 boot_marker_at(msg, 0);
314} 328}
diff --git a/rsnix/src/interrupts.rs b/rsnix/src/interrupts.rs
index 927e4f1..9872389 100644
--- a/rsnix/src/interrupts.rs
+++ b/rsnix/src/interrupts.rs
@@ -210,6 +210,10 @@ impl ByteRing {
210 210
211static KEYBUF: ByteRing = ByteRing::new(); 211static KEYBUF: ByteRing = ByteRing::new();
212static MOUSEBUF: ByteRing = ByteRing::new(); 212static MOUSEBUF: ByteRing = ByteRing::new();
213/// One-shot flag: the first unhandled interrupt logs its vector, so real
214/// hardware shows which device raised a stray IRQ (before the LAPIC EOI
215/// drops it).
216static STRAY_LOGGED: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);
213 217
214// --------------------------------------------------------------------------- 218// ---------------------------------------------------------------------------
215// Interrupt stubs (assembly) and dispatch 219// Interrupt stubs (assembly) and dispatch
@@ -412,7 +416,7 @@ pub fn enter_dump() {
412 } 416 }
413} 417}
414 418
415fn in_dump() -> bool { 419pub(crate) fn in_dump() -> bool {
416 unsafe { IN_DUMP } 420 unsafe { IN_DUMP }
417} 421}
418 422
@@ -488,7 +492,35 @@ unsafe extern "C" fn isr_dispatch(frame: &IntFrame) -> usize {
488 } 492 }
489 crate::sched::fault_dump("exception", 0, frame) 493 crate::sched::fault_dump("exception", 0, frame)
490 } 494 }
491 _ => fp, 495 _ => {
496 // Unhandled/stray interrupt. On real hardware an
497 // IO-APIC-delivered IRQ (USB/SATA/NIC INTx) that is never
498 // EOI'd stays pending in the local APIC and re-fires the
499 // instant we iretq — an interrupt storm that starves the
500 // timer tick (higher vector priority) and freezes the whole
501 // machine with no panic. EOI the LAPIC so an
502 // edge-triggered stray drops (a genuinely-spurious LAPIC
503 // IRQ needs no EOI, so that case is a benign no-op).
504 // Level-triggered strays are additionally stopped by the
505 // drivers masking their PCI INTx / interrupt enables.
506 if !STRAY_LOGGED.swap(true, Ordering::Relaxed) {
507 crate::serial_println!("intr: stray vector {:#x} — LAPIC EOI'd", frame.vector);
508 }
509 // EOI the local APIC when it is enabled (MSR 0x1B, bit 11).
510 let (lo, hi): (u32, u32);
511 core::arch::asm!(
512 "rdmsr",
513 in("ecx") 0x1Bu32,
514 out("eax") lo,
515 out("edx") hi,
516 options(nostack, preserves_flags)
517 );
518 let apic = ((hi as u64) << 32) | lo as u64;
519 if apic & (1 << 11) != 0 {
520 core::ptr::write_volatile(0xFEE0_00B0usize as *mut u32, 0);
521 }
522 fp
523 }
492 } 524 }
493 }; 525 };
494 crate::sched::set_in_isr(false); 526 crate::sched::set_in_isr(false);
diff --git a/rsnix/src/klog.rs b/rsnix/src/klog.rs
index 2e1fc82..300a597 100644
--- a/rsnix/src/klog.rs
+++ b/rsnix/src/klog.rs
@@ -95,6 +95,16 @@ pub fn emit(args: core::fmt::Arguments<'_>) {
95 crate::serial::write_raw(line.as_str()); 95 crate::serial::write_raw(line.as_str());
96 crate::serial::write_raw("\r\n"); 96 crate::serial::write_raw("\r\n");
97 append(line.as_str(), ms); 97 append(line.as_str(), ms);
98 // Screen echo: mirror the serial line to the VGA console, so the whole
99 // boot log is visible on real hardware without a serial console. The
100 // last line on screen when the machine freezes is exactly where boot
101 // stopped (far more precise than the stage marker). Skipped while a
102 // fault dump is rendering its own band (row 0 = boot marker, terminal
103 // lives below it).
104 if crate::vt::ready() && !crate::interrupts::in_dump() && !crate::gop::qr_locked() {
105 crate::vt::write_str(line.as_str());
106 crate::vt::write_str("\n");
107 }
98 crate::sched::unlock(); 108 crate::sched::unlock();
99} 109}
100 110
diff --git a/rsnix/src/lib.rs b/rsnix/src/lib.rs
index bc2553d..f5c3419 100644
--- a/rsnix/src/lib.rs
+++ b/rsnix/src/lib.rs
@@ -17,6 +17,8 @@ pub mod pci;
17pub mod qr; 17pub mod qr;
18pub mod power; 18pub mod power;
19pub mod ps2; 19pub mod ps2;
20pub mod uhci;
21pub mod ehci;
20pub mod usb; 22pub mod usb;
21pub mod rtc; 23pub mod rtc;
22pub mod sched; 24pub mod sched;
diff --git a/rsnix/src/memory.rs b/rsnix/src/memory.rs
index 099f607..2a32da8 100644
--- a/rsnix/src/memory.rs
+++ b/rsnix/src/memory.rs
@@ -1194,6 +1194,50 @@ pub fn alloc_pages(count: usize) -> Option<usize> {
1194 alloc_pages_aligned(count, 1) 1194 alloc_pages_aligned(count, 1)
1195} 1195}
1196 1196
1197/// Allocate `count` contiguous physical pages at or above `min_addr`
1198/// (first-fit upward from there). Used for controller DMA buffers that
1199/// must avoid low conventional memory — the first 640 K on some real
1200/// chipsets is hostile to PCI masters and shows up as a controller
1201/// "System Error" (SE) halt. Page-aligned like `alloc_pages`.
1202pub fn alloc_pages_above(count: usize, min_addr: usize) -> Option<usize> {
1203 if count == 0 {
1204 return None;
1205 }
1206 unsafe {
1207 if BITMAP.is_null() {
1208 return None;
1209 }
1210 let span = SPAN_PAGES;
1211 let min_page = min_addr.div_ceil(PAGE_SIZE).max(BASE_PAGE);
1212 if min_page >= BASE_PAGE + span {
1213 return None;
1214 }
1215 let start_idx = min_page - BASE_PAGE;
1216 let mut run_start = 0usize;
1217 let mut run_len = 0usize;
1218 for idx in start_idx..span {
1219 let free = (*BITMAP.add(idx / 8) >> (idx % 8)) & 1 == 0;
1220 if free {
1221 if run_len == 0 {
1222 run_start = idx;
1223 }
1224 run_len += 1;
1225 if run_len >= count {
1226 let p = (BASE_PAGE + run_start) * PAGE_SIZE;
1227 for k in run_start..run_start + count {
1228 *BITMAP.add(k / 8) |= 1 << (k % 8);
1229 }
1230 FREE_PAGES -= count;
1231 return Some(p);
1232 }
1233 } else {
1234 run_len = 0;
1235 }
1236 }
1237 None
1238 }
1239}
1240
1197/// Allocate `count` contiguous physical pages whose start address is 1241/// Allocate `count` contiguous physical pages whose start address is
1198/// aligned to `align_pages` (a power of two, in page units). 1242/// aligned to `align_pages` (a power of two, in page units).
1199pub fn alloc_pages_aligned(count: usize, align_pages: usize) -> Option<usize> { 1243pub fn alloc_pages_aligned(count: usize, align_pages: usize) -> Option<usize> {
diff --git a/rsnix/src/pci.rs b/rsnix/src/pci.rs
index b9c2265..738c5e9 100644
--- a/rsnix/src/pci.rs
+++ b/rsnix/src/pci.rs
@@ -61,13 +61,32 @@ pub fn config_read(bus: u8, device: u8, func: u8, reg: u8) -> u32 {
61 outl(CONFIG_ADDRESS, config_addr(bus, device, func, reg)); 61 outl(CONFIG_ADDRESS, config_addr(bus, device, func, reg));
62 inl(CONFIG_DATA) 62 inl(CONFIG_DATA)
63} 63}
64
65/// Write a dword to a device's config space (`reg` must be dword-aligned). 64/// Write a dword to a device's config space (`reg` must be dword-aligned).
66pub fn config_write(bus: u8, device: u8, func: u8, reg: u8, val: u32) { 65pub fn config_write(bus: u8, device: u8, func: u8, reg: u8, val: u32) {
67 outl(CONFIG_ADDRESS, config_addr(bus, device, func, reg)); 66 outl(CONFIG_ADDRESS, config_addr(bus, device, func, reg));
68 outl(CONFIG_DATA, val) 67 outl(CONFIG_DATA, val)
69} 68}
70 69
70fn outb(port: u16, val: u8) {
71 unsafe {
72 core::arch::asm!(
73 "out dx, al",
74 in("dx") port,
75 in("al") val,
76 options(nomem, nostack, preserves_flags)
77 );
78 }
79}
80
81/// Write a single byte to config space (any byte offset). The PCI
82/// byte-enable cycle goes out on port 0xCFC + lane, exactly like `pci_
83/// write_config_byte` everywhere else; QEMU and real chipsets both decode
84/// the low two address bits of the config-data port.
85pub fn config_write_byte(bus: u8, device: u8, func: u8, reg: u8, val: u8) {
86 outl(CONFIG_ADDRESS, config_addr(bus, device, func, reg));
87 outb(CONFIG_DATA + ((reg & 3) as u16), val)
88}
89
71/// Read a memory BAR (index 0-5) as a 64-bit address, handling 32/64-bit 90/// Read a memory BAR (index 0-5) as a 64-bit address, handling 32/64-bit
72/// BARs. Returns None for I/O BARs or invalid values. 91/// BARs. Returns None for I/O BARs or invalid values.
73pub fn bar64(bus: u8, device: u8, func: u8, bar_index: u8) -> Option<u64> { 92pub fn bar64(bus: u8, device: u8, func: u8, bar_index: u8) -> Option<u64> {
@@ -90,6 +109,110 @@ pub fn bar64(bus: u8, device: u8, func: u8, bar_index: u8) -> Option<u64> {
90 } 109 }
91} 110}
92 111
112/// Claim USB Legacy ownership from the BIOS on a controller whose MMIO base
113/// exposes the USB Legacy Support extended capability (USBLEGSUP, cap id 1).
114///
115/// EHCI/xHCI “USB Legacy Support” ownership handoff (EHCI spec §5.1).
116///
117/// The 2012-era PCH BIOS drives USB keyboards through SMM USB-legacy
118/// emulation while it owns the controller: its SMI handler polls the EHCI
119/// over USB-error/port-change events. If the OS reprograms the controller
120/// and runs async DMA underneath the still-active BIOS module, the SMM poll
121/// collides with the OS transfer at an unpredictable moment — a classic
122/// real-silicon Host System Error (SE) that QEMU cannot reproduce (no SMM).
123///
124/// CRITICAL: the extended-capability pointer is a PCI *config-space*
125/// offset, not an MMIO offset. Read it from an MMIO register (`eccp_src`
126/// = MMIO dword offset; EHCI: HCCPARAMS @+0x08 bits 15:8; xHCI: HCSPARAMS1
127/// @+0x04 bits 31:16), then walk config space — exactly Linux's
128/// `quirk_usb_handoff_ehci` (pci-quirks.c) and FreeBSD's `ehci_pci_attach`.
129/// Our previous implementation treated EECP as an MMIO offset, read a
130/// reserved register (0), and silently never claimed ownership — the
131/// observed “claim skipped” on the 7010.
132///
133/// Handshake (Linux/FreeBSD-identical): if the BIOS-owned semaphore
134/// (bit 16 of the cap dword) is set, write the OS-owned semaphore byte
135/// (cap+3) = 1 and spin (bounded ~1 s) for bit 16 to clear. USBLEGCTLSTS
136/// (cap+4) is deliberately left alone — Linux notes some systems “get
137/// upset” if it's touched.
138///
139/// Returns: 0 = already released/owned (no claim needed), 1 = handed over,
140/// 2 = BIOS never released, 3 = no legacy capability / chain out of range.
141pub fn claim_usb_legacy(
142 bus: u8,
143 dev: u8,
144 func: u8,
145 mmio_base: usize,
146 eccp_src: usize,
147 eccp_bits_high: bool,
148) -> u8 {
149 let src = unsafe { (mmio_base as *const u32).add(eccp_src).read_volatile() };
150 crate::serial_println!(
151 "usb-legacy: probe mmio=+{:#x} hcc={:#010x} high={}",
152 eccp_src,
153 src,
154 eccp_bits_high
155 );
156 let mut offset = if eccp_bits_high {
157 ((src >> 16) & 0xFFFF) as u8 // xHCI: XECP (u8 caps the walk at 0xFF config)
158 } else {
159 ((src >> 8) & 0xFF) as u8 // EHCI: EECP
160 };
161 if offset == 0 {
162 return 3;
163 }
164 let mut guard = 32; // bounded walk, no panics
165 while offset != 0 && guard > 0 {
166 guard -= 1;
167 let cap = config_read(bus, dev, func, offset);
168 crate::serial_println!(
169 "usb-legacy: cap@{:#x} id={:#04x} next={:#04x} d={:#010x}",
170 offset,
171 cap & 0xFF,
172 (cap >> 8) & 0xFF,
173 cap
174 );
175 let id = cap & 0xFF;
176 let next = ((cap >> 8) & 0xFF) as u8;
177 match id {
178 0 => return 3, // illegal reserved cap terminates the chain
179 1 => {
180 // USB Legacy Support capability.
181 let ctlsts = config_read(bus, dev, func, offset + 4);
182 crate::serial_println!(
183 "usb-legacy: cap@{:#x} bios_sem={} os_sem={} ctlsts={:#x}",
184 offset,
185 (cap >> 16) & 1,
186 (cap >> 24) & 1,
187 ctlsts
188 );
189 if cap & (1 << 24) != 0 {
190 return 0; // OS already owns
191 }
192 if cap & (1 << 16) == 0 {
193 return 0; // BIOS not claiming it
194 }
195 crate::serial_println!("usb-legacy: BIOS owns controller - claiming (OS sem)");
196 config_write_byte(bus, dev, func, offset + 3, 1);
197 let t0 = crate::interrupts::uptime_ms();
198 let mut handed = false;
199 while crate::interrupts::uptime_ms().wrapping_sub(t0) < 1000 {
200 if config_read(bus, dev, func, offset) & (1 << 16) == 0 {
201 handed = true;
202 break;
203 }
204 core::hint::spin_loop();
205 }
206 return if handed { 1 } else { 2 };
207 }
208 _ => {
209 offset = next;
210 }
211 }
212 }
213 3
214}
215
93/// Enumerate every device/function present on all buses. 216/// Enumerate every device/function present on all buses.
94pub fn probe() -> alloc::vec::Vec<DeviceInfo> { 217pub fn probe() -> alloc::vec::Vec<DeviceInfo> {
95 let mut out = alloc::vec::Vec::new(); 218 let mut out = alloc::vec::Vec::new();
diff --git a/rsnix/src/ps2.rs b/rsnix/src/ps2.rs
index 5f4a804..965e4a0 100644
--- a/rsnix/src/ps2.rs
+++ b/rsnix/src/ps2.rs
@@ -169,14 +169,19 @@ pub fn init() {
169 169
170 // Enable the keyboard interrupt on the PS/2 controller. 170 // Enable the keyboard interrupt on the PS/2 controller.
171 // Read Configuration Byte (command 0x20), set bit 0 (IRQ1 enable), 171 // Read Configuration Byte (command 0x20), set bit 0 (IRQ1 enable),
172 // write back (command 0x60). 172 // write back (command 0x60). Every wait is bounded; a wedged
173 // controller bails out instead of hanging the boot.
173 unsafe { 174 unsafe {
174 // Tell the keyboard controller we want to read the config byte. 175 // Tell the keyboard controller we want to read the config byte.
175 // Need to wait for the controller to be ready first. 176 // Need to wait for the controller to be ready first.
176 wait_write(); 177 if !wait_write() {
178 return;
179 }
177 core::arch::asm!("out dx, al", in("dx") 0x64u16, in("al") 0x20u8, 180 core::arch::asm!("out dx, al", in("dx") 0x64u16, in("al") 0x20u8,
178 options(nostack, preserves_flags)); 181 options(nostack, preserves_flags));
179 wait_read(); 182 if !wait_read() {
183 return;
184 }
180 let mut cfg: u8; 185 let mut cfg: u8;
181 core::arch::asm!("in al, dx", out("al") cfg, in("dx") 0x60u16, 186 core::arch::asm!("in al, dx", out("al") cfg, in("dx") 0x60u16,
182 options(nostack, preserves_flags)); 187 options(nostack, preserves_flags));
@@ -184,48 +189,60 @@ pub fn init() {
184 cfg |= 0x01; // enable keyboard interrupt 189 cfg |= 0x01; // enable keyboard interrupt
185 190
186 // Write config byte back 191 // Write config byte back
187 wait_write(); 192 if !wait_write() {
193 return;
194 }
188 core::arch::asm!("out dx, al", in("dx") 0x64u16, in("al") 0x60u8, 195 core::arch::asm!("out dx, al", in("dx") 0x64u16, in("al") 0x60u8,
189 options(nostack, preserves_flags)); 196 options(nostack, preserves_flags));
190 wait_write(); 197 if !wait_write() {
198 return;
199 }
191 core::arch::asm!("out dx, al", in("dx") 0x60u16, in("al") cfg, 200 core::arch::asm!("out dx, al", in("dx") 0x60u16, in("al") cfg,
192 options(nostack, preserves_flags)); 201 options(nostack, preserves_flags));
193 } 202 }
194} 203}
195 204
196/// Wait until the PS/2 controller is ready to receive a command. 205/// Wait until the PS/2 controller is ready to receive a command.
197fn wait_write() { 206/// Bounded: a wedged or missing 8042 must not hang the boot (the classic
207/// real-hardware freeze — the controller reports busy forever on some
208/// machines). Returns false on timeout.
209fn wait_write() -> bool {
198 unsafe { 210 unsafe {
199 loop { 211 for _ in 0..1_000_000 {
200 let status: u8; 212 let status: u8;
201 core::arch::asm!("in al, dx", out("al") status, in("dx") 0x64u16, 213 core::arch::asm!("in al, dx", out("al") status, in("dx") 0x64u16,
202 options(nostack, preserves_flags)); 214 options(nostack, preserves_flags));
203 if status & 0x02 == 0 { 215 if status & 0x02 == 0 {
204 // Input buffer empty — controller ready to receive 216 // Input buffer empty — controller ready to receive
205 break; 217 return true;
206 } 218 }
207 } 219 }
208 } 220 }
221 false
209} 222}
210 223
211/// Wait until the PS/2 controller has data available to read. 224/// Wait until the PS/2 controller has data available to read.
212fn wait_read() { 225/// Bounded like `wait_write`.
226fn wait_read() -> bool {
213 unsafe { 227 unsafe {
214 loop { 228 for _ in 0..1_000_000 {
215 let status: u8; 229 let status: u8;
216 core::arch::asm!("in al, dx", out("al") status, in("dx") 0x64u16, 230 core::arch::asm!("in al, dx", out("al") status, in("dx") 0x64u16,
217 options(nostack, preserves_flags)); 231 options(nostack, preserves_flags));
218 if status & 0x01 != 0 { 232 if status & 0x01 != 0 {
219 // Output buffer full — data ready to read 233 // Output buffer full — data ready to read
220 break; 234 return true;
221 } 235 }
222 } 236 }
223 } 237 }
238 false
224} 239}
225 240
241/// Drain any pending bytes. Bounded so a stuck output-full flag (or a
242/// controller that keeps raising it with no data) cannot hang the boot.
226fn drain_input() { 243fn drain_input() {
227 unsafe { 244 unsafe {
228 loop { 245 for _ in 0..32 {
229 let status: u8; 246 let status: u8;
230 core::arch::asm!("in al, dx", out("al") status, in("dx") STATUS_PORT, 247 core::arch::asm!("in al, dx", out("al") status, in("dx") STATUS_PORT,
231 options(nostack, preserves_flags)); 248 options(nostack, preserves_flags));
@@ -268,6 +285,15 @@ fn kbd() -> &'static mut KbdState {
268 unsafe { &mut *core::ptr::addr_of_mut!(KBD) } 285 unsafe { &mut *core::ptr::addr_of_mut!(KBD) }
269} 286}
270 287
288/// Feed one scancode-set-1 byte into the shared decoder state machine.
289/// Used by the PS/2 IRQ path (raw bytes) AND the USB HID keyboard driver
290/// (which translates HID usage codes to scancode-set-1 make/break bytes so
291/// the same shift/ctrl/caps decoding applies). Returns a KeyEvent on a
292/// visible press; releases and modifier-only changes return None.
293pub(crate) fn feed_scancode(byte: u8) -> Option<KeyEvent> {
294 process_byte(byte)
295}
296
271fn process_byte(byte: u8) -> Option<KeyEvent> { 297fn process_byte(byte: u8) -> Option<KeyEvent> {
272 let state = kbd(); 298 let state = kbd();
273 299
@@ -466,25 +492,35 @@ pub fn mouse_init() -> bool {
466 drain_input(); 492 drain_input();
467 unsafe { 493 unsafe {
468 // 1. Enable the auxiliary port itself. 494 // 1. Enable the auxiliary port itself.
469 wait_write(); 495 if !wait_write() {
496 return false;
497 }
470 core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_AUX_ENABLE, 498 core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_AUX_ENABLE,
471 options(nostack, preserves_flags)); 499 options(nostack, preserves_flags));
472 500
473 // 2. Configuration Byte: enable IRQ12 + the aux port clock. Read, 501 // 2. Configuration Byte: enable IRQ12 + the aux port clock. Read,
474 // OR the two bits, write back (preserves the keyboard IRQ bit 502 // OR the two bits, write back (preserves the keyboard IRQ bit
475 // that ps2::init set). 503 // that ps2::init set).
476 wait_write(); 504 if !wait_write() {
505 return false;
506 }
477 core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_READ_CFG, 507 core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_READ_CFG,
478 options(nostack, preserves_flags)); 508 options(nostack, preserves_flags));
479 wait_read(); 509 if !wait_read() {
510 return false;
511 }
480 let mut cfg: u8; 512 let mut cfg: u8;
481 core::arch::asm!("in al, dx", out("al") cfg, in("dx") DATA_PORT, 513 core::arch::asm!("in al, dx", out("al") cfg, in("dx") DATA_PORT,
482 options(nostack, preserves_flags)); 514 options(nostack, preserves_flags));
483 cfg |= CFG_AUX_IRQ | CFG_AUX_CLOCK; 515 cfg |= CFG_AUX_IRQ | CFG_AUX_CLOCK;
484 wait_write(); 516 if !wait_write() {
517 return false;
518 }
485 core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_WRITE_CFG, 519 core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_WRITE_CFG,
486 options(nostack, preserves_flags)); 520 options(nostack, preserves_flags));
487 wait_write(); 521 if !wait_write() {
522 return false;
523 }
488 core::arch::asm!("out dx, al", in("dx") DATA_PORT, in("al") cfg, 524 core::arch::asm!("out dx, al", in("dx") DATA_PORT, in("al") cfg,
489 options(nostack, preserves_flags)); 525 options(nostack, preserves_flags));
490 526
@@ -504,10 +540,14 @@ pub fn mouse_init() -> bool {
504/// The wait is bounded so a missing device cannot hang the boot. 540/// The wait is bounded so a missing device cannot hang the boot.
505fn aux_command(cmd: u8) -> bool { 541fn aux_command(cmd: u8) -> bool {
506 unsafe { 542 unsafe {
507 wait_write(); 543 if !wait_write() {
544 return false;
545 }
508 core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_SEND_TO_AUX, 546 core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_SEND_TO_AUX,
509 options(nostack, preserves_flags)); 547 options(nostack, preserves_flags));
510 wait_write(); 548 if !wait_write() {
549 return false;
550 }
511 core::arch::asm!("out dx, al", in("dx") DATA_PORT, in("al") cmd, 551 core::arch::asm!("out dx, al", in("dx") DATA_PORT, in("al") cmd,
512 options(nostack, preserves_flags)); 552 options(nostack, preserves_flags));
513 for _ in 0..100_000 { 553 for _ in 0..100_000 {
diff --git a/rsnix/src/sched.rs b/rsnix/src/sched.rs
index dd1409e..259ddf6 100644
--- a/rsnix/src/sched.rs
+++ b/rsnix/src/sched.rs
@@ -158,6 +158,47 @@ static IN_ISR: AtomicBool = AtomicBool::new(false);
158static PREEMPT_DEPTH: AtomicU64 = AtomicU64::new(0); 158static PREEMPT_DEPTH: AtomicU64 = AtomicU64::new(0);
159 159
160// --------------------------------------------------------------------------- 160// ---------------------------------------------------------------------------
161// Watchdog — turn wedged states into visible panics instead of silent
162// freezes. Real hardware has no serial console, so the panic handler
163// renders to the framebuffer and writes the crash log; a freeze that would
164// otherwise look like a dead machine becomes a reported panic.
165//
166// The watchdog only runs when the PIT tick is firing (interrupts on). A
167// hang with interrupts disabled is invisible to it, so driver waits must
168// stay bounded (timeout loops) — enforced at the driver level.
169// ---------------------------------------------------------------------------
170
171/// Consecutive ticks where the preemption lock was held (depth > 0). The
172/// tick cannot switch tasks while the lock is held, so a hold this long is
173/// a deadlock (a driver wedged inside the serial/klog lock, ...).
174static STALL_TICKS: AtomicU64 = AtomicU64::new(0);
175const WATCHDOG_LOCK_TICKS: u64 = 500; // 5 s at 100 Hz
176
177/// Per-task "same interrupted frame" counter for spin detection.
178struct SpinWatch {
179 last_rsp: usize,
180 same: u32,
181}
182const SPIN_WATCH_INIT: SpinWatch = SpinWatch { last_rsp: 0, same: 0 };
183static mut SPIN_WATCH: [SpinWatch; MAX_TASKS] = [SPIN_WATCH_INIT; MAX_TASKS];
184/// A spinning background task is interrupted at roughly half the tick rate
185/// (it alternates with idle), so 1500 hits is ~30 s of wall time.
186const WATCHDOG_SPIN_TICKS: u32 = 1500;
187/// Boot-phase threshold for task 0 (the shell): before the REPL is live,
188/// task 0 runs init code where a fixed frame means a driver/controller
189/// hang — panic after ~15 s wall. The boot must finish well inside this.
190const WATCHDOG_BOOT_SPIN_TICKS: u32 = 750;
191
192/// True once the shell REPL is live (set by main.rs before shell::run()).
193/// The shell legitimately parks in poll_key with a fixed frame when it is;
194/// before that, task 0 running boot code must not sit in a fixed frame.
195static SHELL_RUNNING: AtomicBool = AtomicBool::new(false);
196
197pub fn mark_shell_running() {
198 SHELL_RUNNING.store(true, Ordering::Relaxed);
199}
200
201// ---------------------------------------------------------------------------
161// Preemption lock 202// Preemption lock
162// --------------------------------------------------------------------------- 203// ---------------------------------------------------------------------------
163 204
@@ -193,12 +234,49 @@ pub fn current() -> usize {
193/// Receives the interrupted task's frame pointer; returns the frame 234/// Receives the interrupted task's frame pointer; returns the frame
194/// pointer the asm should switch to (the same one when no switch). 235/// pointer the asm should switch to (the same one when no switch).
195pub fn on_tick(cur: usize) -> usize { 236pub fn on_tick(cur: usize) -> usize {
196 if !is_ready() || PREEMPT_DEPTH.load(Ordering::Relaxed) > 0 { 237 if !is_ready() {
197 return cur; 238 return cur;
198 } 239 }
199 unsafe { 240 unsafe {
241 // Watchdog: the tick fired, so the CPU is alive. A state we cannot
242 // leave is a hang, not a sleep — surface it as a visible panic.
243 if PREEMPT_DEPTH.load(Ordering::Relaxed) > 0 {
244 let n = STALL_TICKS.fetch_add(1, Ordering::Relaxed) + 1;
245 if n >= WATCHDOG_LOCK_TICKS {
246 watchdog_panic(cur, "preemption lock held");
247 }
248 return cur;
249 }
250 STALL_TICKS.store(0, Ordering::Relaxed);
251
200 TASKS[CURRENT].saved_rsp = cur; 252 TASKS[CURRENT].saved_rsp = cur;
201 TASKS[CURRENT].ticks += 1; 253 TASKS[CURRENT].ticks += 1;
254 // Spin detection: a task whose interrupted frame never moves
255 // (identical RSP on every tick) is stuck in a hot loop — no yield,
256 // no sleep, no block. The shell (id 0) legitimately parks in
257 // poll_key with a fixed frame, and idle (id 1) hlt's; both exempt.
258 let id = CURRENT;
259 let w = &mut SPIN_WATCH[id];
260 if w.last_rsp == cur {
261 w.same += 1;
262 // Background tasks (id >= 2): spinning is never legitimate.
263 // Task 0 (shell): exempt once the REPL is live (poll_key parks
264 // with a fixed frame); during the boot phase a fixed frame
265 // means usb/ehci init is wedged on real hardware.
266 let threshold = if id == SHELL_TID && !SHELL_RUNNING.load(Ordering::Relaxed) {
267 WATCHDOG_BOOT_SPIN_TICKS
268 } else if id >= 2 {
269 WATCHDOG_SPIN_TICKS
270 } else {
271 u32::MAX // idle hlt's with a fixed frame forever — fine
272 };
273 if w.same >= threshold {
274 watchdog_panic(cur, "task spinning without progress");
275 }
276 } else {
277 w.last_rsp = cur;
278 w.same = 1;
279 }
202 let now = crate::interrupts::uptime_ms(); 280 let now = crate::interrupts::uptime_ms();
203 for t in 0..MAX_TASKS { 281 for t in 0..MAX_TASKS {
204 match TASKS[t].state { 282 match TASKS[t].state {
@@ -219,6 +297,29 @@ pub fn on_tick(cur: usize) -> usize {
219 } 297 }
220} 298}
221 299
300/// Render a watchdog verdict through the normal panic path (framebuffer +
301/// serial + crash log) and halt. Runs in tick/ISR context: `cur` is the
302/// interrupted task's IntFrame, so the reported RIP pinpoints the hang.
303fn watchdog_panic(cur: usize, why: &str) -> ! {
304 let id = unsafe { CURRENT };
305 let name = name_str(id);
306 let rip = unsafe { (cur as *const IntFrame).read().rip };
307 let up = crate::interrupts::uptime_ms();
308 if id == SHELL_TID {
309 // Boot-phase hang: report the last completed boot stage so the
310 // wedged init step is identifiable without serial.
311 let stage = crate::gop::last_boot_stage();
312 panic!(
313 "watchdog: {} — task {} \"{}\" rip={:#x} (uptime {} ms) boot-stage: {}",
314 why, id, name, rip, up, stage
315 );
316 }
317 panic!(
318 "watchdog: {} — task {} \"{}\" rip={:#x} (uptime {} ms)",
319 why, id, name, rip, up
320 );
321}
322
222/// Software yield (vector 0x60). 323/// Software yield (vector 0x60).
223pub fn on_yield(cur: usize) -> usize { 324pub fn on_yield(cur: usize) -> usize {
224 if !is_ready() || PREEMPT_DEPTH.load(Ordering::Relaxed) > 0 { 325 if !is_ready() || PREEMPT_DEPTH.load(Ordering::Relaxed) > 0 {
diff --git a/rsnix/src/tty.rs b/rsnix/src/tty.rs
index 7304925..0783521 100644
--- a/rsnix/src/tty.rs
+++ b/rsnix/src/tty.rs
@@ -1,4 +1,8 @@
1//! Line discipline over the PS/2 keyboard — the kernel's tty layer. 1//! Line discipline over the keyboard — the kernel's tty layer.
2//!
3//! Keys come from either the PS/2 controller (IRQ-fed scancode ring) or the
4//! USB HID boot keyboard (polled UHCI driver). Both are translated to the
5//! same `KeyEvent` space, so the shell is input-source agnostic.
2//! 6//!
3//! Two modes, like a real UNIX terminal: 7//! Two modes, like a real UNIX terminal:
4//! - **canonical** (`read_line`): input is echoed, buffered until Enter, 8//! - **canonical** (`read_line`): input is echoed, buffered until Enter,
@@ -34,6 +38,20 @@ pub enum LineInput {
34static mut RAW_QUEUE: [u8; 8] = [0; 8]; 38static mut RAW_QUEUE: [u8; 8] = [0; 8];
35static mut RAW_LEN: usize = 0; 39static mut RAW_LEN: usize = 0;
36 40
41/// Combined blocking key poll: USB HID keyboard first (polled UHCI), then
42/// the PS/2 path (which parks via `hlt` until an IRQ-fed scancode lands).
43fn poll_key() -> KeyEvent {
44 if let Some(ev) = crate::uhci::try_poll_key() {
45 return ev;
46 }
47 ps2::poll_key()
48}
49
50/// Combined non-blocking key poll (USB first, then PS/2).
51fn try_poll_key() -> Option<KeyEvent> {
52 crate::uhci::try_poll_key().or_else(ps2::try_poll_key)
53}
54
37// --------------------------------------------------------------------------- 55// ---------------------------------------------------------------------------
38// canonical mode 56// canonical mode
39// --------------------------------------------------------------------------- 57// ---------------------------------------------------------------------------
@@ -46,7 +64,7 @@ pub fn read_line() -> LineInput {
46 crate::vt::sync_scroll(); 64 crate::vt::sync_scroll();
47 let mut buf = strbuf::String::<LINE_BUF_SIZE>::new(); 65 let mut buf = strbuf::String::<LINE_BUF_SIZE>::new();
48 loop { 66 loop {
49 match ps2::poll_key() { 67 match poll_key() {
50 KeyEvent::Enter => { 68 KeyEvent::Enter => {
51 gop::put_char('\n'); 69 gop::put_char('\n');
52 serial::write_str("\r\n"); 70 serial::write_str("\r\n");
@@ -104,7 +122,7 @@ pub fn read_line() -> LineInput {
104/// streaming commands (cat/hexdump) so a long dump can be interrupted. 122/// streaming commands (cat/hexdump) so a long dump can be interrupted.
105pub fn interrupt_pressed() -> bool { 123pub fn interrupt_pressed() -> bool {
106 let mut pressed = false; 124 let mut pressed = false;
107 while let Some(ev) = ps2::try_poll_key() { 125 while let Some(ev) = try_poll_key() {
108 if matches!(ev, KeyEvent::Ctrl('c')) { 126 if matches!(ev, KeyEvent::Ctrl('c')) {
109 pressed = true; 127 pressed = true;
110 } 128 }
@@ -126,7 +144,7 @@ pub fn read_byte() -> u8 {
126 if let Some(b) = pop_raw() { 144 if let Some(b) = pop_raw() {
127 return b; 145 return b;
128 } 146 }
129 let ev = ps2::poll_key(); 147 let ev = poll_key();
130 enqueue_key(ev); 148 enqueue_key(ev);
131 } 149 }
132} 150}
@@ -136,7 +154,7 @@ pub fn try_read_byte() -> Option<u8> {
136 if let Some(b) = pop_raw() { 154 if let Some(b) = pop_raw() {
137 return Some(b); 155 return Some(b);
138 } 156 }
139 match ps2::try_poll_key() { 157 match try_poll_key() {
140 Some(ev) => { 158 Some(ev) => {
141 enqueue_key(ev); 159 enqueue_key(ev);
142 pop_raw() 160 pop_raw()
diff --git a/rsnix/src/uhci.rs b/rsnix/src/uhci.rs
new file mode 100644
index 0000000..87ec9c6
--- /dev/null
+++ b/rsnix/src/uhci.rs
@@ -0,0 +1,1129 @@
1// ---------------------------------------------------------------------------
2// rsdos UHCI (USB 1.1) companion driver — HID boot keyboard + mouse.
3//
4// Real Intel PCH (e.g. OptiPlex 7010) USB2 ports are handled by an EHCI
5// (high-speed devices) plus its companion UHCI pair (full/low-speed
6// devices). A real USB keyboard/mouse is full- or low-speed, so it lives
7// on the UHCI — the EHCI cannot talk to it without a root-hub TT (Intel
8// PCH EHCI has none; the ports belong to the companion UHCI). QEMU's
9// virtual usb-mouse/usb-kbd are high-speed when attached to usb-ehci, so
10// this class of real hardware was never exercised by the xHCI/EHCI paths.
11//
12// This driver is polled (no IRQ), exactly like ehci.rs: the frame list is
13// walked every 1 ms by the controller; we keep one armed interrupt-IN TD
14// per device and drain reports from the shell task (keyboard) and cursor
15// task (mouse). Both devices share one control pipeline, serialized by a
16// spin lock so the shell and cursor tasks cannot corrupt each other's
17// transfers.
18//
19// Data structures follow the UHCI spec as implemented by QEMU's hcd-uhci
20// and Linux's uhci-hcd (they agree field-for-field):
21// TD: link(0x00) ctrl(0x04) token(0x08) buffer(0x0C)
22// QH: link(0x00) el_link(0x04)
23// ctrl: ACTIVE=1<<23 IOC=1<<24 IOS=1<<25 LS=1<<26 C_ERR=3<<27 SPD=1<<29
24// ACTLEN=bits 10:0 (n-1, written by the HC)
25// token: PID=bits 7:0 (full byte: SETUP=0x2D IN=0x69 OUT=0xE1)
26// DEVADDR=bits 14:8, EP=bits 18:15, TOGGLE=bit 19,
27// EXPLEN=bits 31:21 (n-1)
28// link ptrs: bit0=terminate, bit1=QH type, bit2=depth-first
29// ---------------------------------------------------------------------------
30
31use crate::ps2::MouseEvent;
32use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
33
34// ---- Registers (I/O space; 16-bit unless noted) ----
35const USBCMD: u16 = 0x00;
36const USBSTS: u16 = 0x02;
37const USBINTR: u16 = 0x04;
38const FRNUM: u16 = 0x06;
39const FRBASEADD: u16 = 0x08; // 32-bit
40const SOFMOD: u16 = 0x0C;
41const PORTSC_BASE: u16 = 0x10; // PORTSC1, PORTSC2 (16-bit each)
42
43const CMD_RUN: u16 = 1 << 0;
44const CMD_HCRESET: u16 = 1 << 1;
45const CMD_CF: u16 = 1 << 6;
46
47const STS_HCHALTED: u16 = 1 << 5;
48
49const PSC_CCS: u16 = 1 << 0; // current connect status
50const PSC_CSC: u16 = 1 << 1; // connect change (write 1 to clear)
51const PSC_PE: u16 = 1 << 2; // port enabled
52const PSC_PEDC: u16 = 1 << 3; // enable change (write 1 to clear)
53const PSC_LSDA: u16 = 1 << 8; // low-speed device attached
54const PSC_R: u16 = 1 << 9; // reset
55const PSC_SUSP: u16 = 1 << 12; // suspend
56
57// ---- TD / QH fields ----
58const TD_ACTIVE: u32 = 1 << 23;
59const TD_IOC: u32 = 1 << 24;
60const TD_LS: u32 = 1 << 26;
61const TD_ERR: u32 = 3 << 27; // C_ERR = 3 retries
62const TD_STALLED: u32 = 1 << 22;
63const TD_BABBLE: u32 = 1 << 20;
64const TD_TIMEO: u32 = 1 << 18;
65const TD_ACTLEN: u32 = 0x7FF; // actual length, n-1
66
67const TOK_DEV_SHIFT: u32 = 8;
68const TOK_EP_SHIFT: u32 = 15;
69const TOK_TOGGLE: u32 = 1 << 19;
70const TOK_EXPLEN_SHIFT: u32 = 21;
71
72const PID_SETUP: u32 = 0x2D;
73const PID_IN: u32 = 0x69;
74const PID_OUT: u32 = 0xE1;
75
76const PTR_TERM: u32 = 0x1;
77const PTR_QH: u32 = 0x2;
78
79// ---- HID boot requests ----
80const RT_CLASS_IFACE: u8 = 0x21;
81const REQ_SET_IDLE: u8 = 0x0A;
82const REQ_SET_PROTOCOL: u8 = 0x0B;
83
84// ---- Static state ----
85static IO_BASE: AtomicU32 = AtomicU32::new(0);
86static N_PORTS: AtomicU32 = AtomicU32::new(0);
87static CTRL_UP: AtomicBool = AtomicBool::new(false);
88static MOUSE_PRES: AtomicBool = AtomicBool::new(false);
89static KBD_ARMED: AtomicBool = AtomicBool::new(false);
90static MOUSE_ARMED: AtomicBool = AtomicBool::new(false);
91// Port index each device was found on (usize::MAX = none). Used so the
92// keyboard scan skips the mouse's port and vice versa.
93static KBD_PORT: AtomicU32 = AtomicU32::new(u32::MAX);
94static MOUSE_PORT: AtomicU32 = AtomicU32::new(u32::MAX);
95static DEV_KBD: AtomicU32 = AtomicU32::new(0);
96static DEV_MOUSE: AtomicU32 = AtomicU32::new(0);
97static KBD_EP: AtomicU32 = AtomicU32::new(1);
98static KBD_MPS: AtomicU32 = AtomicU32::new(8);
99static MOUSE_EP: AtomicU32 = AtomicU32::new(1);
100static MOUSE_MPS: AtomicU32 = AtomicU32::new(4);
101// Low-speed flag per device (TD_LS needed for 1.5 Mb/s).
102static KBD_LS: AtomicBool = AtomicBool::new(false);
103static MOUSE_LS: AtomicBool = AtomicBool::new(false);
104// Next USB device address to assign (1, 2, ...).
105static DEV_SEQ: AtomicU32 = AtomicU32::new(1);
106// Throttles for the lazy re-enumeration paths (ms).
107static LAST_KBD_ENUM: AtomicU32 = AtomicU32::new(0);
108static LAST_MOUSE_ENUM: AtomicU32 = AtomicU32::new(0);
109// Serializes control transfers between the shell task (keyboard) and the
110// cursor task (mouse). Bounded spin; on timeout the transfer is skipped
111// and retried on the next throttled pass.
112static CTRL_LOCK: AtomicBool = AtomicBool::new(false);
113// Toggle bit the device expects next (alternates after each report).
114static KBD_TOGGLE: AtomicU32 = AtomicU32::new(0);
115static MOUSE_TOGGLE: AtomicU32 = AtomicU32::new(0);
116
117// ---- DMA: frame list + one struct page ----
118static mut FRAMELIST: *mut u32 = core::ptr::null_mut();
119static mut P: *mut u32 = core::ptr::null_mut(); // QHs, TDs, buffers
120
121// Offsets within the struct page (16-byte-aligned QHs/TDs).
122const OFF_CTRL_QH: usize = 0x000;
123const OFF_KBD_QH: usize = 0x010;
124const OFF_MOUSE_QH: usize = 0x020;
125const OFF_SETUP_TD: usize = 0x030;
126const OFF_DATA_TD: usize = 0x040;
127const OFF_STAT_TD: usize = 0x050;
128const OFF_KBD_TD: usize = 0x060;
129const OFF_MOUSE_TD: usize = 0x070;
130const OFF_REQ: usize = 0x100; // 8-byte setup packet
131const OFF_DESC: usize = 0x200; // device descriptor (64 B)
132const OFF_CFG: usize = 0x300; // config descriptor (256 B)
133const OFF_KBD_REPORT: usize = 0x500; // 8 B
134const OFF_MOUSE_REPORT: usize = 0x520; // 8 B
135
136// Keyboard: previous report + pending scancode FIFO (fed to ps2 decoder).
137static mut KBD_PREV: [u8; 8] = [0; 8];
138const KBDQ_SIZE: usize = 64;
139static mut KBDQ: [u8; KBDQ_SIZE] = [0; KBDQ_SIZE];
140static KBDQ_HEAD: AtomicU32 = AtomicU32::new(0);
141static KBDQ_LEN: AtomicU32 = AtomicU32::new(0);
142
143// ---- port I/O ----
144#[inline]
145unsafe fn inw(port: u16) -> u16 {
146 let v: u16;
147 unsafe {
148 core::arch::asm!("in ax, dx", out("ax") v, in("dx") port, options(nostack, preserves_flags));
149 }
150 v
151}
152#[inline]
153unsafe fn outw(port: u16, v: u16) {
154 unsafe {
155 core::arch::asm!("out dx, ax", in("dx") port, in("ax") v, options(nostack, preserves_flags));
156 }
157}
158#[inline]
159unsafe fn outl(port: u16, v: u32) {
160 unsafe {
161 core::arch::asm!("out dx, eax", in("dx") port, in("eax") v, options(nostack, preserves_flags));
162 }
163}
164
165fn io() -> u16 {
166 IO_BASE.load(Ordering::Relaxed) as u16
167}
168/// Absolute I/O port of root port `p`'s PORTSC register.
169fn prtsc(p: usize) -> u16 {
170 io() + PORTSC_BASE + (p as u16) * 2
171}
172
173// ---- DMA helpers (mirror ehci.rs) ----
174unsafe fn dma_alloc(bytes: usize) -> *mut u8 {
175 unsafe {
176 let pages = bytes.div_ceil(4096);
177 let Some(phys) = crate::memory::alloc_pages(pages) else {
178 return core::ptr::null_mut();
179 };
180 let p = phys as *mut u8;
181 core::ptr::write_bytes(p, 0, pages * 4096);
182 p
183 }
184}
185
186unsafe fn rd(addr: *const u32) -> u32 {
187 unsafe { addr.read_volatile() }
188}
189unsafe fn wr(addr: *mut u32, v: u32) {
190 unsafe { addr.write_volatile(v) }
191}
192
193fn td_addr(o: usize) -> *mut u32 {
194 unsafe { P.add(o / 4) }
195}
196fn buf_addr(o: usize) -> *mut u8 {
197 unsafe { P.add(o / 4) as *mut u8 }
198}
199
200#[inline]
201fn ms_now() -> u64 {
202 crate::interrupts::uptime_ms()
203}
204
205/// Busy-wait for `ms` milliseconds (interrupts stay enabled; the tick keeps
206/// the scheduler alive).
207fn ms_spin(ms: u64) {
208 let t0 = ms_now();
209 while ms_now().wrapping_sub(t0) < ms {
210 core::hint::spin_loop();
211 }
212}
213
214/// Build an 8-byte USB setup packet.
215fn ctrl_req(bm: u8, breq: u8, val: u16, idx: u16, len: u16) -> [u8; 8] {
216 let mut r = [0u8; 8];
217 r[0] = bm;
218 r[1] = breq;
219 r[2] = val as u8;
220 r[3] = (val >> 8) as u8;
221 r[4] = idx as u8;
222 r[5] = (idx >> 8) as u8;
223 r[6] = len as u8;
224 r[7] = (len >> 8) as u8;
225 r
226}
227
228// ---------------------------------------------------------------------------
229// Control transfer pipeline (shared, lock-protected)
230// ---------------------------------------------------------------------------
231
232fn lock_ctrl() -> bool {
233 for _ in 0..200_000 {
234 if !CTRL_LOCK.swap(true, Ordering::Acquire) {
235 return true;
236 }
237 core::hint::spin_loop();
238 }
239 false
240}
241fn unlock_ctrl() {
242 CTRL_LOCK.store(false, Ordering::Release);
243}
244
245/// Issue one control transfer on EP0 of `dev`. `mps` is the device's EP0
246/// max packet, `low_speed` selects the TD_CTRL_LS bit, `req` the setup
247/// packet, `dir_in` a device->host data phase, `buf/len` the data buffer.
248/// Returns bytes received (IN) or 0 (OUT/setup-only), or a negative error.
249/// Completion is polled (STATUS TD active bit) with a 5 s timeout.
250unsafe fn ctl_xfer(
251 dev: u32,
252 _mps: u32,
253 low_speed: bool,
254 req: &[u8; 8],
255 dir_in: bool,
256 buf: *mut u8,
257 len: usize,
258) -> i32 {
259 unsafe {
260 // The controller DMA's the setup packet straight from the request
261 // buffer — copy it there first.
262 core::ptr::copy_nonoverlapping(req.as_ptr(), buf_addr(OFF_REQ), 8);
263 core::sync::atomic::fence(Ordering::SeqCst);
264 let ls = if low_speed { TD_LS } else { 0 };
265
266 // SETUP TD (toggle 0, 8 bytes, always from the request buffer).
267 let st = td_addr(OFF_SETUP_TD);
268 wr(st.add(0), 0); // link: filled below
269 wr(st.add(1), TD_ACTIVE | TD_ERR | ls);
270 wr(
271 st.add(2),
272 PID_SETUP | (dev << TOK_DEV_SHIFT) | (7 << TOK_EXPLEN_SHIFT), // 8 bytes
273 );
274 wr(st.add(3), buf_addr(OFF_REQ) as u32);
275
276 // STATUS TD (toggle 1, opposite direction of the data phase).
277 let sp = td_addr(OFF_STAT_TD);
278 let status_pid = if dir_in { PID_OUT } else { PID_IN };
279 wr(sp.add(0), PTR_TERM);
280 wr(sp.add(1), TD_ACTIVE | TD_ERR | ls);
281 wr(
282 sp.add(2),
283 status_pid | (dev << TOK_DEV_SHIFT) | TOK_TOGGLE | (0 << TOK_EXPLEN_SHIFT),
284 );
285 wr(sp.add(3), buf_addr(OFF_REQ) as u32);
286
287 if len == 0 {
288 // Setup-only transfer: SETUP -> STATUS.
289 wr(st.add(0), td_addr(OFF_STAT_TD) as u32);
290 } else {
291 // DATA TD (toggle 1).
292 let dt = td_addr(OFF_DATA_TD);
293 let dpid = if dir_in { PID_IN } else { PID_OUT };
294 wr(dt.add(0), td_addr(OFF_STAT_TD) as u32);
295 wr(dt.add(1), TD_ACTIVE | TD_ERR | ls);
296 wr(
297 dt.add(2),
298 dpid | (dev << TOK_DEV_SHIFT)
299 | TOK_TOGGLE
300 | (((len as u32).saturating_sub(1) & 0x7FF) << TOK_EXPLEN_SHIFT),
301 );
302 wr(dt.add(3), buf as u32);
303 wr(st.add(0), dt as u32);
304 }
305
306 // Point the control QH at the new chain and let the HC run it.
307 core::sync::atomic::fence(Ordering::SeqCst);
308 wr(td_addr(OFF_CTRL_QH).add(1), td_addr(OFF_SETUP_TD) as u32);
309 core::sync::atomic::fence(Ordering::SeqCst);
310
311 // Wait for completion (STATUS TD clears ACTIVE).
312 let t0 = ms_now();
313 loop {
314 let c = rd(sp.add(1));
315 if c & TD_ACTIVE == 0 {
316 // Error checks: the status TD mirrors the chain's fate.
317 if c & (TD_STALLED | TD_BABBLE | TD_TIMEO) != 0 {
318 return -2;
319 }
320 let got = if len > 0 && dir_in {
321 ((rd(td_addr(OFF_DATA_TD).add(1)) & TD_ACTLEN) + 1) as i32
322 } else {
323 0
324 };
325 return got.min(len as i32);
326 }
327 if ms_now().wrapping_sub(t0) >= 5000 {
328 return -1; // timeout
329 }
330 core::hint::spin_loop();
331 }
332 }
333}
334
335// ---------------------------------------------------------------------------
336// Port management
337// ---------------------------------------------------------------------------
338
339/// Reset a connected root port and enable it. Returns the low-speed flag
340/// (false = full-speed) on success.
341unsafe fn reset_port(p: usize) -> Option<bool> {
342 unsafe {
343 let r = prtsc(p);
344 // Clear connect/enable change bits (write 1s).
345 outw(r, PSC_CSC | PSC_PEDC);
346 // Start reset.
347 outw(r, PSC_R);
348 ms_spin(50);
349 // End reset.
350 outw(r, 0);
351 // Enable the port (device must be connected).
352 outw(r, PSC_PE);
353 let t0 = ms_now();
354 while ms_now().wrapping_sub(t0) < 100 {
355 let psc = inw(r);
356 if psc & PSC_PE != 0 {
357 return Some(psc & PSC_LSDA != 0);
358 }
359 ms_spin(1);
360 }
361 None
362 }
363}
364
365// ---------------------------------------------------------------------------
366// Config-descriptor parsing: find a HID boot interface with the wanted
367// protocol and its first interrupt IN endpoint.
368// ---------------------------------------------------------------------------
369
370struct HidEp {
371 epnum: u8,
372 mps: u16,
373 iface: u8,
374}
375
376/// Walk a config descriptor for a boot-protocol HID interface (class 3,
377/// subclass 1) whose protocol matches `want` (1 = keyboard, 2 = mouse),
378/// returning its first interrupt IN endpoint.
379fn find_hid_ep(cfg: &[u8], want: u8) -> Option<HidEp> {
380 let mut i = 0;
381 while i + 1 < cfg.len() && cfg[i] != 0 {
382 let blen = cfg[i] as usize;
383 let btype = cfg[i + 1];
384 if blen < 2 || i + blen > cfg.len() {
385 return None;
386 }
387 if btype == 4 {
388 // Interface descriptor.
389 if blen >= 9
390 && cfg[i + 5] == 3 // bInterfaceClass = HID
391 && cfg[i + 6] == 1 // bInterfaceSubClass = boot
392 && cfg[i + 7] == want
393 // bInterfaceProtocol
394 {
395 let iface = cfg[i + 2];
396 // Scan this interface's endpoints.
397 let mut j = i + blen;
398 while j + 1 < cfg.len() && cfg[j] != 0 {
399 let elen = cfg[j] as usize;
400 let etype = cfg[j + 1];
401 if etype == 4 {
402 break; // next interface
403 }
404 if etype == 5 && elen >= 7 {
405 let epa = cfg[j + 2];
406 let attrs = cfg[j + 3];
407 if epa & 0x80 != 0 && attrs & 0x03 == 0x03 {
408 let mps = (cfg[j + 4] as u16) | ((cfg[j + 5] as u16) << 8);
409 return Some(HidEp {
410 epnum: epa & 0x0F,
411 mps: mps.max(1),
412 iface,
413 });
414 }
415 }
416 if elen < 2 {
417 return None;
418 }
419 j += elen;
420 }
421 }
422 }
423 i += blen;
424 }
425 None
426}
427
428// ---------------------------------------------------------------------------
429// Device enumeration (one root port)
430// ---------------------------------------------------------------------------
431
432/// Enumerate the device on root port `p` and arm the interrupt-IN endpoint
433/// if it is a HID boot device of protocol `want` (1 = keyboard, 2 = mouse).
434/// Returns true when armed.
435unsafe fn enumerate_port(p: usize, want: u8) -> bool {
436 unsafe {
437 let Some(low_speed) = reset_port(p) else {
438 return false;
439 };
440
441 // 1. GET_DESCRIPTOR(device) at address 0 — 8 bytes for EP0 MPS.
442 let dd = buf_addr(OFF_DESC);
443 core::ptr::write_bytes(dd, 0, 64);
444 let req = ctrl_req(0x80, 6, 0x0100, 0, 8);
445 if ctl_xfer(0, 8, low_speed, &req, true, dd, 8) < 0 {
446 crate::serial_println!("uhci: port {} get-dev-desc failed (want {})", p, want);
447 return false;
448 }
449 let mps0 = core::ptr::read_volatile(dd.add(7)).max(8) as u32;
450
451 // 2. SET_ADDRESS.
452 let dev = DEV_SEQ.fetch_add(1, Ordering::Relaxed);
453 let req = ctrl_req(0x00, 5, dev as u16, 0, 0);
454 if ctl_xfer(0, mps0, low_speed, &req, false, core::ptr::null_mut(), 0) < 0 {
455 crate::serial_println!("uhci: set-address failed on port {}", p);
456 return false;
457 }
458
459 // 3. Full device descriptor at the new address.
460 core::ptr::write_bytes(dd, 0, 64);
461 let req = ctrl_req(0x80, 6, 0x0100, 0, 18);
462 let got = ctl_xfer(dev, mps0, low_speed, &req, true, dd, 18);
463 if got < 0 {
464 crate::serial_println!("uhci: re-get dev-desc failed on port {}", p);
465 return false;
466 }
467 let class = core::ptr::read_volatile(dd.add(4));
468 let _ = class; // HID devices have device class 0x00; the HID class
469 // lives in the interface descriptor (checked below).
470
471 // 4. Full config descriptor.
472 let cfg = buf_addr(OFF_CFG);
473 core::ptr::write_bytes(cfg, 0, 256);
474 let req = ctrl_req(0x80, 6, 0x0200, 0, 256);
475 let got = ctl_xfer(dev, mps0, low_speed, &req, true, cfg, 256);
476 if got < 0 {
477 crate::serial_println!(
478 "uhci: get-config failed on port {} code={} (data ctrl={:#x} stat ctrl={:#x})",
479 p,
480 got,
481 rd(td_addr(OFF_DATA_TD).add(1)),
482 rd(td_addr(OFF_STAT_TD).add(1))
483 );
484 return false;
485 }
486 let Some(ep) = find_hid_ep(core::slice::from_raw_parts(cfg, got as usize), want) else {
487 return false; // not the device we're looking for
488 };
489
490 // 5. SET_CONFIGURATION(1).
491 let req = ctrl_req(0x00, 9, 1, 0, 0);
492 if ctl_xfer(dev, mps0, low_speed, &req, false, core::ptr::null_mut(), 0) < 0 {
493 crate::serial_println!("uhci: set-config failed on port {}", p);
494 return false;
495 }
496
497 // 6. Boot protocol + no idle. Some devices stall these; tolerate.
498 let req = ctrl_req(RT_CLASS_IFACE, REQ_SET_PROTOCOL, 1, ep.iface as u16, 0);
499 if ctl_xfer(dev, mps0, low_speed, &req, false, core::ptr::null_mut(), 0) < 0 {
500 crate::serial_println!("uhci: set-protocol stalled on port {}", p);
501 }
502 let req = ctrl_req(RT_CLASS_IFACE, REQ_SET_IDLE, 0, ep.iface as u16, 0);
503 if ctl_xfer(dev, mps0, low_speed, &req, false, core::ptr::null_mut(), 0) < 0 {
504 crate::serial_println!("uhci: set-idle stalled on port {}", p);
505 }
506
507 // 7. Arm the interrupt-IN TD.
508 arm_in_td(want, dev, &ep, low_speed, p);
509 crate::serial_println!(
510 "uhci: HID boot {} armed (port {} dev {} ep{}({:#x}) mps={} {})",
511 if want == 1 { "keyboard" } else { "mouse" },
512 p,
513 dev,
514 ep.epnum,
515 if want == 1 { 0x81 } else { 0x81 },
516 ep.mps,
517 if low_speed { "low-speed" } else { "full-speed" }
518 );
519 true
520 }
521}
522
523/// Build the interrupt-IN TD for a device and point its QH element at it.
524unsafe fn arm_in_td(want: u8, dev: u32, ep: &HidEp, low_speed: bool, port: usize) {
525 unsafe {
526 let (qho, tdo, buf_off, mps, is_kbd) = if want == 1 {
527 (
528 OFF_KBD_QH,
529 OFF_KBD_TD,
530 OFF_KBD_REPORT,
531 (ep.mps as u32).max(8),
532 true,
533 )
534 } else {
535 (
536 OFF_MOUSE_QH,
537 OFF_MOUSE_TD,
538 OFF_MOUSE_REPORT,
539 (ep.mps as u32).max(4),
540 false,
541 )
542 };
543 let qh = td_addr(qho);
544 let td = td_addr(tdo);
545 let ls = if low_speed { TD_LS } else { 0 };
546 let explen = if is_kbd { 8 } else { 3 };
547 let toggle = 0; // endpoint toggles start at 0 after SET_CONFIG
548
549 wr(td.add(0), PTR_TERM);
550 wr(td.add(1), TD_ACTIVE | TD_ERR | TD_IOC | ls);
551 wr(
552 td.add(2),
553 PID_IN
554 | (dev << TOK_DEV_SHIFT)
555 | ((ep.epnum as u32) << TOK_EP_SHIFT)
556 | (toggle << 19)
557 | (explen << TOK_EXPLEN_SHIFT),
558 );
559 wr(td.add(3), buf_addr(buf_off) as u32);
560 // QH: el_link points at the TD (link/el_link written last).
561 wr(qh.add(0), PTR_TERM); // no next QH (per-device QH at chain end)
562 core::sync::atomic::fence(Ordering::SeqCst);
563 wr(qh.add(1), td as u32);
564 core::sync::atomic::fence(Ordering::SeqCst);
565
566 match is_kbd {
567 true => {
568 KBD_ARMED.store(true, Ordering::Relaxed);
569 KBD_PORT.store(port as u32, Ordering::Relaxed);
570 DEV_KBD.store(dev, Ordering::Relaxed);
571 KBD_EP.store(ep.epnum as u32, Ordering::Relaxed);
572 KBD_MPS.store(mps, Ordering::Relaxed);
573 KBD_LS.store(low_speed, Ordering::Relaxed);
574 KBD_TOGGLE.store(0, Ordering::Relaxed);
575 }
576 false => {
577 MOUSE_ARMED.store(true, Ordering::Relaxed);
578 MOUSE_PORT.store(port as u32, Ordering::Relaxed);
579 DEV_MOUSE.store(dev, Ordering::Relaxed);
580 MOUSE_EP.store(ep.epnum as u32, Ordering::Relaxed);
581 MOUSE_MPS.store(mps, Ordering::Relaxed);
582 MOUSE_LS.store(low_speed, Ordering::Relaxed);
583 MOUSE_TOGGLE.store(0, Ordering::Relaxed);
584 MOUSE_PRES.store(true, Ordering::Relaxed);
585 }
586 }
587 }
588}
589
590/// Scan all root ports and arm the first device of the wanted protocol.
591unsafe fn arm_device(want: u8) -> bool {
592 unsafe {
593 if !lock_ctrl() {
594 return false;
595 }
596 let np = N_PORTS.load(Ordering::Relaxed) as usize;
597 let other = if want == 1 {
598 MOUSE_PORT.load(Ordering::Relaxed)
599 } else {
600 KBD_PORT.load(Ordering::Relaxed)
601 };
602 for p in 0..np {
603 if p as u32 == other {
604 continue; // claimed by the other device
605 }
606 let psc = inw(prtsc(p));
607 if psc & PSC_CCS == 0 {
608 continue;
609 }
610 if enumerate_port(p, want) {
611 unlock_ctrl();
612 return true;
613 }
614 }
615 unlock_ctrl();
616 false
617 }
618}
619
620// ---------------------------------------------------------------------------
621// Keyboard: HID report -> scancode-set-1 make/break bytes -> ps2 decoder
622// ---------------------------------------------------------------------------
623
624/// HID usage -> scancode set 1 (make code). High byte 0xE0 = E0-prefixed.
625/// 0 = unmapped. Table covers the common keyboard usages (0x04..0x63).
626static HID_TO_SCAN: [u16; 0x64] = {
627 let mut t = [0u16; 0x64];
628 let mut i = 0;
629 while i < 0x64 {
630 t[i] = 0;
631 i += 1;
632 }
633 // Letters a-z
634 t[0x04] = 0x1E;
635 t[0x05] = 0x30;
636 t[0x06] = 0x2E;
637 t[0x07] = 0x20;
638 t[0x08] = 0x12;
639 t[0x09] = 0x21;
640 t[0x0A] = 0x22;
641 t[0x0B] = 0x23;
642 t[0x0C] = 0x17;
643 t[0x0D] = 0x24;
644 t[0x0E] = 0x25;
645 t[0x0F] = 0x26;
646 t[0x10] = 0x32;
647 t[0x11] = 0x31;
648 t[0x12] = 0x18;
649 t[0x13] = 0x19;
650 t[0x14] = 0x10;
651 t[0x15] = 0x13;
652 t[0x16] = 0x1F;
653 t[0x17] = 0x14;
654 t[0x18] = 0x16;
655 t[0x19] = 0x2F;
656 t[0x1A] = 0x11;
657 t[0x1B] = 0x2D;
658 t[0x1C] = 0x15;
659 t[0x1D] = 0x2C;
660 // Digits + punctuation
661 t[0x1E] = 0x02;
662 t[0x1F] = 0x03;
663 t[0x20] = 0x04;
664 t[0x21] = 0x05;
665 t[0x22] = 0x06;
666 t[0x23] = 0x07;
667 t[0x24] = 0x08;
668 t[0x25] = 0x09;
669 t[0x26] = 0x0A;
670 t[0x27] = 0x0B;
671 t[0x28] = 0x1C; // Enter
672 t[0x29] = 0x01; // Esc
673 t[0x2A] = 0x0E; // Backspace
674 t[0x2B] = 0x0F; // Tab
675 t[0x2C] = 0x39; // Space
676 t[0x2D] = 0x0C; // -
677 t[0x2E] = 0x0D; // =
678 t[0x2F] = 0x1A; // [
679 t[0x30] = 0x1B; // ]
680 t[0x31] = 0x2B; // backslash
681 t[0x33] = 0x27; // ;
682 t[0x34] = 0x28; // '
683 t[0x35] = 0x29; // `
684 t[0x36] = 0x33; // ,
685 t[0x37] = 0x34; // .
686 t[0x38] = 0x35; // /
687 t[0x39] = 0x3A; // CapsLock
688 // F-keys
689 t[0x3A] = 0x3B; // F1
690 t[0x3B] = 0x3C;
691 t[0x3C] = 0x3D;
692 t[0x3D] = 0x3E;
693 t[0x3E] = 0x3F;
694 t[0x3F] = 0x40;
695 t[0x40] = 0x41;
696 t[0x41] = 0x42;
697 t[0x42] = 0x43;
698 t[0x43] = 0x44; // F10
699 t[0x44] = 0xE057; // F11
700 t[0x45] = 0xE058; // F12
701 // Nav cluster
702 t[0x49] = 0xE052; // Insert
703 t[0x4A] = 0xE047; // Home
704 t[0x4B] = 0xE049; // PageUp
705 t[0x4C] = 0xE053; // Delete
706 t[0x4D] = 0xE04F; // End
707 t[0x4E] = 0xE051; // PageDown
708 t[0x4F] = 0xE04D; // Right
709 t[0x50] = 0xE04B; // Left
710 t[0x51] = 0xE050; // Down
711 t[0x52] = 0xE048; // Up
712 // Keypad
713 t[0x53] = 0x45; // NumLock
714 t[0x54] = 0xE035; // /
715 t[0x55] = 0x37; // *
716 t[0x56] = 0x4A; // -
717 t[0x57] = 0x4E; // +
718 t[0x58] = 0xE01C; // Enter
719 t[0x59] = 0x4F; // 1
720 t[0x5A] = 0x50; // 2
721 t[0x5B] = 0x51; // 3
722 t[0x5C] = 0x4B; // 4
723 t[0x5D] = 0x4C; // 5
724 t[0x5E] = 0x4D; // 6
725 t[0x5F] = 0x47; // 7
726 t[0x60] = 0x48; // 8
727 t[0x61] = 0x49; // 9
728 t[0x62] = 0x52; // 0
729 t[0x63] = 0x53; // .
730 t
731};
732
733/// Modifier bit -> scancode (set 1, high byte 0xE0 = E0-prefixed).
734static MOD_TO_SCAN: [u16; 8] = [
735 0x1D, // 0 LCtrl
736 0x2A, // 1 LShift
737 0x38, // 2 LAlt
738 0xE05B, // 3 LGui
739 0xE01D, // 4 RCtrl
740 0x36, // 5 RShift
741 0xE038, // 6 RAlt
742 0xE05C, // 7 RGui
743];
744
745fn kbdq_push(b: u8) {
746 unsafe {
747 let h = KBDQ_HEAD.load(Ordering::Relaxed) as usize;
748 let l = KBDQ_LEN.load(Ordering::Relaxed) as usize;
749 if l >= KBDQ_SIZE {
750 return;
751 }
752 KBDQ[(h + l) % KBDQ_SIZE] = b;
753 KBDQ_LEN.store((l + 1) as u32, Ordering::Relaxed);
754 }
755}
756fn kbdq_pop() -> Option<u8> {
757 unsafe {
758 let l = KBDQ_LEN.load(Ordering::Relaxed) as usize;
759 if l == 0 {
760 return None;
761 }
762 let h = KBDQ_HEAD.load(Ordering::Relaxed) as usize;
763 let b = KBDQ[h];
764 KBDQ_HEAD.store(((h + 1) % KBDQ_SIZE) as u32, Ordering::Relaxed);
765 KBDQ_LEN.store((l - 1) as u32, Ordering::Relaxed);
766 Some(b)
767 }
768}
769
770/// Push the make (press) or break (release) scancode sequence for a
771/// scancode-map entry (0 = unmapped, 0x00XX = plain, 0xE0XX = E0-prefixed).
772fn push_scancode(scan: u16, press: bool) {
773 if scan == 0 {
774 return;
775 }
776 if scan & 0xFF00 == 0xE000 {
777 kbdq_push(0xE0);
778 let code = (scan & 0xFF) as u8;
779 kbdq_push(if press { code } else { code | 0x80 });
780 } else {
781 let code = scan as u8;
782 kbdq_push(if press { code } else { code | 0x80 });
783 }
784}
785
786/// Diff the completed HID report against the previous one and queue the
787/// scancode transitions (make/break) for the shared PS/2 decoder.
788unsafe fn diff_report(report: &[u8]) {
789 unsafe {
790 let prev = &mut *core::ptr::addr_of_mut!(KBD_PREV);
791 let new_mods = report[0];
792 let old_mods = prev[0];
793 for bit in 0..8 {
794 let new_set = new_mods & (1 << bit) != 0;
795 let old_set = old_mods & (1 << bit) != 0;
796 if new_set != old_set {
797 push_scancode(MOD_TO_SCAN[bit], new_set);
798 }
799 }
800 let new_keys = &report[2..8];
801 let old_keys = &prev[2..8];
802 // Releases first (old keys not in new set).
803 for &k in old_keys {
804 if k == 0 || new_keys.contains(&k) {
805 continue;
806 }
807 if k as usize >= HID_TO_SCAN.len() {
808 continue;
809 }
810 push_scancode(HID_TO_SCAN[k as usize], false);
811 }
812 // Then presses (new keys not in the old set).
813 for &k in new_keys {
814 if k == 0 || old_keys.contains(&k) {
815 continue;
816 }
817 if k as usize >= HID_TO_SCAN.len() {
818 continue;
819 }
820 push_scancode(HID_TO_SCAN[k as usize], true);
821 }
822 prev.copy_from_slice(report);
823 }
824}
825
826/// Re-arm a completed interrupt-IN TD (one-outstanding report pattern).
827unsafe fn rearm_td(qho: usize, tdo: usize, toggle: u32, ls: bool, mps: usize, explen: u32) {
828 unsafe {
829 let qh = td_addr(qho);
830 let td = td_addr(tdo);
831 let t = rd(td.add(2));
832 let dev = (t >> TOK_DEV_SHIFT) & 0x7F;
833 let ep = (t >> TOK_EP_SHIFT) & 0xF;
834 wr(
835 td.add(1),
836 TD_ACTIVE | TD_ERR | TD_IOC | if ls { TD_LS } else { 0 },
837 );
838 wr(
839 td.add(2),
840 PID_IN
841 | (dev << TOK_DEV_SHIFT)
842 | (ep << TOK_EP_SHIFT)
843 | (toggle << 19)
844 | (explen << TOK_EXPLEN_SHIFT),
845 );
846 core::sync::atomic::fence(Ordering::SeqCst);
847 wr(qh.add(1), td as u32);
848 core::sync::atomic::fence(Ordering::SeqCst);
849 let _ = mps;
850 }
851}
852
853// ---------------------------------------------------------------------------
854// Public interface
855// ---------------------------------------------------------------------------
856
857pub fn controller_up() -> bool {
858 CTRL_UP.load(Ordering::Relaxed)
859}
860pub fn mouse_present() -> bool {
861 MOUSE_PRES.load(Ordering::Relaxed)
862}
863
864/// Find the first UHCI companion controller (class 0x0C, subclass 0x03,
865/// prog_if 0x00).
866fn find_uhci() -> Option<(u8, u8, u8)> {
867 for d in crate::pci::probe() {
868 if d.class == 0x0C && d.subclass == 0x03 && d.prog_if == 0x00 {
869 return Some((d.bus, d.device, d.func));
870 }
871 }
872 None
873}
874
875/// Bring up the first UHCI controller: claim it, reset it, run the frame
876/// list, and enumerate any connected boot HID devices.
877pub fn init() -> bool {
878 let Some((b, d, f)) = find_uhci() else {
879 crate::serial_println!("uhci: no UHCI controller");
880 return false;
881 };
882 crate::serial_println!("uhci: USB1.1 UHCI at {:02x}:{:02x}.{}", b, d, f);
883
884 unsafe {
885 // I/O space + bus master (polled: no INTx assertion needed, but
886 // clear it anyway so a stray legacy IRQ can never storm).
887 let cmd = crate::pci::config_read(b, d, f, 0x04);
888 crate::pci::config_write(
889 b,
890 d,
891 f,
892 0x04,
893 (cmd & 0xFFFF_0000) | (cmd & 0xFFFF) | 0x5 | 0x400,
894 );
895 let mut base: u32 = 0;
896 // BAR0 first (Intel ICH), then BAR4 (PIIX3/PIIX4-style). Take the
897 // first nonzero I/O BAR.
898 for bar_idx in [0u8, 4] {
899 let bar = crate::pci::config_read(b, d, f, 0x10 + bar_idx * 4);
900 let cand = bar & !0x3;
901 if cand != 0 {
902 base = cand;
903 break;
904 }
905 }
906 if base == 0 {
907 crate::serial_println!("uhci: no I/O BAR");
908 return false;
909 }
910 IO_BASE.store(base, Ordering::Relaxed);
911
912 // Controller reset: stop, then HCRESET (UHCI clears HCRESET itself
913 // within microseconds).
914 outw(io(), 0);
915 outw(io() + USBCMD, CMD_HCRESET);
916 ms_spin(5);
917 let mut t = 0;
918 while t < 1_000_000 && inw(io() + USBCMD) & CMD_HCRESET != 0 {
919 t += 1;
920 core::hint::spin_loop();
921 }
922 outw(io() + USBINTR, 0);
923
924 // DMA: 4 KiB frame list (1024 entries) + struct page.
925 FRAMELIST = dma_alloc(4096) as *mut u32;
926 P = dma_alloc(4096) as *mut u32;
927 if FRAMELIST.is_null() || P.is_null() {
928 crate::serial_println!("uhci: DMA alloc failed");
929 return false;
930 }
931
932 // Wire the schedule: every frame entry -> CTRL_QH -> KBD_QH ->
933 // MOUSE_QH -> terminate. Each QH's element is set when its device
934 // arms; the control QH element is set per transfer.
935 let fl = FRAMELIST;
936 for i in 0..1024 {
937 wr(fl.add(i), (td_addr(OFF_CTRL_QH) as u32) | PTR_QH);
938 }
939 wr(
940 td_addr(OFF_CTRL_QH).add(0),
941 (td_addr(OFF_KBD_QH) as u32) | PTR_QH,
942 );
943 wr(td_addr(OFF_CTRL_QH).add(1), PTR_TERM);
944 wr(
945 td_addr(OFF_KBD_QH).add(0),
946 (td_addr(OFF_MOUSE_QH) as u32) | PTR_QH,
947 );
948 wr(td_addr(OFF_KBD_QH).add(1), PTR_TERM);
949 wr(td_addr(OFF_MOUSE_QH).add(0), PTR_TERM);
950 wr(td_addr(OFF_MOUSE_QH).add(1), PTR_TERM);
951 core::sync::atomic::fence(Ordering::SeqCst);
952
953 // Frame list base, configure, run.
954 outl(io() + FRBASEADD, fl as u32);
955 outw(io() + SOFMOD, 64);
956 outw(io() + FRNUM, 0);
957 outw(io() + USBCMD, CMD_CF);
958 outw(io() + USBCMD, CMD_CF | CMD_RUN);
959 ms_spin(2);
960 if inw(io() + USBSTS) & STS_HCHALTED != 0 {
961 crate::serial_println!("uhci: controller failed to run");
962 return false;
963 }
964
965 // Port count: UHCI has 2 ports at 0x10/0x12; a few have more.
966 let mut np = 0;
967 while np < 4 {
968 let psc = inw(prtsc(np));
969 if psc == 0xFFFF || psc == 0xff7f {
970 break;
971 }
972 np += 1;
973 }
974 N_PORTS.store(np as u32, Ordering::Relaxed);
975 CTRL_UP.store(true, Ordering::Relaxed);
976 crate::serial_println!("uhci: controller up (io={:#x} ports={})", base, np);
977
978 // Enumerate a boot keyboard first (input priority), then a mouse.
979 crate::serial_println!(
980 "uhci: dbg P={:#x} FL={:#x} ctrlqh={:#x} el={:#x} fl0={:#x} setup_td={:#x} setup_link={:#x}",
981 P as usize,
982 FRAMELIST as usize,
983 td_addr(OFF_CTRL_QH) as usize,
984 rd(td_addr(OFF_CTRL_QH).add(1)),
985 rd(FRAMELIST.add(0)),
986 td_addr(OFF_SETUP_TD) as usize,
987 rd(td_addr(OFF_SETUP_TD).add(0))
988 );
989 let _ = arm_device(1);
990 let _ = arm_device(2);
991 true
992 }
993}
994
995/// Non-blocking keyboard poll: drains the scancode FIFO (fed by completed
996/// HID reports) through the shared PS/2 decoder. Lazy re-enumeration is
997/// throttled to one attempt per second.
998pub fn try_poll_key() -> Option<crate::ps2::KeyEvent> {
999 unsafe {
1000 // Fast path: pending scancodes first.
1001 if let Some(b) = kbdq_pop() {
1002 return crate::ps2::feed_scancode(b);
1003 }
1004 if KBD_ARMED.load(Ordering::Relaxed) {
1005 let td = td_addr(OFF_KBD_TD);
1006 let c = rd(td.add(1));
1007 if c & TD_ACTIVE == 0 {
1008 if c & (TD_STALLED | TD_BABBLE | TD_TIMEO) != 0 {
1009 // Transfer error: drop the arm; bounded re-enum follows.
1010 KBD_ARMED.store(false, Ordering::Relaxed);
1011 return None;
1012 }
1013 // Completed: diff the report, re-arm, then drain the FIFO.
1014 let rep = buf_addr(OFF_KBD_REPORT);
1015 let mut report = [0u8; 8];
1016 for (i, b) in report.iter_mut().enumerate() {
1017 *b = core::ptr::read_volatile(rep.add(i));
1018 }
1019 diff_report(&report);
1020 let tog = KBD_TOGGLE.load(Ordering::Relaxed) ^ 1;
1021 KBD_TOGGLE.store(tog, Ordering::Relaxed);
1022 rearm_td(
1023 OFF_KBD_QH,
1024 OFF_KBD_TD,
1025 tog,
1026 KBD_LS.load(Ordering::Relaxed),
1027 KBD_MPS.load(Ordering::Relaxed) as usize,
1028 8,
1029 );
1030 return kbdq_pop().and_then(crate::ps2::feed_scancode);
1031 }
1032 return None;
1033 }
1034 // Not armed: bring the keyboard up lazily, never hot-loop.
1035 if CTRL_UP.load(Ordering::Relaxed) {
1036 let now = crate::interrupts::uptime_ms() as u32;
1037 if now.wrapping_sub(LAST_KBD_ENUM.load(Ordering::Relaxed)) >= 1000 {
1038 LAST_KBD_ENUM.store(now, Ordering::Relaxed);
1039 let _ = arm_device(1);
1040 }
1041 }
1042 kbdq_pop().and_then(crate::ps2::feed_scancode)
1043 }
1044}
1045
1046/// Poll for a mouse report (mirrors ehci::poll_mouse): hot path reads the
1047/// armed TD's ACTIVE bit in DMA memory; completion decodes the report and
1048/// re-arms; a stalled/TD-error transfer drops the arm for a bounded
1049/// re-enumeration.
1050pub fn poll_mouse() -> Option<MouseEvent> {
1051 unsafe {
1052 if MOUSE_ARMED.load(Ordering::Relaxed) {
1053 let td = td_addr(OFF_MOUSE_TD);
1054 let c = rd(td.add(1));
1055 if c & TD_ACTIVE == 0 {
1056 if c & (TD_STALLED | TD_BABBLE | TD_TIMEO) != 0 {
1057 MOUSE_ARMED.store(false, Ordering::Relaxed);
1058 return None;
1059 }
1060 let hb = buf_addr(OFF_MOUSE_REPORT);
1061 let b0 = core::ptr::read_volatile(hb);
1062 let b1 = core::ptr::read_volatile(hb.add(1));
1063 let b2 = core::ptr::read_volatile(hb.add(2));
1064 let ev = Some(MouseEvent {
1065 dx: (b1 as i8) as i16,
1066 dy: (b2 as i8) as i16,
1067 left: b0 & 0x01 != 0,
1068 right: b0 & 0x02 != 0,
1069 middle: b0 & 0x04 != 0,
1070 });
1071 let tog = MOUSE_TOGGLE.load(Ordering::Relaxed) ^ 1;
1072 MOUSE_TOGGLE.store(tog, Ordering::Relaxed);
1073 rearm_td(
1074 OFF_MOUSE_QH,
1075 OFF_MOUSE_TD,
1076 tog,
1077 MOUSE_LS.load(Ordering::Relaxed),
1078 MOUSE_MPS.load(Ordering::Relaxed) as usize,
1079 3,
1080 );
1081 return ev;
1082 }
1083 return None;
1084 }
1085 if CTRL_UP.load(Ordering::Relaxed) {
1086 let now = crate::interrupts::uptime_ms() as u32;
1087 if now.wrapping_sub(LAST_MOUSE_ENUM.load(Ordering::Relaxed)) >= 1000 {
1088 LAST_MOUSE_ENUM.store(now, Ordering::Relaxed);
1089 let _ = arm_device(2);
1090 }
1091 }
1092 None
1093 }
1094}
1095
1096/// One-line status for the shell `usb` command.
1097pub fn diagnose() -> alloc::string::String {
1098 let mut s = alloc::string::String::new();
1099 s.push_str(&alloc::format!(
1100 "uhci io={:#x} ports={} kbd={} mouse={} kbd_dev={} mouse_dev={}\n",
1101 IO_BASE.load(Ordering::Relaxed),
1102 N_PORTS.load(Ordering::Relaxed),
1103 if KBD_ARMED.load(Ordering::Relaxed) {
1104 "armed"
1105 } else {
1106 "-"
1107 },
1108 if MOUSE_ARMED.load(Ordering::Relaxed) {
1109 "armed"
1110 } else {
1111 "-"
1112 },
1113 DEV_KBD.load(Ordering::Relaxed),
1114 DEV_MOUSE.load(Ordering::Relaxed)
1115 ));
1116 let np = N_PORTS.load(Ordering::Relaxed) as usize;
1117 for p in 0..np.min(4) {
1118 let v = unsafe { inw(prtsc(p)) };
1119 s.push_str(&alloc::format!(
1120 " uport{}: {}{}{}{:#x}\n",
1121 p,
1122 if v & PSC_CCS != 0 { "conn " } else { "none " },
1123 if v & PSC_PE != 0 { "en " } else { "" },
1124 if v & PSC_LSDA != 0 { "low " } else { "" },
1125 v
1126 ));
1127 }
1128 s
1129}
diff --git a/rsnix/src/usb.rs b/rsnix/src/usb.rs
index 71df781..3ed3a0c 100644
--- a/rsnix/src/usb.rs
+++ b/rsnix/src/usb.rs
@@ -229,6 +229,15 @@ unsafe fn wr64(a: usize, v: u64) {
229fn op(off: usize) -> usize { 229fn op(off: usize) -> usize {
230 unsafe { OPB + off } 230 unsafe { OPB + off }
231} 231}
232
233/// 16-bit I/O read (UHCI port status/control registers live in I/O space).
234unsafe fn io_iow16(port: u16) -> u16 {
235 let v: u16;
236 unsafe {
237 core::arch::asm!("in ax, dx", out("ax") v, in("dx") port, options(nomem, nostack, preserves_flags));
238 }
239 v
240}
232fn db(slot: u32) -> usize { 241fn db(slot: u32) -> usize {
233 unsafe { DBB }.wrapping_add(slot as usize * 4) 242 unsafe { DBB }.wrapping_add(slot as usize * 4)
234} 243}
@@ -727,7 +736,13 @@ pub fn init() -> bool {
727 dev, 736 dev,
728 func, 737 func,
729 0x04, 738 0x04,
730 (cmd & 0xFFFF0000) | ((cmd & 0xFFFF) | 0x6), 739 // Memory space + bus master + INTx disable (bit 10). The
740 // driver is polled (event ring drained from the cursor task),
741 // so the controller must never assert INTx: an unhandled
742 // xHCI interrupt would hit the kernel's catch-all handler
743 // and (being level-triggered) re-fire forever on real
744 // hardware.
745 (cmd & 0xFFFF_0000) | ((cmd & 0xFFFF) | 0x6 | 0x400),
731 ); 746 );
732 let bar = crate::pci::bar64(b, dev, func, 0); 747 let bar = crate::pci::bar64(b, dev, func, 0);
733 let base = bar.unwrap() as usize; 748 let base = bar.unwrap() as usize;
@@ -745,6 +760,15 @@ pub fn init() -> bool {
745 MAXPORTS = (hcs1 >> 24) & 0xff; // MaxPorts is bits 31:24 (bits 23:16 are MaxIntrs) 760 MAXPORTS = (hcs1 >> 24) & 0xff; // MaxPorts is bits 31:24 (bits 23:16 are MaxIntrs)
746 let _ = hcc; 761 let _ = hcc;
747 CTRL_CS = if hcc & (1 << 2) != 0 { 64 } else { 32 }; 762 CTRL_CS = if hcc & (1 << 2) != 0 { 64 } else { 32 };
763
764 // Claim USB Legacy ownership from the BIOS before resetting the
765 // controller (same SMM-legacy rationale as the EHCI driver).
766 match crate::pci::claim_usb_legacy(b, dev, func, base, 0x04, true) {
767 0 => crate::serial_println!("usb: xHCI USB legacy: already released (no claim needed)"),
768 1 => crate::serial_println!("usb: xHCI USB legacy handed over by BIOS"),
769 2 => crate::serial_println!("usb: xHCI USB legacy claim timed out (BIOS sem stuck)"),
770 _ => crate::serial_println!("usb: xHCI USB legacy: no capability found"),
771 }
748 } 772 }
749 773
750 // Reset the controller (stop, then reset). Wait for reset completion 774 // Reset the controller (stop, then reset). Wait for reset completion
@@ -802,8 +826,10 @@ pub fn init() -> bool {
802 wr(rt(RT_ERSTSZ), 1); 826 wr(rt(RT_ERSTSZ), 1);
803 wr64(rt(RT_ERSTBA), ERST as u64); 827 wr64(rt(RT_ERSTBA), ERST as u64);
804 wr64(rt(RT_ERDP), EV.raw as u64); 828 wr64(rt(RT_ERDP), EV.raw as u64);
805 // Interrupter 0: clear pending, enable. 829 // Interrupter 0: keep the interrupt DISABLED (IE=0, clear any
806 wr(rt(RT_IMAN), rd(rt(RT_IMAN)) | 0x2); 830 // pending IP) — the driver is polled and the controller must not
831 // assert INTx (see the PCI command-register comment above).
832 wr(rt(RT_IMAN), (rd(rt(RT_IMAN)) & !0x2) | 0x1);
807 wr(rt(RT_IMOD), 0); 833 wr(rt(RT_IMOD), 0);
808 // Run. 834 // Run.
809 wr(op(OP_USBCMD), rd(op(OP_USBCMD)) | CMD_RUN); 835 wr(op(OP_USBCMD), rd(op(OP_USBCMD)) | CMD_RUN);
@@ -973,5 +999,120 @@ pub fn diagnose() -> alloc::string::String {
973 unsafe { QUEUED } 999 unsafe { QUEUED }
974 )); 1000 ));
975 } 1001 }
1002
1003 // Cross-controller attach scan: report which PCI USB controller(s)
1004 // currently have a device with bit0 (connect) set, across xHCI / EHCI /
1005 // UHCI / OHCI. Read-only diagnostic to locate the mouse on real hw.
1006 s.push_str("usbctl: scanning all USB controllers:\n");
1007 unsafe {
1008 for d in crate::pci::probe() {
1009 if d.class != 0x0C || d.subclass != 0x03 {
1010 continue;
1011 }
1012 let lbl = match d.prog_if {
1013 0x00 => "UHCI",
1014 0x10 => "OHCI",
1015 0x20 => "EHCI",
1016 0x30 => "xHCI",
1017 _ => "USB?",
1018 };
1019 let bar = crate::pci::bar64(d.bus, d.device, d.func, 0);
1020 let base = bar.unwrap_or(0);
1021 s.push_str(&alloc::format!(
1022 " {} {:02x}:{:02x}.{} bar={:#x}\n",
1023 lbl, d.bus, d.device, d.func, base
1024 ));
1025 if base == 0 || base == u64::MAX {
1026 // BAR not decoded / not enabled — nothing to probe.
1027 continue;
1028 }
1029 match d.prog_if {
1030 0x30 => {
1031 if base == 0 {
1032 continue;
1033 }
1034 let caplen = rd(base as usize);
1035 let hcs1 = rd(base as usize + 0x04);
1036 let np = ((hcs1 >> 24) & 0xff) as usize;
1037 let opb = base as usize + (caplen & 0xff) as usize;
1038 s.push_str(&alloc::format!(
1039 " xhci ports={} sc=", np
1040 ));
1041 for p in 0..np.min(16) {
1042 let v = rd(opb + 0x400 + p * 0x10);
1043 s.push_str(&alloc::format!(
1044 "{}{:#x}",
1045 if v & 1 != 0 { "*" } else { "" },
1046 v & 0x3ff
1047 ));
1048 if p + 1 < (np.min(16)) {
1049 s.push(' ');
1050 }
1051 }
1052 s.push('\n');
1053 }
1054 0x20 => {
1055 // EHCI: MMIO BAR. CAPLENGTH byte0, HcSParams (nports) @0x04.
1056 let cl = rd(base as usize) & 0xff;
1057 let sp = rd(base as usize + 0x04);
1058 let hc = rd(base as usize + 0x08);
1059 let np = (sp & 0xf) as usize;
1060 let opb = base as usize + cl as usize;
1061 let cmd = crate::pci::config_read(d.bus, d.device, d.func, 0x04);
1062 let eccp = crate::pci::config_read(d.bus, d.device, d.func, 0x34);
1063 s.push_str(&alloc::format!(
1064 " ehci nports={} hrs={} hcc={:#x} cmd={:#x} eccp={:#x}\n",
1065 np, sp, hc, cmd, (eccp >> 8) & 0xff
1066 ));
1067 let cfg = rd(opb + 0x40);
1068 s.push_str(&alloc::format!(
1069 " ehci cfgflag={:#x} ", cfg
1070 ));
1071 for p in 0..np.min(16) {
1072 let v = rd(opb + 0x44 + p * 0x04);
1073 s.push_str(&alloc::format!(
1074 "{}{:#x},",
1075 if v & 1 != 0 { "*" } else { "" },
1076 v & 0xffff
1077 ));
1078 }
1079 s.push('\n');
1080 // Legacy BIOS handoff status (USBLEGSUP at EECP).
1081 let eo = ((eccp >> 8) & 0xff) as usize;
1082 if eo != 0 && (eccp & 0xff) != 0 {
1083 let hs = crate::pci::config_read(
1084 d.bus,
1085 d.device,
1086 d.func,
1087 (eo & 0xfc) as u8,
1088 );
1089 s.push_str(&alloc::format!(
1090 " ehci legacy=usblegsup={:#x} os-owner={} bios-owned={}\n",
1091 hs,
1092 (hs & (1 << 24)) != 0,
1093 (hs & (1 << 16)) != 0 || (hs & 0x100) != 0
1094 ));
1095 }
1096 }
1097 0x00 => {
1098 // UHCI: I/O BAR (bar64 refuses I/O bars — read raw).
1099 let lo = crate::pci::config_read(d.bus, d.device, d.func, 0x10);
1100 let io = (lo & 0xFFF0) as u16;
1101 if io == 0 {
1102 continue;
1103 }
1104 for p in 0..2 {
1105 let pr = io_iow16(io + 0x10 + (p as u16) * 2);
1106 s.push_str(&alloc::format!(
1107 "{}{:x}:", if pr & 1 != 0 { "*" } else { "" },
1108 (pr & 0xff)
1109 ));
1110 }
1111 s.push('\n');
1112 }
1113 _ => {}
1114 }
1115 }
1116 }
976 s 1117 s
977} 1118}
diff --git a/rsnix/src/vt.rs b/rsnix/src/vt.rs
index 4ffb44d..9444063 100644
--- a/rsnix/src/vt.rs
+++ b/rsnix/src/vt.rs
@@ -121,9 +121,11 @@ pub fn init() {
121 COLS = cols; 121 COLS = cols;
122 CELLS = Box::into_raw(Box::new(vec![BLANK; rows * cols])); 122 CELLS = Box::into_raw(Box::new(vec![BLANK; rows * cols]));
123 RENDERED = Box::into_raw(Box::new(vec![BLANK; rows * cols])); 123 RENDERED = Box::into_raw(Box::new(vec![BLANK; rows * cols]));
124 CURSOR_R = 0; 124 // Home below row 0: row 0 is the boot-marker status line, so the
125 // terminal (shell + klog screen echo) must never write to it.
126 CURSOR_R = 1;
125 CURSOR_C = 0; 127 CURSOR_C = 0;
126 LAST_CURSOR = (0, 0); 128 LAST_CURSOR = (1, 0);
127 CURSOR_VISIBLE = true; 129 CURSOR_VISIBLE = true;
128 SGR_FG = 7; 130 SGR_FG = 7;
129 SGR_BG = 0; 131 SGR_BG = 0;
@@ -133,6 +135,13 @@ pub fn init() {
133 } 135 }
134} 136}
135 137
138/// True once the terminal is initialised (grids live). The klog screen
139/// echo checks this so serial lines printed before vt::init (very early
140/// boot) are skipped instead of hitting a null grid.
141pub fn ready() -> bool {
142 unsafe { ROWS != 0 && !CELLS.is_null() }
143}
144
136/// (rows, cols) — matches the editor's expected (rows, cols) ordering. 145/// (rows, cols) — matches the editor's expected (rows, cols) ordering.
137pub fn size() -> (usize, usize) { 146pub fn size() -> (usize, usize) {
138 unsafe { (ROWS, COLS) } 147 unsafe { (ROWS, COLS) }
@@ -667,10 +676,41 @@ pub fn sync_scroll() {
667// public writing API 676// public writing API
668// --------------------------------------------------------------------------- 677// ---------------------------------------------------------------------------
669 678
679/// Disable interrupts unless they are already off (nested-safe). Returns
680/// whether they were on, so `irq_restore` re-enables exactly when needed.
681/// The terminal write path uses this so a klog screen-echo (which can run
682/// in ISR context) can never tear a shell write, and a task-context write
683/// can never be split by a tick.
684fn irq_off() -> bool {
685 let rflags: u64;
686 unsafe {
687 core::arch::asm!(
688 "pushfq; pop {}",
689 out(reg) rflags,
690 options(nostack, preserves_flags)
691 );
692 if rflags & (1 << 9) != 0 {
693 core::arch::asm!("cli", options(nostack, preserves_flags));
694 true
695 } else {
696 false
697 }
698 }
699}
700
701fn irq_restore(was_on: bool) {
702 if was_on {
703 unsafe {
704 core::arch::asm!("sti", options(nostack, preserves_flags));
705 }
706 }
707}
708
670/// Write raw bytes (ANSI-capable). Used by full-screen apps. UTF-8 709/// Write raw bytes (ANSI-capable). Used by full-screen apps. UTF-8
671/// sequences are decoded so Unicode glyphs (box-drawing, arrows) render; 710/// sequences are decoded so Unicode glyphs (box-drawing, arrows) render;
672/// undecodable bytes become '?'. 711/// undecodable bytes become '?'.
673pub fn write_bytes(b: &[u8]) { 712pub fn write_bytes(b: &[u8]) {
713 let on = irq_off();
674 let mut i = 0; 714 let mut i = 0;
675 while i < b.len() { 715 while i < b.len() {
676 let byte = b[i]; 716 let byte = b[i];
@@ -701,20 +741,27 @@ pub fn write_bytes(b: &[u8]) {
701 } 741 }
702 } 742 }
703 flush(); 743 flush();
744 irq_restore(on);
704} 745}
705 746
706/// Write a UTF-8 string (plain text; Unicode glyphs render, others '?'). 747/// Write a UTF-8 string (plain text; Unicode glyphs render, others '?').
748/// Atomic with respect to interrupts: a klog screen-echo in ISR context
749/// cannot interleave with a shell write, and a tick cannot split a write.
707pub fn write_str(s: &str) { 750pub fn write_str(s: &str) {
751 let on = irq_off();
708 for c in s.chars() { 752 for c in s.chars() {
709 write_char(c); 753 write_char(c);
710 } 754 }
711 flush(); 755 flush();
756 irq_restore(on);
712} 757}
713 758
714/// Write a single ASCII byte / char. 759/// Write a single ASCII byte / char.
715pub fn put_char(b: u8) { 760pub fn put_char(b: u8) {
761 let on = irq_off();
716 put_byte(b); 762 put_byte(b);
717 flush(); 763 flush();
764 irq_restore(on);
718} 765}
719 766
720/// Clear the whole screen, reset colours, home the cursor. 767/// Clear the whole screen, reset colours, home the cursor.