From af8d5aa4538ac71f11a5418ef94ae478868a0484 Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 31 Jul 2026 02:28:10 +0200 Subject: interrupts: IDT, PIC remap, PIT timer, PS/2 keyboard IRQ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IDT with 256 entries, loaded via lidt (CS read from current context) - PIC 8259A remapped to vectors 32-47, timer+keyboard IRQs unmasked - PIT channel 0 at ~100 Hz (mode 2, rate generator) - PS/2 keyboard IRQ (IRQ1 → vector 33): reads port 0x60, pushes byte to lock-free SPSC ring buffer - Timer IRQ (IRQ0 → vector 32): sends EOI, increments tick counter - ps2::poll_key() now reads from ring buffer with cli/sti+hlt for race-free sleep (instead of busy-polling the data port) - build.rs: use idiomatic Result<_, Box> return type - tools/qemu.sh: fix SC2207 shellcheck warning --- build.rs | 4 +- src/interrupts.rs | 385 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 5 + src/ps2.rs | 55 +++----- tools/qemu.sh | 2 +- 5 files changed, 414 insertions(+), 37 deletions(-) create mode 100644 src/interrupts.rs diff --git a/build.rs b/build.rs index 44e6344..35447e9 100644 --- a/build.rs +++ b/build.rs @@ -26,8 +26,8 @@ fn main() -> Result<(), Box> { return Err("not a PSF2 font".into()); } - let hdr_size = u32::from_le_bytes(raw[8..12].try_into()?) as usize; - let num_glyphs = u32::from_le_bytes(raw[16..20].try_into()?) as usize; + let hdr_size = u32::from_le_bytes(raw[8..12].try_into()?) as usize; + let num_glyphs = u32::from_le_bytes(raw[16..20].try_into()?) as usize; let bytes_per_glyph = u32::from_le_bytes(raw[20..24].try_into()?) as usize; if num_glyphs != 256 { diff --git a/src/interrupts.rs b/src/interrupts.rs new file mode 100644 index 0000000..204d3d7 --- /dev/null +++ b/src/interrupts.rs @@ -0,0 +1,385 @@ +// --------------------------------------------------------------------------- +// Interrupt handling: IDT, PIC remap, ISR dispatch. +// --------------------------------------------------------------------------- +use core::arch::asm; +use core::sync::atomic::{AtomicU16, 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; + +/// Set up PIT channel 0 in mode 2 (rate generator) at `hz` (1–1000ish). +fn pit_set_rate(hz: u64) { + let divisor = (PIT_FREQ / hz) as u16; + unsafe { + // Mode 2, channel 0, lobyte/hibyte, binary + asm!("out dx, al", in("dx") PIT_CMD, in("al") 0b00_11_010_0u8, + 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)); + } +} + +// --------------------------------------------------------------------------- +// 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 { + // UnsafeCell requires const initialisation in a const ctx. + // Build an array the long way — one cell at a time. + const NEW_CELL: core::cell::UnsafeCell = core::cell::UnsafeCell::new(0); + // `[NEW_CELL; 256]` needs Clone, which UnsafeCell doesn't have. + // Use a union trick or just mark the whole buffer init with zeroes. + // SAFETY: we initialise via the const-fn path in init(). + Self { + buf: [ + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, + ], + 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) as u16 > 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 rdi, rsp", // first arg: pointer to saved registers + " 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. +#[no_mangle] +extern "C" fn isr_dispatch(frame: &IntFrame) { + match frame.vector { + 32 => timer_irq(), + 33 => keyboard_irq(), + 0xFF => { /* spurious — ignore */ } + other => { + // Log unexpected interrupt on serial (but don't halt — might be benign). + crate::serial::write_str("intr: unhandled vector "); + crate::serial::put_byte(other as u8); + crate::serial::put_byte(b'\n'); + } + } +} + +/// Timer IRQ (PIT channel 0 ~ 100 Hz). +fn timer_irq() { + eoi(0); +} + +/// Keyboard IRQ — read scancode from port 0x60 and push to ring buffer. +fn keyboard_irq() { + let byte: u8; + unsafe { + asm!("in al, dx", out("al") byte, in("dx") 0x60, + options(nostack, preserves_flags)); + } + 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). +pub static TICKS: AtomicU16 = AtomicU16::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 = IDT.as_mut_ptr(); + 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) as u16, // 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) &IDTR, options(nostack, preserves_flags)); + + // Remap PIC and mask everything. + remap_pic(); + + // Set up PIT timer at ~100 Hz. + pit_set_rate(100); + + // 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! +// --------------------------------------------------------------------------- +extern "C" { + fn isr_stub_32(); + fn isr_stub_33(); + fn isr_stub_spurious(); +} diff --git a/src/main.rs b/src/main.rs index 2b1df77..9a901f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ extern crate alloc; mod font; mod gop; +mod interrupts; mod memory; mod ps2; mod serial; @@ -127,6 +128,10 @@ pub extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut SystemT ps2::init(); serial_println!("ps2: keyboard initialized"); + // Interrupts — IDT, PIC, PIT timer, keyboard IRQ handler + interrupts::init(); + serial_println!("intr: IDT loaded, PIC remapped, timer ~100 Hz"); + // Memory: the bump allocator was set up during capture_memory_map. // `alloc::Vec`, `alloc::String`, `format!` etc. are now usable. diff --git a/src/ps2.rs b/src/ps2.rs index e972658..3a42c1a 100644 --- a/src/ps2.rs +++ b/src/ps2.rs @@ -179,47 +179,34 @@ fn drain_input() { } } -/// Poll for a key event. Blocks until a key is pressed. +/// Poll for a key event. Blocks via `hlt` until a key is pressed. +/// The keyboard interrupt handler pushes raw scancodes to a ring buffer; +/// this function pops and decodes them. pub fn poll_key() -> KeyEvent { loop { - let byte = read_data(); - if let Some(ev) = process_byte(byte) { - return ev; - } - } -} + // Disable interrupts so the buffer-empty → hlt window is atomic. + unsafe { core::arch::asm!("cli", options(nostack, preserves_flags)); } -/// Non-blocking poll. -pub fn try_poll_key() -> Option { - unsafe { - let status: u8; - core::arch::asm!("in al, dx", out("al") status, in("dx") STATUS_PORT, - options(nostack, preserves_flags)); - if status & STAT_OUTPUT_FULL == 0 { - return None; + if let Some(byte) = crate::interrupts::read_scancode() { + unsafe { core::arch::asm!("sti", options(nostack, preserves_flags)); } + if let Some(ev) = process_byte(byte) { + return ev; + } + } else { + // Re-enable and halt. If a keyboard IRQ arrived between the + // buffer check and `sti`, the CPU sees a pending interrupt + // and `hlt` returns immediately. + unsafe { + core::arch::asm!("sti; hlt", options(nostack, preserves_flags)); + } } - let data: u8; - core::arch::asm!("in al, dx", out("al") data, in("dx") DATA_PORT, - options(nostack, preserves_flags)); - process_byte(data) } } -/// Read a byte from the keyboard data port (blocking). -fn read_data() -> u8 { - unsafe { - loop { - let status: u8; - core::arch::asm!("in al, dx", out("al") status, in("dx") STATUS_PORT, - options(nostack, preserves_flags)); - if status & STAT_OUTPUT_FULL != 0 { - let data: u8; - core::arch::asm!("in al, dx", out("al") data, in("dx") DATA_PORT, - options(nostack, preserves_flags)); - return data; - } - } - } +/// Non-blocking poll — read from the interrupt ring buffer if available. +pub fn try_poll_key() -> Option { + let byte = crate::interrupts::read_scancode()?; + process_byte(byte) } /// Process a single scancode byte. Returns Some(KeyEvent) when a visible diff --git a/tools/qemu.sh b/tools/qemu.sh index 428954f..b75bed4 100755 --- a/tools/qemu.sh +++ b/tools/qemu.sh @@ -81,7 +81,7 @@ QEMU_CMD=( [ -n "$GUI_FLAG" ] && QEMU_CMD+=("$GUI_FLAG") if [ -n "$DEBUG_FLAG" ]; then # Split the flag string safely into words - read -ra dbg <<< "$DEBUG_FLAG" + read -ra dbg <<<"$DEBUG_FLAG" QEMU_CMD+=("${dbg[@]}") fi -- cgit v1.2.3