Loading learning content...
Every real-time system lives and dies by its ability to respond to external events. When a proximity sensor detects an obstacle, when a motor encoder reports a position change, when a network packet arrives—the system must respond within microseconds. Interrupt handling is the mechanism that makes this possible.
In general-purpose operating systems, interrupts are optimized for throughput—batching and coalescing interrupts to reduce overhead. In real-time systems, interrupts are optimized for latency and determinism—responding to every event as quickly and predictably as possible.
By the end of this page, you will understand the complete interrupt processing pipeline from hardware to software, the architectural patterns for real-time interrupt handling, nested and prioritized interrupt schemes, interrupt threading and bottom-half processing, and the critical trade-offs in interrupt system design.
An interrupt is a hardware mechanism that causes the CPU to suspend its current execution and transfer control to a predefined handler routine. Understanding the hardware-software interaction is essential for designing effective real-time interrupt systems.
Modern processors support multiple interrupt types with different characteristics:
Hardware Interrupts (IRQs)
Software Interrupts (Traps)
Exceptions
Non-Maskable Interrupts (NMI)
| Type | Source | Timing | Maskable | Use Case |
|---|---|---|---|---|
| Hardware IRQ | Devices | Asynchronous | Yes | I/O, timers, sensors |
| Software Interrupt | Program | Synchronous | Yes | System calls |
| Exception | CPU | Synchronous | Some | Error handling |
| NMI | Critical hardware | Asynchronous | No | Watchdog, power fail |
The Interrupt Controller is a hardware component that manages interrupt signals from multiple sources and delivers them to the CPU in priority order. Understanding its architecture is crucial for minimizing interrupt latency.
Nested Vectored Interrupt Controller (NVIC)
The NVIC is ARM's interrupt controller for Cortex-M processors, designed specifically for real-time embedded systems:
1234567891011121314151617181920212223242526272829303132333435
/* ARM Cortex-M NVIC Configuration for Real-Time */ #include <stdint.h>#include "core_cm4.h" /* CMSIS header */ /* Priority grouping: more preemption bits = more nesting levels */#define NVIC_PRIORITYGROUP_4 3U /* 4 bits preempt, 0 bits sub */ void configure_nvic_for_realtime(void) { /* Set priority grouping - critical for nesting behavior */ NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4); /* Configure high-priority sensor interrupt */ NVIC_SetPriority(SENSOR_IRQn, 0); /* Highest priority */ NVIC_EnableIRQ(SENSOR_IRQn); /* Configure medium-priority motor control */ NVIC_SetPriority(MOTOR_IRQn, 4); /* Medium priority */ NVIC_EnableIRQ(MOTOR_IRQn); /* Configure low-priority communication */ NVIC_SetPriority(UART_IRQn, 8); /* Lower priority */ NVIC_EnableIRQ(UART_IRQn); /* System timer - typically high priority */ NVIC_SetPriority(SysTick_IRQn, 2);} /* Tail chaining optimization example: * If UART interrupt arrives while MOTOR is completing, * NVIC transitions directly without full stack operations */ /* Late arrival optimization: * If SENSOR interrupt arrives during MOTOR's stack push, * CPU switches to SENSOR handler, defers MOTOR */Interrupt Service Routines (ISRs) in real-time systems must be designed with extreme care. Every microsecond spent in an ISR is time stolen from application tasks and other interrupt handlers.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
/* Real-Time ISR Design Patterns */ /* ============================================ * Pattern 1: Minimal ISR with Task Signaling * Best for: Complex sensor processing * ============================================ */ /* Shared data between ISR and task */static volatile uint32_t sensor_data;static volatile bool data_ready = false;static osSemaphoreId_t data_semaphore; /* ISR: ~2μs execution time */void __attribute__((interrupt)) sensor_isr_minimal(void) { /* 1. Acknowledge interrupt FIRST */ SENSOR_REGS->STATUS = SENSOR_INT_CLEAR; /* 2. Grab data (one register read) */ sensor_data = SENSOR_REGS->DATA; /* 3. Signal task (non-blocking) */ data_ready = true; osSemaphoreRelease(data_semaphore); /* Total: 3 operations, <2μs at 100MHz */} /* Task: Runs when signaled, can take as long as needed */void sensor_processing_task(void *arg) { while (1) { osSemaphoreAcquire(data_semaphore, osWaitForever); if (data_ready) { /* Complex processing here - doesn't affect ISR latency */ filtered_value = apply_filter(sensor_data); detect_anomalies(filtered_value); log_to_storage(filtered_value); transmit_over_network(filtered_value); data_ready = false; } }} /* ============================================ * Pattern 2: Bounded Queue ISR * Best for: High-frequency events * ============================================ */ #define QUEUE_SIZE 32#define QUEUE_MASK (QUEUE_SIZE - 1) static struct { uint32_t data[QUEUE_SIZE]; volatile uint32_t head; /* Written by ISR */ volatile uint32_t tail; /* Written by task */} event_queue; /* ISR: ~1μs, never blocks */void __attribute__((interrupt)) event_isr_queue(void) { EVENT_REGS->STATUS = EVENT_INT_CLEAR; uint32_t head = event_queue.head; uint32_t next = (head + 1) & QUEUE_MASK; if (next != event_queue.tail) { /* Queue not full */ event_queue.data[head] = EVENT_REGS->DATA; event_queue.head = next; } else { /* Queue full - record overflow */ increment_overflow_counter(); }} /* ============================================ * Pattern 3: State Machine ISR * Best for: Multi-step protocols (SPI, I2C) * ============================================ */ typedef enum { SPI_STATE_IDLE, SPI_STATE_SENDING, SPI_STATE_RECEIVING, SPI_STATE_COMPLETE} spi_state_t; static volatile spi_state_t spi_state = SPI_STATE_IDLE;static uint8_t *spi_buffer;static volatile size_t spi_index, spi_length; /* Each ISR invocation handles one byte - bounded time */void __attribute__((interrupt)) spi_isr_statemachine(void) { switch (spi_state) { case SPI_STATE_SENDING: if (spi_index < spi_length) { SPI_REGS->DATA = spi_buffer[spi_index++]; } else { spi_state = SPI_STATE_RECEIVING; spi_index = 0; } break; case SPI_STATE_RECEIVING: spi_buffer[spi_index++] = SPI_REGS->DATA; if (spi_index >= spi_length) { spi_state = SPI_STATE_COMPLETE; signal_completion(); } break; default: break; } SPI_REGS->STATUS = SPI_INT_CLEAR;}Nested interrupts allow higher-priority interrupts to preempt lower-priority interrupt handlers. This is essential for real-time systems where critical events must be serviced immediately, regardless of what the system is currently doing.
Just as with tasks, interrupts can suffer from priority inversion. Consider:
Solutions:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
/* Nested Interrupt Management */ #include <stdint.h> /* ARM Cortex-M: Automatic nesting via NVIC *//* Higher priority = lower number (0 is highest) */ void configure_interrupt_priorities(void) { /* Safety-critical: Priority 0 (never preempted by other IRQs) */ NVIC_SetPriority(EMERGENCY_STOP_IRQn, 0); /* Control loop: Priority 1 (only preempted by emergency) */ NVIC_SetPriority(MOTOR_CONTROL_IRQn, 1); /* High-speed sensor: Priority 2 */ NVIC_SetPriority(ENCODER_IRQn, 2); /* Communications: Priority 8 (can be preempted by control) */ NVIC_SetPriority(CAN_IRQn, 8); NVIC_SetPriority(UART_IRQn, 9); /* Background: Priority 15 (lowest, preempted by everything) */ NVIC_SetPriority(ADC_IRQn, 15);} /* ============================================ * ISR-Safe Resource Sharing * ============================================ */ /* Spin lock that disables interrupts at or below given priority */typedef struct { volatile uint32_t locked; uint32_t saved_basepri;} isr_spinlock_t; static inline void isr_spin_lock(isr_spinlock_t *lock, uint32_t priority) { /* Mask interrupts at this priority and below */ lock->saved_basepri = __get_BASEPRI(); __set_BASEPRI(priority << 4); /* NVIC priority field position */ /* Memory barrier */ __DMB(); lock->locked = 1;} static inline void isr_spin_unlock(isr_spinlock_t *lock) { lock->locked = 0; __DMB(); /* Restore previous interrupt mask */ __set_BASEPRI(lock->saved_basepri);} /* Usage in ISR */static isr_spinlock_t shared_data_lock;static volatile uint32_t shared_sensor_sum; void __attribute__((interrupt)) sensor_isr_with_shared_data(void) { SENSOR_REGS->STATUS = SENSOR_INT_CLEAR; uint32_t reading = SENSOR_REGS->DATA; /* Lock only for the critical update - minimize locked time */ isr_spin_lock(&shared_data_lock, SENSOR_IRQ_PRIORITY); shared_sensor_sum += reading; isr_spin_unlock(&shared_data_lock);}Each level of interrupt nesting adds stack frame overhead. With deep nesting and multiple interrupt sources, stack overflow is a real risk. Calculate worst-case stack usage: (max nesting depth) × (largest ISR stack frame) + base system usage. ARM Cortex-M uses a dedicated Main Stack for handlers, simplifying this analysis.
Interrupt threading converts hardware interrupt handlers into kernel threads. This technique, pioneered by the PREEMPT_RT Linux patches, enables priority-based scheduling of interrupt work and eliminates interrupt-disabled regions.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
/* Linux Kernel Threaded IRQ Example */ #include <linux/interrupt.h>#include <linux/module.h> /* Hard IRQ: Minimal work, runs with interrupts disabled */static irqreturn_t sensor_hardirq(int irq, void *dev_id) { struct sensor_device *dev = dev_id; /* Check if this device generated the interrupt */ if (!sensor_interrupt_pending(dev)) { return IRQ_NONE; /* Not our interrupt */ } /* Acknowledge interrupt to hardware */ sensor_acknowledge_irq(dev); /* Tell kernel to wake the thread handler */ return IRQ_WAKE_THREAD;} /* Thread handler: Runs as kernel thread with given priority */static irqreturn_t sensor_threadfunc(int irq, void *dev_id) { struct sensor_device *dev = dev_id; /* This code CAN be preempted by higher-priority RT tasks! */ /* Read data from device */ u32 data = sensor_read_data(dev); /* Process data - can take significant time */ process_sensor_data(dev, data); /* Can call functions that might sleep! */ mutex_lock(&dev->data_mutex); /* With priority inheritance */ update_shared_state(dev, data); mutex_unlock(&dev->data_mutex); return IRQ_HANDLED;} /* Registration with threaded handler */static int sensor_probe(struct platform_device *pdev) { struct sensor_device *dev; int ret; dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; /* Request threaded IRQ */ ret = request_threaded_irq( dev->irq, /* IRQ number */ sensor_hardirq, /* Hard IRQ handler */ sensor_threadfunc, /* Thread handler */ IRQF_ONESHOT, /* Keep IRQ disabled until thread completes */ "sensor", /* Name (appears in /proc/interrupts) */ dev /* Cookie passed to handlers */ ); if (ret) { dev_err(&pdev->dev, "Failed to request IRQ: %d\n", ret); return ret; } return 0;} /* IRQF_ONESHOT is crucial: * - Hard IRQ disables the interrupt line * - Thread handler runs * - Interrupt re-enabled only when thread completes * - Prevents interrupt storm during slow handling */The top-half/bottom-half model splits interrupt handling into two phases:
Different operating systems provide various bottom-half mechanisms:
| Mechanism | Context | Preemptible | Can Sleep | Use Case |
|---|---|---|---|---|
| Softirqs (Linux) | Interrupt | No | No | High-frequency network/block I/O |
| Tasklets (Linux) | Interrupt | No | No | Deferred per-device handling |
| Workqueues (Linux) | Process | Yes | Yes | Complex processing, blocking ops |
| Threaded IRQs | Process | Yes | Yes | PREEMPT_RT, priority control |
| DSR (Windows) | Interrupt | IRQL-dependent | No | Driver deferred procedure calls |
| IST (QNX) | Process | Yes | Yes | Interrupt Service Thread |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
/* Bottom-Half Processing Patterns */ /* ============================================ * Pattern 1: Linux Workqueue * Process context, can sleep, fully preemptible * ============================================ */ #include <linux/workqueue.h> struct sensor_work { struct work_struct work; u32 data; struct sensor_device *dev;}; /* Bottom-half handler - runs in worker thread context */static void sensor_work_handler(struct work_struct *work) { struct sensor_work *sw = container_of(work, struct sensor_work, work); /* Can do anything here - sleep, allocate, take mutexes */ process_sensor_reading(sw->dev, sw->data); kfree(sw);} /* Top-half - minimal work, schedules bottom-half */static irqreturn_t sensor_irq_handler(int irq, void *dev_id) { struct sensor_device *dev = dev_id; struct sensor_work *sw; /* Acknowledge hardware */ sensor_ack_irq(dev); /* Allocate work (from cache, not heap) - OK in IRQ with GFP_ATOMIC */ sw = kmem_cache_alloc(sensor_work_cache, GFP_ATOMIC); if (sw) { sw->data = sensor_read_data_reg(dev); sw->dev = dev; INIT_WORK(&sw->work, sensor_work_handler); queue_work(dev->workqueue, &sw->work); } return IRQ_HANDLED;} /* ============================================ * Pattern 2: RTOS Deferred Service Routine * Common in embedded RTOS designs * ============================================ */ /* Pre-allocated message pool - no allocation in ISR */#define MAX_PENDING_EVENTS 16static event_msg_t event_pool[MAX_PENDING_EVENTS];static volatile uint32_t pool_index = 0; /* Message queue for ISR -> Task communication */static osMessageQueueId_t event_queue; /* ISR: Grab buffer from pool, post to queue */void __attribute__((interrupt)) event_isr_deferred(void) { EVENT_REGS->STATUS = EVENT_INT_CLEAR; /* Get pre-allocated buffer (lock-free in single-producer case) */ uint32_t idx = pool_index++; if (pool_index >= MAX_PENDING_EVENTS) { pool_index = 0; /* Wrap (or handle overflow) */ } event_msg_t *msg = &event_pool[idx]; msg->timestamp = get_system_time(); msg->raw_data = EVENT_REGS->DATA; /* Post to queue - non-blocking, ISR-safe */ osMessageQueuePut(event_queue, &msg, 0, 0);} /* Deferred handler task */void event_handler_task(void *arg) { event_msg_t *msg; while (1) { /* Block waiting for events */ if (osMessageQueueGet(event_queue, &msg, NULL, osWaitForever) == osOK) { /* Full processing with blocking allowed */ validate_event(msg); log_event_to_flash(msg); /* May block on SPI */ transmit_event(msg); /* May block on network */ } }}For real-time systems, you must be able to calculate and bound interrupt latency. This requires understanding all contributors to latency.
Worst Case Interrupt Latency =
Hardware Recognition Time
+ Maximum Interrupt Disable Duration
+ Higher Priority Interrupt Handlers
+ Context Switch to ISR
+ ISR Prologue
| Component | Typical Value | Depends On | How to Minimize |
|---|---|---|---|
| Hardware recognition | 0.1-1 μs | CPU architecture | Select faster CPU |
| Interrupt disable time | 0.5-5000+ μs | OS/driver code | Audit all critical sections |
| Higher-priority handlers | Σ(HPi × Ci) | System design | Minimize high-priority ISRs |
| Controller arbitration | 0.1-0.5 μs | Controller hardware | Configure priorities carefully |
| Context save | 0.5-5 μs | CPU, register count | Use optimal ABI settings |
| ISR prologue | 0.2-2 μs | Compiler, code | Use naked/interrupt attributes |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
/* Interrupt Latency Measurement and Analysis */ #include <stdint.h> /* Hardware timer for precise measurement */#define TIMER_COUNTER (*(volatile uint32_t *)0x40001004)#define TIMER_PRESCALER 1 /* Count at CPU frequency */ /* Latency measurement structure */typedef struct { uint32_t min_latency; uint32_t max_latency; uint64_t total_latency; uint32_t count; uint32_t histogram[100]; /* 1μs buckets */} latency_stats_t; static latency_stats_t irq_latency_stats; /* External signal captures timer value at interrupt assertion */static volatile uint32_t interrupt_assertion_time; /* ISR measures its own entry latency */void __attribute__((interrupt)) measured_sensor_isr(void) { /* FIRST instruction: capture current time */ uint32_t entry_time = TIMER_COUNTER; /* Calculate latency */ uint32_t latency = entry_time - interrupt_assertion_time; /* Update statistics */ if (latency < irq_latency_stats.min_latency) { irq_latency_stats.min_latency = latency; } if (latency > irq_latency_stats.max_latency) { irq_latency_stats.max_latency = latency; } irq_latency_stats.total_latency += latency; irq_latency_stats.count++; /* Histogram (convert cycles to microseconds) */ uint32_t latency_us = cycles_to_us(latency); if (latency_us < 100) { irq_latency_stats.histogram[latency_us]++; } /* Normal ISR work */ SENSOR_REGS->STATUS = SENSOR_INT_CLEAR; process_sensor();} /* Analysis functions */void print_latency_analysis(void) { printf("=== Interrupt Latency Analysis ===\n"); printf("Samples: %lu\n", irq_latency_stats.count); printf("Min latency: %lu cycles (%lu μs)\n", irq_latency_stats.min_latency, cycles_to_us(irq_latency_stats.min_latency)); printf("Max latency: %lu cycles (%lu μs)\n", irq_latency_stats.max_latency, cycles_to_us(irq_latency_stats.max_latency)); printf("Avg latency: %lu cycles (%lu μs)\n", (uint32_t)(irq_latency_stats.total_latency / irq_latency_stats.count), cycles_to_us((uint32_t)(irq_latency_stats.total_latency / irq_latency_stats.count))); /* Print histogram */ printf("\nHistogram (μs):\n"); for (int i = 0; i < 100; i++) { if (irq_latency_stats.histogram[i] > 0) { printf("%3d μs: %lu (%.2f%%)\n", i, irq_latency_stats.histogram[i], 100.0 * irq_latency_stats.histogram[i] / irq_latency_stats.count); } }}You now have a deep understanding of interrupt handling in real-time operating systems. From hardware controllers to software patterns, you can design interrupt systems that meet strict timing requirements. The next page examines timer resolution—the mechanisms that enable precise time measurement and scheduling in RTOS.