Loading header...

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.

flowchart LR TMR["Timer overflow"] --> IRQ["IRQ request"] UART["UART RX byte"] --> IRQ PIN["External edge"] --> IRQ IRQ --> IC["Interrupt controller"] IC --> PEND["Pending bit"] PEND --> CU["CPU control unit"] CU --> DECIDE["Next fetch decision"]

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.

flowchart TB subgraph VT["Vector table"] RST["Reset vector"] INT0["External interrupt vector"] T0["Timer0 overflow vector"] UARTV["UART RX vector"] ADCV["ADC complete vector"] end EVT["Timer0 overflow pending"] --> T0 T0 --> ISR["Timer0 ISR"]

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:

  1. A peripheral sets an interrupt condition.
  2. The interrupt controller latches a pending bit.
  3. The CPU finishes the current instruction.
  4. Global and local interrupt enables are checked.
  5. Priority is resolved if more than one interrupt is pending.
  6. The CPU saves a return address and status information.
  7. The CPU loads the ISR address from the vector table.
  8. ISR code runs.
  9. A special return instruction restores state and resumes the main code.
sequenceDiagram participant Main as Main code participant IC as Interrupt controller participant Stack as Stack participant ISR as ISR Main->>Main: Execute instruction N IC->>IC: Latch pending IRQ Main->>IC: Check after instruction retires Main->>Stack: Save PC and status IC->>ISR: Select vector ISR->>ISR: Service event ISR->>Stack: Interrupt return restores PC and status Stack->>Main: Resume instruction N plus 1

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 that volatile does 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.

Mind Map

mindmap root((Interrupts)) Core concept Hardware event Pending bit Vector to ISR Return to main Entry path Finish instruction Check masks Resolve priority Save PC and status Load handler address Timing Latency event to ISR Jitter varies latency Worst case matters Tail chaining on Cortex M Design rules Keep ISR short Clear flags correctly Protect shared data Avoid blocking calls Practical checks Vector address Enable bits Stack margin Atomic access Common mistakes Missing global enable Long ISR Volatile as atomic Wrong flag order

-> Address Decoding and Chip Select