The Interrupt Pattern in Embedded Systems

The Interrupt Pattern in Embedded Systems


Introduction

In embedded systems, responsiveness and timing precision are often critical. When a hardware event occurs — such as a GPIO edge, UART byte received, or timer overflow — the system must react immediately.

This is where the Interrupt Pattern shines. Instead of constantly checking for changes (as in polling), interrupts let hardware notify the CPU directly when an event occurs. The CPU can sleep or perform other tasks and only react when necessary, improving both responsiveness and power efficiency.

Interrupt-driven designs are at the heart of modern embedded systems, from simple sensor nodes to real-time automotive controllers and industrial PLCs. However, using interrupts safely requires discipline — poor design can cause concurrency issues, missed events, or system lockups.

The Interrupt Pattern provides a structured, maintainable approach to handling hardware events efficiently while ensuring stability, predictability, and low power consumption.


What is the Interrupt Pattern?

The Interrupt Pattern uses hardware interrupt signals to handle asynchronous events immediately. When a peripheral or external pin generates an interrupt request (IRQ):

  1. The processor pauses normal execution.
  2. The Interrupt Service Routine (ISR) runs to handle the event.
  3. The ISR performs minimal, time-critical work (like reading a register or flag).
  4. Heavy or deferred work is scheduled for later (main loop or RTOS task).

This ensures low-latency responses while keeping the system stable and testable.

Key Responsibilities

  1. Low latency: Acknowledge and respond to hardware events immediately.
  2. Minimal ISR logic: Perform only essential operations inside the ISR.
  3. Deferred processing: Offload heavy or non-critical work to tasks or threads.
  4. Safe concurrency: Prevent race conditions and priority conflicts.
  5. Power efficiency: Use wake-on-interrupt to exit sleep modes efficiently.
  6. Error handling: Detect and handle spurious or missed interrupts safely.

When to Use It

Use the Interrupt Pattern when:

  • The system must react quickly to hardware events (microseconds to milliseconds).
  • Power consumption must be minimized via sleep and wake-on-event behavior.
  • Events occur infrequently or unpredictably.
  • Deterministic, low-latency responses are required (real-time systems).
  • Multiple peripherals generate independent asynchronous signals.

Avoid using interrupts when:

  • The event frequency is extremely high (polling or DMA may be more efficient).
  • Timing is fully deterministic and easier to control via loops.
  • The software architecture cannot handle concurrency safely.

Benefits

Fast response time – immediate reaction to hardware events.
Energy efficiency – CPU sleeps until an interrupt occurs.
Scalable concurrency – multiple peripherals can operate independently.
Reduced CPU load – only handle events when necessary.
Improved real-time behavior – deterministic response to time-critical signals.

Drawbacks

Increased complexity – concurrency, nesting, and timing must be managed.
Debug difficulty – timing-related bugs can be hard to reproduce.
Potential race conditions – shared data between ISR and main context.
Priority tuning required – incorrect ISR priority can cause starvation.


Design Variants

1. Simple ISR (Single Function Handler)

ISR directly handles the event with minimal work.

  • Use Case: Trivial operations (toggle LED, increment counter).
  • Pros: Simple, fast.
  • Cons: Not scalable; unsuitable for heavy work.

2. ISR + Deferred Processing (Recommended)

ISR acknowledges hardware, then defers heavy work to a background process (e.g., via queue, flag, or RTOS notification).

  • Use Case: UART RX, DMA completion, ADC conversion ready.
  • Pros: Safe, modular, efficient.
  • Cons: Slightly higher latency for final result.

3. DMA with Interrupt Completion

ISR starts or stops DMA transfers; completion handled in ISR callback.

  • Use Case: High-speed data streams.
  • Pros: Low CPU load.
  • Cons: More setup complexity, buffer management needed.

4. Nested Interrupts (Priority Layered)

Higher-priority ISRs preempt lower ones.

  • Use Case: Critical real-time control (motor or safety systems).
  • Pros: Deterministic for top priorities.
  • Cons: Complex debugging, potential jitter.

5. Shared Interrupt Dispatcher

Multiple peripherals share one IRQ line. ISR reads source registers and dispatches accordingly.

  • Use Case: Low-pin-count MCUs.
  • Pros: Saves pins and IRQ lines.
  • Cons: Adds branching overhead.

ISR Design Rules

  • Keep ISRs short and deterministic — aim for microseconds.
  • Acknowledge interrupt as the first action (clear flag, read register).
  • Avoid blocking calls, delays, or loops.
  • Do not allocate or free memory in ISRs.
  • Use only ISR-safe APIs (e.g., xQueueSendFromISR in FreeRTOS).
  • Defer complex work to task/thread context.
  • Protect shared resources using atomic operations or critical sections.

Example — Bare-Metal Interrupt with Deferred Work

#include <stdint.h>
#include <stdbool.h>

volatile bool uart_rx_flag = false;
volatile uint8_t last_byte = 0;

void UART_IRQHandler(void) {
    if (UART_RX_READY()) {
        last_byte = UART_READ();
        uart_rx_flag = true;  // signal to main loop
    }
}

int main(void) {
    uart_init();

    while (1) {
        if (uart_rx_flag) {
            uart_rx_flag = false;
            process_uart_byte(last_byte);  // deferred processing
        }
        enter_low_power_mode();  // wait for interrupt
    }
}

Example — RTOS-Based Interrupt (Deferred Task)

#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"

static QueueHandle_t uartQueue;

void UART_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    uint8_t byte = UART_READ();
    xQueueSendFromISR(uartQueue, &byte, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

void uartTask(void *param) {
    uint8_t byte;
    for (;;) {
        if (xQueueReceive(uartQueue, &byte, portMAX_DELAY)) {
            process_uart_byte(byte);
        }
    }
}

int main(void) {
    uartQueue = xQueueCreate(16, sizeof(uint8_t));
    xTaskCreate(uartTask, "UART", 512, NULL, 1, NULL);
    vTaskStartScheduler();
}

Power Management

Interrupts are key to low-power embedded design:

  • MCU can enter deep sleep, waking only on interrupts (GPIO, timers, sensors).
  • Reduces active time, saving battery.
  • Combine with debouncing or observer patterns to avoid unnecessary wakeups.
  • Ensure all interrupts are properly configured as wake sources.

Testing and Debugging

  • Measure ISR latency and execution time using timers or trace tools.
  • Use logic analyzers or GPIO toggles for ISR timing visualization.
  • Simulate interrupt storms to test queue overflow and handling.
  • Verify nested interrupt handling (priorities and masking).
  • Include watchdog timers to recover from stuck ISRs.

Anti-Patterns

  • Doing heavy computation inside ISR.
  • Using blocking synchronization (mutex, delay).
  • Missing interrupt clearing — leads to endless retrigger.
  • Race conditions between ISR and main code.
  • Overly complex nesting or unbounded ISRs.

Comparison: Interrupt Pattern vs Polling Pattern

FeatureInterrupt PatternPolling Pattern
Response timeImmediate, hardware-drivenLimited by polling period
Power efficiencyExcellent – CPU sleeps until eventModerate – CPU active periodically
ComplexityHigher (concurrency, priorities, ISRs)Lower (simple loops, deterministic timing)
CPU utilizationLow – work only on eventsConstant – checks continuously
DeterminismEvent-driven, less predictable timingFully deterministic, loop-based
Best suited forAsynchronous, urgent eventsSlow or periodic signals
Debugging difficultyHarder – timing/race issuesEasier – linear control flow
Example use caseUART RX, GPIO edge, DMA completeTemperature sensor, battery voltage monitor

Best Practices & Checklist

  • Keep ISRs short and bounded.
  • Defer heavy work to background context.
  • Use ISR-safe APIs for RTOS communication.
  • Define clear interrupt priority mapping.
  • Test for missed or spurious interrupts.
  • Measure ISR latency and execution time.
  • Ensure proper clearing of interrupt flags.
  • Protect shared data using atomics or critical sections.
  • Combine with Polling Pattern where periodic validation is needed.

Conclusion

The Interrupt Pattern is a cornerstone of responsive, energy-efficient embedded design. By reacting immediately to hardware events and deferring heavy processing, it achieves a balance between performance and reliability.

In contrast to the Polling Pattern, which checks inputs periodically, interrupts provide event-driven reactivity — ideal for time-critical systems. Yet, both patterns are complementary: use polling for slow or predictable signals, and interrupts for urgent or asynchronous events.

Mastering when and how to use each is key to designing embedded systems that are fast, efficient, and dependable.

Leave a Reply

Your email address will not be published. Required fields are marked *