c++arduinofunction-pointersesp32pointer-to-member

C++ Callback to member function pointers on ESP32 / Arduino


It seems a difficult thing to do in C++. A callback to a member function in a class in C style, thus making this a void(*)() to be able to use this in a C-style callback function. In this case I want to use this on an ESP32/Arduino to attachInterrupt to a member function.

(I've researched the internet and spend a couple of days to search for a solution, but to no avail).

Good news, I found a solution myself, it's tricky I think, but I haven't seen such a solution before. It works locally running on my Mac, but the bad news, it doesn't work on an ESP32.

On PlatformIO the line:

attachInterrupt(pin,_lambdas.back(),RISING) ; 

gives a compile error:

"no suitable conversion function from "std::function<void ()>" to "void (*)()" exists"

whilst the definition of of the attachInterrupt function is exactly the same.

The code is pretty straightforward and simple to understand. The tricky thing I do is storing a Lambda function in a vector. I'm actually surprised that it works, but it does on my Mac. I'm a bit of a noob, but any help to get this to work on an EPS32 is appreciated!!!

See code below:

Main.cpp:

#include "button.hpp"

void pushedA() { printf("Pushed A\n") ; }
void pushedB() { printf("Pushed B\n") ; }
void pushedC() { printf("Pushed C\n") ; }

int main() {
    printf("Start\n") ;
    Button a(27,pushedA) ;
    Button b(25,pushedB) ;
    Button c(23,pushedC) ;

    printf("Execute\n") ;
    // So in the interruptStore we have functionpointers to class members. Now execute!
    for (auto interruptRoutine : interruptStore) {
        interruptRoutine() ;
    }
    return 0 ;
}

Button.hpp:

#ifndef BUTTON_H
#define BUTTON_H
#include <vector>
#include <functional>
/* Helper routines to simulate Arduino "attachInterrupt" */
extern std::vector<std::function<void(void)>>interruptStore ;
#define RISING 1
// Same function definition as the Arduino definition, but on the Arduino this doesn't seem to work :-(
inline void attachInterrupt(uint8_t pin, std::function<void(void)> intRoutine, int mode) { interruptStore.push_back(intRoutine) ; }
inline void detachInterrupt(uint8_t pin) { ; }
/**************************************************/

class Button {
public:
    Button(uint8_t pin, void (*callbackFunction)()) ;
  ~Button() ;
private:
  inline void pressed() ;
  uint32_t _pin ;
  void    (*_func)() ;
  uint32_t _debounce ;
  static std::vector<std::function<void(void)>>_lambdas ;
} ;
#endif

Button.cpp:

#include "button.hpp"

std::vector<std::function<void(void)>>interruptStore ;

Button::Button(uint8_t pin, void (*callbackFunction)()) {
    _func = callbackFunction ;
    auto pressedFunction =  [this]() {this->pressed() ;} ;
    _lambdas.push_back(pressedFunction) ;
    attachInterrupt(pin,_lambdas.back(),RISING) ;
    _pin = pin ;
}

Button::~Button() {
    detachInterrupt(_pin) ;
}

inline void Button::pressed() {
//  if (millis()-_debounce>60) { // Here we would normally handle the debounce
        printf("Button on pin %d pressed\n",(int)_pin) ; 
        _func() ;
//      _debounce = millis() ;
//  }
}

std::vector<std::function<void(void)>>Button::_lambdas ; // We need to define a static here, declaration is done in button.hpp

or on Github: https://github.com/MatersM/Arduino-Button

ps. I couldn't believe it either but it really works on a Mac: enter image description here

Output on terminal, to show it works:

Launching: '/Users/mmaters/Documents/code/BUTTON-TEST/output/main'
Working directory: '/Users/mmaters/Documents/code/BUTTON-TEST'
1 arguments:
argv[0] = '/Users/mmaters/Documents/code/BUTTON-TEST/output/main'
Start
Execute
Button on pin 27 pressed
Pushed A
Button on pin 25 pressed
Pushed B
Button on pin 23 pressed
Pushed C
Process exited with status 0
logout

Saving session...
...copying shared history...
...saving history...truncating history files...
...completed.

[Proces voltooid]

Link to working code: code on godbolt


Solution

  • The ability to use an interrupt callback of this form is an extension present on the ESP32 Arduino/PlatformIO library that isn't available in the original AVR Arduino. It's defined in header you must include to get this extra overload definition of attachInterrupt().

    #include <FunctionalInterrupt.h>
    

    It works on the other code you wrote to test because you wrote the attachInterrupt() definition yourself and you used the one from FunctionalInterrupt.h.

    The reason this can work is because on ESP32 the ESP-IDF core runtime includes interrupt handlers that have a parameter passed to them that is supplied when the handler was registered. This parameter could be anything, but for C++ a common use would be as the this pointer for class.

    This fixes the root problem with non-static class methods as interrupt handlers: There is no way to pass the this pointer to the method when they're called.

    std::function<> is internally a class called a functor. It has a method named operator() that is executed when the class object is called with the syntax of a function. Since we can now call class methods, it can be used as an interrupt handler.