Programs so far have had a predictable structure: initialization, then an infinite loop in which the processor keeps asking the same things. The structure works as long as the world around has the patience to be asked. The problem shows up when it does not: a button pressed for a few tenths of a millisecond, a byte arriving on the serial line before the previous one has been read, a short pulse from a speed sensor. The mechanism that solves exactly this problem is called an interrupt - the doorbell, instead of repeated trips to the door to check whether someone has arrived.
1Purpose and structure of the course6 min
We will follow the interrupt step by step: what mechanically happens in the processor when it occurs, how to correctly write a handler routine, what the vector table is, what sources the ATmega328P has, how priority works on AVR and, most important for practice, why communication between a handler routine and the main program hides two traps that only discipline solves, not talent.
- Reading an input is done from PINx, never from PORTx (Lecture 04)
- Sampling a pin can miss a pulse shorter than the duration of a polling loop (Lecture 04) - exactly the problem this lecture solves
- Explain why cyclic polling can miss short events, with a concrete figure
- List, in order, the operations the processor executes when accepting an interrupt
- Write a correct, short handler routine, with no delays and no slow operations
- Explain what priority means on AVR and why there is no preemption between interrupts
- Recognize and fix a variable not protected with volatile and a race condition
2Cyclic polling and the interrupt alternative12 min
The classic method with no interrupts is called cyclic polling: in the main loop, the program periodically checks the state of each source it cares about.
while (1) {
new_state = (PIND & (1<<PD2)) ? 1 : 0;
if (old_state == 1 && new_state == 0) PORTB ^= (1<<PB5);
old_state = new_state;
read_temperature_sensor(); /* takes a few milliseconds */
update_display(); /* takes even longer */
}
The program has two flaws that no rewrite can fix. The first: wasted time - the processor reads PIND tens of thousands of times a second, and in the vast majority of cases the answer is "nothing happened"; in a battery-powered application, a processor that keeps asking can never be put into a low-power mode. The second, more serious: loss of information - between two checks of the button, the program runs the sensor read and the display update; if these take five milliseconds, a shorter pulse can fall entirely within that interval and disappear without a trace.
An optical sensor generates a 40 µs pulse on every rotation. The main loop, on an ATmega328P at 16 MHz, needs 1200 cycles for one complete pass.
See the solution
One cycle: T = 1/16×10⁶ = 62.5 ns. One complete loop pass: 1200×62.5 ns =
75 µs.
The 40 µs pulse is shorter than the interval between two consecutive reads - it can start and end completely between two glances of the program. Detection is not impossible, but it is random: the measured speed will be wrong unpredictably. For reliable detection by polling, the loop would need to close in under 640 cycles - very little room for the rest of the application.
3Characteristics of the interrupt system7 min
An interrupt is generated in response to a physical effect of a peripheral: a pin changing, a time period ending, a serial transmission finishing, an analog-to-digital conversion finishing. Their common trait is asynchrony - they occur at moments uncorrelated with the program's current position. This is precisely where the efficiency gain comes from: the program no longer needs to be built around the moments when events might occur.
Handling requires a dedicated subroutine, the handler routine, invoked automatically by hardware, never explicitly from the program. The occurrence of the event sets a flip-flop called the interrupt flag - exactly what solves the short-pulse problem: the pulse can physically disappear, but the bit that marks it stays set until it is handled or cleared.
4The operating principle, step by step13 min
The starting point is the main program (MPF), with the program counter (PC) holding the address of the next instruction. Assume a peripheral sets a flag and all three conditions are met.
The sequence of operations
1. The processor finishes the instruction currently in progress - a correctness guarantee, not a detail: stopping halfway would leave a partial write or an uncomputed result. An interrupt is never accepted in the middle of an instruction, only at the boundary between two. A long instruction adds to the response delay.
2. Saves the PC onto the stack (push(PC)) - the exact return
address. On the ATmega328P, two bytes, SP decreases by two. If the stack is corrupted, returning
sends the program to an arbitrary place in Flash.
3. Automatically disables interrupts, clearing the I bit in SREG - the equivalent of "unplugging the doorbell" while we are talking to the courier. Without this step, a second interrupt would arrive immediately, would save yet another address, would itself be interrupted in turn - the stack would grow without bound.
4. Loads into the PC the address of the corresponding vector, from the interrupt vector table.
5. Executes the handler routine - the main program is completely stopped, does not run in parallel, does not run more slowly, does not run at all.
6. Ends with reti (return from interrupt) - pops the address off the stack into the
PC, then re-enables interrupts, setting the I bit back. The difference from
ret is exactly this re-enabling.
The program would return correctly to the address, but would keep running with interrupts off - the system would appear "stuck" after the first interrupt, with no error message. It is a mistake impossible to make in C (the ISR macro always generates reti), but a real one in assembly.
Response latency
The time elapsed between an event and the first useful instruction of the routine is called latency - it is neither zero nor constant. Components: finishing the current instruction, recognizing the request and saving the PC, the jump from the vector table, the routine's prologue (saving context).
ATmega328P at 16 MHz. Recognizing the request takes 4 clock periods, the jump from the vector table 3 periods. Estimate the minimum latency.
See the solution
T = 1/16×10⁶ = 62.5 ns. Recognition + saving the PC: 4×62.5 = 250 ns.
Jump: 3×62.5 = 187.5 ns. Total: ~437.5 ns.
To this interval must be added finishing the instruction in progress (up to four periods) and the prologue generated by the compiler (for a routine with several variables, it can exceed twenty periods). Conclusion: the real latency is on the order of a microsecond, not tens of nanoseconds, and varies from one occurrence to another - for precise measurements we do not rely on the moment of entering the routine, but on a value captured directly by hardware (timer capture).
5The interrupt handler routine11 min
The handler routine, ISR (Interrupt Service Routine), is written like any function, but behaves differently.
Saving and restoring context
The hardware saves only the PC. Everything else - the SREG register with the condition flags (zero, carry, sign, overflow) and the working registers - is left up to the routine.
The main program performs a comparison, followed by a conditional branch based on it. If an interrupt occurs between the two, and the routine performs any arithmetic operation, the flags are overwritten - on return, the branch is taken based on the routine's computation, not the original comparison. The bug depends on the exact timing of the interrupt, so it appears once in a few thousand runs.
The set of SREG plus the registers used is called the context. In C, the ISR()
macro from avr/interrupt.h automatically generates the prologue and epilogue: it saves SREG, saves
exactly the registers the body actually uses, executes the body, restores everything in reverse
order and ends with reti.
#include <avr/io.h>
#include <avr/interrupt.h>
ISR(INT0_vect)
{
PORTB ^= (1 << PB5); /* the routine's body: a single operation */
}
INT0_vect is not just any parameter, but the symbolic name that ties the function to
its position in the table. Misspelled, the compiler accepts the code without complaint - the routine
is never called. When the event occurs, the program jumps into an unhandled entry and
resets (see the section on the vector table).
Writing rules
For the duration of the routine, the main program is stopped and, on AVR, the other interrupts are blocked. Any cycle wasted needlessly is a cycle in which the application responds to nothing else.
_delay_ms,
delay) - they stop the entire system. No slow operations - serial, EEPROM, floating point,
LCD, all take milliseconds. It returns no values, receives no parameters - there is no one to
pass them to or take them from, the call comes from hardware, not from the program. The central pattern: the
routine only notes the event, the main program processes it.6The interrupt vector table8 min
The link between the physical source and the handler routine is a fixed data structure in memory: the interrupt vector table. On AVR it sits at the start of Flash, from address zero.
On the ATmega328P (32 KB), each entry contains a full jmp instruction (two
words), because the distance to the routine can exceed the range of a relative jump. The vectors sit
two words apart: RESET at 0x0000, INT0 at 0x0002, INT1 at 0x0004.
It is the starting point after power-up, an external reset, or a watchdog timeout. A useful consequence: if an interrupt occurs with no routine written for it, the default entry sends execution to the RESET vector, and the program starts over from the beginning. A microcontroller that resets at seemingly random intervals is, very often, one with an interrupt source enabled and unhandled.
| Address | Source | Meaning |
|---|---|---|
| 0x0000 | RESET | power-up, external reset, watchdog |
| 0x0002 | INT0 | external interrupt 0 (PD2) |
| 0x0004 | INT1 | external interrupt 1 (PD3) |
| 0x0006-0x000A | PCINT0-2 | level change on ports B, C, D |
| 0x0012, 0x001A, 0x0020 | TIMERn_OVF | overflow of counters 0-2 |
| 0x0024, 0x0028 | USART_RX, USART_TX | reception / transmission finished |
| 0x002A | ADC | analog-to-digital conversion finished |
The regularity of the table is not accidental: sources that need a fast reaction and are directly tied to the outside (external interrupts) sit at the head of the table, the slow ones (tied to memory) at the end - the position in the table has an extra role, discussed under priorities.
7Interrupt sources: external and internal9 min
Sources fall into external (the event comes from a pin) and internal (the event is produced by a module inside the chip).
External interrupts
Detection happens in two ways. Level-triggered: the request exists continuously as long as the line is in the active state - if the routine ends and the line is still low, the request reappears immediately, execution without end until the source releases the line. Edge-triggered: the request appears just once, at the transition (rising, falling, or either) - the mode used in most applications, because the event is by nature a single point in time.
The ATmega328P has two full external interrupts, INT0 (PD2) and INT1 (PD3), configured in
EICRA through the pairs ISC01/ISC00 and ISC11/ISC10: 00 low level, 01 any
change, 10 falling edge, 11 rising edge. Local enable through
EIMSK, flags in EIFR (INTF0, INTF1).
Communication peripherals (USART, SPI, I2C) generate interrupts when data is ready to transfer or has been received. At 115200 b/s, a character arrives every ~87 µs - without an interrupt, the program would need to check the status register at least twice that frequency, impossible to reconcile with any other activity.
Internal interrupts
Counters generate overflow (a fixed period, dictated by size and divider) or compare match (a freely chosen period, through the compare register). The ADC generates an interrupt at the end of a conversion - the natural solution for acquisitions: the conversion is started, the processor does something else for tens of microseconds, the routine picks up the result.
For most sources, the flag is cleared automatically by hardware on entering the routine. If we handle it through polling, with no routine, manual clearing is done by writing a one into the bit, not zero. For other sources (USART reception), the flag clears itself when the data register is read - if the routine forgets to read UDR0, the flag stays set and the interrupt keeps recurring endlessly.
8Priorities and interrupt nesting9 min
Priority decides the handling order when several requests occur simultaneously, or when a new request arrives while another is being handled. It can be hardware (fixed by construction) or software (chosen by the programmer, through a configuration register) - 32-bit architectures usually offer a controller with programmable levels.
A mechanism common to all architectures is the interrupt mask: temporarily disabling some sources, regardless of priority. A masked source does not disappear - its flag keeps being set, and when the mask is lifted, the accumulated request is handled immediately.
How things stand on AVR
Priority matters only when two or more requests are pending at the same time, at the moment the processor is ready to accept an interrupt - then the one with the smaller vector is handled first, the other stays pending and is handled right after. No request is ever lost.
Once inside a routine, with the I bit cleared automatically, no other interrupt can interrupt it,
however high its priority. An INT0 request arriving while an ADC interrupt routine is running does not
pull the processor out of the ADC routine - it waits patiently until reti. Practical consequence:
on AVR, priority matters much less than the duration of the routines - the best guarantee of a
fast response for a critical source is not its position in the table, but the fact that all
other routines are short.
Manually re-enabling interrupts inside a routine (sei() or
ISR_NOBLOCK) produces nesting - routines that can interrupt each other. Almost
always a bad idea, for three reasons: stack consumption grows with every level of
nesting, on a 2 KB memory; the risk of recursion, if the flag of the routine's own source is not
handled before re-enabling; and the resulting complexity makes it impossible to verify all
the possible interleavings of execution. The correct fix for a routine that is too long is not
nesting, but shortening it.
10A complete application using the interrupt system11 min
A complete application: the LED toggles state on every press of a button, using INT0, on the rising edge.
The setup and initialization
The button on PD2 (the only pin with INT0), wired between the pin and VCC, with an external ~10 kΩ pull-down - low at rest, high when pressed (rising edge). The LED on PB5, with a limiting resistor calculated as in Lecture 04.
static void init_ports(void) {
DDRB = (1 << DDB5); PORTB = 0x00; /* PB5 output, LED off */
DDRD = 0x00; PORTD = 0x00; /* PD2 input, no pull-up (external) */
}
static void init_interrupt(void) {
EICRA |= (1 << ISC01) | (1 << ISC00); /* rising edge on INT0 */
EIFR |= (1 << INTF0); /* clear any leftover flag */
EIMSK |= (1 << INT0); /* local enable */
}
ISR(INT0_vect) { PORTB ^= (1 << PB5); } /* toggle the LED; nothing else */
int main(void) {
init_ports(); init_interrupt();
sei(); /* global enable, after all initialization */
while (1) { /* free for anything else */ }
}
sei() is always called at the end of initialization: if
interrupts were active earlier, a routine might run before the data structures it
uses have been initialized.The Arduino variant
volatile bool toggle_request = false;
void handle_button() { toggle_request = true; } /* just flags it */
void setup() {
pinMode(PIN_LED, OUTPUT);
pinMode(PIN_BUTTON, INPUT);
attachInterrupt(digitalPinToInterrupt(PIN_BUTTON), handle_button, RISING);
}
void loop() {
if (toggle_request) {
toggle_request = false;
digitalWrite(PIN_LED, !digitalRead(PIN_LED));
}
}
The difference is not just syntax: here the routine does not directly command the output, it only
raises a flag, and the toggle happens in loop() - because
digitalRead/digitalWrite are not elementary operations (they translate a pin into
port+mask, check for an active PWM), taking a few microseconds, a lot for a handler
routine. The same pattern: the routine notes it, the loop processes it.
11The contact bounce problem6 min
The program above works perfectly in simulation. On a real setup, the LED will sometimes toggle once, other times three or five times for a single press - contact bounce (Lecture 04), seen by the interrupt system just as faithfully as any other edge.
const unsigned long MIN_INTERVAL = 40; /* milliseconds */
volatile bool toggle_request = false;
volatile unsigned long last_moment = 0;
void handle_button() {
unsigned long now = millis(); /* can be read, does not advance inside the routine */
if (now - last_moment >= MIN_INTERVAL) {
toggle_request = true;
last_moment = now;
}
}
The first form stays correct even when the millisecond counter exceeds its maximum value and wraps to zero - subtraction in unsigned arithmetic gives the correct difference regardless. The second form produces, at the moment of wraparound, a button that stops responding for a long interval.
The hardware alternative, an RC filter possibly followed by a Schmitt trigger, does not consume processor time, but requires one extra component per button - the decision depends on how many buttons the setup has and how critical processor time is.
millis() relies on a counter interrupt. Since
interrupts are disabled inside a handler routine, the value returned by
millis() does not advance for the duration of its execution - it can be read (useful for
marking the moment of the event), but it cannot measure a duration inside the routine, and even
less can it be used for waiting. A delay() placed inside a handler routine never
finishes.12Frequent mistakes4 min
- "A longer handler routine does no harm, it just does a bit more." False - for its duration, the main program is completely stopped and, on AVR, all other interrupts are blocked. Any wasted cycle is a cycle in which the application responds to nothing else, including critical events. The routine only notes the event (a flag, a read, an increment); processing happens in the main loop.
- "A global variable read both in an interrupt and in the main loop does not need anything
special if it is small."
False - without
volatile, the compiler may keep a stale copy in a register, and the loop never sees the update. For variables larger than one byte, even withvolatile, the read is not atomic and can reassemble a value that never existed. Declare any shared variablevolatile; protect reads of more than one byte withATOMIC_BLOCK. - "If a source has higher priority, it can interrupt any other routine in progress." False on AVR - there is no preemption. Once inside a routine, no other interrupt can interrupt it, regardless of priority; it waits patiently until reti. Do not rely on priority for a fast response - the real guarantee comes from the fact that all routines are short.
13Summary and glossary5 min
Cyclic polling can miss events shorter than the duration of one loop iteration;
the interrupt solves this through a hardware flag that remembers the event even if the
physical signal has already disappeared. On accepting a request, the processor finishes the current
instruction, saves the PC onto the stack, disables interrupts globally, jumps to the vector from
the table, executes the routine and returns with reti, which re-enables interrupts - unlike
ret. A handler routine must be short, with no delays and no slow operations: it notes the
event, leaving processing for the main loop. On AVR, priority is given exclusively by the
vector's position in the table and only matters between simultaneously pending requests - there is no
preemption between routines already in progress. Communication through global variables requires
volatile for visibility correctness and, for variables larger than one byte, critical
sections for atomic reads - otherwise race conditions appear, hard to reproduce and devastating when
they do.
14Self-check questions7 min
- Explain why cyclic polling cannot guarantee the detection of a short event, even if the loop is optimized. What quantity must be compared with the event's duration?
- List, in order, the operations of the AVR processor between accepting a request and the first instruction of the routine. Which are hardware and which are generated by the compiler?
- What is the difference between ret and reti? What happens if a routine mistakenly ends with ret?
- What is meant by the processor's context, and why must it be saved on entering a routine? Give a concrete example of a bug caused by not saving SREG.
- An ATmega328P resets at irregular intervals, even though the program contains no reset instruction, and the watchdog is disabled. Propose a plausible explanation.
- On AVR, INT0 has higher priority than USART reception. An INT0 request arrives while the reception routine is executing. What happens, and why?
- A 16-bit counter is incremented in a routine and read in the main program. Construct a sequence of events in which the program obtains a value that never existed.
15Directions for further study2 min
The next lecture uses exactly the interrupt mechanism discussed here, applied to a specific peripheral: timer/counter modules - the time base, generating PWM and capturing external events with clock-cycle precision.
The setup, initialization and handler routine from this lecture become a real setup in Laboratory 03.