Interrupt Handling on Microcontrollers

IoTEmbeddedFirmwareARM
Share on LinkedIn

The UART RX interrupt fired, set packet_ready = 1, and the main loop never noticed because the compiler kept the flag in a register across a tight polling loop. That bug took two days and a logic analyzer to find — and it is entirely preventable if you treat interrupt handling as a concurrency problem, not a hardware checkbox. On ARM Cortex-M and similar MCUs, interrupts preempt your main code unpredictably. ISRs that run too long, touch shared state without rules, or fight over NVIC priorities will corrupt data or miss deadlines long before your application logic gets blamed.

NVIC and priority groups

Cortex-M uses the Nested Vectored Interrupt Controller. Each IRQ has a priority (lower number = higher urgency on most vendors). Configure in startup or HAL:

// STM32 HAL example — set TIM2 lower priority number than UART
HAL_NVIC_SetPriority(TIM2_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(TIM2_IRQn);

HAL_NVIC_SetPriority(USART1_IRQn, 3, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);

Priority grouping (preemption vs subpriority) varies by vendor reference manual — read it once and draw a table:

IRQ Preemption Purpose
EXTI fault 0 Emergency stop
ADC DMA complete 1 Control loop sample
USART1 2 Command channel
SysTick 3 RTOS tick

Never enable interrupts before priorities are configured. Default priorities after reset are often identical — tie-breaking by IRQ number is not a design.

ISR structure: minimal and deterministic

volatile uint8_t rx_byte;
volatile bool rx_pending = false;

void USART1_IRQHandler(void) {
    if (USART1->SR & USART_SR_RXNE) {
        rx_byte = (uint8_t)(USART1->DR & 0xFF);
        rx_pending = true;
        // Clear flags per reference manual — some status bits clear on read
    }
}

Do not printf in an ISR. Do not malloc. Do not wait on mutexes with indefinite timeout.

For multi-byte buffers, use a ring buffer written in ISR and read in main:

typedef struct {
    uint8_t buf[256];
    volatile uint16_t head;
    volatile uint16_t tail;
} ring_t;

bool ring_push(ring_t *r, uint8_t b) {
    uint16_t next = (r->head + 1) % sizeof(r->buf);
    if (next == r->tail) return false;  // full — drop or count overflow
    r->buf[r->head] = b;
    r->head = next;
    return true;
}

On Cortex-M3+, aligned 16-bit reads of head and tail are often safe one-way (ISR produces, main consumes) if each index is updated by only one context. For both-direction sharing, use __atomic or critical sections.

Deferred processing with RTOS

With FreeRTOS, defer work to a task:

BaseType_t woken = pdFALSE;

void TIM2_IRQHandler(void) {
    if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE)) {
        __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);
        vTaskNotifyGiveFromISR(sensorTaskHandle, &woken);
        portYIELD_FROM_ISR(woken);
    }
}

The task waits on ulTaskNotifyTake, processes the sample batch with interrupts enabled, and can block on queues safely.

Critical sections in main code

When main reads a multi-field snapshot that ISRs update:

uint32_t enter_critical(void) {
    uint32_t primask = __get_PRIMASK();
    __disable_irq();
    return primask;
}

void exit_critical(uint32_t primask) {
    if (!primask) __enable_irq();
}

void read_snapshot(Snapshot *out) {
    uint32_t m = enter_critical();
    *out = shared;  // copy struct atomically w.r.t. ISRs
    exit_critical(m);
}

Alternatively raise BASEPRI to mask interrupts below a threshold — useful in RTOS when you must not block the SysTick.

Debugging spurious and missed interrupts

Use GPIO toggle pins around ISR entry/exit for scope measurement. If ISR duration exceeds your control loop period, redesign deferral.

Priority inversion on Cortex-M

NVIC priority groups matter when ISRs interact with RTOS:

// Lower number = higher priority (ARM convention)
NVIC_SetPriority(UART_IRQn, 5);      // data acquisition — high
NVIC_SetPriority(SysTick_IRQn, 15);  // RTOS tick — lower
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY = 5;  // FreeRTOS config

ISRs above configMAX_SYSCALL_INTERRUPT_PRIORITY cannot call FreeRTOS API — calling xQueueSendFromISR from a too-high-priority ISR corrupts the kernel. Chart ISR priorities on paper before wiring drivers.

Zero-latency vs deferred tradeoff

Pattern Latency Complexity Use when
Full handling in ISR Microseconds Low Single register flip
Flag + main loop Milliseconds Low Non-critical sampling
RTOS deferred task Milliseconds Medium Buffer processing, I/O
DMA + half-complete callback Minimal CPU High Streaming ADC, UART

Motor control and fast safety interlocks belong in ISR or hardware (comparator + timer break). Logging, JSON parsing, and network TX belong in deferred context — no exceptions.

Testing interrupt behavior

Pair with embedded memory constrained design when ISR stacks compete with heap budgets.

Production checklist

Common production mistakes

Teams get interrupt handling wrong in predictable ways:

Production implementations of interrupt handling fail when staging mirrors production topology poorly, rollback is untested, and on-call runbooks describe the happy path only.

Resources

Frequently asked questions

How long should an interrupt service routine be?

Keep ISRs as short as possible — typically clear the interrupt flag, capture hardware state or timestamp, and signal a task via semaphore or flag. Heavy processing belongs in main loop or RTOS task context where interrupts can be re-enabled and stack usage is predictable. Long ISRs increase latency for all lower-priority interrupts.

What NVIC priority scheme should I use on Cortex-M?

Assign highest priority (lowest numeric value) to safety-critical, latency-bound ISRs like motor fault or communication timeout. Medium priority for periodic sampling. Lowest for soft events. Never call blocking RTOS APIs from ISRs unless using FromISR variants. Document the priority map — conflicts cause subtle starvation.

Why do I need volatile for variables shared between ISR and main code?

The compiler optimizes main-loop code assuming no concurrent modification. Without volatile, it may cache a flag in a register and miss ISR updates. Use volatile for simple flags and counters; for multi-byte structures use atomic operations or disable interrupts briefly in main code during reads.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →