Arduino: interrupts in class and callback functions

 Author:   Posted on:   Updated on:  2019-05-01T12:14:11Z

How to attach an interrupt from a class (library) and set callback function from sketch

Arduino is a popular open source electronics development platform. The programming language is nothing else but C/C++. The predefined Arduino libraries provide easy to use functions for most usual tasks, like writing and reading to MCU pins, data transfer using common protocols etc.

If you're working on a complex project or you are developing your own library, chances are you are creating new classes. That's because a class can contain member data (just like data structures) and member functions (which modify, process or generate data). Access to class members is usually governed by an access specifier. Private members are accessible only from within other members of the same class, while public members can be accessed from anywhere where the class object is visible. This is the C/C++ programming language. If you're not familiar with it I suggest starting with this tutorial about classes.

Arduino: interrupts in class and callback functions

To demonstrate the concepts in this post, we'll make a simple Arduino library that will set up an interrupt on a pin and "notify" the sketch when the state of the pin has changed. Go to the libraries folder from the sketchbook folder (where Arduino IDE saves your sketches) and make a new folder. Let's call it testlib. In this folder make an empty plain text file named testlib.h. Define the new class in it. Here are the entire contents of testlib.h:

#ifndef TESTLIB_H
#define TESTLIB_H

class testLib {

public:
  void begin(int interruptPin) { pinMode(interruptPin, INPUT); }

} ;

#endif

The first two lines and the last one are preprocessor directives. Let's assume you will use this class in more than one file of the same project. Maybe not on Arduino, but it's something common to have #include "testlib.h" in more than one code file on other platforms. If the class has been included once, TESTLIB_H has been defined and further #include directives for the same class produce no effect. That's because everything between #ifndef and #endif is ignored. If it wouldn't be for this checking, multiple #include would mean multiple definitions for the same class (or data types or whatever is in those included headers). And this does not compile.

Returning to the class definition, it is as simple as possible, with just one public function, begin(), which takes as argument the pin that will trigger the interrupt.

Attach interrupt

We want to attach the interrupt in begin(). We also need an interrupt handler, which is just a (callback) function that takes no input parameters and returns nothing. Let's do it. Here's how the functions should look.

class testLib {

public:
  void begin(int interruptPin) { 
                pinMode(interruptPin, INPUT); 
                attachInterrupt(digitalPinToInterrupt(interruptPin), libInterruptHandler, CHANGE);
                }

private:
  void libInterruptHandler(void);

} ;

Note the definition of the private interrupt handler. Now, if you attempt to use your library in a sketch, compilation will fail. This happens because libInterruptHandler() is a class member. The only way to make the code compile is to attach the interrupt to a global, outside of class function. Easy to do, but still, the handling of the interrupt must be performed by class member functions.

Yet, it is possible. Declare a pointer to the class and define a global interrupt handler function. In the class constructor (my test class does not have a constructor) or any member function, assign the current class instance to the pointer. Do this before using the pointer (that's why I recommended the constructor). Next, in the global interrupt handler function, just call the class interrupt handler using the instance pointer. The class interrupt handler must be public! We'll let only function declarations in header and make a source file with function definitions.

testlib.h

#ifndef TESTLIB_H
#define TESTLIB_H

#include "Arduino.h" // must be included

class testLib {

public:
  void begin(int interruptPin);
  void classInterruptHandler(void);

} ;

#endif

testlib.cpp

#include "testlib.h"

// Outside of class
testLib *pointerToClass; // declare a pointer to testLib class

static void outsideInterruptHandler(void) { // define global handler
  pointerToClass->classInterruptHandler(); // calls class member handler
}

// Class members
void testLib::begin(int interruptPin) {
  pinMode(interruptPin, INPUT);
  
  pointerToClass = this; // assign current instance to pointer (IMPORTANT!!!)
  attachInterrupt(digitalPinToInterrupt(interruptPin), outsideInterruptHandler, CHANGE);
}

void testLib::classInterruptHandler(void) {
  // TO DO: call the... callback function
}

Callback functions

At this point, we have classInterruptHandler() which gets called every time the state of the pin changes. But how do we use this in the sketch? We could set a flag every time this handler gets called and in the main sketch you will check this flag endlessly in loop(). It is a way of doing it, but there is one even better: using callback functions. Both interrupt handlers (the global and the class member) are callback functions too. But these are previously known functions.

The callback in main sketch is user defined and we don't know its name. That's why we'll add another public member function which assigns a pointer to the user defined callback and then use this pointer to call it from class. A private variable is needed to store the pointer to the function. To make things even more complicated, user callback will contain a parameter (current pin state).

testlib.h

#ifndef TESTLIB_H
#define TESTLIB_H

#include "Arduino.h"

class testLib {

public:
  void begin(int interruptPin);
  void classInterruptHandler(void);
  void setCallback(void (*userDefinedCallback)(const int)) {
                      localPointerToCallback = userDefinedCallback; }

private:
  int localInterruptPin; // need to store pin because it will be used in another function
  void (*localPointerToCallback)(const int);

} ;

#endif

testlib.cpp

#include "testlib.h"

// Outside of class
testLib *pointerToClass; // declare a pointer to testLib class

static void outsideInterruptHandler(void) { // define global handler
  pointerToClass->classInterruptHandler(); // calls class member handler
}

// Class members
void testLib::begin(int interruptPin) {
  pinMode(interruptPin, INPUT);
  
  pointerToClass = this; // assign current instance to pointer (IMPORTANT!!!)
  attachInterrupt(digitalPinToInterrupt(interruptPin), outsideInterruptHandler, CHANGE);

  localInterruptPin = interruptPin;
}

void testLib::classInterruptHandler(void) {
  localPointerToCallback(digitalRead(localInterruptPin));
}

The library is now finished. Let's see how you can use it.

Usage

Just as with any library, include it and declare an instance of the class provided by library. We also have to define the callback. The constraints of this function were set earlier in void (*userDefinedCallback)(const int). So, it must return nothing (void) and must take an integer argument (const int). You can give the function and its argument any names you want. The sketch that prints interrupt pin state to serial port, when it changes, is the following:

#include "testlib.h"

const int pin = 2;
testLib myLib; // object instance

void writePinStateToSerial(const int state) {
  Serial.print("Pin "); Serial.print(pin, DEC);
  Serial.print(" state is: ");
  Serial.println(state ? "HIGH" : "LOW");
}

void setup() {
  Serial.begin(115200);
  
  myLib.begin(pin);
  myLib.setCallback(writePinStateToSerial);
}

void loop() {
  // nothing here
}

Test on hardware

After all, Arduino is an electronics development platform. So, let's upload the compiled binary to a board. If you're using an ATmega328 based board, interrupt pin can only be 2 or 3. Pull it up with a 1k to 10k resistor (5V-resistor-D2). And pull it down with a pushbutton (D2-button-GND). Upload the code and look in the serial monitor. A single push (or release) of the button generates multiple interrupts. That is normal behavior because the button is not debounced.

Testing the library with Nano board

Testing the library with Nano board (ATmega328p)

Reading a button is just for demonstration purposes. The same method can be used in libraries to read sensors or devices that trigger an interrupt when they need to send data to host and expect a request from host.

1 comment :

Please read the comments policy before publishing your comment.