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
- Keep interrupt service routines (ISRs) short: capture data, clear the flag, defer the rest to the main loop or a task.
- Mark ISR-shared variables
volatileand protect multi-step updates with critical sections or atomic operations. - Set interrupt priorities deliberately and measure latency and handler duration.
- Debounce mechanical inputs in time, not with delays inside ISRs.
- Use hardware timers for periodic work, PWM, input capture, and timeouts; enable a watchdog.
- Write drivers as non-blocking state machines with timeouts, error states, and interfaces that can be tested off-target.
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:
- Read the data or status, clear the interrupt flag, and defer work by setting a flag, pushing to a queue, or notifying an RTOS task (FreeRTOS's
...FromISRAPIs, Zephyr's work queues). - Never block: no delays, no waiting on buses, no
printf, no dynamic allocation. - Are measured: toggling a GPIO at entry and exit shows duration on a logic analyzer. See Hardware Debugging with JTAG and Logic Analyzers.
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
- Non-blocking: start an operation, return immediately, and complete it later via interrupts, DMA callbacks, or polling a state machine.
- Timeouts and recovery for every operation that depends on external hardware. See I2C, SPI, and UART.
- Explicit states and errors instead of hidden flags, with error counters exposed for diagnostics.
- Thin hardware layer: isolate register access behind a small interface so driver logic can be unit-tested on a host with fakes.
- Vendor HALs (STM32 HAL and LL, ESP-IDF drivers, Zephyr device drivers) speed development; read their behavior on errors and their blocking variants carefully. See STM32.
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
- Microcontrollers — Peripherals, interrupts, and memory
- Embedded C — volatile, registers, and fixed-width types
- Real-Time Operating Systems — Deferring work to tasks
- I2C, SPI, and UART — Buses that drivers talk to
- STM32 — HAL, LL drivers, and NVIC
- Hardware Debugging with JTAG and Logic Analyzers — Measuring interrupt timing