Interrupts - How a CPU Responds Between Instructions
A microcontroller cannot spend all its time asking every peripheral, "Do you need service now?" Timers overflow, UART bytes arrive, ADC conversions finish, and external pins change while the main program is doing something else. Interrupts give hardware a controlled way to pause the normal instruction stream, run a short service routine, and return.
An interrupt is best understood as a hardware-triggered function call that happens between instructions. The CPU finishes the current instruction, saves enough state to resume later, loads a handler address from a vector table, runs the Interrupt Service Routine (ISR), and restores the saved state on return.
Learning Objectives
By the end of this lesson, you should be able to:
- describe the electrical and architectural path from IRQ line to ISR;
- explain why interrupts occur at instruction boundaries;
- identify the role of vector tables, stack frames, masks, and priority;
- compare simple AVR-style interrupt handling with ARM Cortex-M NVIC behavior;
- write safer rules for ISR design in embedded C and assembly.
What an Interrupt Request Is
An interrupt request is a hardware signal plus a latched pending bit. A peripheral asserts an interrupt condition. The interrupt controller records it. The CPU samples the controller after the current instruction completes.
The CPU normally does not stop halfway through an ordinary instruction. Finishing one instruction before entering the ISR keeps the architecture state consistent: registers, flags, memory writes, and the program counter are not left half-updated.
The Vector Table
When an interrupt is accepted, the CPU must know where the ISR starts. That mapping lives in the vector table.
Different architectures store the table differently:
| Architecture | Vector table style | Important detail |
|---|---|---|
| 8085 | fixed restart entry points | limited, fixed vectors |
| classic AVR | jump instructions near start of flash | each vector usually branches to ISR code |
| ARM Cortex-M | table of 32-bit handler addresses | table base can be relocated with VTOR on many cores |
The vector table is not optional. If the vector points to the wrong location, the CPU will execute the wrong code with interrupt privileges and timing assumptions.
The Interrupt Entry Sequence
A typical interrupt entry looks like this:
- A peripheral sets an interrupt condition.
- The interrupt controller latches a pending bit.
- The CPU finishes the current instruction.
- Global and local interrupt enables are checked.
- Priority is resolved if more than one interrupt is pending.
- The CPU saves a return address and status information.
- The CPU loads the ISR address from the vector table.
- ISR code runs.
- A special return instruction restores state and resumes the main code.
On AVR, the return instruction is RETI. On ARM Cortex-M, exception return is encoded through a special return value in the link register and hardware unstacking.
What Gets Saved
Architectures differ, so always check the manual.
| Item | AVR-style expectation | ARM Cortex-M expectation |
|---|---|---|
| Return address | saved by hardware | stacked by hardware |
| Status flags | often saved/restored by interrupt mechanism or compiler prologue | xPSR stacked by hardware |
| General registers | compiler/assembly ISR must preserve used registers | hardware stacks a defined core set; compiler preserves more if needed |
| Global interrupt enable | usually cleared on entry | controlled by priority/masking rules |
The practical lesson is simple: do not write naked ISR assembly unless you know exactly which registers and flags must be preserved.
Timing, Latency, and Jitter
Interrupt latency is the time from event occurrence to the first useful instruction in the ISR. Interrupt jitter is variation in that latency.
title "Illustrative interrupt latency"
time start=0 end=10 unit=us divisions=10
EVENT: pulse label="IRQ event" low=0 high=1 at=1 width=0.4 unit=logic color=#dc2626
PEND: step label="pending bit" low=0 high=1 at=1 unit=logic color=#f59e0b
CPU: pulse label="ISR starts" low=0 high=1 at=3 width=4 unit=logic color=#2563eb
marker FINISH at=2.4 label="current instruction ends"
marker VECTOR at=3 label="vector loaded"
This waveform is explanatory, not a measured trace. Real latency depends on clock rate, current instruction length, memory wait states, interrupt masking, priority, and the architecture's entry sequence.
Priority, Masking, and Nesting
Interrupt systems need rules for conflicts.
- Masking disables interrupt acceptance globally or for a specific source.
- Priority decides which pending interrupt is handled first.
- Nesting allows a higher-priority ISR to interrupt a lower-priority ISR.
- Tail-chaining lets some ARM Cortex-M cores move directly from one ISR to another without fully restoring and stacking again.
| Feature | Simple AVR-style system | ARM Cortex-M NVIC |
|---|---|---|
| Priority | mostly fixed or simple | programmable priority fields |
| Nesting | usually disabled unless explicitly enabled | natural for higher-priority exceptions |
| Entry overhead | small and predictable | hardware stacking, priority logic, tail-chaining |
| Best fit | tight simple control loops | many peripherals with real priority needs |
Priority is useful, but it is also a design responsibility. A high-priority ISR that runs too long can starve lower-priority work.
Worked Example: UART Receive Interrupt
Polling version:
while (1) {
if (uart_rx_ready()) {
byte = uart_read();
process(byte);
}
run_control_loop();
}
Interrupt-driven version:
volatile uint8_t rx_ring[64];
volatile uint8_t head;
void USART_RX_vect(void) {
uint8_t b = UART_DATA_REG;
uint8_t next = (uint8_t)((head + 1u) & 63u);
rx_ring[head] = b;
head = next;
}
The ISR should do the minimum: read the hardware register before it overflows, store the byte, and return. Parsing commands, printing logs, or waiting for another byte belongs in the main loop or a task, not in the ISR.
Safe ISR Rules
- Keep ISRs short and bounded.
- Clear or acknowledge the peripheral interrupt flag exactly as the datasheet requires.
- Mark variables shared with main code as
volatile, but remember thatvolatiledoes not make multi-byte operations atomic. - Protect shared multi-byte data with critical sections or lock-free patterns.
- Avoid blocking calls, delays, dynamic memory allocation, and long formatted printing inside ISRs.
- Confirm stack size, especially when nested interrupts or deep call chains are possible.
- Measure worst-case interrupt latency if the event protects safety, timing, or data integrity.
Common Mistakes
- Forgetting to enable the peripheral interrupt and the global interrupt mask.
- Clearing an interrupt flag in the wrong order, causing immediate re-entry.
- Doing too much work inside the ISR.
- Sharing data between ISR and main code without atomic protection.
- Assuming interrupts can occur in the middle of an ordinary instruction.
- Using non-reentrant library code from both main code and ISR.
- Changing vector-table placement without updating startup code or linker settings.
Summary
Interrupts let hardware request service without constant polling. The CPU accepts the request at an instruction boundary, saves return state, vectors to an ISR, and returns with a special interrupt return sequence. Correct interrupt design depends on vector-table setup, flag handling, priority, latency, atomic shared data, and short bounded handlers.
Further Reading
- Microchip, AVR Instruction Set Manual,
RETI, status register, and interrupt behavior. - Arm, Cortex-M Generic User Guide, exceptions, NVIC, priority, and exception return.
- Jack Ganssle, The Art of Designing Embedded Systems, interrupt latency and embedded timing.
- Barr Group, Embedded C Coding Standard, ISR and shared-data guidance.