summaryrefslogtreecommitdiffstats
path: root/src/interrupts.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-07-31 02:28:10 +0200
committersalmonik <salmonik@tuta.io>2026-07-31 02:28:10 +0200
commitaf8d5aa4538ac71f11a5418ef94ae478868a0484 (patch)
treec235d89cbdd1486dd2239d08abc750be2e5a88d2 /src/interrupts.rs
parentInitial commit: raisernics toy kernel (diff)
downloadrs-dos-af8d5aa4538ac71f11a5418ef94ae478868a0484.tar.gz
rs-dos-af8d5aa4538ac71f11a5418ef94ae478868a0484.zip
interrupts: IDT, PIC remap, PIT timer, PS/2 keyboard IRQ
- 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<dyn Error>> return type - tools/qemu.sh: fix SC2207 shellcheck warning
Diffstat (limited to '')
-rw-r--r--src/interrupts.rs385
1 files changed, 385 insertions, 0 deletions
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 @@
1// ---------------------------------------------------------------------------
2// Interrupt handling: IDT, PIC remap, ISR dispatch.
3// ---------------------------------------------------------------------------
4use core::arch::asm;
5use core::sync::atomic::{AtomicU16, Ordering};
6
7// ---------------------------------------------------------------------------
8// IDT entry and table
9// ---------------------------------------------------------------------------
10
11/// A single 64-bit IDT entry (16 bytes).
12#[derive(Copy, Clone)]
13#[repr(C, packed)]
14struct IdtEntry {
15 offset_lo: u16,
16 selector: u16,
17 ist: u8,
18 flags: u8,
19 offset_mid: u16,
20 offset_hi: u32,
21 _reserved: u32,
22}
23
24/// Fill one IDT entry at the given offset (in bytes).
25unsafe fn idt_set_entry(idt: *mut u8, vec: u8, handler: u64, selector: u16, flags: u8) {
26 unsafe {
27 let off = (vec as usize) * 16;
28 let p = idt.add(off) as *mut IdtEntry;
29 p.write(IdtEntry {
30 offset_lo: handler as u16,
31 selector,
32 ist: 0,
33 flags: flags | 0x80, // bit 7 = present
34 offset_mid: (handler >> 16) as u16,
35 offset_hi: (handler >> 32) as u32,
36 _reserved: 0,
37 });
38 }
39}
40
41/// The one true IDT (live in .bss, zero-initialised).
42static mut IDT: [u8; 256 * 16] = [0u8; 256 * 16];
43static mut IDTR: [u16; 5] = [0; 5]; // 2-byte limit + 4-byte base (48-bit pseudo-desc)
44
45// ---------------------------------------------------------------------------
46// PIC (8259A)
47// ---------------------------------------------------------------------------
48
49const PIC1_CMD: u16 = 0x20;
50const PIC1_DATA: u16 = 0x21;
51const PIC2_CMD: u16 = 0xA0;
52const PIC2_DATA: u16 = 0xA1;
53
54const PIC_EOI: u8 = 0x20;
55
56const ICW1: u8 = 0x11; // edge, cascade, icw4
57const ICW4: u8 = 0x01; // x86 mode
58
59/// Remap PIC IRQs 0-15 to IDT vectors 32-47, then mask all.
60unsafe fn remap_pic() {
61 unsafe {
62 asm!("out dx, al", in("dx") PIC1_CMD, in("al") ICW1, options(nostack, preserves_flags));
63 asm!("out dx, al", in("dx") PIC2_CMD, in("al") ICW1, options(nostack, preserves_flags));
64 asm!("out dx, al", in("dx") PIC1_DATA, in("al") 0x20u8, options(nostack, preserves_flags));
65 asm!("out dx, al", in("dx") PIC2_DATA, in("al") 0x28u8, options(nostack, preserves_flags));
66 asm!("out dx, al", in("dx") PIC1_DATA, in("al") 0x04u8, options(nostack, preserves_flags));
67 asm!("out dx, al", in("dx") PIC2_DATA, in("al") 0x02u8, options(nostack, preserves_flags));
68 asm!("out dx, al", in("dx") PIC1_DATA, in("al") ICW4, options(nostack, preserves_flags));
69 asm!("out dx, al", in("dx") PIC2_DATA, in("al") ICW4, options(nostack, preserves_flags));
70 asm!("out dx, al", in("dx") PIC1_DATA, in("al") 0xFFu8, options(nostack, preserves_flags));
71 asm!("out dx, al", in("dx") PIC2_DATA, in("al") 0xFFu8, options(nostack, preserves_flags));
72 }
73}
74
75/// Send End-Of-Interrupt to the master PIC (and slave if needed).
76fn eoi(irq: u8) {
77 unsafe {
78 // Slave IRQs are 8-15, mapped to vectors 40-47.
79 if irq >= 8 {
80 asm!("out dx, al", in("dx") PIC2_CMD, in("al") PIC_EOI,
81 options(nostack, preserves_flags));
82 }
83 asm!("out dx, al", in("dx") PIC1_CMD, in("al") PIC_EOI,
84 options(nostack, preserves_flags));
85 }
86}
87
88// ---------------------------------------------------------------------------
89// PIT timer (channel 0, ~100 Hz)
90// ---------------------------------------------------------------------------
91
92const PIT_CMD: u16 = 0x43;
93const PIT_CH0: u16 = 0x40;
94const PIT_FREQ: u64 = 1_193_182;
95
96/// Set up PIT channel 0 in mode 2 (rate generator) at `hz` (1–1000ish).
97fn pit_set_rate(hz: u64) {
98 let divisor = (PIT_FREQ / hz) as u16;
99 unsafe {
100 // Mode 2, channel 0, lobyte/hibyte, binary
101 asm!("out dx, al", in("dx") PIT_CMD, in("al") 0b00_11_010_0u8,
102 options(nostack, preserves_flags));
103 asm!("out dx, al", in("dx") PIT_CH0, in("al") (divisor & 0xFF) as u8,
104 options(nostack, preserves_flags));
105 asm!("out dx, al", in("dx") PIT_CH0, in("al") (divisor >> 8) as u8,
106 options(nostack, preserves_flags));
107 }
108}
109
110// ---------------------------------------------------------------------------
111// Lock-free SPSC ring buffer for keyboard scancodes
112// ---------------------------------------------------------------------------
113
114/// Single-producer (ISR) / single-consumer (main) lock-free ring buffer.
115struct KeyBuf {
116 buf: [core::cell::UnsafeCell<u8>; 256],
117 head: AtomicU16, // next slot to read (written by consumer / main)
118 tail: AtomicU16, // next slot to write (written by producer / ISR)
119}
120
121unsafe impl Sync for KeyBuf {}
122
123impl KeyBuf {
124 const fn new() -> Self {
125 // UnsafeCell requires const initialisation in a const ctx.
126 // Build an array the long way — one cell at a time.
127 const NEW_CELL: core::cell::UnsafeCell<u8> = core::cell::UnsafeCell::new(0);
128 // `[NEW_CELL; 256]` needs Clone, which UnsafeCell doesn't have.
129 // Use a union trick or just mark the whole buffer init with zeroes.
130 // SAFETY: we initialise via the const-fn path in init().
131 Self {
132 buf: [
133 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
134 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
135 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
136 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
137 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
138 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
139 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
140 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
141 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
142 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
143 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
144 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
145 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
146 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
147 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
148 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
149 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
150 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
151 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
152 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
153 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
154 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
155 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
156 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
157 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
158 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
159 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
160 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
161 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
162 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
163 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
164 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
165 ],
166 head: AtomicU16::new(0),
167 tail: AtomicU16::new(0),
168 }
169 }
170
171 /// Push one byte — called from ISR (interrupts disabled, single-producer).
172 fn push(&self, byte: u8) {
173 let t = self.tail.load(Ordering::Relaxed);
174 let h = self.head.load(Ordering::Acquire);
175 let next = t.wrapping_add(1);
176 // Full if the distance between head and tail == capacity (256).
177 if next.wrapping_sub(h) as u16 > 255 {
178 return; // drop — ring is full
179 }
180 unsafe { *self.buf[t as usize & 255].get() = byte; }
181 // Ensure byte is globally visible before incrementing tail.
182 self.tail.store(next, Ordering::Release);
183 }
184
185 /// Pop one byte — called from main context. Returns None when empty.
186 fn pop(&self) -> Option<u8> {
187 let h = self.head.load(Ordering::Relaxed);
188 let t = self.tail.load(Ordering::Acquire);
189 if h == t {
190 return None;
191 }
192 let byte = unsafe { *self.buf[h as usize & 255].get() };
193 self.head.store(h.wrapping_add(1), Ordering::Release);
194 Some(byte)
195 }
196}
197
198static KEYBUF: KeyBuf = KeyBuf::new();
199
200// ---------------------------------------------------------------------------
201// Interrupt stubs (assembly) and dispatch
202// ---------------------------------------------------------------------------
203
204core::arch::global_asm!(
205 // ISR stubs — push vector number + error code, jump to common.
206 ".globl isr_stub_32",
207 ".p2align 4",
208 "isr_stub_32:",
209 " push 0", // error code (non-exception)
210 " push 32", // vector
211 " jmp isr_common",
212 "",
213 ".globl isr_stub_33",
214 ".p2align 4",
215 "isr_stub_33:",
216 " push 0",
217 " push 33",
218 " jmp isr_common",
219 "",
220 // Catch-all for unexpected interrupts.
221 ".globl isr_stub_spurious",
222 ".p2align 4",
223 "isr_stub_spurious:",
224 " push 0",
225 " push 0xFF",
226 " jmp isr_common",
227 "",
228 // Common handler — save all registers, call into Rust, restore, iretq.
229 "isr_common:",
230 " push rax",
231 " push rcx",
232 " push rdx",
233 " push rbx",
234 " push rbp",
235 " push rsi",
236 " push rdi",
237 " push r8",
238 " push r9",
239 " push r10",
240 " push r11",
241 " push r12",
242 " push r13",
243 " push r14",
244 " push r15",
245 "",
246 " mov rdi, rsp", // first arg: pointer to saved registers
247 " call isr_dispatch",
248 "",
249 " pop r15",
250 " pop r14",
251 " pop r13",
252 " pop r12",
253 " pop r11",
254 " pop r10",
255 " pop r9",
256 " pop r8",
257 " pop rdi",
258 " pop rsi",
259 " pop rbp",
260 " pop rbx",
261 " pop rdx",
262 " pop rcx",
263 " pop rax",
264 " add rsp, 16", // pop vector + error code
265 " iretq",
266);
267
268/// Saved register frame (as pushed by `isr_common`).
269#[repr(C)]
270struct IntFrame {
271 r15: u64, r14: u64, r13: u64, r12: u64,
272 r11: u64, r10: u64, r9: u64, r8: u64,
273 rdi: u64, rsi: u64, rbp: u64, rbx: u64,
274 rdx: u64, rcx: u64, rax: u64,
275 vector: u64,
276 error: u64,
277 rip: u64,
278 cs: u64,
279 rflags: u64,
280}
281
282/// Called from the asm common handler — dispatches to the right Rust handler.
283#[no_mangle]
284extern "C" fn isr_dispatch(frame: &IntFrame) {
285 match frame.vector {
286 32 => timer_irq(),
287 33 => keyboard_irq(),
288 0xFF => { /* spurious — ignore */ }
289 other => {
290 // Log unexpected interrupt on serial (but don't halt — might be benign).
291 crate::serial::write_str("intr: unhandled vector ");
292 crate::serial::put_byte(other as u8);
293 crate::serial::put_byte(b'\n');
294 }
295 }
296}
297
298/// Timer IRQ (PIT channel 0 ~ 100 Hz).
299fn timer_irq() {
300 eoi(0);
301}
302
303/// Keyboard IRQ — read scancode from port 0x60 and push to ring buffer.
304fn keyboard_irq() {
305 let byte: u8;
306 unsafe {
307 asm!("in al, dx", out("al") byte, in("dx") 0x60,
308 options(nostack, preserves_flags));
309 }
310 KEYBUF.push(byte);
311 eoi(1);
312}
313
314// ---------------------------------------------------------------------------
315// Public API
316// ---------------------------------------------------------------------------
317
318/// Read one scancode from the interrupt-driven ring buffer (non-blocking).
319pub fn read_scancode() -> Option<u8> {
320 KEYBUF.pop()
321}
322
323/// Global tick counter (incremented by timer IRQ, ~100 Hz).
324pub static TICKS: AtomicU16 = AtomicU16::new(0);
325
326/// Initialise the IDT, remap the PIC, set up the PIT, and enable interrupts.
327pub fn init() {
328 unsafe {
329 // Read the current code segment selector from CS.
330 let cs: u64;
331 asm!("mov {0}, cs", out(reg) cs, options(nostack, preserves_flags));
332 let cs = cs as u16;
333
334 // Build the IDT.
335 let idt_ptr = IDT.as_mut_ptr();
336 let spurious = isr_stub_spurious as *const () as u64;
337
338 // Fill all 256 entries with the spurious handler.
339 for vec in 0..=255u8 {
340 idt_set_entry(idt_ptr, vec, spurious, cs, 0x0Eu8);
341 }
342
343 // Override vectors for interrupt we actually handle.
344 idt_set_entry(idt_ptr, 32, isr_stub_32 as *const () as u64, cs, 0x0Eu8);
345 idt_set_entry(idt_ptr, 33, isr_stub_33 as *const () as u64, cs, 0x0Eu8);
346
347 // Build the IDTR pseudo-descriptor.
348 IDTR = [
349 (256u16 * 16 - 1) as u16, // limit = 256*16 - 1
350 idt_ptr as u16, // base lo
351 (idt_ptr as u64 >> 16) as u16, // base mid
352 (idt_ptr as u64 >> 32) as u16, // base hi
353 (idt_ptr as u64 >> 48) as u16, // base top
354 ];
355
356 // Load IDT.
357 asm!("lidt [{}]", in(reg) &IDTR, options(nostack, preserves_flags));
358
359 // Remap PIC and mask everything.
360 remap_pic();
361
362 // Set up PIT timer at ~100 Hz.
363 pit_set_rate(100);
364
365 // Unmask timer (IRQ0 → bit 0) and keyboard (IRQ1 → bit 1) on master.
366 let mut mask: u8;
367 asm!("in al, dx", out("al") mask, in("dx") PIC1_DATA,
368 options(nostack, preserves_flags));
369 mask &= !0x03u8;
370 asm!("out dx, al", in("dx") PIC1_DATA, in("al") mask,
371 options(nostack, preserves_flags));
372
373 // Enable interrupts.
374 asm!("sti", options(nostack));
375 }
376}
377
378// ---------------------------------------------------------------------------
379// Extern symbols from global_asm!
380// ---------------------------------------------------------------------------
381extern "C" {
382 fn isr_stub_32();
383 fn isr_stub_33();
384 fn isr_stub_spurious();
385}