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
| Aspect | Polling | Interrupts |
|---|---|---|
| CPU Usage | High—continuously checks even when idle | Low—CPU free until event occurs |
| Response Time | Depends on polling frequency and loop duration | Fast—responds immediately (within μs) |
| Missed Events | Can miss fast events if polling is slow | No—hardware captures the event |
| Complexity | Simple, easy to debug | More complex—concurrency issues, ISR constraints |
| Power Consumption | High—CPU always running | Low—CPU can sleep between interrupts |
| Predictability | Deterministic, easy to control timing | Can disrupt timing-critical code |
When to Use Polling
✅ Good Use Cases
- Events happen frequently or continuously (e.g., sensor reads every 100ms)
- Simple applications where CPU has nothing else to do
- Timing-critical loops where interrupts would disrupt precision (e.g., bit-banging protocols)
- Easy debugging needed—no ISR race conditions
- 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
- Rare or unpredictable events (e.g., button press, external signal)
- Low-power applications where CPU should sleep
- Fast response required (e.g., emergency shutdown, communication protocols)
- Multiple concurrent tasks (RTOS or multitasking)
- 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
volatilefor shared data - Reentrancy: ISRs can interrupt each other—protect critical sections
- Debugging is harder: Race conditions, timing issues
Golden Rules for ISRs
- Set flags, don’t do work: Process data in main loop
- No blocking calls: No
delay(),printf(),malloc() - Minimize execution time: Keep ISR under 10-50 μs if possible
- 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?
| Scenario | Recommendation |
|---|---|
| Button press detection | Interrupt (with debouncing) |
| High-speed ADC continuous sampling | Polling or DMA |
| UART/SPI data reception | Interrupt + buffer |
| Reading sensor every 1 second | Polling (with sleep between reads) |
| Emergency stop signal | Interrupt (highest priority) |
| LED blinking | Polling (simple timer) |
| Battery-powered device | Interrupt (to allow sleep modes) |
| Real-time critical loop | Polling (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.