Skip to content

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
  • Timer-based scheduling: Preemptive multitasking
  • Error handling: Memory access violations, undefined instructions

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):

Offset | Exception Source              | Exception Type
-------+--------------------------------+-------------------
0x000  | Current EL with SP0 (unused)  | Synchronous
0x080  |                               | IRQ
0x100  |                               | FIQ
0x180  |                               | SError
-------+--------------------------------+-------------------
0x200  | Current EL with SPx (kernel)  | Synchronous
0x280  |                               | IRQ ← We use this
0x300  |                               | FIQ
0x380  |                               | SError
-------+--------------------------------+-------------------
0x400  | Lower EL (AArch64)            | Synchronous
0x480  |                               | IRQ
0x500  |                               | FIQ
0x580  |                               | SError
-------+--------------------------------+-------------------
0x600  | Lower EL (AArch32)            | Synchronous
0x680  |                               | IRQ
0x700  |                               | FIQ
0x780  |                               | SError

Each entry must be 128 bytes (0x80) apart.

Implementing the Vector Table

Assembly: boot/vectors.S

.balign 0x800
.global vector_table
vector_table:

    // Current EL with SP0 (not used)
    .balign 0x80
    b   exception_hang  // Synchronous
    .balign 0x80
    b   exception_hang  // IRQ
    .balign 0x80
    b   exception_hang  // FIQ
    .balign 0x80
    b   exception_hang  // SError

    // Current EL with SPx (kernel mode)
    .balign 0x80
    b   sync_el1_handler  // Synchronous
    .balign 0x80
    b   irq_el1_handler   // IRQ ← Our handler
    .balign 0x80
    b   fiq_el1_handler   // FIQ
    .balign 0x80
    b   serror_el1_handler // SError

    // ... (Lower EL entries)

IRQ Handler (Assembly)

The IRQ handler must: 1. Save all registers 2. Call C handler 3. Restore all registers 4. Return from exception (eret)

irq_el1_handler:
    // Save all general-purpose registers
    stp x0, x1, [sp, #-16]!
    stp x2, x3, [sp, #-16]!
    // ... (x4-x30)

    // Call C interrupt handler
    bl  irq_handler

    // Restore all registers
    ldp x0, x1, [sp], #16
    ldp x2, x3, [sp], #16
    // ... (x4-x30)

    eret  // Return from exception

Installing the Vector Table

1
2
3
4
install_vector_table:
    adr x0, vector_table
    msr vbar_el1, x0  // Set Vector Base Address Register
    ret

C Interrupt Handler

File: kernel/interrupts.c

#include "interrupt.h"
#include "uart.h"

void interrupt_init(void) {
    install_vector_table();
    uart_puts("[IRQ] Vector table installed\n");
}

void enable_irq(void) {
    asm volatile("msr daifclr, #2");  // Clear IRQ mask
}

void irq_handler(void) {
    // Check which interrupt fired
    // For now, we handle timer interrupts

    uart_puts("[IRQ] Interrupt received!\n");
}

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

void timer_interrupt_init(void) {
    // Read timer frequency (typically 54 MHz)
    uint64_t freq;
    asm volatile("mrs %0, cntfrq_el0" : "=r"(freq));

    // Set trigger for 1 second from now
    uint64_t current;
    asm volatile("mrs %0, cntvct_el0" : "=r"(current));

    uint64_t compare = current + freq;
    asm volatile("msr cntv_cval_el0, %0" :: "r"(compare));
}

void timer_interrupt_enable(void) {
    // Enable timer (bit 0 = enable, bit 1 = mask)
    uint32_t ctrl = (1 << 0);  // Enable, don't mask
    asm volatile("msr cntv_ctl_el0, %0" :: "r"(ctrl));

    enable_irq();
}

Handling Timer Interrupts

void irq_handler(void) {
    // Check if timer interrupt
    uint32_t ctrl;
    asm volatile("mrs %0, cntv_ctl_el0" : "=r"(ctrl));

    if (ctrl & (1 << 2)) {  // Bit 2 = interrupt status
        uart_puts("[TICK]\n");

        // Reschedule for next tick
        uint64_t freq, current;
        asm volatile("mrs %0, cntfrq_el0" : "=r"(freq));
        asm volatile("mrs %0, cntvct_el0" : "=r"(current));

        uint64_t compare = current + freq;
        asm volatile("msr cntv_cval_el0, %0" :: "r"(compare));
    }
}

Building and Testing

1. Build

1
2
3
cd os-rasp/build
cmake -DBUILD_EXAMPLES=ON ..
make

2. Deploy

Copy interrupt_demo.img to SD card (rename to kernel8.img).

3. Expected Output

Interrupt Demo - Timer IRQ
==============================

[IRQ] Vector table installed
[IRQ] Timer interrupt configured for 1 Hz
[IRQ] Timer interrupt enabled
Timer interrupt enabled. Waiting for ticks...

[TICK] 1
[TICK] 2
[TICK] 3
...

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:

1
2
3
while(1) {
    asm volatile("wfi");  // Wait For Interrupt
}

The CPU sleeps until an interrupt occurs, saving power.

Complete Source Code

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!