From be868a43185826b02bf6d091019bb779678525bb Mon Sep 17 00:00:00 2001 From: salmonik Date: Mon, 17 Aug 2026 14:53:52 +0200 Subject: USB2/1.1 (EHCI/UHCI) drivers + real-hardware bring-up hardening - Add polled EHCI (USB2) and UHCI (USB1.1) host-controller drivers with HID boot mouse support, wired into cursor polling and the 'usb' shell cmd - Add USB Legacy ownership handoff (claim_usb_legacy) plus config_write_byte - Harden boot: verify the PIT/timer ticks before shell entry, EOI stray interrupts in the LAPIC to stop interrupt storms, bound PS/2 controller waits, and INTx-disable AHCI (polled driver) - Add boot stage markers, klog screen echo, and QR lock; scheduler mark_shell_running; NO_USB stopgap to keep BIOS USB-Legacy input alive - alloc_pages_above for DMA buffers that must avoid low conventional memory - Expand DEBUG_NOTES with EHCI/QEMU bring-up findings --- rsnix/src/sched.rs | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) (limited to 'rsnix/src/sched.rs') diff --git a/rsnix/src/sched.rs b/rsnix/src/sched.rs index dd1409e..259ddf6 100644 --- a/rsnix/src/sched.rs +++ b/rsnix/src/sched.rs @@ -157,6 +157,47 @@ static SCHED_READY: AtomicBool = AtomicBool::new(false); static IN_ISR: AtomicBool = AtomicBool::new(false); static PREEMPT_DEPTH: AtomicU64 = AtomicU64::new(0); +// --------------------------------------------------------------------------- +// Watchdog — turn wedged states into visible panics instead of silent +// freezes. Real hardware has no serial console, so the panic handler +// renders to the framebuffer and writes the crash log; a freeze that would +// otherwise look like a dead machine becomes a reported panic. +// +// The watchdog only runs when the PIT tick is firing (interrupts on). A +// hang with interrupts disabled is invisible to it, so driver waits must +// stay bounded (timeout loops) — enforced at the driver level. +// --------------------------------------------------------------------------- + +/// Consecutive ticks where the preemption lock was held (depth > 0). The +/// tick cannot switch tasks while the lock is held, so a hold this long is +/// a deadlock (a driver wedged inside the serial/klog lock, ...). +static STALL_TICKS: AtomicU64 = AtomicU64::new(0); +const WATCHDOG_LOCK_TICKS: u64 = 500; // 5 s at 100 Hz + +/// Per-task "same interrupted frame" counter for spin detection. +struct SpinWatch { + last_rsp: usize, + same: u32, +} +const SPIN_WATCH_INIT: SpinWatch = SpinWatch { last_rsp: 0, same: 0 }; +static mut SPIN_WATCH: [SpinWatch; MAX_TASKS] = [SPIN_WATCH_INIT; MAX_TASKS]; +/// A spinning background task is interrupted at roughly half the tick rate +/// (it alternates with idle), so 1500 hits is ~30 s of wall time. +const WATCHDOG_SPIN_TICKS: u32 = 1500; +/// Boot-phase threshold for task 0 (the shell): before the REPL is live, +/// task 0 runs init code where a fixed frame means a driver/controller +/// hang — panic after ~15 s wall. The boot must finish well inside this. +const WATCHDOG_BOOT_SPIN_TICKS: u32 = 750; + +/// True once the shell REPL is live (set by main.rs before shell::run()). +/// The shell legitimately parks in poll_key with a fixed frame when it is; +/// before that, task 0 running boot code must not sit in a fixed frame. +static SHELL_RUNNING: AtomicBool = AtomicBool::new(false); + +pub fn mark_shell_running() { + SHELL_RUNNING.store(true, Ordering::Relaxed); +} + // --------------------------------------------------------------------------- // Preemption lock // --------------------------------------------------------------------------- @@ -193,12 +234,49 @@ pub fn current() -> usize { /// Receives the interrupted task's frame pointer; returns the frame /// pointer the asm should switch to (the same one when no switch). pub fn on_tick(cur: usize) -> usize { - if !is_ready() || PREEMPT_DEPTH.load(Ordering::Relaxed) > 0 { + if !is_ready() { return cur; } unsafe { + // Watchdog: the tick fired, so the CPU is alive. A state we cannot + // leave is a hang, not a sleep — surface it as a visible panic. + if PREEMPT_DEPTH.load(Ordering::Relaxed) > 0 { + let n = STALL_TICKS.fetch_add(1, Ordering::Relaxed) + 1; + if n >= WATCHDOG_LOCK_TICKS { + watchdog_panic(cur, "preemption lock held"); + } + return cur; + } + STALL_TICKS.store(0, Ordering::Relaxed); + TASKS[CURRENT].saved_rsp = cur; TASKS[CURRENT].ticks += 1; + // Spin detection: a task whose interrupted frame never moves + // (identical RSP on every tick) is stuck in a hot loop — no yield, + // no sleep, no block. The shell (id 0) legitimately parks in + // poll_key with a fixed frame, and idle (id 1) hlt's; both exempt. + let id = CURRENT; + let w = &mut SPIN_WATCH[id]; + if w.last_rsp == cur { + w.same += 1; + // Background tasks (id >= 2): spinning is never legitimate. + // Task 0 (shell): exempt once the REPL is live (poll_key parks + // with a fixed frame); during the boot phase a fixed frame + // means usb/ehci init is wedged on real hardware. + let threshold = if id == SHELL_TID && !SHELL_RUNNING.load(Ordering::Relaxed) { + WATCHDOG_BOOT_SPIN_TICKS + } else if id >= 2 { + WATCHDOG_SPIN_TICKS + } else { + u32::MAX // idle hlt's with a fixed frame forever — fine + }; + if w.same >= threshold { + watchdog_panic(cur, "task spinning without progress"); + } + } else { + w.last_rsp = cur; + w.same = 1; + } let now = crate::interrupts::uptime_ms(); for t in 0..MAX_TASKS { match TASKS[t].state { @@ -219,6 +297,29 @@ pub fn on_tick(cur: usize) -> usize { } } +/// Render a watchdog verdict through the normal panic path (framebuffer + +/// serial + crash log) and halt. Runs in tick/ISR context: `cur` is the +/// interrupted task's IntFrame, so the reported RIP pinpoints the hang. +fn watchdog_panic(cur: usize, why: &str) -> ! { + let id = unsafe { CURRENT }; + let name = name_str(id); + let rip = unsafe { (cur as *const IntFrame).read().rip }; + let up = crate::interrupts::uptime_ms(); + if id == SHELL_TID { + // Boot-phase hang: report the last completed boot stage so the + // wedged init step is identifiable without serial. + let stage = crate::gop::last_boot_stage(); + panic!( + "watchdog: {} — task {} \"{}\" rip={:#x} (uptime {} ms) boot-stage: {}", + why, id, name, rip, up, stage + ); + } + panic!( + "watchdog: {} — task {} \"{}\" rip={:#x} (uptime {} ms)", + why, id, name, rip, up + ); +} + /// Software yield (vector 0x60). pub fn on_yield(cur: usize) -> usize { if !is_ready() || PREEMPT_DEPTH.load(Ordering::Relaxed) > 0 { -- cgit v1.2.3