06. Interrupts and Exception Handling¶
Interrupts are the foundation of responsive, event-driven operating systems. Let's implement ARM exception handling and timer interrupts to enable preemptive multitasking.
What are Interrupts?¶
Interrupts allow hardware to signal the CPU that an event has occurred, causing the CPU to pause current execution and handle the event immediately.
Use Cases¶
- Hardware events: Keyboard press, network packet arrival
- Error handling: Memory access violations, undefined instructions
The Flow of an Interrupt¶
sequenceDiagram
participant HW as Hardware (Timer/Button)
participant GIC as GIC (Secretary)
participant CPU as CPU (CEO)
participant Code as Handler (Code)
HW->>GIC: "Hey! Something happened!"
GIC->>GIC: Check Priority & Mask
GIC->>CPU: "Excuse me, Sir. Important Event."
CPU->>CPU: Pause Hardware Task
CPU->>Code: Jump to Vector Table
Code->>Code: Save State (Context)
Code->>Code: Handle Event
Code->>Code: Restore State
Code->>CPU: Return (eret)
CPU->>CPU: Resume Hardware Task
ARM Exception Levels¶
ARM CPUs have 4 Exception Levels (privilege levels):
| Level | Name | Use |
|---|---|---|
| EL0 | User | Unprivileged applications |
| EL1 | Kernel | Operating system (we run here) |
| EL2 | Hypervisor | Virtualization |
| EL3 | Secure Monitor | Trusted execution environment |
Our bare-metal OS runs at EL1 (kernel mode).
Exception Types¶
ARM defines 4 types of exceptions:
| Type | Description | Example |
|---|---|---|
| Synchronous | Caused by instruction execution | Undefined instruction, data abort |
| IRQ | Interrupt Request (normal interrupts) | Timer, GPIO, UART |
| FIQ | Fast Interrupt Request (high priority) | Critical hardware events |
| SError | System Error (asynchronous abort) | Memory errors |
IRQ vs FIQ¶
- IRQ: Standard interrupts, can be masked
- FIQ: Faster response (dedicated registers), higher priority
For most use cases, IRQ is sufficient.
Exception Vector Table¶
The ARM exception vector table defines where the CPU jumps when an exception occurs. It contains 16 entries (4 exception types × 4 sources):
block-beta
columns 2
block:T1
text1["0x000"]
text2["Current EL with SP0 (Sync/IRQ/FIQ/Error)"]
end
space
block:T2
text3["0x200"]
text4["Current EL with SPx (We use this!)"]
text5["0x280: IRQ Handler"]
end
space
block:T3
text6["0x400"]
text7["Lower EL (AArch64)"]
end
space
block:T4
text8["0x600"]
text9["Lower EL (AArch32)"]
end
space
Each entry must be 128 bytes (0x80) apart.
Implementing the Vector Table¶
Assembly: boot/vectors.S¶
IRQ Handler (Assembly)¶
The IRQ handler must:
1. Save all registers
2. Call C handler
3. Restore all registers
4. Return from exception (eret)
Analogy: Game Save/Load (Context Switching)¶
Imagine you are playing a game (executing code) and mom calls you for dinner (Interrupt).
- Save Game (
stp): You pause and save your progress (registers) so you don't lose your place. - Dinner (
bl irq_handler): You go eat dinner (handle the event). - Load Game (
ldp): You return and load your save code. - Resume (
eret): You continue playing exactly where you left off.
Installing the Vector Table¶
C Interrupt Handler¶
File: kernel/interrupts.c
Timer Interrupts¶
The ARM Generic Timer can generate interrupts when a compare value is reached.
Registers¶
| Register | Purpose |
|---|---|
CNTV_CVAL_EL0 |
Compare value (trigger when count reaches this) |
CNTV_CTL_EL0 |
Control (enable, mask, status) |
CNTVCT_EL0 |
Current counter value |
Timer Interrupt Setup¶
Handling Timer Interrupts¶
The Problem: Too Many Devices!¶
We have a Timer, a UART, GPIO buttons, maybe a Network card... all of them want to interrupt the CPU. However, the CPU usually only has one main interrupt line (IRQ).
If everyone screams at once, the CPU won't know who to listen to. We need a manager.
The Solution: Interrupt Controller (GIC)¶
The Generic Interrupt Controller (GIC) sits between the hardware devices and the CPU.
graph LR
Timer[Timer] --> GIC
UART[UART] --> GIC
GPIO[GPIO] --> GIC
subgraph "Interrupt Controller"
GIC["GIC (The Manager)"]
end
GIC -->|One IRQ Line| CPU[CPU]
The GIC's job is to: 1. Collect signals from all devices. 2. Prioritize them (e.g., "Timer is more important than Keyboard"). 3. Forward the most important one to the CPU. 4. Tell the CPU exactly who interrupted it (Interrupt ID).
Raspberry Pi 4 (BCM2711) Specifics: GIC¶
On Raspberry Pi 4, this manager is the GIC-400 (GICv2). Unlike older Raspberry Pi models (which used a simpler custom controller), the GIC is a standard ARM component found in many smartphones and servers.
Analogy: The CEO and the Secretary¶
- CPU = Busy CEO: Trying to do actual work (running instructions).
- GIC = Receptionist/Secretary: Manages incoming calls (interrupts). - Masking: "The CEO is in a meeting, hold calls." - Priority: "The building is on fire! Interrupt immediately!" (FIQ/High Priority) - Routing: "This call is for Core 1, not Core 0."
GIC Initialization¶
We need to configure the Distributor (GICD) and CPU Interface (GICC) to enable interrupts.
Updated IRQ Handler for GIC¶
When using GIC, the IRQ handler must: 1. Read Interrupt Acknowledge Register (IAR) to get the Interrupt ID. 2. Handle the interrupt. 3. Write to End of Interrupt Register (EOIR) to signal completion.
Ensure you call gic_init() from interrupt_init()!
Building and Testing¶
1. Build¶
2. Deploy¶
Copy interrupt_demo.img to SD card (rename to kernel8.img).
3. Expected Output¶
Messages appear every 1 second, triggered by the timer interrupt!
Wait For Interrupt (WFI)¶
Instead of busy-waiting, use the wfi instruction to enter low-power mode:
The CPU sleeps until an interrupt occurs, saving power.
Complete Source Code¶
- boot/vectors.S - Exception vector table
- kernel/interrupts.c - Interrupt handlers
- include/interrupt.h - API
- examples/06_interrupt_demo.c - Demo
Troubleshooting¶
| Problem | Solution |
|---|---|
| No interrupts firing | Check vbar_el1 is set correctly |
| System hangs on exception | Verify vector table alignment (0x800) |
| Timer doesn't trigger | Ensure IRQ is enabled (daifclr) |
| Random crashes | Check register save/restore in handler |
Important Notes¶
UART in Interrupts
Using UART (uart_puts) from interrupt context is not ideal for production. It can block and cause timing issues. Use a ring buffer instead.
Exception Level
This code assumes you're running at EL1. Check with: mrs x0, CurrentEL
What's Next?¶
With interrupts working, we can now: - Implement preemptive multitasking (timer-based task switching) - Add GPIO interrupts for button presses - Handle exceptions properly (page faults, undefined instructions)
The next article covers Framebuffer and Graphics, which will allow us to display output on screen instead of just serial console!