// --------------------------------------------------------------------------- // Interrupt handling: IDT, PIC remap, ISR dispatch. // --------------------------------------------------------------------------- use core::arch::asm; use core::sync::atomic::{AtomicBool, AtomicU16, AtomicU64, Ordering}; // --------------------------------------------------------------------------- // IDT entry and table // --------------------------------------------------------------------------- /// A single 64-bit IDT entry (16 bytes). #[derive(Copy, Clone)] #[repr(C, packed)] struct IdtEntry { offset_lo: u16, selector: u16, ist: u8, flags: u8, offset_mid: u16, offset_hi: u32, _reserved: u32, } /// Fill one IDT entry at the given offset (in bytes). unsafe fn idt_set_entry(idt: *mut u8, vec: u8, handler: u64, selector: u16, flags: u8, ist: u8) { unsafe { let off = (vec as usize) * 16; let p = idt.add(off) as *mut IdtEntry; p.write(IdtEntry { offset_lo: handler as u16, selector, ist, flags: flags | 0x80, // bit 7 = present offset_mid: (handler >> 16) as u16, offset_hi: (handler >> 32) as u32, _reserved: 0, }); } } /// The one true IDT (live in .bss, zero-initialised). static mut IDT: [u8; 256 * 16] = [0u8; 256 * 16]; static mut IDTR: [u16; 5] = [0; 5]; // 2-byte limit + 4-byte base (48-bit pseudo-desc) // --------------------------------------------------------------------------- // PIC (8259A) // --------------------------------------------------------------------------- const PIC1_CMD: u16 = 0x20; const PIC1_DATA: u16 = 0x21; const PIC2_CMD: u16 = 0xA0; const PIC2_DATA: u16 = 0xA1; const PIC_EOI: u8 = 0x20; const ICW1: u8 = 0x11; // edge, cascade, icw4 const ICW4: u8 = 0x01; // x86 mode /// Remap PIC IRQs 0-15 to IDT vectors 32-47, then mask all. unsafe fn remap_pic() { unsafe { asm!("out dx, al", in("dx") PIC1_CMD, in("al") ICW1, options(nostack, preserves_flags)); asm!("out dx, al", in("dx") PIC2_CMD, in("al") ICW1, options(nostack, preserves_flags)); asm!("out dx, al", in("dx") PIC1_DATA, in("al") 0x20u8, options(nostack, preserves_flags)); asm!("out dx, al", in("dx") PIC2_DATA, in("al") 0x28u8, options(nostack, preserves_flags)); asm!("out dx, al", in("dx") PIC1_DATA, in("al") 0x04u8, options(nostack, preserves_flags)); asm!("out dx, al", in("dx") PIC2_DATA, in("al") 0x02u8, options(nostack, preserves_flags)); asm!("out dx, al", in("dx") PIC1_DATA, in("al") ICW4, options(nostack, preserves_flags)); asm!("out dx, al", in("dx") PIC2_DATA, in("al") ICW4, options(nostack, preserves_flags)); asm!("out dx, al", in("dx") PIC1_DATA, in("al") 0xFFu8, options(nostack, preserves_flags)); asm!("out dx, al", in("dx") PIC2_DATA, in("al") 0xFFu8, options(nostack, preserves_flags)); } } /// Send End-Of-Interrupt to the master PIC (and slave if needed). fn eoi(irq: u8) { unsafe { // Slave IRQs are 8-15, mapped to vectors 40-47. if irq >= 8 { asm!("out dx, al", in("dx") PIC2_CMD, in("al") PIC_EOI, options(nostack, preserves_flags)); } asm!("out dx, al", in("dx") PIC1_CMD, in("al") PIC_EOI, options(nostack, preserves_flags)); } } // --------------------------------------------------------------------------- // PIT timer (channel 0, ~100 Hz) // --------------------------------------------------------------------------- const PIT_CMD: u16 = 0x43; const PIT_CH0: u16 = 0x40; const PIT_FREQ: u64 = 1_193_182; // PIT clock: 1.193182 MHz const PIT_HZ: u64 = 100; /// Counter divisor loaded for the ~100 Hz tick (matches `pit_set_rate`). pub const PIT_DIVISOR: u16 = (PIT_FREQ / PIT_HZ) as u16; /// Set up PIT channel 0 in mode 2 (rate generator) at the fixed 100 Hz /// tick rate — the divisor must stay `PIT_DIVISOR`, which `uptime_ms` /// relies on for interpolation. fn pit_set_rate() { let divisor = PIT_DIVISOR; unsafe { // Mode 2, channel 0, lobyte/hibyte, binary asm!("out dx, al", in("dx") PIT_CMD, in("al") 0b0011_0100u8, options(nostack, preserves_flags)); asm!("out dx, al", in("dx") PIT_CH0, in("al") (divisor & 0xFF) as u8, options(nostack, preserves_flags)); asm!("out dx, al", in("dx") PIT_CH0, in("al") (divisor >> 8) as u8, options(nostack, preserves_flags)); } } /// Latch and read the PIT channel 0 down-counter (0..=PIT_DIVISOR). /// In mode 2 the counter counts down and reloads on reaching 0, so it /// only ever jumps *up* when a tick fires. fn pit_count() -> u16 { unsafe { asm!("out dx, al", in("dx") PIT_CMD, in("al") 0x00u8, options(nomem, nostack, preserves_flags)); let lo: u8; let hi: u8; asm!("in al, dx", out("al") lo, in("dx") PIT_CH0, options(nomem, nostack, preserves_flags)); asm!("in al, dx", out("al") hi, in("dx") PIT_CH0, options(nomem, nostack, preserves_flags)); u16::from_le_bytes([lo, hi]) } } /// True once the PIT tick is configured (`interrupts::init`). Before /// that the PIT counter is uninitialised, so `uptime_ms` reports 0 /// (early-boot messages are all at [0000.000], which is truthful — the /// clock hasn't started). static PIT_READY: AtomicBool = AtomicBool::new(false); /// Monotonic milliseconds since boot: the 100 Hz tick (10 ms each) plus /// interpolation from the PIT's live 1.19 MHz down-counter for the /// sub-10 ms fraction. The countdown is read twice around the tick /// load: if it jumped up, a tick fired between the reads. pub fn uptime_ms() -> u64 { if !PIT_READY.load(Ordering::Relaxed) { return 0; } let c1 = pit_count(); let ticks = TICKS.load(Ordering::Relaxed); let c2 = pit_count(); let ticks = if c2 > c1 { ticks + 1 } else { ticks }; let c = u64::from(c2.min(PIT_DIVISOR)); ticks * (1000 / PIT_HZ) + (u64::from(PIT_DIVISOR) - c) * 1000 / PIT_FREQ } // --------------------------------------------------------------------------- // Lock-free SPSC ring buffers for PS/2 input (keyboard scancodes + mouse // bytes). One per device: the ISR routes each data-port byte to the right // ring via the controller status register's aux bit (bit 5), so the two // devices never steal each other's bytes even when both are pending at once. // --------------------------------------------------------------------------- /// Single-producer (ISR) / single-consumer (main) lock-free ring buffer. struct ByteRing { buf: [core::cell::UnsafeCell; 256], head: AtomicU16, // next slot to read (written by consumer / main) tail: AtomicU16, // next slot to write (written by producer / ISR) } unsafe impl Sync for ByteRing {} impl ByteRing { const fn new() -> Self { // Inline-const array repeat (stable since 1.79): each element is a // fresh `UnsafeCell::new(0)` value, so no interior-mutable const. Self { buf: [const { core::cell::UnsafeCell::new(0) }; 256], head: AtomicU16::new(0), tail: AtomicU16::new(0), } } /// Push one byte — called from ISR (interrupts disabled, single-producer). fn push(&self, byte: u8) { let t = self.tail.load(Ordering::Relaxed); let h = self.head.load(Ordering::Acquire); let next = t.wrapping_add(1); // Full if the distance between head and tail == capacity (256). if next.wrapping_sub(h) > 255 { return; // drop — ring is full } unsafe { *self.buf[t as usize & 255].get() = byte; } // Ensure byte is globally visible before incrementing tail. self.tail.store(next, Ordering::Release); } /// Pop one byte — called from main context. Returns None when empty. fn pop(&self) -> Option { let h = self.head.load(Ordering::Relaxed); let t = self.tail.load(Ordering::Acquire); if h == t { return None; } let byte = unsafe { *self.buf[h as usize & 255].get() }; self.head.store(h.wrapping_add(1), Ordering::Release); Some(byte) } } static KEYBUF: ByteRing = ByteRing::new(); static MOUSEBUF: ByteRing = ByteRing::new(); // --------------------------------------------------------------------------- // Interrupt stubs (assembly) and dispatch // --------------------------------------------------------------------------- core::arch::global_asm!( // ISR stubs — push vector number + error code, jump to common. // Exceptions that push their own error code (8, 14) skip the push-0. ".globl isr_stub_08", ".p2align 4", "isr_stub_08:", " push 8", // vector (CPU already pushed the error code) " jmp isr_common", "", ".globl isr_stub_14", ".p2align 4", "isr_stub_14:", " push 14", " jmp isr_common", "", ".globl isr_stub_32", ".p2align 4", "isr_stub_32:", " push 0", // error code (non-exception) " push 32", // vector " jmp isr_common", "", ".globl isr_stub_33", ".p2align 4", "isr_stub_33:", " push 0", " push 33", " jmp isr_common", "", // PS/2 mouse IRQ (IRQ12 → vector 44). ".globl isr_stub_44", ".p2align 4", "isr_stub_44:", " push 0", " push 44", " jmp isr_common", "", // Software yield (vector 0x60 — outside the PIC's 32-47 range, which // is why 0x40 was rejected: 0x40 = 64 decimal = slave IRQ 8 slot). ".globl isr_stub_60", ".p2align 4", "isr_stub_60:", " push 0", " push 0x60", " jmp isr_common", "", // Catch-all for unexpected interrupts. ".globl isr_stub_spurious", ".p2align 4", "isr_stub_spurious:", " push 0", " push 0xFF", " jmp isr_common", "", // Common handler — save all registers, call into Rust (which returns // the next task's frame pointer in rax), switch rsp, restore, iretq. "isr_common:", " push rax", " push rcx", " push rdx", " push rbx", " push rbp", " push rsi", " push rdi", " push r8", " push r9", " push r10", " push r11", " push r12", " push r13", " push r14", " push r15", "", " mov rcx, rsp", // first arg (rcx = MS x64 ABI) " call isr_dispatch", " mov rsp, rax", // context switch: rax = next task's frame ptr "", " pop r15", " pop r14", " pop r13", " pop r12", " pop r11", " pop r10", " pop r9", " pop r8", " pop rdi", " pop rsi", " pop rbp", " pop rbx", " pop rdx", " pop rcx", " pop rax", " add rsp, 16", // pop vector + error code " iretq", ); /// Saved register frame (as pushed by `isr_common`). #[repr(C)] pub(crate) struct IntFrame { pub(crate) r15: u64, pub(crate) r14: u64, pub(crate) r13: u64, pub(crate) r12: u64, pub(crate) r11: u64, pub(crate) r10: u64, pub(crate) r9: u64, pub(crate) r8: u64, pub(crate) rdi: u64, pub(crate) rsi: u64, pub(crate) rbp: u64, pub(crate) rbx: u64, pub(crate) rdx: u64, pub(crate) rcx: u64, pub(crate) rax: u64, pub(crate) vector: u64, pub(crate) error: u64, pub(crate) rip: u64, pub(crate) cs: u64, pub(crate) rflags: u64, // QEMU's 64-bit interrupt entry ALWAYS pushes SS/RSP/RFLAGS/CS/RIP // (40 bytes) and iretq always pops all five, so the tail must be // 40 bytes — these two fields are the CPU-pushed RSP/SS. Synthetic // task frames set them to the task's initial stack top and the // kernel SS so the first iretq lands with a valid stack. pub(crate) rsp: u64, pub(crate) ss: u64, } /// Called from the asm common handler — dispatches to the right Rust /// handler and returns the frame pointer the asm should switch to (the /// next task's, or the same one when no switch happens). #[unsafe(no_mangle)] unsafe extern "C" fn isr_dispatch(frame: &IntFrame) -> usize { let fp = frame as *const IntFrame as usize; crate::sched::set_in_isr(true); let next = unsafe { match frame.vector { 32 => { TICKS.fetch_add(1, Ordering::Relaxed); eoi(0); crate::sched::on_tick(fp) } 33 | 44 => { // PS/2 data byte. The controller's status register tells us // which device it came from (bit 5 = auxiliary/mouse), so the // byte is routed to the matching ring — both handlers must // check, because when both devices have data pending the // first IRQ serviced can find the other device's byte in the // output buffer. let status: u8; core::arch::asm!("in al, dx", out("al") status, in("dx") 0x64u16, options(nostack, preserves_flags)); if status & 0x01 != 0 { let byte: u8; core::arch::asm!("in al, dx", out("al") byte, in("dx") 0x60u16, options(nostack, preserves_flags)); if status & 0x20 != 0 { MOUSEBUF.push(byte); } else { KEYBUF.push(byte); } } eoi((frame.vector - 32) as u8); fp } 8 => { crate::serial_println!("double fault — halting"); halt() } 14 => { let cr2: u64; core::arch::asm!("mov {}, cr2", out(reg) cr2, options(nostack, preserves_flags)); crate::sched::page_fault(cr2, fp) } 0x60 => crate::sched::on_yield(fp), _ => fp, } }; crate::sched::set_in_isr(false); next } // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- /// Read one scancode from the interrupt-driven ring buffer (non-blocking). pub fn read_scancode() -> Option { KEYBUF.pop() } /// Read one mouse data byte from the interrupt-driven ring buffer /// (non-blocking). Packets are assembled in `ps2::try_poll_mouse`. pub fn read_mouse_byte() -> Option { MOUSEBUF.pop() } /// Global tick counter (incremented by timer IRQ, ~100 Hz). u64 so /// uptime never wraps; `uptime_ms` interpolates the PIT counter for the /// sub-tick fraction. pub static TICKS: AtomicU64 = AtomicU64::new(0); /// Initialise the IDT, remap the PIC, set up the PIT, and enable interrupts. pub fn init() { unsafe { // Read the current code segment selector from CS. let cs: u64; asm!("mov {0}, cs", out(reg) cs, options(nostack, preserves_flags)); let cs = cs as u16; // Build the IDT. let idt_ptr = core::ptr::addr_of_mut!(IDT[0]); let spurious = isr_stub_spurious as *const () as u64; // Fill all 256 entries with the spurious handler. for vec in 0..=255u8 { idt_set_entry(idt_ptr, vec, spurious, cs, 0x0Eu8, 0); } // Override vectors for interrupt we actually handle. idt_set_entry(idt_ptr, 32, isr_stub_32 as *const () as u64, cs, 0x0Eu8, 0); idt_set_entry(idt_ptr, 33, isr_stub_33 as *const () as u64, cs, 0x0Eu8, 0); // PS/2 mouse (IRQ12). idt_set_entry(idt_ptr, 44, isr_stub_44 as *const () as u64, cs, 0x0Eu8, 0); // Yield (software interrupt). idt_set_entry( idt_ptr, 0x60, isr_stub_60 as *const () as u64, cs, 0x0Eu8, 0, ); // Double fault and page fault run on dedicated IST stacks (see // gdt.rs) so a task fault can't corrupt the faulting task's stack. idt_set_entry(idt_ptr, 8, isr_stub_08 as *const () as u64, cs, 0x0Eu8, 2); idt_set_entry(idt_ptr, 14, isr_stub_14 as *const () as u64, cs, 0x0Eu8, 1); // Build the IDTR pseudo-descriptor. IDTR = [ (256u16 * 16 - 1), // limit = 256*16 - 1 idt_ptr as u16, // base lo (idt_ptr as u64 >> 16) as u16, // base mid (idt_ptr as u64 >> 32) as u16, // base hi (idt_ptr as u64 >> 48) as u16, // base top ]; // Load IDT. asm!("lidt [{}]", in(reg) core::ptr::addr_of!(IDTR), options(nostack, preserves_flags)); // Remap PIC and mask everything. remap_pic(); // Set up PIT timer at ~100 Hz. pit_set_rate(); PIT_READY.store(true, Ordering::Relaxed); // Unmask timer (IRQ0), keyboard (IRQ1) and the cascade line (IRQ2) // on the master; unmask IRQ12 (PS/2 mouse) on the slave. let mut mask: u8; asm!("in al, dx", out("al") mask, in("dx") PIC1_DATA, options(nostack, preserves_flags)); mask &= !0x07u8; asm!("out dx, al", in("dx") PIC1_DATA, in("al") mask, options(nostack, preserves_flags)); let mut mask2: u8; asm!("in al, dx", out("al") mask2, in("dx") PIC2_DATA, options(nostack, preserves_flags)); mask2 &= !0x10u8; // IRQ12 → bit 4 of the slave PIC asm!("out dx, al", in("dx") PIC2_DATA, in("al") mask2, options(nostack, preserves_flags)); // Enable interrupts. asm!("sti", options(nostack)); } } /// Halt the CPU forever with interrupts disabled. pub fn halt() -> ! { loop { unsafe { core::arch::asm!("cli; hlt", options(nostack, preserves_flags)); } } } // --------------------------------------------------------------------------- // Extern symbols from global_asm! // --------------------------------------------------------------------------- unsafe extern "C" { fn isr_stub_08(); fn isr_stub_14(); fn isr_stub_32(); fn isr_stub_33(); fn isr_stub_44(); fn isr_stub_60(); fn isr_stub_spurious(); }