Catto / Topic Map · Digital Electronics IV Year 7
Digital Electronics IV · 120 h · Topic 3 of 6

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.

Vector Latency Priorities volatile

01What exactly happens

mainprograminterruptroutinenormal coderesumes where it left offinterrupt routineeventlatency1. The peripheral sets its request flagThe event occurred. The flag stays set even if the interrupt is not serviced yet.2. The core finishes the current instructionAn instruction is never interrupted halfway: it is completed first.3. It saves the context on the stackReturn address and processor state, so it can come back to exactly the same point.4. It jumps to that interrupt’s vectorIt looks up the routine’s address in the vector table and jumps there.5. The routine runs, kept shortIt sets a flag or stores a piece of data. In many families the request must be cleared by hand.6. It restores the context and continuesThe return instruction puts everything back as it was: the main program never notices.Latency grows enormously if another routine is running or a critical section is open: that is why routines must be short.
Figure 1. The complete sequence, animated. Between the event and the first instruction of the interrupt service routine some time passes —the latency— which you need to know when timing matters.
StepWhat happens
1The peripheral detects the event and sets its request flag.
2If that interrupt is enabled and the global interrupts are too, the core finishes the current instruction.
3It saves the return address and the processor state on the stack: this is the context.
4It looks up the address of the interrupt service routine in the vector table and jumps there.
5The routine runs. In many families you have to clear the flag by hand.
6The return instruction restores the context and the program carries on as if nothing had happened.
Latency

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

SourceWhat it is used for
External, on a pinA pushbutton, a limit switch, a sensor. Configurable for rising edge, falling edge, both, or level.
Pin change on a portA single vector for a whole group of pins: the routine has to find out which one changed.
TimerOverflow or compare: it is the basis of every periodic task and of the system timebases.
Serial communicationByte received or transmit register empty. It avoids losing data without having to keep watching.
A/D converterConversion complete: the program does not wait, it gets notified.
Input captureStores the exact value of the timer when an edge occurs. It is the precise way to measure periods and pulse widths.
WatchdogIf the program hangs and does not refresh it, it resets the device.

03Priorities and nesting

timemainlow priorityhigh priorityA arrivesB arrivesmainroutine Aroutine BmainB waitsWithout nestingB arrives while A is running and has to wait for A to finish. If A is long, B is served late or lost.mainAroutine BA continuesmainWith nestingB interrupts A because it has higher priority, and A resumes afterwards. Better response, more stack and harderto reason about.In simple 8-bit micros, priority is set by position in the vector table; on ARM it is configurable.
Figure 2. Two interrupts that arrive almost together, animated. With nesting, the higher-priority one interrupts the lower-priority one; without nesting, it waits for the other to finish.
How ties are resolved
  • 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.
How they are assigned
  • 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 */
The two rules that prevent 90% of the problems
  1. Every variable shared between an interrupt routine and the main program is declared volatile.
  2. 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.

What never goes inside an interrupt service routine
  • 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();
    }
}
Why this pattern works

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

Lab 1 · Polling versus interrupt

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.

Lab 2 · Measuring the latency

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.

Lab 3 · Reproducing the volatile bug

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.

Lab 4 · Measuring frequency by capture

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

SymptomUsual cause
The interrupt fires nonstopThe request flag is not cleared inside the routine.
The interrupt never firesIt 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 loopThe shared variable is not declared volatile.
A multi-byte counter gives absurd valuesIt is read without a critical section and the interrupt lands in the middle of the read.
A pushbutton generates several eventsMechanical bounce. Filter it by time, not by adding waits inside the routine.
The system becomes erratic when interrupts are addedStack overflow caused by nesting, or routines that are too long.
It fails at exactly 65 secondsA 16-bit millisecond counter overflows and is compared incorrectly.
Everything stops for momentsThere 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.

Development of the topic “Interrupts” of Digital Electronics IV (Year 7), based on the “Curriculum Proposal – Second Cycle of the Technical-Vocational Track, Secondary Education – Electronics,” Ministry of Education of the Province of Córdoba, DGETyFP. Back to the Topic Map · catto.ar