Interrupts, Timers, and Peripheral Drivers

Microcontroller firmware reacts to the outside world through interrupts — a button press, a byte arriving on a UART, a timer expiring, a DMA transfer completing — and controls it through peripherals such as timers, ADCs, serial buses, and GPIO. Code that handles these well is responsive and predictable; code that does not hangs when two interrupts collide, corrupts data shared between an interrupt and the main loop, or misses events because one handler took too long.

The core discipline is simple to state: do as little as possible in interrupt handlers, protect shared data, let hardware timers and DMA do the timing and data movement, and structure drivers as non-blocking state machines that never wait forever. This page covers each piece with patterns that work with or without an RTOS.

TL;DR

Quick Example

A debounced button interrupt feeding an event queue, with the main loop doing the real work (C, with generic HAL calls):

A non-blocking sensor driver written as a state machine with a timeout:

Core Concepts

Interrupt Handlers

When an interrupt fires, the CPU suspends the current code, runs the ISR, and returns. While it runs, lower-priority interrupts wait, so long handlers increase latency for everything else and can cause missed events. Good handlers:

Shared Data, volatile, and Atomicity

volatile tells the compiler a variable can change outside normal program flow, so it must be re-read each time — required for variables shared with ISRs and for hardware registers. It does not make operations atomic. A 32-bit counter on an 8-bit microcontroller, a read-modify-write of a flag word, or a multi-field struct update can be interrupted halfway. Protect them with critical sections (briefly disabling interrupts), atomic instructions where available, or single-producer, single-consumer designs like the queue above. See Embedded C.

Priorities and Nesting

Interrupt controllers such as the Arm NVIC support priorities and nesting: higher-priority interrupts preempt lower ones. Assign priorities by deadline — motor control and communication receive paths high, housekeeping low — and remember that with an RTOS, interrupts that call kernel APIs must be at or below the kernel's configured maximum syscall priority. See Real-Time Operating Systems.

Debouncing

Mechanical switches bounce for several milliseconds, generating many edges per press. Debounce by ignoring edges within a time window (as above), by sampling the pin periodically from a timer and requiring stable readings, or with hardware filtering. Never use busy-wait delays inside an ISR to debounce.

Timers

Use hardware timers for anything timing-critical instead of software loops, and handle counter wraparound by comparing differences (now - start >= interval) with unsigned arithmetic, as in the examples. Kick the watchdog only from a place that proves the whole system is healthy, not from an ISR that keeps running when the main loop hangs.

DMA

Direct memory access moves data between peripherals and memory without the CPU: ADC samples into buffers, UART reception into ring buffers, SPI transfers to displays. Use double buffering or half-transfer interrupts to process one half while the other fills, and handle cache coherency on cores with data caches.

Designing Peripheral Drivers

Best Practices

Defer Work Out of Interrupts

Capture, clear, and signal in the ISR; process in the main loop or a task.

Protect Every Shared Variable

Use volatile for visibility and critical sections or lock-free single-producer designs for atomicity.

Let Hardware Keep Time

Use timers, input capture, PWM, and DMA instead of software delays and polling loops.

Bound Every Wait

Give each driver operation a timeout and a recovery path, and count failures.

Common Mistakes

Doing Real Work in the ISR

Torn Reads of Shared Data

Blocking Forever on a Peripheral

FAQ

Why should interrupt handlers be short?

Because while one runs, lower-priority interrupts and the main code are delayed. Long handlers increase latency, cause missed events, and make timing unpredictable.

Does volatile make a variable thread-safe?

No. It prevents the compiler from caching the variable, but multi-step or multi-byte operations still need critical sections or atomic operations to avoid being interrupted midway.

How do I debounce a button?

Ignore additional edges for a short window after the first one, or sample the input periodically from a timer and accept a change only after several consistent readings.

When should I use DMA?

For moving blocks of data between peripherals and memory — ADC sampling, serial reception, display updates — so the CPU can sleep or do other work during the transfer.

What is a watchdog timer?

A hardware timer that resets the microcontroller if firmware does not periodically refresh it, recovering the device from hangs. Refresh it only when the system is demonstrably making progress.

Related Topics

References