Embedded
Interrupts
A mechanism for the hardware to force the CPU to stop what it's doing and run a specific handler in response to an event — a timer tick, an incoming UART byte, a GPIO edge — without the main loop having to poll for it.
- What it is
- Hardware forcing the CPU to pause the main code and jump to a handler in response to an event, instead of software polling for it.
- Mental model
- A tap on the shoulder that interrupts whatever you're doing — the CPU finishes its current instruction, handles the tap, then resumes exactly where it left off.
- Key numbers
- Latency = time to first ISR instruction; jitter = variation in that latency, usually the number that matters more for control loops.
- Reach for it when
- An event happens unpredictably or rarely and polling for it would waste CPU cycles — a button press, a byte arriving, a timer tick.
The basic flow
- A peripheral (or the core itself) asserts an interrupt request.
- The interrupt controller (e.g. NVIC on Cortex-M) checks it's enabled and its priority beats what's currently running.
- The CPU finishes its current instruction, saves context (registers, return address), and jumps to the handler address in the vector table.
- The ISR (Interrupt Service Routine) runs.
- On return, the CPU restores context and resumes exactly where it left off.
Priority and nesting
Most microcontroller interrupt controllers support priority levels: a higher-priority interrupt can preempt a lower-priority one already running (nesting), while equal-or-lower priority interrupts wait. Getting priority assignment wrong is a common source of missed real-time deadlines — e.g. a low-priority ISR that runs too long can delay a time-critical one if priorities aren't set correctly.
Latency and jitter
- Latency
- Time from the triggering event to the first instruction of the ISR — includes any currently-running higher/equal priority code, plus fixed hardware entry overhead (register stacking, pipeline flush).
- Jitter
- Variation in that latency between occurrences — often the more important number for control loops, since a fixed delay can be compensated for but variance can't.
Shared state: the classic bug class
An ISR and the main loop (or two ISRs of different priority) can run "at the same time" from the main loop's perspective, so any data they both touch is a race condition waiting to happen.
// BUG: main loop reads a 32-bit counter the ISR increments.
// On an 8-bit core, the read isn't atomic — the ISR can fire
// between reading the low and high half, producing a torn value.
volatile uint32_t tick_count;
void TIMER_IRQHandler(void) {
tick_count++;
}
uint32_t safe_read_tick_count(void) {
uint32_t v;
__disable_irq(); // or a narrower critical section
v = tick_count;
__enable_irq();
return v;
}
volatilestops the compiler from caching a value in a register across accesses — it does not make an access atomic or provide any ordering/memory-barrier guarantee.- Keep ISRs short: do the minimum (capture data, set a flag), defer real work to the main loop. Long ISRs increase latency for everything else.
- Disabling interrupts to protect a critical section works, but widens worst-case latency for every other interrupt — scope it as tightly as possible.
Related buses
Peripherals like SPI and I2C commonly use an interrupt (transfer-complete, or a dedicated data-ready pin serviced by a GPIO edge interrupt) instead of polling a status register in a busy loop.
Real-world applications
- UART receive — a byte-received interrupt lets firmware react immediately without a busy-wait loop burning CPU time between bytes.
- Sensor data-ready pins — an accelerometer or ADC toggling a GPIO when a new sample is ready is far more efficient than polling a status register.
- Real-time control loops — a timer interrupt firing at a fixed rate (e.g. a motor PWM update or a PID loop tick) is the backbone of most closed-loop control firmware.
- Wake-from-sleep — a button press or RTC alarm interrupt is typically what brings a low-power MCU out of sleep mode.
- Safety-critical faults — overcurrent or overtemperature comparators trigger an interrupt so firmware can shut something down within microseconds, not on the next polling pass.