// --------------------------------------------------------------------------- // 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) { 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: 0, 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 buffer for keyboard scancodes // --------------------------------------------------------------------------- /// Single-producer (ISR) / single-consumer (main) lock-free ring buffer. struct KeyBuf { 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 KeyBuf {} impl KeyBuf { 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: KeyBuf = KeyBuf::new(); // --------------------------------------------------------------------------- // Interrupt stubs (assembly) and dispatch // --------------------------------------------------------------------------- core::arch::global_asm!( // ISR stubs — push vector number + error code, jump to 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", "", // 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, 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", "", " 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)] struct IntFrame { r15: u64, r14: u64, r13: u64, r12: u64, r11: u64, r10: u64, r9: u64, r8: u64, rdi: u64, rsi: u64, rbp: u64, rbx: u64, rdx: u64, rcx: u64, rax: u64, vector: u64, error: u64, rip: u64, cs: u64, rflags: u64, } /// Called from the asm common handler — dispatches to the right Rust handler. #[unsafe(no_mangle)] unsafe extern "C" fn isr_dispatch(frame: &IntFrame) { unsafe { match frame.vector { 32 => { TICKS.fetch_add(1, Ordering::Relaxed); eoi(0); } 33 => { let byte: u8; core::arch::asm!("in al, dx", out("al") byte, in("dx") 0x60u16, options(nostack, preserves_flags)); // Push scancode to ring buffer (called with interrupts disabled). KEYBUF.push(byte); eoi(1); } _ => {} } } } // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- /// Read one scancode from the interrupt-driven ring buffer (non-blocking). pub fn read_scancode() -> Option { KEYBUF.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); } // Override vectors for interrupt we actually handle. idt_set_entry(idt_ptr, 32, isr_stub_32 as *const () as u64, cs, 0x0Eu8); idt_set_entry(idt_ptr, 33, isr_stub_33 as *const () as u64, cs, 0x0Eu8); // 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 → bit 0) and keyboard (IRQ1 → bit 1) on master. let mut mask: u8; asm!("in al, dx", out("al") mask, in("dx") PIC1_DATA, options(nostack, preserves_flags)); mask &= !0x03u8; asm!("out dx, al", in("dx") PIC1_DATA, in("al") mask, options(nostack, preserves_flags)); // Enable interrupts. asm!("sti", options(nostack)); } } // --------------------------------------------------------------------------- // Extern symbols from global_asm! // --------------------------------------------------------------------------- unsafe extern "C" { fn isr_stub_32(); fn isr_stub_33(); fn isr_stub_spurious(); }