The APIC module provides a modern interrupt handling infrastructure for x86_64 systems, replacing the legacy 8259 PIC (Programmable Interrupt Controller). This module manages three critical components: the Local APIC (LAPIC) for per-CPU interrupt handling, the I/O APIC for routing external hardware interrupts, and the LAPIC timer for precise timing and scheduling.
- Local APIC (LAPIC): Per-CPU interrupt controller
- I/O APIC: Central hub for routing hardware IRQs to CPUs
- LAPIC Timer: Programmable per-CPU timer
The 8259 PIC has several limitations that make it unsuitable for modern operating systems:
| Feature | Legacy 8259 PIC | Modern APIC |
|---|---|---|
| CPU Support | Single CPU only | Multiple CPUs (SMP ready) |
| Interrupt Lines | 15 IRQs (cascaded) | 24+ IRQs via I/O APIC |
| Priority Levels | Fixed priority | Dynamic priority |
| Interrupt Routing | Fixed routing | Flexible per-IRQ routing |
| Timer Resolution | Limited | High precision |
| Performance | Slow (ISA bus) | Fast (system bus) |
| EOI (End of Interrupt) | Per-PIC required | Single LAPIC EOI |
apic/
├── src/
│ ├── lib.rs # LAPIC management, PIC disable
│ ├── ioapic.rs # I/O APIC IRQ routing
│ └── timer.rs # LAPIC timer configuration
└── Cargo.toml
const APIC_BASE: u64 = 0xFEE00000;The Local APIC is accessed via memory-mapped I/O at a fixed physical address. All LAPIC registers are 32-bit aligned at 16-byte boundaries.
fn lapic_reg(offset: u32) -> *mut u32 {
(APIC_BASE + offset as u64) as *mut u32
}Purpose: Calculates the pointer to a specific LAPIC register.
Safety: All LAPIC register accesses must use read_volatile() and write_volatile() to prevent compiler optimizations that could break MMIO semantics.
| Offset | Register | Purpose |
|---|---|---|
| 0xF0 | SVR (Spurious Interrupt Vector Register) | Enable/disable LAPIC |
| 0xB0 | EOI (End of Interrupt) | Signal interrupt completion |
| 0x320 | LVT Timer Register | Configure timer interrupt |
| 0x380 | Initial Count Register | Set timer period |
| 0x3E0 | Divide Configuration Register | Set timer divider |
pub unsafe fn disable_pic()Purpose: Properly disables the legacy 8259 PIC to prevent conflicts with APIC.
Procedure:
- Initialization Command Word 1 (ICW1): Start initialization sequence (0x11)
- Initialization Command Word 2 (ICW2): Remap IRQs to vectors 32-47
- Master PIC (IRQ 0-7) → Vectors 32-39
- Slave PIC (IRQ 8-15) → Vectors 40-47
- Initialization Command Word 3 (ICW3): Configure cascading
- Master: IRQ2 is slave
- Slave: Cascade identity = 2
- Initialization Command Word 4 (ICW4): Set 8086 mode (0x01)
- Mask All Interrupts: Write 0xFF to both data ports
Why Remap Before Disabling? The PIC defaults to vectors 0-15, which conflict with CPU exceptions. Even when disabling, we remap to safe vectors to prevent spurious interrupts from causing confusion.
Port Addresses:
- Master PIC Command: 0x20
- Master PIC Data: 0x21
- Slave PIC Command: 0xA0
- Slave PIC Data: 0xA1
pub unsafe fn enable()Purpose: Enables the Local APIC through MSR (Model Specific Register) and local enable bit.
Procedure:
// Read current MSR value
let mut apic_base: u64;
core::arch::asm!("rdmsr", in("ecx") 0x1Bu32, lateout("eax") apic_base, ...);
// Set bit 11 (APIC Global Enable)
if (apic_base & (1 << 11)) == 0 {
apic_base | = 1 << 11;
// Write back to MSR
core::arch::asm!("wrmsr", in("ecx") 0x1Bu32, ...);
}MSR Layout (IA32_APIC_BASE):
- Bits 0-7: Reserved
- Bits 8-11: BSP flag (bit 8), Reserved, Reserved, Global Enable (bit 11)
- Bits 12-35: APIC Base address (4KB aligned)
- Bits 36-63: Reserved
let svr = lapic_reg(0xF0);
let val = svr.read_volatile() | 0x100; // Set bit 8
svr.write_volatile(val);SVR Register (0xF0):
- Bits 0-7: Spurious vector number
- Bit 8: APIC Software Enable (must be 1)
- Bit 9: Focus Processor Checking (legacy, should be 0)
- Bits 10-11: Reserved
- Bit 12: EOI Broadcast Suppression
Two-Level Enable: Both MSR bit 11 and SVR bit 8 must be set for LAPIC to function.
pub unsafe fn set_timer(vector: u8, divide: u32, initial_count: u32)Purpose: Configures the LAPIC timer for periodic interrupts.
Parameters:
vector: Interrupt vector number (e.g., 0x31 = 49 decimal)divide: Divider configuration (0x3 = divide by 16)initial_count: Timer period in bus cycles
Register Configuration:
-
Divide Configuration Register (0x3E0):
lapic_reg(0x3E0).write_volatile(divide);
- Determines timer frequency divider
- Value 0x3 = divide bus clock by 16
-
LVT Timer Register (0x320):
lapic_reg(0x320).write_volatile((vector as u32) | 0x20000);
- Bits 0-7: Vector number
- Bit 17 (0x20000): Timer mode (0 = one-shot, 1 = periodic)
- Bit 16: Mask (0 = not masked)
3. **Initial Count Register (0x380)**:
```rust
lapic_reg(0x380).write_volatile(initial_count);
- Timer countdown value
- Counts down to zero, then reloads (in periodic mode)
Timer Frequency Calculation:
Interrupt Frequency = Bus Clock / (Divider × Initial Count)
Example: 1 GHz bus / (16 × 100,000) ≈ 625 Hz (1.6 ms period)
pub unsafe fn send_eoi()Purpose: Signals to LAPIC that the current interrupt has been handled.
Critical: Must be called at the end of every interrupt handler, or the LAPIC will not deliver further interrupts at the same or lower priority.
Implementation:
let eoi = lapic_reg(0xB0);
eoi.write_volatile(0);Writing any value (typically 0) to the EOI register signals completion. The written value is ignored.
const IOAPIC_BASE: u64 = 0xFEC00000;The I/O APIC is accessed via MMIO at a fixed address. Unlike LAPIC, I/O APIC uses an indirect access model with two registers:
- IOREGSEL (0x00): Register selector
- IOWIN (0x10): Register data window
fn ioapic_reg(offset: u32) -> *mut u32 {
(IOAPIC_BASE + offset as u64) as *mut u32
}
unsafe fn ioapic_read(reg: u32) -> u32 {
ioapic_reg(0x00).write_volatile(reg);
ioapic_reg(0x10).read_volatile()
}
unsafe fn ioapic_write(reg: u32, value: u32) {
ioapic_reg(0x00).write_volatile(reg);
ioapic_reg(0x10).write_volatile(value);
}Procedure:
- Write register index to IOREGSEL (offset 0x00)
- Read/write data via IOWIN (offset 0x10)
pub unsafe fn map_irq(irq: u8, vector: u8)Purpose: Routes a hardware IRQ to a specific interrupt vector.
Implementation:
let reg = 0x10 + (irq as u32 * 2);
ioapic_write(reg, vector as u32);
ioapic_write(reg + 1, 0);Redirection Table Entry Format:
Each IRQ has a 64-bit redirection entry split across two 32-bit registers:
Lower 32 bits (reg): Configuration
- Bits 0-7: Vector number
- Bits 8-10: Delivery mode (000 = Fixed)
- Bit 11: Destination mode (0 = Physical)
- Bit 12: Delivery status (read-only)
- Bit 13: Polarity (0 = Active high)
- Bit 14: Remote IRR (read-only)
- Bit 15: Trigger mode (0 = Edge)
- Bit 16: Mask (0 = Not masked)
Upper 32 bits (reg + 1): Destination
- Bits 24-31: Destination CPU (APIC ID)
pub unsafe fn init_ioapic()Purpose: Initializes I/O APIC by routing essential IRQs:
map_irq(1, 33); // Keyboard (IRQ1) → Vector 33
map_irq(0, 32); // Timer (IRQ0) → Vector 32Standard IRQ Assignments:
- IRQ 0: PIT Timer (usually disabled in favor of LAPIC timer)
- IRQ 1: Keyboard (PS/2)
- IRQ 2: Cascade from slave PIC (unused in APIC mode)
- IRQ 3-15: Various hardware devices
Future Expansion: As more drivers are added, additional IRQs will be routed here.
pub const TIMER_VECTOR: u8 = 0x31; // Interrupt vector 49
pub const TIMER_DIVIDE_CONFIG: u32 = 0x3; // Divide by 16
pub const TIMER_INITIAL_COUNT: u32 = 100_000; // Countdown valueextern "x86-interrupt" fn timer_interrupt(_stack_frame: InterruptStackFrame)Purpose: Handles LAPIC timer interrupts for timekeeping and scheduling.
Implementation:
unsafe {
TICKS += 1;
send_eoi(); // Signal interrupt completion
}Tick Counter:
static mut TICKS: u64 = 0;
pub fn ticks() -> u64 {
unsafe { TICKS }
}Usage: Provides a monotonically increasing counter for:
- Uptime measurement
- Timeout implementation
- Scheduling quantum tracking
The timer uses a two-phase initialization to avoid race conditions:
pub unsafe fn register_handler()Purpose: Registers the timer interrupt handler with the IDT module.
Called: Before idt::init_idt() in kernel initialization
Implementation:
idt::register_interrupt_handler(TIMER_VECTOR, timer_interrupt);pub unsafe fn init_hardware()Purpose: Configures LAPIC timer hardware registers.
Called: After x86_64::instructions::interrupts::enable() in kernel initialization
Implementation:
// Divide Configuration Register
lapic_reg(0x3E0).write_volatile(TIMER_DIVIDE_CONFIG);
// LVT Timer Register - periodic mode (bit 17)
lapic_reg(0x320).write_volatile((TIMER_VECTOR as u32) | 0x20000);
// Initial Count Register
lapic_reg(0x380).write_volatile(TIMER_INITIAL_COUNT);
// Enable interrupts globally
hal::cpu::enable_interrupts();Why Two Phases?
- IDT must have handler entry before timer starts firing
- Hardware can't be configured until LAPIC is fully enabled
- Interrupts must be enabled before timer generates interrupts
- hal: Hardware abstraction for serial output, CPU control
- idt: Interrupt descriptor table for registering handlers
- keyboard: Integration for timer-based keyboard polling (future)
- x86_64 (0.15.2): x86_64 abstractions and interrupt stack frame
unsafe {
// Phase 1: Disable PIC and enable APIC
apic::enable();
// Phase 2: Route IRQs through I/O APIC
apic::ioapic::init_ioapic();
// Phase 3: Register timer handler
apic::timer::register_handler();
}
// Load IDT
idt::init_idt();
// Enable interrupts
x86_64::instructions::interrupts::enable();
// Phase 4: Start timer hardware
unsafe {
apic::timer::init_hardware();
}extern "x86-interrupt" fn keyboard_handler(_frame: InterruptStackFrame) {
// Handle keyboard input
let scancode = read_keyboard_port();
process_scancode(scancode);
// Signal interrupt completion
unsafe {
apic::send_eoi();
}
}Physical: 0xFEE00000 - 0xFEE00FFF (4KB page)
Virtual: Direct mapped (identity or offset)
Key Registers:
0xFEE00020: Local APIC ID
0xFEE00080: Task Priority Register
0xFEE000B0: EOI Register
0xFEE000F0: Spurious Interrupt Vector
0xFEE00320: LVT Timer Register
0xFEE00380: Timer Initial Count
0xFEE00390: Timer Current Count
0xFEE003E0: Timer Divide Configuration
Physical: 0xFEC00000 - 0xFEC000FF (256 bytes)
Virtual: Direct mapped (identity or offset)
Registers:
0xFEC00000: IOREGSEL (Register Select)
0xFEC00010: IOWIN (Data Window)
0-31: CPU Exceptions (reserved by x86_64)
32: Timer (IRQ0) - currently remapped but not used
33: Keyboard (IRQ1)
49 (0x31): LAPIC Timer
50-255: Available for future use
With current configuration:
Divider: 16
Initial Count: 100,000
Typical Bus Clock: ~1 GHz
Interrupt Period = (16 × 100,000) / 1,000,000,000
= 1.6 ms
Interrupt Frequency ≈ 625 Hz
Adjustable: By changing TIMER_INITIAL_COUNT, interrupt frequency can be tuned for different scheduling needs.
Proper timer calibration involves:
- Read TSC (Time Stamp Counter) before timer start
- Wait for known number of PIT ticks
- Read TSC after
- Calculate bus frequency
- Adjust LAPIC timer initial count for desired frequency
Current implementation targets single-core systems. For SMP support:
pub unsafe fn init_ap(cpu_id: u8) {
// Each CPU must initialize its own LAPIC
enable();
// Configure per-CPU timer
set_timer(TIMER_VECTOR, TIMER_DIVIDE_CONFIG, TIMER_INITIAL_COUNT);
}pub unsafe fn send_ipi(dest_cpu: u8, vector: u8) {
// Set destination
lapic_reg(0x310).write_volatile((dest_cpu as u32) << 24);
// Send IPI
let icr_low = vector as u32 | (0 << 8) | (0 << 11) | (1 << 14);
lapic_reg(0x300).write_volatile(icr_low);
}Use Cases:
- TLB shootdown (invalidate TLB on other CPUs)
- Scheduler wakeup
- Panic synchronization
// Check CPUID for APIC support
let cpuid = CpuId::new();
if !cpuid.get_feature_info().unwrap().has_apic() {
panic!("APIC not supported by CPU");
}// Some systems may relocate APIC base via MSR
let apic_base_msr = read_msr(0x1B);
let relocated_base = apic_base_msr & 0xFFFF_F000;Symptoms: Timer handler never called, keyboard unresponsive
Checks:
- APIC enabled in MSR and SVR?
- IDT handler registered?
- Global interrupts enabled (IF flag)?
- Timer initial count non-zero?
- LVT Timer not masked (bit 16 = 0)?
Symptoms: System hangs, serial output stops
Causes:
- Missing
send_eoi()in handler - PIC not properly disabled (dual interrupts)
- IRQ triggered faster than handler completes
Symptoms: Unexpected vector 0xFF interrupts
Cause: LAPIC generates spurious interrupts in some edge cases
Solution: Register spurious interrupt handler (vector 0xFF) that just does send_eoi()
pub unsafe fn dump_lapic_regs() {
serial_println!("LAPIC ID: {:08x}", lapic_reg(0x20).read_volatile());
serial_println!("LAPIC Version: {:08x}", lapic_reg(0x30).read_volatile());
serial_println!("TPR: {:08x}", lapic_reg(0x80).read_volatile());
serial_println!("SVR: {:08x}", lapic_reg(0xF0).read_volatile());
serial_println!("LVT Timer: {:08x}", lapic_reg(0x320).read_volatile());
serial_println!("Timer Current: {:08x}", lapic_reg(0x390).read_volatile());
}pub unsafe fn dump_ioapic_redirs() {
for irq in 0..24 {
let reg = 0x10 + (irq * 2);
let low = ioapic_read(reg);
let high = ioapic_read(reg + 1);
serial_println!("IRQ {}: {:08x} {:08x}", irq, high, low);
}
}Higher Frequency (shorter period):
- Pros: More responsive scheduling, better time resolution
- Cons: Higher interrupt overhead, reduced throughput
Lower Frequency (longer period):
- Pros: Lower overhead, better throughput
- Cons: Coarser time resolution, less responsive scheduling
Typical Values:
- Linux: 100-1000 Hz (1-10 ms period)
- Windows: ~64 Hz (15.6 ms period)
- Real-time systems: 1000+ Hz
Modern x86_64 supports "EOI Broadcast Suppression" (SVR bit 12), which prevents LAPIC from broadcasting EOI to I/O APIC. This improves performance in systems with many I/O APICs.
- Intel 64 and IA-32 Architectures Software Developer's Manual, Volume 3A: System Programming Guide, Chapter 10 (APIC)
- OSDev APIC
- OSDev IOAPIC
- MultiProcessor Specification
GPL-3.0 (see LICENSE file in repository root)