Using a member function as an ISR using macros

This is in reply to an old locked topic I came across recently in which someone was trying to pass a parameter to an ISR (https://forum.arduino.cc/t/using-isr-with-a-parameter). Here's what they were hoping to accomplish:

void GetEncoderPulse(int pin){
//do some stuff with using the value of pin
}
attachInterrupt(digitalPinToInterrupt(encoderPin1), GetEncoderPulse(1), CHANGE);

This doesn't work because ISRs require a function that takes no arguments. A work around that I didn't see mentioned in that thread is to use a macro to generate a function at compile time which accomplishes this task:

// #define attachPulseInterrupt(pin) \
// void GetEncoderPulse_##pin(){ /* define a new ISR for each pin */ \
//     /* do some stuff with using the value of pin, eg, writePin((pin)); */ \
// } \
// attachInterrupt(digitalPinToInterrupt((pin)), GetEncoderPulse_##pin, CHANGE);

I believe this solves the issue in a pretty efficient way, although I guess you could run into code bloat issues if you were spamming this. Additionally, this approach allows you to use member functions as ISRs in a fashion by doing something like the following:

#define MAKE_ENCODER(a_pin, b_pin, sw_pin) \
Encoder NAME = Encoder((a_pin), (b_pin), (sw_pin)); \
void IRAM_ATTR NAME##_a_isr() { \
    if ((NAME)._bounce_danger != Encoder::A) { \
        if (!(NAME)._a_stable) { \
            (NAME)._spinrate_isr_us = (NAME)._spinspeedTimer.elapsed(); \
            (NAME)._spinspeedTimer.reset(); \
            (NAME)._delta += digitalRead(b_pin) ? -1 : 1; \
        } \
        (NAME)._bounce_danger = Encoder::A; \
    } \
}; \
void IRAM_ATTR NAME##_b_isr() { \
    if ((NAME)._bounce_danger != Encoder::B) { \
        (NAME)._a_stable = digitalRead(a_pin); \
        (NAME)._bounce_danger = Encoder::B; \
    } \
};

#define SETUP_ENCODER(NAME) \
(NAME)._a_isr = &NAME##_a_isr; \
(NAME)._b_isr = &NAME##_b_isr; \
(NAME).setup();

I figured I would throw this answer out there for anyone that's interested, and also to see if anyone can spot a good reason why this should be avoided.

As all arguments to an ISR have to be fixed before the interrupt occurs, above "pin" could become a #define or a global constant or variable. Where "global" means shareable (non-local) scope, e.g. in a dedicated namespace.

I think the problem is that only static member functions can be used as ISRs, because they don't have a "this" pointer passed to them, which would be a problem for non-static functions.

Right, but I think you can get around this if you define your object at globally, since you can reference it inside the ISR. Using the macro is just sugar so you don't have to write a separate function each time you want to use the same member function with a different object.

Right, but you still end up having to define a different ISR for each pin:

#define PIN1 1
#define PIN2 2

void GetEncoderPulse1() {
// use PIN1
}
void GetEncoderPulse2() {
// use PIN2
}

If you use a macro, you only have to write one function and you don't even need to define the pin names. It's not much of gain, but as I mentioned if you're trying to pass in more complex args like objects it can be a useful approach.

void GetEncoderPulse1() {
  call_whatever.you_like(...);
}

These days you can also use lambda expressions to do this:

const unsigned encPin1 = 1;
const unsigned encPin2 = 2;

void onPin(int p)
{
  // do something with pin p
}

void setup()
{
   attachInterrupt(digitalPinToInterrupt(encPin1), []{onPin(encPin1);},RISING);
   attachInterrupt(digitalPinToInterrupt(encPin2), []{onPin(encPin2);},RISING);
}

void loop()
{
}

Here some more information about this:

(please note: the first sections in the write up are independent on the used board. Lhe later sections require support for std::function which is not available for AVR boards)

... and since you seem to use an ESP which supports std::function, you can use an improved version of attachInterrupt to easily attach a pin interrupt to a member function. Here a minimal example. The lambda syntax looks weird at a first glance but this goes away once you are used to it :smile:

#include "FunctionalInterrupt.h"

class Encoder
{
public:
  Encoder(unsigned pinA, unsigned pinB)
  {
    attachInterrupt(pinA, [this, pinA]{ myISR(pinA); }, CHANGE);
    attachInterrupt(pinB, [this, pinB]{ myISR(pinB); }, CHANGE);
  }

protected:
  void myISR(unsigned pin)
  {
    // do something with the pin
  }
};

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

void setup()
{
  Encoder e1(1, 2);
  Encoder e2(3, 4);
}

void loop()
{
}

This is the correct approach.

The background work is being done by FunctionalInterrupt.h, which must be included for lambda capture to work correctly. Otherwise you'll get cryptic error messages of the form:

error: cannot convert 'Encoder::setup(unsigned int)::<lambda()>' to 'void (*)()'

This was actually my original issue. I tried using lambda capture and hit that error message. The only relevant thread I found was the one I linked to, which only used non-capturing lambdas. They got around this issue using the '+' operator for lambdas, which converts the lambda to a plain function pointer. That trick works fine for non-capturing lambdas, but gives the wrong function type for capturing ones. Hence why I developed the work-around I posted. In FunctionalInterrupt.h, however, attachInterrupt() is overridden with a version that handles std::function function pointers correctly, so it can be used with capturing lambdas. Yay!

Thanks!

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.