3

I intended to use a timer interrupt for limited instances of a class in my ESP32 Arduino project. My first code conception was as follows:

portEXIT_CRITICAL_ISR(&lock0);              
  }

hw_timer_t * timer[3] = NULL;

class myClass
{
private:
  irqCallback timmerCB;
  volatile uint8_t pri_var1, pri_var2;
  static uint8_t class_instance_number=0;
public:
  volatile uint8_t pub_var1, pub_var2;
  myClass(/* args */);
  ~myClass();
};

myClass::myClass(/* args */)
{
  timmerCB=timerCBArray[class_instance_number];
  timer[class_instance_number] = timerBegin(0, 80, true);  
  timerAttachInterrupt(timer[class_instance_number], &timmerCB, true);
  class_instance_number++;
}

However, I have this problem: how can I change the variable inside the interrupt regarding to each class instance?

0

2 Answers 2

12

Interrupt Service Routine (ISR) outside a class

Let's consider a simple use of interrupts:

volatile bool switchChanged;

void switchPressed ()
  {
  switchChanged = true;
  }  // end of switchPressed

void setup ()
  {
  pinMode (2, INPUT_PULLUP);
  attachInterrupt (0, switchPressed, CHANGE);
  }  // end of setup

void loop ()
  {
  // whatever    
  }  // end of loop

That compiles fine.


ISR inside a class as a class function (method)

Now let's imagine we want to put the interrupt handling into a class for our convenience.

class myClass
  {
  volatile bool switchChanged;
  
  public:

  void begin ()
    {
    pinMode (2, INPUT_PULLUP);
    attachInterrupt (0, switchPressed, CHANGE);   // <--- line 10
    }  // end of myClass::begin
    
  void switchPressed ()
    {
    switchChanged = true;
    }  // end of myClass::switchPressed
    
  };  // end of class myClass
  
myClass foo;  // make an instance of myClass
  
void setup ()
  {
  foo.begin ();
  }  // end of setup

void loop ()
  {
  // whatever    
  }  // end of loop

That does not compile:

ISR_in_class_test.ino: In member function ‘void myClass::begin()’:
ISR_in_class_test:10: error: argument of type ‘void (myClass::)()’ does not match ‘void (*)()’

What is going on here?

ISRs have to be static functions, taking no arguments. However (non-static) class functions have an implied this-> pointer which points to the particular instance of the class.

For example, if we have two instances:

myClass foo;  
myClass bar; 

If we call foo.begin() then this-> points to "foo", and if we call bar.begin then this-> points to "bar".

However an ISR, when fired by the processor, cannot know whether this-> is "foo" or "bar" or something else. Thus the compiler cannot compile that line.


ISR inside a class as a static class function

We can try to work around this by making the class function static. Doing that means that the function is not tied to any particular instance, and thus the attachInterrupt line will compile.

class myClass
  {
  volatile bool switchChanged;   // <--- line 3
  
  public:

  void begin ()
    {
    pinMode (2, INPUT_PULLUP);
    attachInterrupt (0, switchPressed, CHANGE);
    }  // end of myClass::begin
    
  static void switchPressed ()
    {
    switchChanged = true;   // <--- line 15
    }  // end of myClass::switchPressed
    
  };  // end of class myClass
  
myClass foo;  // make an instance of myClass
  
void setup ()
  {
  foo.begin ();
  }  // end of setup

void loop ()
  {
  // whatever    
  }  // end of loop

However now we have a different problem:

ISR_in_class_test.ino: In static member function ‘static void myClass::switchPressed()’:
ISR_in_class_test:3: error: invalid use of member ‘myClass::switchChanged’ in static member function
ISR_in_class_test:15: error: from this location

A non-static class variable cannot be called from a static class function. Why? Because the compiler doesn't know which variable you want. Is it foo.switchChanged or bar.switchChanged?


ISR inside a class as a static class function with static variables

To make the static function work, it can only access static variables. So we can make switchChanged static. Plus we need to define an instance of this static variable.

class myClass
  {
  static volatile bool switchChanged;  // declare
  
  public:

  void begin ()
    {
    pinMode (2, INPUT_PULLUP);
    attachInterrupt (0, switchPressed, CHANGE);
    }  // end of myClass::begin
    
  static void switchPressed ()
    {
    switchChanged = true;
    }  // end of myClass::switchPressed
    
  };  // end of class myClass
  
volatile bool myClass::switchChanged;  // define
  
myClass foo;  // make an instance of myClass
  
void setup ()
  {
  foo.begin ();
  }  // end of setup

void loop ()
  {
  // whatever    
  }  // end of loop

Now the class compiles. However we have thrown away most of the advantages of having a class in the first place, as we are forced to use a static function, and that static function can only access static variables.


Glue routines

To work around this problem we can write short "glue" routines. These are functions that interface between an ISR and an instance of a class.

class myClass
  {
  volatile bool switchChanged;

  static myClass * instances [2];
 
  static void switchPressedExt0 ()
    {
    if (myClass::instances [0] != NULL)
      myClass::instances [0]->switchPressed ();
    }  // end of myClass::switchPressedExt0
  
  static void switchPressedExt1 ()
    {
    if (myClass::instances [1] != NULL)
      myClass::instances [1]->switchPressed ();
    }  // end of myClass::switchPressedExt1


  public:

  void begin (const byte whichPin)
    {
    pinMode (whichPin, INPUT_PULLUP);
    switch (whichPin)
      {
      case 2: 
        attachInterrupt (0, switchPressedExt0, CHANGE);
        instances [0] = this;
        break;
        
      case 3: 
        attachInterrupt (1, switchPressedExt1, CHANGE);
        instances [1] = this;
        break;
        
      } // end of switch
    }  // end of myClass::begin
    
  void switchPressed ()
    {
    switchChanged = true; 
    }
    
  };  // end of class myClass
  
myClass * myClass::instances [2] = { NULL, NULL };

// instances of our class  
myClass foo; 
myClass bar;

void setup ()
  {
  foo.begin (2);   // pin D2
  bar.begin (3);   // pin D3
  }  // end of setup

void loop ()
  {
  // whatever    
  }  // end of loop

This is a bit fiddly, however what it is doing is remembering in an array which instance of the class is associated with which interrupt. The "glue" routines switchPressedExt0 and switchPressedExt1 call the appropriate instance of the switchPressed function by using the remembered class pointer.

Now the non-static function switchPressed can access non-static class variables.


This answer was reproduced from my forum, but I can't post link-only answers here.

3
  • The Glue routines make myclass only work for specific scenarios. Can you use myclass as a base class and then use inheritance to create new class definitions for each scenario? Mar 26 at 15:46
  • I tried it and realized inheritance does not work since we inherit the static variable. Creating two classes with identical code and different names works...but that's duplicated code which is also not a clean solution. Mar 26 at 21:04
  • @PhilippWerminghausen You only have a fixed amount of hardware. It is hard to make a general class (which you might instantiate hundreds of times) but which have to share a single timer or interrupt. Compromises have to be made.
    – Nick Gammon
    Mar 27 at 22:08
0

I know this is a late answer but I think it is possible to get past the problem with this if you use std::bind

Below is a full example but if that is too long to read, here is a minimal example.

class ISRClass {
public:
  ISRClass(int pin) : pin_(pin) {}
  
  void setup() {
    attachInterrupt(digitalPinToInterrupt(pin_),
                    std::bind(&ISRClass::sensePinIsr, this), CHANGE);
  }

private:
  int pin_;
  ulong change_count_;

  void IRAM_ATTR sensePinIsr() { 
    // do something fast and simple
    change_count_++;
  }
};

The function returned by std::bind takes no arguments, not even this.

Full example follows.

Here is an example. This class uses interrupts to simply set the state of a member variable when the pin has changed state. It then only reads the pin when needed. You still have to query the member variable in the loop, but that is about 10x faster than reading the pin.

#include "FunctionalInterrupt.h"
#include <Arduino.h>
#include <functional>
#include <optional>
#include <map>

using CallBack = std::function<void(int)>;

class PinMonitor {
public:
  PinMonitor(int pin) : pin_(pin) {}
  void setup() {
    pin_state_ = digitalRead(pin_);
    attachInterrupt(digitalPinToInterrupt(pin_),
                    std::bind(&PinMonitor::sensePinIsr, this), CHANGE);
  }

  std::optional<int> CheckForNewPinState(int debounce) {
    if (!change_count_) {
      return std::nullopt;
    }
    if (!is_debouncing_) {
      is_debouncing_ = true;
      debouce_started_at_ = millis();
      return std::nullopt;
    }
    auto time = millis();
    if (time < debouce_started_at_) {
      debouce_started_at_ = time; // handle timer rollover
    } 
    if (time < debouce_started_at_ + debounce) {
      return std::nullopt;
    }
    is_debouncing_ = false;
    change_count_ = 0;
    auto new_pin_state = digitalRead(pin_);
    if (new_pin_state != pin_state_) {
      pin_state_ = new_pin_state;
      for ( auto [_, callback] : callbacks_) {
        callback(pin_state_);
      }
      return pin_state_;
    }
    return std::nullopt;
  }

  int DoIfPinStateChanges(const CallBack callback) {
    next_callback_key++;
    callbacks_[next_callback_key] = callback;
    return next_callback_key;
  }

  void RemoveCallback(int key) {
    callbacks_.erase(key);
  }

private:
  int pin_;
  int pin_state_;
  ulong change_count_;
  CallBack callback_ = [](auto a) {};
  std::map<int, CallBack> callbacks_;
  bool is_debouncing_{};
  ulong debouce_started_at_{};
  int next_callback_key{};

  void IRAM_ATTR sensePinIsr() { change_count_++; }
};

Then in main you could use this like below.

NetworkConnection networkConnection;
OTAHandler otaHandler;
Logger &logger = Logger::getInstance();

PinMonitor pinMonitor1(16);
PinMonitor pinMonitor2(17);

int call_count{};
int removable_callback{};

void setup() {
  networkConnection.connect();
  logger.startWebSocket();
  otaHandler.setup();
  pinMode(16, INPUT_PULLDOWN);
  pinMode(17, INPUT_PULLDOWN);
  auto chip_id = ESP.getEfuseMac();

  pinMonitor1.setup();
  pinMonitor1.DoIfPinStateChanges([chip_id](auto a) {
    logger.infoln("chip %12llx, reports pin1 new state of %d detected", chip_id, a);
  });
  removable_callback = pinMonitor1.DoIfPinStateChanges([chip_id](auto a) {
    logger.infoln("second call back was called on pin1, new state of %d detected", a);
    call_count++;
  });
  pinMonitor2.setup();
  pinMonitor2.DoIfPinStateChanges([chip_id](auto a) {
    logger.infoln("chip %12llx, reports pin2 new state of %d detected", chip_id, a);
  });

}

void loop() {
  otaHandler.loop();
  logger.checkWebSocket();
 
  std::optional<int> new_pin_state1;
  std::optional<int> new_pin_state2;
  
  new_pin_state1 = pinMonitor1.CheckForNewPinState(100);
  if (new_pin_state1) {logger.infoln("new pin1 state is %d", *new_pin_state1);}

  new_pin_state2 = pinMonitor2.CheckForNewPinState(100);
  if (new_pin_state2) {logger.infoln("new pin2 state is %d", *new_pin_state2);}
  

  if (call_count > 5) {pinMonitor1.RemoveCallback(removable_callback);}

}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.