Using an Object for Interrupt (and other) Call Backs

gfvalvo

Well-known member
Hi. This really isn’t a question / support issue but a request for comments on an idea. First posted it at the Arduino forum without much response. Thought maybe I’d do better here with the generally higher level of coding experience.

Many folks (myself included) find when they’re first starting with C++ / Arduino that you can’t use attachInterrupt() to attach an object’s instance function. The reason you can’t get a compliant pointer to an instance function has been discussed on many forums. This also applies to other code that uses callback functions.

It seems a really slick way to handle this would be to define an abstract “callback” class:
Code:
#ifndef CALLBACK_H_
#define CALLBACK_H_

class CallBack {
  public:
    virtual void *callback(void *ptr = nullptr) = 0;
    virtual ~CallBack() {
    }
};

#endif /* CALLBACK_H_ */

You’d then create your own classes that inherit from this and pass a base class pointer to the attach function. One example that comes to mind would be a class that handles rotary encoders using interrupts.

In theory, it should be fairly straight-forward adding the capability to use callback objects to the code that attaches, detaches, and services interrupts. You’d create an array of CallBack class pointers similar to the array of callback function pointers. The attachInterrupt() would be overloaded take a pointer to a CallBack object. A flag would be set for each interrupt to tell the ISR vector function whether to use the CallBack object or the standard callback function.

The use (if any) of the passed and returned void * pointers would be up to the specific application employing callbacks. For instance, with interrupts you could pass a pointer to the interrupt number that fired to the callback.

Well, that’s the theory. The first problem I see is files that contain the interrupt-related code, such as:

Teensy -- pins_teensy.c
ESP8266 -- core_esp8266_wiring_digital.c
Arduino M0 -- WInterrupts.c (in SAMD Core)
AVR -- WInterrupts.c (in AVR Core)


These are obviously all .c files. Thus, it doesn’t seem the above idea would work in them as it requires C++ techniques. So, the first question is do they have to be .c files? I don’t know enough about the nuances involved with how the tool chain compiles and links C code verses C++ code to know.

After that, maybe the bigger question is if a change like this is even possible given inertia of the Arduino ecosystem and large number of board packages that would need to be modified. It doesn’t seem to me that the change would affect existing application code as the old attach method would still be available. But, not I don’t know how to tell for sure.

Anyway, I’m just throwing it out there because of the vast breadth and depth of experience people on this forum have. Maybe it has been considered already and deemed impractical / impossible.
 
Last edited:
Not sure it would relate to general usage but in the Talkie library - .cpp class code - it starts a timer function to push out PWM data for the voice. It was a blocking call as the original AVR code sat and waited for the sound bits to be pushed out in small blocks from the whole of the sound bite.

The code sets up some future data block the _isr pushes out - then as needed sets up the next block of data. Frank_B provided a scheme to save the 'Talkie *this' to be statically stored. When the code gets the first block of data sets up starts the _isr and returns. The timer _isrs eats the data at a fixed rate or number if calls to the _isr. On the Xth call when new data is needed it uses the stored pointer to call a class func to pull the next block of data to feed the _isr and leaves. Using Frank's idea this worked and I extended Talkie to queue up a number of sound data arrays in addition to the current one. So it is now not only non blocking on one sound but allows calling code to cue up sounds in a batch and continue to have them play.

So if that represents the general idea Teensy can and does do it on a localized basis. As always the _isr is just entered with no params - it is up to the Talkie code in this case to keep context know what to do next. Perhaps that can be generalized for some set of other examples - give it a look.
 
Could you give an example code how to use this?
Sure. Sorry if what follows is a little verbose. But, I'm trying to be very clear.

As an example, if you've looked at the source code for the (rotary) Encoder class, you've seen that it has to jump through a lot of hoops to get the proper interrupts associated with each instance of the class. Imagine if the system allowed you to attached a pointer to a CallBack object to the interrupt vector as an alternative to the traditional callback function pointer. Let's also imagine that it's able to pass the interrupt number to the CallBack object.

The following code is just notional and not specific to any processor. But, it's based (very) loosely on how 'pins_teensy.c' handles attachInterrupt() and the ISR. The overloaded attachInterrupt() and interrupt vector function would look something like:
Code:
struct CallBackPair {
  CallBack *callbackPtr;
  uint8_t intNumber;
};

static CallBackPair callbackTable[MAX_INTERRUPTS];
static void (*functionTable[MAX_INTERRUPTS])();

void attachInterrupt(uint8_t intNumber, CallBack *ptr, int mode) {  // Attach interrupt using CallBack class
  uint8_t interruptIndex;
  //
  // Processor-specific interrupt configuration here
  // Determine index into callback table based on interrupt #
  //
  callbackTable[interruptIndex].callbackPtr = ptr;
  callbackTable[interruptIndex].intNumber = intNumber;
  functionTable[interruptIndex] = nullptr;
}

void attachInterrupt(uint8_t intNumber, void (*ptr)(), int mode) {  // Attach interrupt using traditional callback function
  uint8_t interruptIndex;
  //
  // Processor-specific interrupt configuration here
  // Determine index into callback table based on interrupt #
  //
  functionTable[interruptIndex] = ptr;
}


// ACTUAL HARDWARE INTERRUPT VECTOR FUNCTION
static void port_A_isr() {
  uint8_t interruptIndex;
  CallBack *ptr;

  // Interrupt vector function
  // Handle processor-specific registers and flags
  // Determine index into callback table based on what interrupt(s) this vector handles
  //

  // Now call the callback:
  if (functionTable[interruptIndex]) {
    functionTable[interruptIndex]();
  } else {
    ptr = callbackTable[interruptIndex].callbackPtr;
    ptr->callback(((void *)(&callbackTable[interruptIndex].intNumber)));
  }
}

So, now the new Encoder class would inherit from the CallBack class and look something like this skeletal outline:
Code:
#include "CallBack.h"
class Encoder : public CallBack {
  public:

    Encoder(uint8_t PinA, uint8_t PinB) : pinAinterrupt(digitalPinToInterrupt(PinA)), pinBinterrupt(digitalPinToInterrupt(PinB)) {
      attachInterrupt(pinAinterrupt, this, FALLING);
      attachInterrupt(pinBinterrupt, this, FALLING);
    }

    virtual ~Encoder() {
    }

    // Handle the two pin interrupts
    virtual void *callback(void *ptr) {
      uint8_t intNumber = *((uint8_t *)ptr);

      if (intNumber == pinAinterrupt) {
        // Handle interrupt on Pin A, determine encoder position
      }

      if (intNumber == pinBinterrupt) {
        // Handle interrupt on Pin B, determine encoder position
      }
      return nullptr;
    }

    int16_t getPosition() {
      return position;
    }

  private:
    uint8_t pinAinterrupt;
    uint8_t pinBinterrupt;
    volatile int16_t position;
};

Finally, in application code:
Code:
#include "NewEncoder.h"

Encoder encoder1(4, 5);
Encoder encoder2(6, 7);

void setup() {
}

void loop() {
  int16_t pos1, pos2;

  pos1 = encoder1.getPosition();
  pos2 = encoder2.getPosition();
}

Doing it this way allows you to associate an interrupt with an instance of a class. That class overloads the callback() method in the base class via polymorphism. Thus the interrupt vector function is able to call the appropriate method, avoiding the problems of getting a pointer to an instance function.

Also, note how passing in the interrupt number allows one callback object ISR to gracefully handle the interrupts for multiple pins.

Hope I've been somewhat clear with this example.

Thanks.
 
Last edited:
yeah, what he said...

Sure. Sorry if what follows is a little verbose. But, I'm trying to be very clear.

That was a superbly clear and very convincing example.

4-1/2 years later, I am still wishing for exactly this.
 
That was a superbly clear and very convincing example.

4-1/2 years later, I am still wishing for exactly this.

Thanks. Actually, 4-1/2 years later, I've refined my wish a little bit as exactly what I want already exists in ESP32 and ESP8266. Instead of creating a new CallBack class, the Arduino core for those boards allow attachInterrupt() to accept an argument of type std::function<void ()>. Several other functions in those cores that register callbacks also support this. This is perfect as std::function can wrap a pointer to a free function, a std::bind with pointer to member function / object pair, a lambda expression, etc. An issue with adding this to the Teensy core is that the code that implements attachInterrupt() is written in 'C'.
 
4-1/2 years later, I am still wishing for exactly this.
There is some activity going on in the current Teensyduino beta where the IntervalTimer already has a std::function like interface. Don't know if Paul plans to add this to attachInterrupt as well.
Anyway, you can always use attachInterruptEx which extends the stock attachInterrupt with a std::function interface. Here an example:

Code:
#include "Arduino.h"
#include "attachInterruptEx.h"

class EdgeCounter
{
 public:
    void begin(unsigned pin)
    {
        counter = 0;
        pinMode(pin, INPUT_PULLUP);
        attachInterruptEx(pin, [this] { ISR(); }, CHANGE);
    }

    unsigned getCounter() { return counter; }

 protected:
    void ISR(){
        counter++;
    }

    unsigned counter;
};

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

EdgeCounter ec1, ec2;

void setup(){
    ec1.begin(0);
    ec2.begin(1);
}

void loop(){
    Serial.printf("Detected edges: Pin1: %u Pin2:%u\n", ec1.getCounter(), ec2.getCounter());
    delay(250);
}

Just copy the two attached files to your sketch folder to use it
 

Attachments

  • attachInterruptEx.zip
    843 bytes · Views: 18
There is some activity going on in the current Teensyduino beta where the IntervalTimer already has a std::function like interface. Don't know if Paul plans to add this to attachInterrupt as well.
Anyway, you can always use attachInterruptEx which extends the stock attachInterrupt with a std::function interface. Here an example:

...
Just copy the two attached files to your sketch folder to use it

The link in p#7 starts:
Code:
Lightweight C++ callbacks
I'm considering changing all or most of Teensy callback APIs (IntervalTimer, attachInterrupt, usbMIDI, more to be added) to properly support C++ functions. But not std::function which allocates memory on the heap. Something "lightweight". Maybe one of these?

Didn't follow closely for inclusion, but 'attachInterrupt' is indicated as intended - seems not yet incorporated?

Cool there is a working version for Teensy.

Is there a github link to the ESP32 solution source - assuming it is licensed for unrestricted use to emulate?

edit: uses: github.com/espressif/arduino-esp32/blob/master/LICENSE.md
Code:
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
...
 
Last edited:
The link in p#7 starts:
Is there a github link to the ESP32 solution source - assuming it is licensed for unrestricted use to emulate?

edit: uses: github.com/espressif/arduino-esp32/blob/master/LICENSE.md
Code:
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
...[/QUOTE]
I don't know squat about licensing, but if you install the ESP32 core, you can find the files in "....\esp32\hardware\esp32\2.0.11\cores\esp32":
esp32-hal-gpio.c
FunctionalInterrupt.h
FunctionalInterrupt.cpp
 

Anyway, you can always use attachInterruptEx which extends the stock attachInterrupt with a std::function interface.

To me, @luni's solution is preferable to reinventing the wheel as Paul proposes with his "Lightweight C++ callbacks" idea.

If I understand correctly, std::function won't occupy significant heap space if you just wrap a free (regular) function, static class function, stateless lambda, etc. The current 'attachInterrupt()' supports those now.

But, the added flexibility of using other std::function flavors would be there (with acknowledged heap use) if needed.

So, to me, having a "standard" approach (excuse the pun) is preferable to making up a new interface / method.
 
Last edited:
Back
Top