An interrupt is a hardware or software signal that causes the processor to suspend its current execution context, save some or all state (the extent varies by architecture), and branch to a dedicated handler routine called an interrupt service routine (ISR). Once the ISR completes, the processor restores the saved state and resumes normal execution.
In practice
Interrupts are the primary mechanism by which embedded systems respond to asynchronous events without continuously polling peripherals. A UART receive interrupt, for example, fires when a new byte arrives in the receive register, allowing the CPU to spend the rest of its time doing useful work rather than spinning in a polling loop. Similarly, a timer interrupt can trigger at a precise interval to drive a control loop or update a software clock tick.
Writing correct ISRs requires discipline. ISRs should be kept as short as possible: read or clear the hardware flag, copy data to a shared buffer or set a flag, and return. Long ISRs block lower-priority interrupts, increase latency, and make the system harder to reason about. The blog post "Introduction to Microcontrollers - Interrupts" covers the fundamentals of this discipline clearly.
Data shared between an ISR and the main code (or between two ISRs) is a common source of bugs. Variables shared across these contexts should be declared volatile where appropriate to prevent the compiler from caching them in registers, but volatile alone does not provide atomicity, mutual exclusion, or guaranteed memory ordering. Access to multi-byte or non-atomic values must also be protected against torn reads and writes using techniques such as disabling the relevant interrupt around the access, using a critical section, or using C11 atomic operations. The post "Scorchers, Part 3: Bare-Metal Concurrency With Double-Buffering and the Revolving Fireplace" illustrates practical concurrency techniques in exactly this scenario.
In an RTOS environment, ISRs typically do minimal work and then post to a queue, semaphore, or event flag to wake a task. Most RTOS APIs provide ISR-safe variants of these calls (e.g., FreeRTOS's xSemaphoreGiveFromISR). Calling a non-ISR-safe API from an ISR is a frequent source of hard-to-reproduce crashes.
Frequently asked
What is the difference between an interrupt and an ISR?
An interrupt is the event or signal that diverts the processor from its current task. An ISR (interrupt service routine) is the function the processor executes in response to that event. The two terms are often used interchangeably in casual conversation, but they refer to different things: one is the trigger, the other is the handler.
Why must variables shared with an ISR be declared volatile?
The compiler does not know that an ISR can modify a variable asynchronously. Without the
volatile qualifier, it may optimize repeated reads into a single cached register value, causing the main code to never see an update written by the ISR. volatile tells the compiler not to optimize away accesses to the object, forcing it to re-read from memory each time. Note that volatile does not guarantee atomicity, mutual exclusion, or memory ordering across cores or memory buses; for those guarantees, also use interrupt disabling, critical sections, or C11 atomic operations as appropriate.
How do I safely share multi-byte data between an ISR and main code?
Reading or writing a multi-byte value is not guaranteed to be atomic. If an ISR fires in the middle of a read in main code, you can get a torn value combining old and new bytes. The standard approaches are to disable the relevant interrupt around the access, use a
critical section, or use a double-buffer scheme. The post "Scorchers, Part 3: Bare-Metal Concurrency With Double-Buffering and the Revolving Fireplace" covers practical patterns for this.
What happens if an interrupt fires while another ISR is already running?
This depends on the architecture and priority configuration. On many MCUs, a higher-priority interrupt can preempt a running ISR (
nested interrupts), while an interrupt of equal or lower priority will normally wait until the current ISR returns. However, nesting behavior depends on the CPU core and interrupt controller configuration: some systems disable all interrupts during ISR entry by default, and some provide only limited nesting. Improperly configured priorities or re-entrant ISRs that are not designed for nesting can cause
stack overflows or data corruption. The post "Cortex-M Exception Handling (Part 1)" goes into detail on how Cortex-M handles this.
What does 'keeping the ISR short' actually mean in practice?
It means doing only what cannot be deferred:
acknowledge or clear the hardware interrupt flag, copy incoming data to a buffer, or set a flag/semaphore to signal a task or the main loop. Avoid calling blocking functions, performing lengthy computations, or running communication protocols inside the ISR. Move that work into a task or the main loop that processes the flag or buffer set by the ISR. The post "Introduction to Microcontrollers - More On Interrupts" expands on this principle.
Differentiators vs similar concepts
An interrupt is often confused with polling. Polling has the CPU repeatedly check a status register in a loop, consuming cycles regardless of whether an event has occurred. An interrupt lets the hardware notify the CPU only when an event occurs, freeing the CPU in between. The choice between the two involves trade-offs in
latency, power consumption, and code complexity: polling can have lower and more deterministic latency for very fast events, while interrupts are more efficient when events are infrequent or unpredictable.