Interrupts
A program that keeps asking “did anything happen?” wastes the processor and arrives late. An interrupt reverses the relationship: the hardware gives notice, the program drops what it was doing, handles the event and returns exactly where it was.
01What exactly happens
| Step | What happens |
|---|---|
| 1 | The peripheral detects the event and sets its request flag. |
| 2 | If that interrupt is enabled and the global interrupts are too, the core finishes the current instruction. |
| 3 | It saves the return address and the processor state on the stack: this is the context. |
| 4 | It looks up the address of the interrupt service routine in the vector table and jumps there. |
| 5 | The routine runs. In many families you have to clear the flag by hand. |
| 6 | The return instruction restores the context and the program carries on as if nothing had happened. |
It is the time between the event and the first instruction of the routine. It is made up of what remains to finish the current instruction, saving the context and the jump: typically between 4 and 20 cycles. At 16 MHz that is on the order of a microsecond.
But the real latency can be vastly longer if at that moment another interrupt service routine is running, or a critical section with interrupts disabled. That is the reason why routines must be short.
02Where they come from
| Source | What it is used for |
|---|---|
| External, on a pin | A pushbutton, a limit switch, a sensor. Configurable for rising edge, falling edge, both, or level. |
| Pin change on a port | A single vector for a whole group of pins: the routine has to find out which one changed. |
| Timer | Overflow or compare: it is the basis of every periodic task and of the system timebases. |
| Serial communication | Byte received or transmit register empty. It avoids losing data without having to keep watching. |
| A/D converter | Conversion complete: the program does not wait, it gets notified. |
| Input capture | Stores the exact value of the timer when an edge occurs. It is the precise way to measure periods and pulse widths. |
| Watchdog | If the program hangs and does not refresh it, it resets the device. |
03Priorities and nesting
- In simple 8-bit microcontrollers, priority is given by the position in the vector table: it is not configurable, and there is no nesting unless you enable it by hand.
- On ARM Cortex-M, the interrupt controller lets you assign a numeric priority to each source, and nesting is automatic: lower number, higher priority.
- Nesting gives a better response to urgent events, but it uses more stack and makes it much harder to reason about the program.
- Highest: safety. Emergency stop, overcurrent, power failure.
- High: whatever is lost if it arrives late. Edge capture, high-speed serial reception.
- Medium: timebases and periodic control.
- Low: user interface, logging, non-critical communication.
04The problem with shared variables
An interrupt can occur between any two instructions of the main program. Everything that both of them touch is a potential source of errors that show up once in a thousand times, and only with the equipment in production.
/* BAD: the compiler may keep 'listo' in a register and never reread it */ uint8_t listo = 0; ISR(INT0_vect) { listo = 1; } while (!listo) { } /* may hang forever */ /* GOOD: volatile forces it to be read from memory on every pass */ volatile uint8_t listo = 0; /* BAD: 'pulsos' is 32 bits; on an 8-bit micro it is read in four steps */ volatile uint32_t pulsos; total = pulsos; /* if the interrupt falls in the middle, an impossible value comes out */ /* GOOD: critical section, as short as possible */ cli(); /* disables interrupts */ total = pulsos; sei(); /* enables them again */
- Every variable shared between an interrupt routine and the main program is declared volatile.
- Every access to a variable wider than the processor’s word size is done inside a critical section, as short as possible.
And a third one, more important than the other two: the interrupt service routine does the bare minimum. It sets a flag, stores a piece of data, and the heavy work is done by the main program.
- Waits and delays. They block the whole system.
- Writing to the serial port or to a display. These are slow operations.
- Floating-point calculations on a microcontroller without a floating-point unit.
- Allocating dynamic memory.
- Waiting for another interrupt: if they are disabled, it will never arrive.
05The pattern worth using every time
/* Pushbutton debouncing by interrupt + timer */ volatile uint8_t evento = 0; volatile uint16_t t_ultimo = 0; ISR(INT0_vect) /* falling edge on the pushbutton */ { uint16_t ahora = ms_actual(); if ((uint16_t)(ahora - t_ultimo) > 30) { /* 30 ms of bounce */ evento = 1; t_ultimo = ahora; } } /* the routine finishes in microseconds */ int main(void) { inicializar(); while (1) { if (evento) { cli(); evento = 0; sei(); /* the event is consumed without losing another one */ atender_pulsador(); /* here it is fine to take a while */ } otras_tareas(); } }
The routine is short and deterministic, the debouncing does not burn waiting time, and the heavy work happens in the main loop, where it can take as long as it needs without affecting anyone. The detail of the subtraction with an explicit cast keeps the count correct when the millisecond counter wraps around, which is the bug that shows up after 65 seconds of operation and baffles everybody.
06In the lab
Detect a pushbutton in two ways: by polling in the main loop and by interrupt, with the program also doing a long task. Use the oscilloscope to measure the time between the edge and the response in each case, and compare the worst case of the two.
Set a pin high as the first instruction of the interrupt service routine and observe on the oscilloscope the distance to the edge that triggered it. Repeat with another interrupt active and with a long critical section in the main program: you can see the latency grow.
Write the wait loop without volatile and compile with optimization enabled. Verify that it hangs. Look at the assembly listing to see that the variable is not even reread. Add volatile and check the difference. It is the best way to make sure you never forget it again.
Use the input capture interrupt to measure the period of a signal, and compare the accuracy with measuring it by polling. With capture, the timer value is taken by hardware at the instant of the edge: the difference in accuracy is remarkable.
07Common mistakes
| Symptom | Usual cause |
|---|---|
| The interrupt fires nonstop | The request flag is not cleared inside the routine. |
| The interrupt never fires | It has not been enabled individually, or the global interrupts have not been enabled, or the vector name is misspelled. |
| The program hangs in a wait loop | The shared variable is not declared volatile. |
| A multi-byte counter gives absurd values | It is read without a critical section and the interrupt lands in the middle of the read. |
| A pushbutton generates several events | Mechanical bounce. Filter it by time, not by adding waits inside the routine. |
| The system becomes erratic when interrupts are added | Stack overflow caused by nesting, or routines that are too long. |
| It fails at exactly 65 seconds | A 16-bit millisecond counter overflows and is compared incorrectly. |
| Everything stops for moments | There is a wait, a serial write or a heavy calculation inside an interrupt service routine. |
08Self-assessment
What does the processor save when it services an interrupt?
The return address and the processor state —the context— on the stack, so it can continue exactly where it was when the routine finishes.
What is the vector table?
A table in memory where each interrupt source has the address of its interrupt service routine. The core consults it to know where to jump.
What is latency and what does it depend on?
The time between the event and the first instruction of the routine. It depends on the current instruction, on saving the context and, above all, on whether another routine is running or a critical section is open.
Why must a shared variable be declared volatile?
Because without that keyword the compiler may keep it in a register and not read it from memory again. Since the interrupt modifies it “from outside,” the main program never finds out about the change.
What is a critical section and when is it needed?
A stretch of code with interrupts disabled. It is needed when reading or writing variables wider than the processor’s word size, so that the interrupt does not land in the middle of the operation.
Name three things that must not go inside an interrupt service routine.
Waits or delays, slow writes such as the serial port or a display, and heavy calculations or dynamic memory allocation.
How is pushbutton bounce handled with interrupts?
By comparing the current instant with that of the last valid event and discarding the ones that arrive within about 20 to 50 ms. Never with a wait inside the routine.
What advantage does the capture interrupt have over measuring by polling?
The timer value is taken by hardware at the exact instant of the edge, so the measurement does not depend on when the program gets around to reading it.
How are priorities assigned in a real system?
Highest for safety —emergency stops, overcurrent—, high for whatever is lost if it arrives late, medium for the timebases, and low for the user interface and logging.
Why must interrupt service routines be short?
Because while one is running, the others wait: every long routine increases the latency of all the others and can cause events to be lost.