Polling vs Interrupts in Embedded Systems: When to Use Each

When you need to read a sensor, monitor a button press, or receive data from a peripheral, you have two fundamental approaches: polling and interrupts. Each has its place, and choosing the wrong one can waste CPU cycles, miss events, or make your code unnecessarily complex.

This post explains both approaches, their pros and cons, platform-specific implementations, and when to use each.


What is Polling?

Polling means repeatedly checking a condition or reading a register in a loop to detect changes or new data.

Simple Polling Example (Arduino)

void loop() {
  if (digitalRead(BUTTON_PIN) == LOW) {
    handleButtonPress();
  }
  delay(10);  // Check every 10ms
}

The CPU actively checks the button state every cycle. If nothing happens, the CPU is still busy checking.


What is an Interrupt?

An interrupt is a hardware signal that stops the CPU from its current task and executes a special function called an Interrupt Service Routine (ISR). The CPU returns to what it was doing afterward.

Simple Interrupt Example (Arduino)

volatile bool buttonPressed = false;

void setup() {
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);
}

void buttonISR() {
  buttonPressed = true;  // Set flag, do minimal work
}

void loop() {
  if (buttonPressed) {
    buttonPressed = false;
    handleButtonPress();  // Do heavy work here, not in ISR
  }
}

The CPU does not check the button constantly. When the button is pressed, the hardware triggers the ISR automatically.


Polling vs Interrupts: Quick Comparison

AspectPollingInterrupts
CPU UsageHigh—continuously checks even when idleLow—CPU free until event occurs
Response TimeDepends on polling frequency and loop durationFast—responds immediately (within μs)
Missed EventsCan miss fast events if polling is slowNo—hardware captures the event
ComplexitySimple, easy to debugMore complex—concurrency issues, ISR constraints
Power ConsumptionHigh—CPU always runningLow—CPU can sleep between interrupts
PredictabilityDeterministic, easy to control timingCan disrupt timing-critical code

When to Use Polling

✅ Good Use Cases

  1. Events happen frequently or continuously (e.g., sensor reads every 100ms)
  2. Simple applications where CPU has nothing else to do
  3. Timing-critical loops where interrupts would disrupt precision (e.g., bit-banging protocols)
  4. Easy debugging needed—no ISR race conditions
  5. Reading multiple inputs where interrupt overhead would be excessive

Example: ADC Polling in Bare Metal ARM

// STM32 ADC polling
void read_adc() {
  ADC1->CR2 |= ADC_CR2_SWSTART;  // Start conversion
  while (!(ADC1->SR & ADC_SR_EOC));  // Poll until end-of-conversion
  uint16_t value = ADC1->DR;  // Read result
  process_adc_value(value);
}

When to Use Interrupts

✅ Good Use Cases

  1. Rare or unpredictable events (e.g., button press, external signal)
  2. Low-power applications where CPU should sleep
  3. Fast response required (e.g., emergency shutdown, communication protocols)
  4. Multiple concurrent tasks (RTOS or multitasking)
  5. Avoiding blocking operations in main loop

Example: UART Interrupt in Linux Kernel

static irqreturn_t uart_irq_handler(int irq, void *dev_id) {
  struct uart_port *port = dev_id;
  unsigned int status = readl(port->membase + UART_STATUS);
  
  if (status & UART_RX_READY) {
    char data = readl(port->membase + UART_DATA);
    uart_insert_char(port, data);  // Add to buffer
  }
  
  return IRQ_HANDLED;
}

Platform-Specific Examples

Arduino/AVR

// Polling
void loop() {
  int sensor = analogRead(A0);
  if (sensor > 512) {
    digitalWrite(LED_PIN, HIGH);
  }
}

// Interrupt
ISR(PCINT0_vect) {  // Pin change interrupt
  if (digitalRead(SENSOR_PIN)) {
    dataReady = true;
  }
}

ARM Cortex-M (STM32)

// Enable EXTI interrupt on GPIO
void setup_button_interrupt() {
  RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;
  SYSCFG->EXTICR[0] |= SYSCFG_EXTICR1_EXTI0_PA;
  EXTI->IMR |= EXTI_IMR_MR0;
  EXTI->FTSR |= EXTI_FTSR_TR0;  // Falling edge
  NVIC_EnableIRQ(EXTI0_IRQn);
}

void EXTI0_IRQHandler(void) {
  if (EXTI->PR & EXTI_PR_PR0) {
    EXTI->PR = EXTI_PR_PR0;  // Clear flag
    button_pressed = 1;
  }
}

Linux Userspace (GPIO with poll())

// Use poll() to wait for GPIO interrupt from userspace
struct pollfd pfd;
pfd.fd = open("/sys/class/gpio/gpio17/value", O_RDONLY);
pfd.events = POLLPRI;

while (1) {
  poll(&pfd, 1, -1);  // Block until interrupt
  lseek(pfd.fd, 0, SEEK_SET);
  char value;
  read(pfd.fd, &value, 1);
  handle_gpio_event();
}

Python (Raspberry Pi with RPi.GPIO)

import RPi.GPIO as GPIO

# Polling
while True:
    if GPIO.input(BUTTON_PIN) == GPIO.LOW:
        handle_button()
    time.sleep(0.01)

# Interrupt
def button_callback(channel):
    print("Button pressed!")

GPIO.add_event_detect(BUTTON_PIN, GPIO.FALLING, callback=button_callback)

Common Pitfalls and Best Practices

Polling Pitfalls

  • CPU waste: Checking too fast wastes power
  • Missed events: Checking too slow misses short signals
  • Blocking: Tight polling loops prevent other tasks

Interrupt Pitfalls

  • Keep ISRs short: No delays, prints, or heavy computation
  • Volatile variables: Use volatile for shared data
  • Reentrancy: ISRs can interrupt each other—protect critical sections
  • Debugging is harder: Race conditions, timing issues

Golden Rules for ISRs

  1. Set flags, don’t do work: Process data in main loop
  2. No blocking calls: No delay(), printf(), malloc()
  3. Minimize execution time: Keep ISR under 10-50 μs if possible
  4. Use atomic operations: Protect shared variables

Hybrid Approach: Interrupt + Polling

Many real systems combine both:

volatile bool data_ready = false;

void UART_IRQHandler(void) {
  data_ready = true;  // Signal via interrupt
}

void main_loop() {
  while (1) {
    if (data_ready) {  // Poll the flag
      data_ready = false;
      process_uart_data();  // Heavy processing here
    }
    do_other_work();
  }
}

The interrupt catches the event immediately, but heavy processing happens outside the ISR.


Which Should You Use?

ScenarioRecommendation
Button press detectionInterrupt (with debouncing)
High-speed ADC continuous samplingPolling or DMA
UART/SPI data receptionInterrupt + buffer
Reading sensor every 1 secondPolling (with sleep between reads)
Emergency stop signalInterrupt (highest priority)
LED blinkingPolling (simple timer)
Battery-powered deviceInterrupt (to allow sleep modes)
Real-time critical loopPolling (avoid interrupt jitter)

Summary

  • Polling: Simple, predictable, good for frequent or continuous events. Wastes CPU when idle.
  • Interrupts: Efficient, fast response, good for rare events. More complex, requires careful ISR design.
  • Hybrid: Best of both—interrupt sets flag, main loop polls and processes.

Choose based on event frequency, power requirements, CPU utilization, and system complexity. When in doubt, start simple (polling), then optimize with interrupts if needed.

Understanding both patterns is essential for efficient embedded programming across all platforms—from Arduino to bare-metal ARM to Linux kernel drivers.