diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-17 14:53:52 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-17 14:53:52 +0200 |
| commit | be868a43185826b02bf6d091019bb779678525bb (patch) | |
| tree | 586988c15f1d700b1621c7f0208584b30878a56c /rsnix/src/sched.rs | |
| parent | v0.5.10: change em-dashes to dashes (diff) | |
| download | rs-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 '')
| -rw-r--r-- | rsnix/src/sched.rs | 103 |
1 files changed, 102 insertions, 1 deletions
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); | |||
| 158 | static PREEMPT_DEPTH: AtomicU64 = AtomicU64::new(0); | 158 | static 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, ...). | ||
| 174 | static STALL_TICKS: AtomicU64 = AtomicU64::new(0); | ||
| 175 | const WATCHDOG_LOCK_TICKS: u64 = 500; // 5 s at 100 Hz | ||
| 176 | |||
| 177 | /// Per-task "same interrupted frame" counter for spin detection. | ||
| 178 | struct SpinWatch { | ||
| 179 | last_rsp: usize, | ||
| 180 | same: u32, | ||
| 181 | } | ||
| 182 | const SPIN_WATCH_INIT: SpinWatch = SpinWatch { last_rsp: 0, same: 0 }; | ||
| 183 | static 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. | ||
| 186 | const 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. | ||
| 190 | const 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. | ||
| 195 | static SHELL_RUNNING: AtomicBool = AtomicBool::new(false); | ||
| 196 | |||
| 197 | pub 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). |
| 195 | pub fn on_tick(cur: usize) -> usize { | 236 | pub 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. | ||
| 303 | fn 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). |
| 223 | pub fn on_yield(cur: usize) -> usize { | 324 | pub 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 { |
