Tangible Computing
14. Interrupts




14.1 How does the outside world get my attention?

The Arduino programs you have created detect changes in the world by polling. That is, your program reads some kind of input from the external world and sees if it has changed. Thus, you detect that a button has been pressed by noticing that it has changed from being up the last time you polled to being down for this poll. Think of polling as in taking a survey - to detect a change in mood of the population you have to ask them again.

The problem with polling is that most change events happen infrequently, and so much of the time a poll is not detecting any change in the world. Thus a substantial amount of processor time is wasted. Even worse, if the change event is of very short duration, then unless you poll at a rate fast enough to see the event, you will miss it. So the worst kind of events to look for, as far as a processor is concerned, are infrequent and of short-duration.

One way to address this problem is through an interrupt. An interrupt is the hardware equivalent of the unhaappy crowd throwing a brick through your window. No need to poll them to see if they are unhappy.

Interrupts are conceptually simple. An interrupt is associated with an outside event by attaching an interrupt handling procedure to it. When the event occurs, if enabled, it will create an interrupt that causes the interrupt handling procedure to be called. You can think of an interrupt as creating an unexpected procedure call in the middle of whatever code happens to be running. When the interrupt handler returns, the program resumes right after the point it was interrupted. The process of detecting an event, interrupting the current execution of the process, calling the hander, doing work associated with the interrupt, and then returning back to the original execution is called servicing the interrupt.

Interrupts are such a powerful notion that they are even used in casual conversation, as in, "hold on a bit while I service this phone interrupt from my mother."

14.2 The process for servicing an interrupt

Here is a naive example:

code/Interrupts/IntBlink/IntBlink.cpp

    #include <Arduino.h>
    #include <TimerThree.h>
    // variables that can be concurrently updated should be marked volatile
    // so the compiler will not optimize them.
     
    volatile uint8_t state = 0;  // the LED state 0 LOW or 1 HIGH
     
    #define LED 12
     
    void tick() {
      // this procedure is called on each timer period expire
      state = !state;
      digitalWrite(LED, state);
      }
     
    long int tick_period = 500000;
     
    void setup()
    { Serial.begin( 9600 );
      Serial.println("Starting");
     
      Timer3.initialize();
     
      pinMode(LED, OUTPUT);
     
      // tick will occur every 500000 uS, i.e. 1/2 second.
      Timer3.attachInterrupt(tick, tick_period);
    }
     
    void loop() {
     
        Serial.print("x");
        delay(3000);
     
    }




14.3 Pitfalls

There are many pitfalls to consider when handling interrupts, but the most important are:

  1. Modifying a shared data structure while the same data structure is in the process of being modified by another block of code. This concurrent update can result in an inconsistent state for the data structure.

  2. Taking too much time to handle the interrupt. Since the handler interrupts the flow of the currently executing code, it makes the original code run longer. This may be a factor if the original code was doing something time critical, like generating a video signal.

  3. Attempting to perform an operation that requires interrupts to be enabled. While executing an interrupt handler, interrupts are usually disabled. Thus you should not invoke an operation that requires interrupts in order to succeed. For example, reading from a serial port usually requires an interrupt to signal the arrival of a character. So it is generally not a good idea to read inside and interrupt handler.
The issue of concurrent updates can be quite subtle. Even updating a single variable can be a problem. An int on the Arduino consists of two bytes. So even something simple like x = y to assign one int to another takes a number of instructions. The interrupt can occur during this sequence, and corrupt the assignment if it happens to also modify x or y. (Note, this case has actually occured in radiotheraphy machines, and caused patient deaths.)

To address this problem requires the notion of a critical section. In essence, a critical section is a piece of code that once entered must run to completion without being interrupted. For example, the code that updates a shared resource must be allowed to complete the update in order to maintain consistency of the resource. Concurrent updates and critical sections appear in all non-trivial systems, and must be carefully addressed.

Things get more complicated if higher priority interrupts are allowed to interrupt a handler in the process of servicing the interrupt. This can lead to a priority inversion, in which a low priority task prevents a high priority one from running. One way to prevent priority inversions is to disable all interrupt processing while an interrupt is being serviced - all the more important to service an interrupt quickly. This is done with the interrupts() procedure to turn them on, and noInterrupts() to turn them off.

You may noteice the keyword volatile in the code below. Basically it's a directive to the compiler signaling that this variable may change "on its own" (e.g., by an interrupt call changing the variable's value). Compilers know that under normal circumstances a variable doesn't change values unless a new value is assigned to it. It can use this fact to optimize code to run faster. For example, suppose a variable is read from memory into a register in order to do a comparison on it, then some other computation is done unrelated to that variable, and then finally the code checks that original variable again; the compiler knows that it does not have to reload the value from memory, since it's the same value still in the loaded register. By labelling a variable as volatile, the compiler is informed that the variable's value may change independently of the executing code. And so it should not just use the value from the register but should reload it from memory since an interrupt may have caused it to change. See Volatile variable for more detail.

14.4 Examples of Timer Interrupts



code/Interrupts/IntBlink2/IntBlink2.cpp

    #include <Arduino.h>
    #include <TimerThree.h>
     
    // n beats against m 
     
    #define LED1 11
    #define LED2 12
     
    // 1/2 of the period of each of the lights, in milliseconds
    // the light is on for half and off for half
    int half_period1;  
    int half_period2;  
     
    // the number of ticks since the last light change
    int nticks1 = 0;
    int nticks2 = 0;  
     
    // the current state of the light
    int state1 = 0;
    int state2 = 0;
     
    // the clock ticks once every millisecond
    void tick() {
      // this procedure is called 1000 times per second
     
      // if exceeded the period, change state of output value and reset
      if ( nticks1 >= half_period1 ) {
        state1 = ! state1;
        digitalWrite(LED1, state1);
        nticks1 = 0;
      } else {
        nticks1++;
      }
     
      // if exceeded the period, change state of output value and reset
      if ( nticks2 >= half_period2 ) {
        state2 = ! state2;
        digitalWrite(LED2, state2);
        nticks2 = 0;
      } else {
        nticks2++;
      }
     
    }
     
    void setup()
    { Serial.begin( 9600 );
      Serial.println("Starting");
     
      Timer3.initialize();
     
      pinMode(LED1, OUTPUT);
      pinMode(LED2, OUTPUT);
     
      // tick will occur at 1000 Hz
      Timer3.attachInterrupt(tick, 1000);
     
      half_period1 = 300;
      half_period2 = 200;
    }
     
    void loop() {
     
        // do something boring
        Serial.print("x");
        delay(50);
     
     
    }


code/Interrupts/CriticalSect/CriticalSect.cpp

    #include <Arduino.h>
    #include <TimerThree.h>
     
    // Example of incorrect critical section handling
     
    #define LED1 11
    #define LED2 12
     
    /* interrupts can be manipulated with 
        noInterrupts(); 
        interrupts(); 
    we can protect critical sections with noInterrups - interupts pairs.
    We have to make sure any concurrent accesses are protected, not just
    modifying ones.
    */
     
     
    long int tick_period = 1000;
    const int list_len = 32;
    volatile int list[list_len];
     
    // add 1 to each element of list l
    void inc_list(volatile int * l, int len) {
        int i;
        for (i=0; i<len; i++) { l[i] = l[i] + 1; }
        }
     
    // check that l is in increasing order by increment 1,
    // return true of so, false if not
    int list_ok(volatile int * l, int len) {
        int i;
        for (i=0; i<len-1; i++) { 
            if ( l[i+1] != l[i] + 1 ) {
                return 0;
                }
            }
        return 1;
        }
     
    // the clock ticks once every timer expire
    void tick() {
      inc_list(list, list_len);
    }
     
    void setup()
    { Serial.begin( 9600 );
      Serial.println("Starting");
     
      Timer3.initialize();
     
      pinMode(LED1, OUTPUT);
      pinMode(LED2, OUTPUT);
     
      // initialize the list
      for (int i=0; i < list_len; i++) {
        list[i] = i;
        } 
     
      // tick is called 10^6/tick_period times per second
      Timer3.attachInterrupt(tick, tick_period);
     
    }
     
    long int step_num = 0;
    void loop() {
        step_num++;
     
        // noInterrupts();
        inc_list(list, list_len);
        // interrupts();
     
        if ( ! list_ok(list, list_len) ) {
            Serial.print(step_num);
            Serial.println(" List is broken");
            }
        delay(50);
    }




14.5 External Interrupts and Switch Debouncing

More interesting, and less predicable interrupts from from outside the processor chip. This example attaches an external button to one of the interrupt pins (pin 21) which is attached to interrupt number 2.

code/Interrupts/Button/Button.cpp

    // the external LED is attached to this pin.
    // onboard LED is attached to pin 13, 
    byte ledPin = 13;
    byte pressPin = 11;
     
    /* interrupt information
     
    Most Arduino boards have two external interrupts: 
        0 (on digital pin 2) 
        1 (on digital pin 3) 
    The Arduino Mega has an additional four: 
        2 (pin 21)
        3 (pin 20)
        4 (pin 19)
        5 (pin 18). 
    */
     
    // the pushbutton is attached to this digital input pin
    int buttonPin = 21;
    int buttonInterrupt = 2;
     
    // state of the button: 0 is up, 1 is pushed
    // volatile because altered in an interrupt service routine
     
    volatile byte buttonState = 0;
     
    void buttonPress() {
     
      if ( buttonState == 0 ) {
          buttonState = 1;
          digitalWrite(pressPin, HIGH);
          /* 
            Normally you should not do I/O like this in a handler.
          The only reason it works here is that interrupts are not
          required for output to the on-chip serial ports.  So this
          works.  Interrupts are needed for reading, so you should not
          do a read inside a handler.
          */
     
          Serial.println("*");
      }
    }
     
    void buttonHandled() {
      // indicate that the button interrupt has been handled and
      // we are ready for more
      
      digitalWrite(pressPin, LOW);
      /* since the only other code that could set the button state
        is in the interrupt handler, and buttonState is a single byte,
        then this operation is safe.  buttonState changes in one 
        instruction.
      */
      buttonState = 0;
    }
      
    void setup() {
      Serial.begin( 9600 );
      Serial.println("Starting");
     
      // configure ledPin to be a digital output
      pinMode(ledPin, OUTPUT);
      pinMode(pressPin, OUTPUT);
      
      digitalWrite(pressPin, LOW);
     
      // set buttonPin to INPUT and 
      // turn on internal pull up resistor 
      pinMode(buttonPin, INPUT);
      digitalWrite(buttonPin, HIGH);
      
      // establish interrupts on button transitions
     
      // FALLING means create an interrupt on a HIGH to LOW change
      attachInterrupt(buttonInterrupt, buttonPress, FALLING);
     
      // LOW means create an interrupt as long as LOW is present
      // attachInterrupt(buttonInterrupt, buttonPress, LOW);
     
      Serial.println("OK");
    }
     
    void loop() {
     
      Serial.println(buttonState, DEC);
      // see if button was pressed at some point
      if ( buttonState == 1 ) {
        Serial.println("TaDa");
        delay(75);
        buttonHandled();  
      }
     
     
      delay(200);
     
    }
     


The problem with this code is that it is very sensitive to switch bouncing when the external button is pressed. You can do the debouncing in software (see below) by timestamping when the interrupts occur and ignoring subsequent interrupts that occur "too soon", thus indicating bouncing and not a legitimate key press. But this can be very processor intensive depending on the kind of bouncing that the switch does.

You can see just how much bouncing occurs in this oscilloscope trace of the input from a simple switch. The yellow line is the voltage appearing at the input pin of the Arduino. When pressed, the voltage should go smoothly from high (5V) down to low (0V). Instead a series of pulses lasting over 1 mS are generated before the voltage finally stays at 0. Each one of these pulses would generate an interrupt. The image on the right shows what signal the arduino is reading on its digital input. The blue line is taken from a digital output that is simply copying the value from the digital input attached to the switch. That is, the blue trace represnets what the Arduino "sees" on its input.



Notice how some input pulses (yellow) are too narrow to be seen by the Arduino program.

The program that is reading and writing the switch signal is this: code/Interrupts/DebounceTest/echo.cpp

    #include <Arduino.h>
     
    /*  Debouncing demonstration program.  
     
    The purpose of this program is to show how the debouncing of an input
    translates to a well-defined digital signal.  It simply monitors a digital 
    input and passes through the state to a digital output.
     
    If we monitor the raw input signal and the digital output, we can show
    how the Schmidt triggers clean up the input signal.
     
    */
     
    const uint8_t button_pin = 10;
    const uint8_t output_pin = 13;
     
    void setup() {
        Serial.begin(9600);
        // set button_pin to INPUT and 
        pinMode(button_pin, INPUT);
        // turn on internal pull up resistor 
        digitalWrite(button_pin, HIGH);
     
        // set output_pin to OUTPUT
        pinMode(output_pin, OUTPUT);
     
        }
     
    void loop() {
        uint8_t state = digitalRead(button_pin);
        digitalWrite(output_pin, state);
        }


When we attach some debouncing hardware to the switch, in the form of an additional capacitor and two resistors, then the bouncing of the contacts is filtered out. Here is the circuit, and beside it is the same input signal for one button press as above, but debounced. Notice how the yellow trace is a smooth curve, with a rapid descent and a gradual rise.



R1 internal pull up
R2 560 Ω
C1 1.0 µ F


How does the debounce circuit work? When the normally open switch is pressed, it quickly discharges C1 at a rate faster than it can charge, provided that R1 is substantially greater than R2. Assuming the down time of the switch contact is roughly equal to the up time while bouncing, any bouncing discharges the capacitor far more than it can charge it, so the voltage should be tending toward 0V. As it decends it will rise and fall slightly, depending on the relative values of the resistors, but the overall direction will be toward 0.

This could still cause bouncing to be perceived at the input, were it not for the presence at the digital inputs on the Ardunio of a special circuit called a Schmitt trigger. The Schmitt trigger has a threshold behaviour, so that once the voltage passes through one threshold and changes state, it has to pass through the other threshold in the opposite direction before it will change state again. When the signal is high, it has to go below the lower threshold before it is perceived as low. When the signal is low, it has to go above the high threshold before it is perceived as high. Between thresholds the signal can vary, but the perception of it will remain constant.



You can see this on the image above. The upper threshold of the trigger is 2.35 V and the lower threshold is 2.25 V. As the output from the switch (yellow trace) is falling, the moment it gets below the lower threshold the signal read at the digital input (blue trace) goes low. As the button is released and the capacitor starts charging, then the yellow trace has to pass above the upper threshold before the input senses the change to high.

What values does one chose for R1, R2, and C2? For the switches and parts we have in the kit, we used the values above. The ultimate choice of values depends on the nature of the bouncing for the switches you have. Large values of R1 and C2 increase the recovery time of the capacitor, and thus make a button press longer, lasting some time after the button is released. You can use this to prevent users from pushing the button too frequently. Smaller values of R2 will discharge the capacitor quickly, which is desirable. But normally you do not want to short out a charged capacitor to ground, so some kind of resister along the path from the capacitor, through the switch, and to ground is desirable.

Finally, even if you are not attaching buttons to interrupts, it is probably a good idea to debounce them.



14.6 Subtle Issues

Here is an example where we want to detect a button press and do something involving the lcd display. Because the lcd display interface requires reads from the serial port, it needs interrupts to be enabled. So no lcd operations are possible inside the interrupt handler. Instead we need to queue up the actions to be performed after the button has been pressed.

We also want to debounce the key presses via software, instead of using external hardware on the button. So the debounce code needs to know the last time that the last key was pressed, and not process any further "presses" until a suitable debounce time has passed.

If we debounce in hardware, then we will only get one key press interrupt. If we debouncd in software, then we will get a flurry of key press events, each of which needs to be handled.

The following example shows just how much bouncing goes on. Note the use of critical sections around the linked list accessing.

code/Interrupts/Bounce/Bounce.cpp

    #include "assert13.h"
    #include "ll.h"
     
    // the external LED is attached to this pin.
    // onboard LED is attached to pin 13, 
    byte ledPin = 13;
    byte pressPin = 11;
     
    /* interrupt information
     
    Most Arduino boards have two external interrupts: 
        0 (on digital pin 2) 
        1 (on digital pin 3) 
    The Arduino Mega has an additional four: 
        2 (pin 21)
        3 (pin 20)
        4 (pin 19)
        5 (pin 18). 
    */
     
    // The pushbutton is attached to this digital input pin
    // and ground.  With the pullup resistor turned on this
    // creates a falling signal when the button is pushed.
    // To create many interrupts, just rub the two wires 
    // together.
    int buttonPin = 21;
    int buttonInterrupt = 2;
     
    /* button press event */
    typedef struct {
        unsigned long time;
        int data;
        } intr_event;
     
    // a list of interrupt events 
    // volatile because altered in an interrupt service routine
    volatile linked_list intr_list;
     
    void buttonPress() {
      // Serial.println("Push");
      // return;
      
      /* add this button press event to the processing queue */
      intr_event * event;
      
      // these must be freed when removed from list!
      event = (intr_event *) malloc(sizeof(intr_event));
      assert13(event != 0, 1);
     
      event->time = micros();
      event->data = 1; 
     
      addElement(intr_list, event);
     
    }
     
      
    void setup() {
      Serial.begin( 9600 );
      Serial.println("Starting");
     
      // configure ledPin to be a digital output
      pinMode(ledPin, OUTPUT);
      pinMode(pressPin, OUTPUT);
      
      digitalWrite(pressPin, LOW);
     
      // set buttonPin to INPUT and 
      // turn on internal pull up resistor 
      pinMode(buttonPin, INPUT);
      digitalWrite(buttonPin, HIGH);
     
      intr_list = initList(); 
      // establish interrupts on button transitions
     
      // FALLING means create an interrupt on a HIGH to LOW change
      attachInterrupt(buttonInterrupt, buttonPress, FALLING);
     
      Serial.println("OK");
    }
     
    void loop() {
      intr_event * event;
      
      // get the next event from the queue and display it.
      
      noInterrupts(); // begin critical section wrt intr_list
      event = (intr_event *) removeElement(intr_list); 
      interrupts(); // end critical section
     
      if ( event != 0 ) {
          Serial.print(event->time, DEC);
          Serial.print(":");
          Serial.println(event->data);
          free(event);
          }
        
      delay(100);
    }


code/Interrupts/Bounce/assert13.h

    #ifndef _assert13_h
    #define _assert13_h
    void assert13(int invariant, int code);
    #endif


code/Interrupts/Bounce/assert13.cpp

    #include <Arduino.h>
    #include "assert13.h"
    // create a busy-loop delay, since timers are off
    int hard_loop(unsigned long int n) {
      // this introduction of y and the Serial.print
      // are to mess with gcc's optimization of the loop
      int y = 0;
      while (n > 0) {
        y = y + n;
        n--;
      }
      Serial.print(""); 
      return y;
    }
     
    #define DELAY_COUNT 1000000L
     
    /* assertion checker
       assert13(invariant, code);
    if invariant is false (i.e. 0) then fail and enter
    a hard loop with interrupts disabled and repeatedly
    sending code to the serial monitor, while blinking 
    the LED on pin 13.
     
    There is a small window in which an interrupt could
    occur, and in which a failure could call assert13.
    How would we guard against this?
    */
     
    void assert13(int invariant, int code) {
      unsigned long int count;
      if ( invariant ) { return; }
      Serial.println("Assertion failure");
      noInterrupts();
      pinMode(13, OUTPUT);
      while ( 1 ) {
        Serial.println(code);
        digitalWrite(13, LOW);
        // Serial.println("LOW");
        hard_loop( DELAY_COUNT );
        digitalWrite(13, HIGH);
        // Serial.println("HIGH");
        hard_loop( DELAY_COUNT );    
      }
    }


code/Interrupts/Bounce/ll.h

    #ifndef _ll_h
    #define _ll_h
    /*
       A linked list contains nodes which hold the information for that node
       and the pointer to the next node in the list 
    */
    typedef struct node {
        void * val;
        node * next;
    };
     
    /* A linked list is represented by a handle, which is a 
       pointer to the linked list control block.
       
       The control block maintains a pointer to the head and tail nodes
       We maintain length information so that we don't have to compute it.
    */
    typedef struct linked_list_cb {
        node * head;
        node * tail;
        int length;
    };
     
    // typedef linked_list_cb * linked_list;
    typedef linked_list_cb * linked_list;
     
    // create a linked list and return the handle
    linked_list initList();
    void addElement(linked_list list, void * val);
    void * removeElement(linked_list list);
    int getLength(linked_list list);
    #endif


code/Interrupts/Bounce/ll.cpp

    #include <stdlib.h>
    #include "assert13.h"
    #include "ll.h"
     
    // create a linked list and return the handle
    linked_list initList() {
      linked_list list;
      list = (linked_list) malloc(sizeof(linked_list_cb));
      assert13(list != 0, 16); 
      
      list->head = 0;
      list->tail = 0;
      list->length = 0;
      return list;
    }
     
    int getLength(linked_list list) {
      return list->length;
    }
     
    void addElement(linked_list list, void * val) {
        node * n = (node *) malloc(sizeof(node));
        assert13(n != 0, 17);
     
        n->val = val;
        n->next = 0;
        if ( list->length != 0 ) {
          list->tail->next = n;
        } else {
          list->head = n;
        }
        list->tail = n;
        list->length++;
    }
     
     
    // Should not call if list->length == 0
    void * removeElement(linked_list list) {
        if (list->length == 0) {
            // return the null pointer if the list is empty
            return 0;
            }
            
        void * rval = list->head->val;
     
        node * new_head = list->head->next;
        /* Now we can free the old head so that we don't leave unused memory 
           allocated
        */
     
        free(list->head);
        list->head = new_head;
        if ( list->head == 0 ) {
          list->tail = 0;
        }
     
        list->length--;
     
        return rval;
    }
     


14.7 Code examples

Code can be obtained from this diretory: code/Interrupts
14. Interrupts
Tangible Computing / Version 3.20 2013-03-25