Chapter 12: Interrupts
- The difference between exceptions and interrupts
- How the GIC (Generic Interrupt Controller) works
- Interrupt types: SPI, PPI, SGI
- How to configure and enable interrupts for a device
- How to write an interrupt handler
- How to acknowledge and end interrupts in the GIC
12.1 Exceptions vs Interrupts
Interrupts are a type of exception, but they differ from synchronous exceptions in an important way: interrupts are asynchronous. They happen at any time, unrelated to the currently executing instruction. A timer fires, a network packet arrives, a key is pressed -- these events trigger interrupts.
ARM64 provides two interrupt lines: IRQ (normal interrupt) and FIQ (fast interrupt). FIQ has higher priority and a separate set of banked registers, allowing faster handling. Most systems use IRQ for all devices and reserve FIQ for special purposes.
12.2 The Generic Interrupt Controller (GIC)
The GIC is a hardware component that manages interrupt delivery. It receives interrupt signals from devices, prioritizes them, and sends them to the appropriate CPU core. On QEMU virt, the GICv3 is at physical address 0x08000000.
The GIC has three main components:
- Distributor (GICD): Global configuration, enables/disables interrupts, sets priority
- Redistributor (GICR): Per-CPU configuration, pending management, wake-up
- CPU Interface (ICC): Per-CPU interrupt handling, acknowledge, end-of-interrupt
12.3 Interrupt Types
| Type | ID Range | Description |
|---|---|---|
| SGI (Software Generated) | 0-15 | Inter-processor interrupts (IPI). One CPU sends to another. |
| PPI (Private Peripheral) | 16-31 | Per-CPU interrupts (timer, PMU). Each CPU has its own. |
| SPI (Shared Peripheral) | 32-1019 | Device interrupts shared across CPUs (UART, network, disk). |
Key interrupt IDs on QEMU virt:
| ID | Type | Device |
|---|---|---|
| 30 | PPI | System timer (CNTP) |
| 33 | SPI | UART (PL011) |
| 34 | SPI | RTC |
| 64+ | SPI | VirtIO devices |
12.4 GIC Register Access
/* Distributor registers (offsets from 0x08000000) */
#define GICD_CTLR 0x0000 /* Control register */
#define GICD_TYPER 0x0004 /* Type register (number of interrupts) */
#define GICD_ISENABLER 0x0100 /* Interrupt set-enable (one bit per ID) */
#define GICD_ICENABLER 0x0180 /* Interrupt clear-enable */
#define GICD_ISPEND 0x0200 /* Interrupt set-pending */
#define GICD_ICPEND 0x0280 /* Interrupt clear-pending */
#define GICD_ICFGR 0x0C00 /* Interrupt config (level/edge) */
/* CPU interface registers (offsets from 0x08000000 + 0x40000) */
#define ICC_PMR 0x0004 /* Priority mask register */
#define ICC_IAR0 0x000C /* Interrupt acknowledge register (current EL) */
#define ICC_EOIR0 0x0010 /* End of interrupt register */
#define ICC_HPPIR0 0x0018 /* Highest pending interrupt */
/* Enable a specific interrupt */
void gic_enable_interrupt(uint32_t irq_id) {
volatile uint32_t *isenabler = (uint32_t *)(GICD_BASE + GICD_ISENABLER);
isenabler[irq_id / 32] = (1 << (irq_id % 32));
}
/* Acknowledge an interrupt (read the interrupt ID) */
uint32_t gic_acknowledge(void) {
volatile uint32_t *icc_iar = (uint32_t *)(GICD_BASE + 0x40000 + ICC_IAR0);
return *icc_iar & 0x3FF;
}
/* Signal end of interrupt */
void gic_end_of_interrupt(uint32_t irq_id) {
volatile uint32_t *icc_eoir = (uint32_t *)(GICD_BASE + 0x40000 + ICC_EOIR0);
*icc_eoir = irq_id;
}
12.5 Writing an Interrupt Handler
The interrupt handler follows a fixed sequence:
- Save context (registers we will modify)
- Acknowledge the interrupt (read ICC_IAR0 to get the interrupt ID)
- Handle the interrupt (call the device-specific handler)
- Signal end-of-interrupt (write to ICC_EOIR0)
- Restore context and return via ERET
void irq_handler(void) {
uint32_t irq_id = gic_acknowledge();
switch (irq_id) {
case 30: timer_handler(); break;
case 33: uart_handler(); break;
default:
uart_puts("Spurious IRQ: ");
uart_puthex(irq_id);
uart_puts("\r\n");
}
gic_end_of_interrupt(irq_id);
}
12.6 Interrupt Priority and Masking
The GIC supports 16 priority levels (0-255, with 0 being highest). The Priority Mask Register (ICC_PMR) sets the minimum priority that will be delivered to the CPU. Interrupts with priority higher (lower numeric value) than PMR are delivered; those with lower priority are held pending.
/* Set priority mask: only interrupts with priority < 0x80 will fire */
volatile uint32_t *icc_pmr = (uint32_t *)(GICD_BASE + 0x40000 + ICC_PMR);
*icc_pmr = 0x80;
At the CPU level, the DAIF bits in PSTATE also control interrupt delivery:
msr DAIFSet, #1: mask FIQmsr DAIFSet, #2: mask IRQmsr DAIFSet, #4: mask SErrormsr DAIFClr, #2: unmask IRQ
12.7 Our Implementation
Our kernel's interrupt handling is initialized in this order:
gic_init(): configure distributor, set all interrupts to non-secure group 1gic_enable_interrupt(id): enable specific interrupts for UART, timer, etc.msr DAIFClr, #2: unmask IRQs at the CPU level- The IRQ vector in the exception table catches all interrupts
irq_handler()in C dispatches to device-specific handlers
For now, we have two interrupt sources: the system timer (for the scheduler tick) and the UART (for serial input). As we add more device drivers, we will add more interrupt IDs to the dispatch table.
12.8 Exercises
Exercise 1: Interrupt Count
Add a counter to irq_handler that increments on every IRQ. Print the count every time the timer fires.
Exercise 2: Spurious Interrupt Detection
Read ICC_HPPIR0 before acknowledging. If it reads 1023 (spurious), skip the handler. Print a message for spurious interrupts.
12.9 Summary
Interrupts are asynchronous exceptions that allow the CPU to respond to hardware events. The GIC receives interrupts from devices, prioritizes them, and delivers them to the CPU. Our kernel initializes the GIC, installs an IRQ handler that acknowledges the interrupt, dispatches to the appropriate device handler, and signals end-of-interrupt. This mechanism is the foundation for all device driver interaction.