Class member function as callback for interrupt

Hi all,

I'm trying to give the control of the motors of a self-balancing robot some structure by integrating it into a class. I want to control their speed by letting the encoders mounted on them triggering an interrupt that calls a method within the same class. So far the class is looking like this:

#include <Motor.h>

Motor::Motor() {}
Motor::Motor(uint8_t pinIN1, uint8_t pinIN2, uint8_t pinEncA, uint8_t pinEncB) {
    this->pinIN1 = pinIN1;
    this->pinIN2 = pinIN2;
    this->pinEncA = pinEncA;
    this->pinEncB = pinEncB;

    pinMode(this->pinIN1, OUTPUT);
    pinMode(this->pinIN2, OUTPUT);
    pinMode(this->pinEncA, INPUT);
    pinMode(this->pinEncB, INPUT);

    this->nEncPulsesPerRev = 231; // 11 * 21; 

    this->nEncoder = 0;

    attachInterrupt(this->pinEncA, cbkEncA, RISING);
}

Motor::~Motor() {}

void Motor::setPWM(uint8_t pwm, int8_t direction) {

    if (direction > 0) {
        analogWrite(this->pinIN1, pwm);
        analogWrite(this->pinIN2, 0);
    } else if (direction < 0) {
        analogWrite(this->pinIN1, 0);
        analogWrite(this->pinIN2, pwm);
    } else {
        analogWrite(this->pinIN1, 0);
        analogWrite(this->pinIN2, 0);
    }
}

void Motor::setPWM(float pwm) {
    uint8_t pwm_u8 = (uint8_t)abs(pwm);
    if (pwm > 0) {
        this->setPWM(pwm_u8, 1);
    } else if (pwm < 0) {
        this->setPWM(pwm_u8, -1);
    } else {
        this->setPWM(pwm_u8, 0);
    }
}

void Motor::cbkEncA() {
    if (analogRead(pinEncB) > 0) {
        this->nEncoder++;
    } else {
        this->nEncoder--;
    }    
}

long Motor::getNEncoder() {
    return this->nEncoder;
}

The compiler throws this error:

Compiling .pio\build\esp32-s3-devkitc-1\src\Motor.cpp.o
src/Motor.cpp: In constructor 'Motor::Motor(uint8_t, uint8_t, uint8_t, uint8_t)':
src/Motor.cpp:22:51: error: invalid use of non-static member function 'void Motor::cbkEncA()'

I'm using a self-made ESP32-S3 board and VS Code with PlatformIO.

I've seen it's a pretty common question, with Google showing quite a few posts in several different forums, but after having reviewed many of them I wasn't still able to find a solution that works for me. I'm definitely not the biggest expert in C++ nor I'm familiar with lambda-functions, which seem to be a solution many suggest, so it would be great to get some help here based on my own code.

Thanks in advance!

your title states "class method" and your example seems to indicate an "instance method"

➜ In C++, a "class method" (static method) belongs to the class itself and can be called without an object, while an instance method operates on a specific object and requires an instance to be called.

The issue you face is that the interrupt does not know which object to call the method on if it's not a "class method". The easy way to handle this is to add a general function as the callback and in this callback you call the method on the object.

There are alternatives but they become more sophisticated...

1 Like

Thanks, I've adjusted the title.

With "general function" you mean one outside of the class? It's something I had expeceted to avoid. I like the idea of having everything encapsulated within the class.

I understand, this is indeed a common request. The issue is that when you attach an interrupt, you can’t pass a context (like a this pointer), you just give it a function within the current scope ➜ so the workaround is to have that function somehow know which instance it’s supposed to call.

As discussed, the classic, lightweight embedded way is to use a static function as the ISR, and inside that function, forward the call to the correct instance.

If you’re not constrained to embedded C++, and you have access to the STL, another clean way is to use std::map

In modern C++, if your interrupt system accepts function pointers with captures — like on ESP32 or other systems where you can attach lambdas or std::function — you can also forward the call using a lambda with a captured instance pointer. But on classic Arduino and AVR/STM32 HAL, interrupts only accept raw function pointers with no captures, which rules out lambdas and std::bind.

My take on your case is that if you have only one instance of your class, you are making your life very difficult by trying to embed that into a class.

1 Like

Ok, I get it, thanks again.

I'm planning to have several instances of the class. The first use of the class is a self-balancing motor with two motors, but I'd like to be able to eventually use it with more motors in the future (i.e. a 4WD car).

I've found this discussion in which you actually also participated and looks quite promising. I'll give it a try when I'm back home, but any other suggestion is welcome.

You do what you want as long as you stick with ESP32.

But first, as a stylistic note, your use of this-> notation inside the class is superfluous and just adds noise.

This:

    this->nEncoder = 0;

Is equivalent to:

    nEncoder = 0;

To use an instance function as an ISR (ESP, STM32 only):

#include "FunctionalInterrupt.h"  // ESP32 Only

class Motor {
  public:
    Motor(uint8_t p): pinEncA(p) {};
    void begin();

  private:
    uint8_t pinEncA;
    void cbkEncA ();
};

void Motor::begin() {
  pinMode(pinEncA, INPUT_PULLUP);
  auto isr = [this]() {
    this->cbkEncA();
  };
  attachInterrupt(digitalPinToInterrupt(pinEncA), isr, RISING);
}

void Motor::cbkEncA() {
}


Motor motor(14);

void setup() {
  motor.begin();
}

void loop() {
}

STM32 will also accept std::function. From WInterrupts.h:

#include <functional>

typedef std::function<void(void)> callback_function_t;
void attachInterrupt(uint32_t pin, callback_function_t callback, uint32_t mode);

Right now I'm only using ESPs (classic, S3 and C6). And thanks for your comment on the unnecessary use of this.

Your solution worked like a charm, thanks!! Then I kept adding other functionality to my class, including an interrupt attached to a hw_timer_t to calculate de speed of the motor, and faced the same error as with the pin-triggered interrupt. Sure of knowing the solution, I tried this:

void Motor::begin() {    
    auto isrPulse = [this]() {
        this->cbkEncA();
    };
    auto isrTimer = [this]() {
        this->cbkTimer();
    };
    attachInterrupt(digitalPinToInterrupt(pinEncA), isrPulse, RISING);
    this->timer = timerBegin(0, 8000, true);
    timerAttachInterrupt(timer, isrTimer);    
}

But the compiled laughed in my face and said:

Compiling .pio\build\esp32-s3-devkitc-1\src\Motor.cpp.o
src/Motor.cpp: In member function 'void Motor::begin()':
src/Motor.cpp:37:33: error: cannot convert 'Motor::begin()::<lambda()>' to 'void (*)()'

What's different between attachInterrupt and timerAttachInterrupt? How can I pass the isr here?

Thanks again!

cool - thx

On ESP32, FunctionalInterrupt replaces (overloads) the standard attachInterrupt with its own version that can accept lambdas with captures. It does this by internally managing the lambda storage and registering a plain static ISR that calls the stored lambda ➜ When you call attachInterrupt after including FunctionalInterrupt, you’re actually using their enhanced function, not the original Arduino one

FunctionalInterrupt does not offer the same service for timerAttachInterrupt() which still expects a plain pointer to a void function with no parameters (void (*)()) - hence the compiler barking at you.

Note that FunctionalInterrupt just basically created a function for you that will call the specific instance which you could have done yourself as discussed and will need to do for timerAttachInterrupt()...

The latter doesn't accept a std::function argument.

Please post your complete code (or better yet, an MRE) rather than snippets.

Great explanation, got it, thanks!

main.cpp

#define sgn(x) ((x) < 0 ? -1 : ((x) > 0 ? 1 : 0))
#include <Adafruit_NeoPixel.h>
#include <NimBLEDevice.h>
#include <SPI.h>
#include "ICM42688.h"

// Para flash OTA
#include <WiFi.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
#include <Arduino.h>

#include <Motor.h>

Motor motA = Motor(9, 10, 11, 12); 
Motor motB = Motor(17, 16, 18, 8); 

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

void loop() {
  motA.setPWM(200.0);
  delay(500);
  motA.setPWM(-200.0);
  delay(500);
  motA.setPWM(0.0);
  delay(500);
  motB.setPWM(200.0);
  delay(500);
  motB.setPWM(-200.0);
  delay(500);
  motB.setPWM(0.0);
  delay(500);

  Serial.println(motA.getNEncoder());  
}

motor.h

#ifndef __MOTOR_H__
#define __MOTOR_H__

#include <Arduino.h>
#include "FunctionalInterrupt.h" 

class Motor {
    private:
        uint8_t pinIN1;
        uint8_t pinIN2;
        uint8_t pinEncA;
        uint8_t pinEncB;

        long nEncoder, nEncoder_;
        int nEncPulsesPerRev;  
        float thetaPerPulse;        

        float om;

        hw_timer_t* timer = NULL;

    public:
        Motor();
        Motor(uint8_t, uint8_t, uint8_t, uint8_t);

        void begin();
        void setPWM(uint8_t, int8_t);
        void setPWM(float);
        
        void cbkEncA();
        void cbkTimer();
        long getNEncoder();
        ~Motor();

};

#endif

motor.cpp

#include <Motor.h>

Motor::Motor() {}
Motor::Motor(uint8_t pinIN1, uint8_t pinIN2, uint8_t pinEncA, uint8_t pinEncB) {
    this->pinIN1 = pinIN1;
    this->pinIN2 = pinIN2;
    this->pinEncA = pinEncA;
    this->pinEncB = pinEncB;

    pinMode(this->pinIN1, OUTPUT);
    pinMode(this->pinIN2, OUTPUT);
    pinMode(this->pinEncA, INPUT_PULLUP);
    pinMode(this->pinEncB, INPUT_PULLUP);    

    this->nEncPulsesPerRev = 231; // 11 * 21; 
    this->nEncoder  = 0;  
    this->nEncoder_ = 0;  
    this->thetaPerPulse = 2*PI/nEncPulsesPerRev
        
}

Motor::~Motor() {}

void Motor::begin() {    
    auto isrPulse = [this]() {
        this->cbkEncA();
    };
    auto isrTimer = [this]() {
        this->cbkTimer();
    };
    attachInterrupt(digitalPinToInterrupt(pinEncA), isrPulse, RISING);
    this->timer = timerBegin(0, 8000, true);
    timerAttachInterrupt(timer, isrTimer);    
}

void Motor::setPWM(uint8_t pwm, int8_t direction) {

    if (direction > 0) {
        analogWrite(this->pinIN1, pwm);
        analogWrite(this->pinIN2, 0);
    } else if (direction < 0) {
        analogWrite(this->pinIN1, 0);
        analogWrite(this->pinIN2, pwm);
    } else {
        analogWrite(this->pinIN1, 0);
        analogWrite(this->pinIN2, 0);
    }
}

void Motor::setPWM(float pwm) {
    uint8_t pwm_u8 = (uint8_t)abs(pwm);
    if (pwm > 0) {
        this->setPWM(pwm_u8, 1);
    } else if (pwm < 0) {
        this->setPWM(pwm_u8, -1);
    } else {
        this->setPWM(pwm_u8, 0);
    }
}

void Motor::cbkEncA() {    
    if (analogRead(pinEncB) > 0) {
        this->nEncoder++;        
    } else {
        this->nEncoder--;        
    }        
}

void Motor::cbkTimer() {
    this->om = this->thetaPerPulse/(this->nEncoder - this->nEncoder_) / 1e6;
}

long Motor::getNEncoder() {
    return this->nEncoder;
}

From that form of the timerBegin() function, it appears you're using ESP32 Arduino Core 2.x. If so, your options are limited. If you switch to Core 3.x, you can use timerAttachInterruptArg(). However, doing so will change the required call format for timerBegin() and perhaps other APIs.

class Motor {
  private:
    hw_timer_t* timer = NULL;
    
    static void StaticTimerISR(void *arg) {
      Motor *ptr {reinterpret_cast<Motor *> (arg)};
      ptr->instanceTimerIsr();
    }

    void instanceTimerIsr() {  
      // Instance-specific ISR Code here
    }

  public:
    void begin() {
      timerAttachInterruptArg(timer, StaticTimerISR, this);
    }
};

Motor motor;

void setup() {
  motor.begin();
}

void loop() {
}

Yes, I realized when I moved the code from the Arduino IDE to VS Code + PIO. I had to adjust the arguments of timerBegin.

I have been trying for two hours to switch to Arduino Core 3.2.0 in PIO and failed miserably (mostly with the help of ChatGPT, which might have been a mistake). ChatGPT insisted on cloning that core into the project folder and forcing PIO to use it, but it kept compiling with the older one (2.0.17 maybe).

I don't know what else can I try. If you know of any up-to-date reference of how to do it, please let me know. The ones I have found didn't help.

Thanks again.

So, I have arrived to something that seems to be doing what I was trying to achieve: a class with basic functionality for measuring the velocity of up to four motors using their encoders, with interrupts calling class member functions as callbacks without needing any code outside of the class to assign them.

Tested with VS Code + PIO and Arduino Core 2.x. A word of advice: my programming skills in C++ are rather rudimentary, so please take this code as a functional example rather than a best-practice reference. It seems to work for my use case, but there may be better or safer ways to implement certain parts. Use at your own risk and feel free to suggest improvements!

main.cpp

#include <Arduino.h>

#include <Motor.h>

Motor motA = Motor(9, 10, 11, 12, 20.4, 11); // JGA25-371 motors seem to have encoders with 11 pulses/rev
Motor motB = Motor(17, 16, 18, 8, 20.4, 11); 

void setup() {  
  Serial.begin(115200);
  
  motA.begin(1, 50.0, 0.02);  // #timer [0-3], sample frequency [Hz], time constant [s] (first order filter)
  motB.begin(2, 50.0, 0.02);
  
}

void loop() {  
    motA.setPWM(60, 1);
    motB.setPWM(60, 1);

    while(millis()<5000) {                        
      if (motA.getTimerTriggered()) {          
          Serial.printf(  "Time = %04.3f s, omA = %+05.2f rpm, omB = %+03.2f rpm\n", (float)millis()/1000, motA.getOm_rev_m(), motB.getOm_rev_m());          
          motA.resetTimerTriggered();
      }        
    }     

    motA.stop();
    motB.stop();
    while(1){}
  }

Motor.h

#ifndef __MOTOR_H__
#define __MOTOR_H__

#include <Arduino.h>
#include "FunctionalInterrupt.h" 

class Motor {
    private:
        uint8_t pinIN1,  pinIN2;
        uint8_t pinEncA, pinEncB;        

        long  nPulses;
        int   nPulsesSinceLastSample; // Counter of encoder pulses
        int   nPulsesPerEncoderRev; 
        float nPulses2Rev, nPulses2Phi, phi2nPulses; // Relationship between number of puleses to angle in revs and radians
        float iRatio;  // Gear ratio
        

        float phi;  // Angular position
        float om;   // Angular velocity

        void cbkEncA();
        
        float sampleFreq;
        float sampleTime;
        float timeConstant;
        float alpha;
        bool timerTriggered;

        hw_timer_t* timer = nullptr;
        static constexpr int MAX_TIMERS = 4;
        static Motor* instances[MAX_TIMERS];
        static void IRAM_ATTR onTimer0();
        static void IRAM_ATTR onTimer1();
        static void IRAM_ATTR onTimer2();
        static void IRAM_ATTR onTimer3();

    public:
        Motor();
        Motor(uint8_t, uint8_t, uint8_t, uint8_t, float, uint8_t);
        void begin(uint8_t, float, float);
        void stop();
        ~Motor();

        void IRAM_ATTR cbkTimer();
        void setPWM(uint8_t, int8_t);
        void setPWM(float);
        
        long getNPulses();
        bool getTimerTriggered();
        void resetTimerTriggered();
        float getPhi();
        float getPhi_rev();
        float getOm();
        float getOm_rev_s();
        float getOm_rev_m();
        
};

#endif

Motor.cpp

#include <Motor.h>

Motor* Motor::instances[MAX_TIMERS] = {nullptr};

Motor::Motor() {}
Motor::Motor(uint8_t pinIN1, uint8_t pinIN2, uint8_t pinEncA, uint8_t pinEncB, float iRatio, uint8_t nPulsesPerEncoderRev) {
    this->pinIN1 = pinIN1;
    this->pinIN2 = pinIN2;
    this->pinEncA = pinEncA;
    this->pinEncB = pinEncB;

    pinMode(pinIN1, OUTPUT);
    pinMode(pinIN2, OUTPUT);
    pinMode(pinEncA, INPUT_PULLUP);
    pinMode(pinEncB, INPUT_PULLUP); 

    this->nPulsesPerEncoderRev = nPulsesPerEncoderRev;
    this->iRatio  = iRatio;    
    this->nPulses2Rev = nPulsesPerEncoderRev * iRatio;    
    this->nPulses2Phi = nPulses2Rev/(2*PI);
    this->phi2nPulses  = (2*PI)/nPulses2Rev; // Just to change a division into a callback into a multiplication, hoping the coprocessor can cope with it faster

    nPulses  = 0;
    phi = 0;
    om  = 0;
    
    this->timerTriggered = false;
}

Motor::~Motor() {
}

void Motor::begin(uint8_t timerIndex, float sampleFreq, float timeConstant) {   // Timer# [0-3], Sample frequency in Hz, low pass filter / moving average time cosntant
    
    this->sampleFreq = sampleFreq;
    this->sampleTime = 1/sampleFreq;
    this->alpha = sampleTime / (sampleTime + timeConstant);
    
    if  (timerIndex >= MAX_TIMERS) return;

    // Calculate prescale and tick number pro timer activation for the desired frequency 
    uint16_t prescaler = 2;  // Two seems to be the minimum accepted value by the ESP32-S3
    const uint32_t APB_CLK = 80000000;
    uint64_t ticks = APB_CLK / (sampleFreq*prescaler);

    while (ticks > 4294967295ULL && prescaler < 65535) {
        prescaler++;
        ticks = APB_CLK / (sampleFreq * prescaler);
    }   

    instances[timerIndex] = this;    
    timer = timerBegin(timerIndex, prescaler, true);
        
    switch (timerIndex) {
        case 0: timerAttachInterrupt(timer, onTimer0, false); break;
        case 1: timerAttachInterrupt(timer, onTimer1, false); break;
        case 2: timerAttachInterrupt(timer, onTimer2, false); break;
        case 3: timerAttachInterrupt(timer, onTimer3, false); break;
    }
    timerAlarmWrite(timer, ticks, true);    
    timerAlarmEnable(timer);

    auto isrPulse = [this]() {
        this->cbkEncA();
    };

    attachInterrupt(digitalPinToInterrupt(pinEncA), isrPulse, RISING);
}


void Motor::stop() {    
    if (timer) {
        timerAlarmDisable(timer);
        timerDetachInterrupt(timer);
        timerEnd(timer);
        timer = nullptr;        
    }
    
    this->setPWM(0, 0);

    detachInterrupt(digitalPinToInterrupt(pinEncA));    
    
    for (int i=0; i<MAX_TIMERS; i++) {
        if (instances[i] == this) {
            instances[i] = nullptr;
            break;
        }
    }

    timerTriggered = false;
    nPulsesSinceLastSample = 0;
}


void Motor::setPWM(uint8_t pwm, int8_t direction) {

    if (direction > 0) {
        analogWrite(this->pinIN1, pwm);
        analogWrite(this->pinIN2, 0);
    } else if (direction < 0) {
        analogWrite(this->pinIN1, 0);
        analogWrite(this->pinIN2, pwm);
    } else {
        analogWrite(this->pinIN1, 0);
        analogWrite(this->pinIN2, 0);
    }
}

void Motor::setPWM(float pwm) {
    uint8_t pwm_u8 = (uint8_t)abs(pwm);
    if (pwm > 0) {
        this->setPWM(pwm_u8, 1);
    } else if (pwm < 0) {
        this->setPWM(pwm_u8, -1);
    } else {
        this->setPWM(pwm_u8, 0);
    }
}

void IRAM_ATTR  Motor::cbkEncA() {    
    
    if (analogRead(pinEncB) > 0) {
        this->nPulses++;                
        this->nPulsesSinceLastSample++;
    } else {
        this->nPulses--;                
        this->nPulsesSinceLastSample--;
    }        
}

void IRAM_ATTR Motor::onTimer0() { if (instances[0]) instances[0]->cbkTimer(); }
void IRAM_ATTR Motor::onTimer1() { if (instances[1]) instances[1]->cbkTimer(); }
void IRAM_ATTR Motor::onTimer2() { if (instances[2]) instances[2]->cbkTimer(); }
void IRAM_ATTR Motor::onTimer3() { if (instances[3]) instances[3]->cbkTimer(); }

void Motor::cbkTimer() {    
    this->om = (1-alpha)*this->om + alpha*(float)this->nPulsesSinceLastSample * this->sampleFreq * this->phi2nPulses ;  // Rot. velocity in rad/s
    this->nPulsesSinceLastSample = 0;
    this->timerTriggered = true;
}

long Motor::getNPulses() {
    return this->nPulses;
}

bool Motor::getTimerTriggered() {
    return this->timerTriggered;
}

void Motor::resetTimerTriggered() {
    this->timerTriggered = false;
}

float Motor::getPhi() {
    return this->nPulses / nPulses2Phi;
}

float Motor::getPhi_rev() {
    return this->nPulses / nPulses2Rev;
}

float Motor::getOm() {
    return this->om;
}

float Motor::getOm_rev_s() {
    return this->om / (2*PI);
}

float Motor::getOm_rev_m() {
    return this->om / (2*PI) * 60;
}

You need to put the pinMode() in another function than the constructor. Typically it’s called begin()

Can you please develop? It's actually working as it is now.

The constructors for global objects run before the the initialization code that sets up the Hardware for the Arduino API environment. Anything you do to the hardware (eg GPIO) before this initialization can be undone by it. Don't confuse getting lucking and observing that it "works" with proper design that ensures it works.

What is the purpose of this function prototype? There is no corresponding implementation.

The class should use its destructor to clean up after itself. In this case, detach the interrupt, disable the associated timer, clear out the entry in the instances[] array, etc.

Ok, it makes sense, but for some reason moving those pinMode to Motor::begin changed their behaviour in an undesired way when finishing the program . I don't know why. The program is so far doing what I needed, so for now I'll leave it as it is an move forward.

None. It was a left-over from previous tries.

I added that to Motor::stop()

I have edited the code in #16 with these changes.

Thansk again you too for your help!