Question about attaching interrupt within class

The following code snippet that I prepared throws "invalid use of member 'interrupt::counter' in static member function" on compile:

class interrupt{
  public:
  interrupt(){}

  int counter;
  
  void init(){
    pinMode(2,INPUT_PULLUP);
    attachInterrupt(digitalPinToInterrupt(2), interruptL, LOW);
  }

  static void interruptL(){
    counter++;
  }
};

interrupt myInterrupt;

void setup() {
  myinterrupt.init();
}

void loop() {
  Serial.println(myInterrupt.counter);
  delay(200);
}

But after following some forum threads, the following corrected code compiles perfectly:

class interrupt{
  public:
  interrupt(){}

  int counter;
  static interrupt* anchor;
  
  void init(){
    pinMode(2,INPUT_PULLUP);
    attachInterrupt(digitalPinToInterrupt(2), interrupt::marshaller, LOW);
  }

  void interruptL(){
    counter++;
  }

  static void marshaller(){
    anchor->interruptL();
  }
};
interrupt* interrupt::anchor = NULL;

interrupt myInterrupt;

void setup() {
  myInterrupt.init();
}

void loop() {
  Serial.println(myInterrupt.counter);
  delay(200);
}

What I still cannot understand is, why is the existence of a marshal function necessary for attachInterrupt to work? Why can't attachInterrupt just take a member ISR as an argument?

Appreciate any advice.

1 Like

Your second code compiled but will crash when you run. You are dereference "anchor" which is a null pointer.

Your first code didn't compile because you were trying to access non-static member variable in a static function.

1 Like

Delta_G:
An ISR has very specific requirements. it must take no arguments and must return void.

The class definition has an ISR, but when the interrupt occurs, how will it know which instance of the class to call that method from? If you only have one instance of the class, then make it all static and it works. But if there are two instances, then how will it know?

Let's say we have a pet class. And it has an interrupt that says when the pin goes high feed the pet. Awesome. Now I create two instances, lets say one named dog and one named cat. When the pin goes high which should run? Should it be dog.feed() or cat.feed().

When you call a member function, a secret pointer parameter called "this" is passed which indicates which instance is calling the function. Oh but remember what I said on the first line, an ISR can take no parameters. So it can't take the this pointer. So an ISR can't take a member function.

He was passing a static member function. Static member function is treated the same as global function - no hidden this.

I really don't see the point of "marshaller". You can accomplish the same thing by simply making the interrupt handler a static function (which marshaller is). Also make any member functions that are called by the interrupt handler, and any member data accessed by the interrupt handler, static. This does, however, make the class a "singleton". i.e. - there must only ever be ONE instance of the class attempting to be attached to that interrupt at any given time. But it appears to me the code you posted imposes the exact same restrictions, just implemented in a very convoluted manner.

Another simple way to handle the problem, if you cannot tolerate the class being a singleton, is to let the sketch actually do the attach interrupt, with the interrupt being attached to a function in the sketch that does nothing but call the interrupt handler of the appropriate class instance. This is a bit round-about, but very simple, and imposes minimal overhead (one additional function call).

Regards,
Ray L.

Yet another way to get around making member functions or member data static, though still with the same singleton restriction, is for the class constructor to save a copy of the "this" pointer in a static variable. The (static) interrupt handler can then use the stored "this" pointer to access any member data or functions it needs.

Regards,
Ray L.

1 Like

Delta_G:
Oh, I'm sorry. I was answering the larger question of why a member can't be an ISR which seemed like what he started with asking. In his case the first code failed because he used the non-static counter inside the static function. But the problem is the same, from which instance should it get counter?

I was responding to the question:and not just looking at the code.

His idea should work if he make counter static too.

Delta_G:
I was responding to the question:and not just looking at the code.

And I was just looking his code without reading much of his after comments. Oh, well...

RayLivingston:
I really don't see the point of "marshaller". You can accomplish the same thing by simply making the interrupt handler a static function (which marshaller is). Also make any member functions that are called by the interrupt handler, and any member data accessed by the interrupt handler, static. This does, however, make the class a "singleton". i.e. - there must only ever be ONE instance of the class attempting to be attached to that interrupt at any given time. But it appears to me the code you posted imposes the exact same restrictions, just implemented in a very convoluted manner.

Another simple way to handle the problem, if you cannot tolerate the class being a singleton, is to let the sketch actually do the attach interrupt, with the interrupt being attached to a function in the sketch that does nothing but call the interrupt handler of the appropriate class instance. This is a bit round-about, but very simple, and imposes minimal overhead (one additional function call).

Regards,
Ray L.

Letting the sketch handle the interrupt is indeed the simplest solution, but I wanted to challenge myself by seeing if I can let a class handle everything.

I've tried making counter static as follows (note this is edited from case 1):

class interrupt{
  public:
  interrupt(){}

  static int counter;
  
  void init(){
    pinMode(2,INPUT_PULLUP);
    attachInterrupt(digitalPinToInterrupt(2), interruptL, LOW);
  }

  static void interruptL(){
    counter++;
  }
};

interrupt myInterrupt;

void setup() {
  myInterrupt.init();
}

void loop() {
  Serial.println(myInterrupt.counter);
  delay(200);
}

...and that creates another error:

C:\Users\darrel\AppData\Local\Temp\ccOITqgi.ltrans0.ltrans.o: In function `interruptL':

C:\Users\darrel\Desktop\LCD_TEST/LCD_TEST.ino:13: undefined reference to `interrupt::counter'

C:\Users\darrel\Desktop\LCD_TEST/LCD_TEST.ino:13: undefined reference to `interrupt::counter'

C:\Users\darrel\Desktop\LCD_TEST/LCD_TEST.ino:13: undefined reference to `interrupt::counter'

C:\Users\darrel\Desktop\LCD_TEST/LCD_TEST.ino:13: undefined reference to `interrupt::counter'

C:\Users\darrel\AppData\Local\Temp\ccOITqgi.ltrans0.ltrans.o: In function `loop':

C:\Users\darrel\Desktop\LCD_TEST/LCD_TEST.ino:24: undefined reference to `interrupt::counter'

C:\Users\darrel\AppData\Local\Temp\ccOITqgi.ltrans0.ltrans.o:C:\Users\darrel\Desktop\LCD_TEST/LCD_TEST.ino:24: more undefined references to `interrupt::counter' follow

collect2.exe: error: ld returned 1 exit status

exit status 1
Error compiling for board Arduino/Genuino Uno.

Delta_G:
When you call a member function, a secret pointer parameter called "this" is passed which indicates which instance is calling the function. Oh but remember what I said on the first line, an ISR can take no parameters. So it can't take the this pointer. So an ISR can't take a member function.

While I understand the gist of your argument, this part confuses me. The ISR in both cases still takes no arguments right? Do you mean the ISR is being force-fed this as an argument?

1 Like

You need to define counter since it's now a static variable. Like so:

int interrupt::counter = 0;
void setup()

As mentioned in the posts above you don't have any chance (at least to my knowledge) to embed an interrupt handler in a class without making the handler static or calling it through a global pointer to the instance. Basically this leaves you two options:

  1. Pass in a pointer to some handler which is defined outside the class
  2. Static solution which necessarily leads to singleton classes

Both are somehow ugly (at least for me), especially 2) since in your example you'd need to define a dedicated class for each pin to make that work.

But all is not lost. You can use a simple template to make 2) much easier to use:

#include "Arduino.h"

// -----------------------------------------------------
template <uint8_t pin>
class InterruptTest
{
public:
  InterruptTest()
  {
    pinMode(pin, INPUT_PULLUP);
    attachInterrupt(pin, handler, FALLING);
  }

  static volatile int counter;

protected:
  static void handler()
  {
    counter++;
    Serial.print("ISR  -> Pin: ");
    Serial.println(pin);
  }
};

//-------------------------------------------------

InterruptTest<2> testA;  
InterruptTest<3> testB;  

void setup()
{
  Serial.begin(115200);
}

void loop()
{
  Serial.print("loop -> counter A: ");
  Serial.println(testA.counter); 

  Serial.print("loop -> counter B: ");
  Serial.println(testB.counter);
  Serial.println();
   
  delay(200);
}


// need to initialize static member variables
template<uint8_t pin>
volatile int InterruptTest<pin>::counter = 0;

In the code it looks like you generate two instances (testA and testB) of the InterruptTest class but in fact the compiler is generating two different classes from the template defintion. So you still have singletons, but you do not need to explicitly define them for each pin which is very convenient.

1 Like

Wow, that's a lot of info to absorb. Thanks for the replies everyone.

arduino_new:
You need to define counter since it's now a static variable. Like so:

int interrupt::counter = 0;

void setup()

this works, but I was wondering why you can't just do an in-class definition of counter like so:

static int counter = 0

This throws "ISO C++ forbids in-class initialization of non-const static member 'interrupt::counter'". So i'm guessing its just one of the rules of C++?

luni64:
But all is not lost. You can use a simple template to make 2) much easier to use:

Interesting idea, I hadn't thought of that.

Here's a technique that allows you to actually call instance functions as ISRs even when there are multiple instances of the class -- Since at compile time you know the target board / processor you also know the number of interrupts available. So, you can create that many static ISR functions that then call the appropriate instance function. On an Uno (2 external interrupts) for example:

class MyClass {
  public:
    MyClass(uint8_t pin) {
      attach(digitalPinToInterrupt(pin), this);
    }

  private:
    static MyClass *instancePtrs[2];

    void instanceIsr() {
      // the "Real" ISR goes here
    }

    static void attach(uint8_t interruptNum, MyClass *ptr) {
      switch (interruptNum) {
        case 0:
          instancePtrs[0] = ptr;
          attachInterrupt(interruptNum, isr0, RISING);
          break;

        case 1:
          instancePtrs[1] = ptr;
          attachInterrupt(interruptNum, isr1, RISING);
          break;

        default:
          break;
      }
    }

    static void isr0() {
      instancePtrs[0]->instanceIsr();
    }

    static void isr1() {
      instancePtrs[1]->instanceIsr();
    }
};

MyClass *MyClass::instancePtrs[2];

MyClass A(2);
MyClass B(3);

void setup() {
}

void loop() {
}

While this technique may lead you to think that you'd need a different source file for every processor, you can use conditional compilation techniques and the 'CORE_NUM_INTERRUPT' macro to support an arbitrary number of interrupts. I learned this technique from the PJRC Encoder Library and and used a modified version in my own encoder library: NewEncoder/NewEncoder.h at master · gfvalvo/NewEncoder · GitHub.

This method has two advantages over the template technique:

  • It's easier to create an array of pointers to the objects instantiated. With the template method you'd need to use inheritance / polymorphism.

  • The objects can also can be instantiated dynamically (with a run-time variable defining the pin number) using the 'new' operator rather than just at compile time.

Granted though, for most newbies writing code on the simpler Arduino platforms these probably aren't major considerations.

1 Like