LABORATORY 03

The Interrupt System

Duration: 2 hours Material: Chapter 3 Registers: EICRA · EIMSK · SREG PDF handout RO versiunea română

Interrupts let the microcontroller react immediately to external events, without wasting time constantly checking the state of pins. They are the mechanism that makes the difference between a system that misses events and one that responds with a guarantee.

1Lab objectives

  • Understanding the difference between polling and interrupts
  • Configuring the external interrupts INT0 and INT1 through EICRA and EIMSK
  • Correctly writing a handling routine (ISR)
  • Using the volatile qualifier and understanding why
  • Measuring the response latency of both approaches

2Materials needed

  • 1 Arduino Uno
  • 1 Breadboard
  • 2 Buttons
  • 2 LEDs + 220 Ω resistors
  • 6 Jumper wires

3Why interrupts are needed

In a typical program, checking a button happens inside loop(). If the loop also contains other operations that take time - an LCD update, an ADC conversion, a delay() - the event can be missed entirely.

A concrete exampleA loop() that contains delay(200) checks the button only 5 times per second. A short, 80 ms press has roughly a 60% chance of falling entirely inside a pause - and of being lost without a trace.

Interrupts solve the problem at the hardware level: when the event occurs, normal execution is suspended immediately, the handling routine runs, then the program resumes exactly where it left off.

4The handling mechanism

main program ISR routine external event (edge on a pin) 1. the flag is set 2. PC is saved on the stack 3. interrupts disabled 5. reti() restores PC 6. interrupts re-enabled 4. the ISR body runs (must be VERY short) normal execution continues exactly where it left off
Fig. 1 - The complete sequence for handling an interrupt. The main program does not "know" it was suspended: the context is saved and restored automatically.
  1. The event occurs (an edge on a pin, a timer overflow, an ADC conversion finishing, etc.)
  2. The corresponding interrupt flag is set automatically
  3. If interrupts are enabled globally (sei()) and locally, the processor finishes the current instruction
  4. The return address is saved on the stack, and global interrupts are disabled
  5. Execution jumps to the address in the interrupt vector table
  6. The ISR runs; at reti() the context is restored and interrupts are re-enabled

5Polling vs. interrupt

Press the button (or click directly on the chart) to generate an external event. Increase the duration of the CPU load and watch how latency grows in the polling variant, while the interrupt responds instantly, regardless of load.

Interactive timing diagram - response latency
CriterionPollingInterrupt
Typical latencythe duration of one loop iterationunder 5 µs
Worst-case latencythe duration of the entire loopthe duration of the longest active ISR
Processor loadcontinuous, even with no eventsonly when an event occurs
Code complexitysimplerequires volatile and care around critical sections
Suited forslow signals, periodic checksshort, rare, time-critical events

6Interrupt sources on the ATmega328P

SourceArduino pinAVR pinISR vectorNotes
INT0D2PD2INT0_vectconfigurable edge
INT1D3PD3INT1_vectconfigurable edge
PCINT0...7D8-D13port BPCINT0_vectany change, grouped by port
PCINT8...14A0-A5port CPCINT1_vectany change
PCINT16...23D0-D7port DPCINT2_vectany change
Timer1 compare A--TIMER1_COMPA_vectsee Laboratory 4
ADC complete--ADC_vectsee Laboratory 5
USART receive--USART_RX_vectsee Laboratory 6
INT0/INT1 versus PCINTThe INT0 and INT1 interrupts can selectively detect a rising or a falling edge and each has its own vector. PCINT interrupts fire on any change and are grouped by port - the ISR must determine in code which pin changed, and in which direction.
Interrupt priorityThe order in the vector table determines priority: the lower the address, the higher the priority. RESET has the highest priority, followed by INT0, then INT1. Priority only matters when two interrupts become pending at the same time - it does not interrupt an ISR that is already running.

7Configuring the registers

ISC01ISC00INT0 triggerArduino equivalentUse
00LOW level (as long as the pin is 0)LOWwaking from sleep; keeps re-triggering
01any level changeCHANGEencoders, counting both edges
10falling edge (1 → 0)FALLINGbuttons with pull-up
11rising edge (0 → 1)RISINGbuttons with pull-down, sensors active on HIGH

Change the bits below and watch the resulting value of the registers. The default configuration corresponds to detecting a falling edge on INT0, with the interrupt enabled.

EICRA and EIMSK - configuring the external interrupts
The EIMSK register with the INT1 and INT0 bits
Fig. 2 - The EIMSK register (External Interrupt Mask Register): only bits 1 and 0 are used, for individually enabling INT1 and INT0. Fig. 3.2 in the handout
The EIFR register with the INTF1 and INTF0 flags
Fig. 3 - The EIFR register (External Interrupt Flag Register): the flags are set automatically by hardware when the interrupt occurs and clear themselves on entering the ISR. Fig. 3.3 in the handout
Manually clearing a flagThe flags in EIFR are cleared by writing a logic 1 (not 0!) into that position. This is a convention found on most flag registers, across every microcontroller: EIFR |= (1 << INTF0);
The I bit in SREGThis is the global interrupt switch. sei() sets it, cli() clears it. Hardware clears it automatically on entering an ISR and restores it at reti() - which is why, by default, an ISR cannot be interrupted by another one.

8Why volatile is mandatory

The compiler optimizes code by assuming it knows everything that happens to a variable. An ISR, however, runs "from outside" the normal flow, and the compiler cannot anticipate this.

what happens without volatile
bool ready = false;                 // volatile is MISSING!

ISR(INT0_vect) { ready = true; }

void loop() {
  while (!ready) { }                 // the compiler sees that "ready" does not change
                                    // inside the loop, so it loads it ONCE into a
                                    // CPU register and tests the same copy forever
                                    // -> INFINITE LOOP
  ready = false;
  digitalWrite(13, HIGH);           // never reached
}
The effect of optimizationThe code works perfectly compiled with no optimizations and hangs completely with -O2. This is exactly the kind of bug that shows up "only on the real board" and vanishes under a debugger. volatile forces the variable to be re-read from memory on every access.

Atomic access to multi-byte variables

The ATmega328P is an 8-bit processor. Reading a 16- or 32-bit variable takes several steps. If the interrupt occurs in between, the result combines halves from different values:

protected read
volatile uint16_t counter = 0;       // 2 bytes on an 8-bit processor

ISR(INT0_vect) { counter++; }

uint16_t readSafely() {
  uint16_t v;
  uint8_t sreg = SREG;              // save the interrupt state
  cli();                            // critical section: stop interrupts
  v = counter;                      // atomic read, in 2 uninterrupted steps
  SREG = sreg;                      // restore the previous state
  return v;
}
Why SREG is savedA plain cli() followed by sei() would enable interrupts even if the caller had already disabled them. Saving and restoring SREG keeps the exact state.

9Simulator: the flow of an ISR

Watch what happens when the event occurs right in the middle of a delay(). Notice that the ISR runs immediately, and the main program resumes exactly where it was interrupted.

Interrupting an ongoing delay()
The correct patternThe ISR only raises a flag and increments a counter. All the processing - printing, calculations, communication - happens in loop(), where the duration does not affect the interrupt system.

10Mandatory rules for an ISR

Restrictions
  • An ISR must be very short - typically it just sets a flag or increments a counter
  • Do not use delay() - it relies on timer interrupts, which are disabled
  • Do not perform any slow input-output operation - for example sending text to the computer, which you will study in the sixth lab: it takes a long time and itself uses interrupts
  • Any variable shared with the main program must be declared volatile
  • Reading 16/32-bit variables in the main program must be protected
  • millis() can be read inside an ISR, but it does not advance while the ISR is running
What you do in the ISRWhat you do in loop()
incrementing a counterprinting over serial
raising a flagfloating-point calculations
reading a register that would otherwise be lostwriting to an LCD or an SD card
saving a timestampanything that takes more than a few microseconds

11Source code

The Arduino variant

interrupt_arduino.ino
const uint8_t BUTTON_PIN = 2;    // on the Uno only D2 and D3 have INT0/INT1
const uint8_t LED_PIN   = 13;

volatile uint16_t pressCount   = 0;   // volatile: modified in the ISR
volatile bool     newEvent = false;

void handlePress() {             // interrupt routine
  pressCount++;                  // short and fast
  newEvent = true;
}

void setup() {
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(LED_PIN, OUTPUT);
  DDRB |= 0b00001111;                // pins 8..11: the counter, in binary
  attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), handlePress, FALLING);
}

void loop() {
  if (newEvent) {                    // the processing happens HERE
    newEvent = false;
    digitalWrite(LED_PIN, !digitalRead(LED_PIN));
    // show the counter in binary on pins 8..11
    PORTB = (PORTB & 0xF0) | (pressCount & 0x0F);
  }

  delay(200);                        // a long task - the event is NOT lost
}

The direct register access variant

interrupt_registers.ino
volatile uint16_t counter = 0;

ISR(INT0_vect) {              // the external interrupt 0 vector
  counter++;
  PORTB ^= (1 << PB5);        // toggle the LED on pin 13
}

void setup() {
  DDRB  |= (1 << PB5);        // pin 13 output
  DDRD  &= ~(1 << PD2);       // pin 2 input
  PORTD |= (1 << PD2);        // internal pull-up

  EICRA |=  (1 << ISC01);     // ISC01 = 1
  EICRA &= ~(1 << ISC00);     // ISC00 = 0  -> falling edge
  EIMSK |=  (1 << INT0);      // enable INT0

  sei();                      // enable global interrupts
}

void loop() {
  // the main program stays completely free
}
Practical setup with two buttons for the interrupt application
Fig. 4 - The practical setup of the interrupt application: two buttons connected on the pins with INT0 and INT1. Fig. 3.4 in the handout

Debounce inside an interrupt

interrupt_debounce.ino
volatile unsigned long lastInterrupt = 0;
volatile uint16_t realPressCount = 0;

void handlePress() {
  unsigned long now = millis();          // millis() can be READ inside the ISR
  if (now - lastInterrupt > 50) {        // ignore anything within 50 ms
    realPressCount++;
    lastInterrupt = now;
  }
}

void setup() {
  pinMode(2, INPUT_PULLUP);
  DDRB |= 0b00001111;                     // pins 8..11: the counter, in binary
  attachInterrupt(digitalPinToInterrupt(2), handlePress, FALLING);
}

void loop() {
  static uint16_t shown = 0;

  // protected read of the 16-bit variable
  uint8_t sreg = SREG;
  cli();
  uint16_t v = realPressCount;
  SREG = sreg;

  if (v != shown) {                       // it changed: refresh the bar
    shown = v;
    PORTB = (PORTB & 0xF0) | (v & 0x0F);
  }
}

Measuring latency

latency.ino
// Connect pin 7 to pin 2 with a short wire.
// Pin 7 generates the pulse, pin 2 detects it through an interrupt.
// Latency is MEASURED WITH THE OSCILLOSCOPE, on channels 7 and 4:
// the distance between the falling edge on 7 and the rising edge on 4.

void onEdge() {
  PORTD |= (1 << PD4);         // first instruction in the ISR: raise the marker
}

void setup() {
  pinMode(7, OUTPUT);
  digitalWrite(7, HIGH);
  pinMode(4, OUTPUT);          // marker for entering the ISR
  pinMode(2, INPUT);
  attachInterrupt(digitalPinToInterrupt(2), onEdge, FALLING);
  delay(100);
}

void loop() {
  PORTD &= ~(1 << PD4);        // lower the marker before the measurement
  digitalWrite(7, HIGH);
  delay(50);

  digitalWrite(7, LOW);        // generate the falling edge = time zero

  delay(500);
}
What you will measureSet the oscilloscope on the two channels, with a time base of 1 µs/div, and trigger on the falling edge of pin 7. The typical value is 4-8 µs. It includes recognizing the edge, finishing the current instruction, saving the context, jumping to the vector, and calling the function registered through attachInterrupt. An ISR written directly with ISR(INT0_vect) is 1-2 µs faster, because it removes one level of indirection.

12Interactive circuit

The difference between polling and interrupts is best understood when you see a press get lost. The circuit below has two counters shown in binary on LEDs: one updated by polling in a loop with long pauses, the other from an interrupt routine. Press the button rapidly and watch which one falls behind.

Polling vs. interrupt - press quickly and compare
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

/* Each counter is shown in binary on 4 LEDs:
   pins  8..11 = presses caught by INTERRUPT
   pins  3..6  = presses caught by POLLING      */

volatile unsigned char byInterrupt = 0;
unsigned char byPolling = 0;

ISR(INT0_vect)                  // runs at any time, immediately
{
    byInterrupt++;
    PORTB = (PORTB & 0xF0) | (byInterrupt & 0x0F);
}

int main(void)
{
    DDRB  |= 0x0F;                       // pins 8..11, outputs
    DDRD  |= (1 << PD3) | (1 << PD4) | (1 << PD5) | (1 << PD6);
    DDRD  &= ~(1 << PD2);                // pin 2, input
    PORTD |=  (1 << PD2);                // internal pull-up

    EICRA |= (1 << ISC01);      // falling edge
    EIMSK |= (1 << INT0);
    sei();

    while (1) {
        /* the main loop is busy for 400 ms with "something else" */
        _delay_ms(400);

        if (!(PIND & (1 << PD2))) {      // polling: only catches what it happens upon
            byPolling++;
            /* bits 3..6 of PORTD, without disturbing the pull-up on PD2 */
            PORTD = (PORTD & 0x87) | ((byPolling & 0x0F) << 3);
        }
    }
}
Try thisPress the button ten times, quickly. Below each bar you are shown the number in binary and decimal. The interrupt counter catches all of them, because the hardware interrupts execution regardless of where the program happens to be. The polling counter falls well behind: it only sees the button at the instant the program reaches that line, once every 400 ms. The difference between the two numbers is exactly the number of lost presses.
Exercise - why volatile is mandatory
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

volatile unsigned char presses = 0;

ISR(INT0_vect)
{
    /* Rule: an ISR must be very short.
       Here we only count - the rest happens in the main loop. */
    presses++;
}

int main(void)
{
    DDRB  |= (1 << PB5) | 0x0F;   // the threshold LED + the 4-LED bar
    DDRD  &= ~(1 << PD2);
    PORTD |=  (1 << PD2);

    EICRA |= (1 << ISC01);
    EIMSK |= (1 << INT0);
    sei();

    while (1) {
        /* Complete this:
           1. Safely read the variable "presses" into a local copy.
              To do this, stop interrupts with cli(), copy, then sei().
           2. Show the copy in binary on the bar:
                 PORTB = (PORTB & 0xF0) | (copy & 0x0F);
           3. If 5 presses have accumulated, light the LED on pin 13.
           4. At 10 presses, bring the counter back to 0 and turn off the LED. */

        _delay_ms(100);
    }
}
Try thisThe word volatile tells the compiler that the variable can change "behind its back", from an interrupt routine. Without it, the compiler is allowed to read the variable once and keep using the copy in a register - and the loop would never notice the presses.

13Work tasks

  • Build the circuit with a button on pin 2 and an LED on pin 13.
  • Implement counting presses by polling, with delay(200) in the loop. Press rapidly and observe how many presses are lost.
  • Rewrite the program using interrupts and repeat the test - compare the results.
  • Remove the word volatile and compile with optimizations; observe whether the program hangs.
  • Add bounce filtering inside the ISR and check that each physical press produces exactly one increment.
  • Configure the second button on INT1 (pin 3) so that it resets the counter.
  • Change the trigger type from FALLING to CHANGE and explain why the counter increases twice as fast.
  • Measure the interrupt latency with the program above and compare it with the polling latency.

14Extended application

Extension - rotary encoderConnect an incremental rotary encoder to pins D2 and D3. Use interrupts on both channels, triggered on CHANGE, and determine both the number of steps and the direction of rotation, by comparing the state of the two signals at the moment of the edge. Print the position over serial and light a different LED for each direction.

Hint: when rotating clockwise, signal A leads signal B by 90°; rotating the other way, the relationship reverses.

15Review questions

16Resources