attachInterrupt using array of function pointer?

Is there a way to define the handler function of an attachInterrupt() using some indirect method? My code needs to check (outside of loop()) the change in status of 5 physical switches attached to 5 pins of an ESP32 and the code gets repetitive when creating the handler functions as well as when creating the attachInterrupt() ´s.

I mean, it works, but it bugs me to see something that perhaps can be handled inside a for() loop. I went through Function Pointers in Gammon's Forum but my comprehension ability hit a wall.

Thanks!

typedef struct {
  const uint8_t PIN;
  bool changed;
  bool value;
} fSwitch;

fSwitch FS[5] = {{27,0,0},{33,0,0},{15,0,0},{32,0,0},{14,0,0}};

bool sFlag = false;

void IRAM_ATTR chkFS0() {
  FS[0].changed = true; 
  FS[0].value = digitalRead(FS[0].PIN); 
  sFlag = true;}
  
void IRAM_ATTR chkFS1() {
  FS[1].changed = true; 
  FS[1].value = digitalRead(FS[1].PIN); 
  sFlag = true;}
  
void IRAM_ATTR chkFS2() {
  FS[2].changed = true; 
  FS[2].value = digitalRead(FS[2].PIN); 
  sFlag = true;}
  
void IRAM_ATTR chkFS3() {
  FS[3].changed = true; 
  FS[3].value = digitalRead(FS[3].PIN); 
  sFlag = true;}
  
void IRAM_ATTR chkFS4() {
  FS[4].changed = true; 
  FS[4].value = digitalRead(FS[4].PIN); 
  sFlag = true;}
  
void setup() {
  Serial.begin(115200);
  for (byte i=0; i<5; i++) {
    pinMode(FS[i].PIN,INPUT); 
    FS[i].value = digitalRead(FS[i].PIN); // initial state of each switch
  }
  attachInterrupt(FS[0].PIN,chkFS0,CHANGE);
  attachInterrupt(FS[1].PIN,chkFS1,CHANGE);
  attachInterrupt(FS[2].PIN,chkFS2,CHANGE);
  attachInterrupt(FS[3].PIN,chkFS3,CHANGE);
  attachInterrupt(FS[4].PIN,chkFS4,CHANGE);
}

void loop() {
  if (sFlag) {  // if any PIN has changed
    sFlag = false;
    for (byte i=0; i<5; i++)
      if (FS[i].changed) { // determine which PIN changed
        Serial.println("FS" + String(i) + "=" + String(FS[i].value)); 
        FS[i].changed = false;
      }
  }
}

First some notes:

  • Use the volatile qualifier for variables that are shared between interrupt service routines and the main program.
  • Using interrupts for switches is probably a poor design choice.
  • On ESP32, you can use FunctionalInterrupt with a lambda functions.

That being said, you could solve this using compile-time recursion to generate an arbitrary number of functions:

struct FSwitch {
    FSwitch(uint8_t pin) : pin(pin) {}
    const uint8_t pin;
    volatile bool changed = false;
    volatile bool value = false;
};

FSwitch fswitches[] {27, 33, 15, 32, 14};
volatile bool sFlag = false;

void IRAM_ATTR run_isr(unsigned interrupt) {
    auto &fs = fswitches[interrupt];
    fs.changed = true;
    fs.value = digitalRead(fs.pin);
    sFlag = true;
}

using isr_func_ptr = void(*)(void);

template <class T, size_t N>
constexpr size_t len(const T(&)[N]) { return N; }

template <unsigned NumISR>
static constexpr isr_func_ptr get_isr_N(unsigned interrupt) {
    return interrupt == NumISR - 1
               ? []() IRAM_ATTR { run_isr(NumISR - 1); }
               : get_isr_N<NumISR - 1>(interrupt); // Compile-time tail recursion
}
template <>
constexpr isr_func_ptr get_isr_N<0>(unsigned) {
    return nullptr; // Base case
}

isr_func_ptr get_isr(unsigned interrupt) {
    return get_isr_N<len(fswitches)>(interrupt);
}

void setup() {
    Serial.begin(115200);
    for (auto &fs : fswitches) {
        pinMode(fs.pin, INPUT); 
        fs.value = digitalRead(fs.pin); // initial state of each switch
    }
    for (unsigned i = 0; i < len(fswitches); ++i)
        attachInterrupt(digitalPinToInterrupt(fswitches[i].pin), get_isr(i), CHANGE);
}

This generates a different function for each interrupt automatically:

If you compile these functions,

template <unsigned NumISR>
static constexpr isr_func_ptr get_isr_N(unsigned interrupt) {
    return interrupt == NumISR - 1
               ? []() IRAM_ATTR { run_isr(NumISR - 1); }
               : get_isr_N<NumISR - 1>(interrupt); // Compile-time tail recursion
}
template <>
constexpr isr_func_ptr get_isr_N<0>(unsigned) {
    return nullptr; // Base case
}

/* constexpr */ isr_func_ptr get_isr(unsigned interrupt) {
    return get_isr_N<len(fswitches)>(interrupt);
}

the compiler generates the following anonymous functions for you, and the get_isr function returns a function pointer to them.

get_isr_N<5u>(unsigned int)::{lambda()#1}::_FUN():
        entry   sp, 32
        movi.n  a10, 4
        call8   _Z7run_isrj
        retw.n
get_isr_N<4u>(unsigned int)::{lambda()#1}::_FUN():
        entry   sp, 32
        movi.n  a10, 3
        call8   _Z7run_isrj
        retw.n
get_isr_N<3u>(unsigned int)::{lambda()#1}::_FUN():
        entry   sp, 32
        movi.n  a10, 2
        call8   _Z7run_isrj
        retw.n
get_isr_N<2u>(unsigned int)::{lambda()#1}::_FUN():
        entry   sp, 32
        movi.n  a10, 1
        call8   _Z7run_isrj
        retw.n
get_isr_N<1u>(unsigned int)::{lambda()#1}::_FUN():
        entry   sp, 32
        movi.n  a10, 0
        call8   _Z7run_isrj
        retw.n
get_isr(unsigned int):
        entry   sp, 32
        l32r    a8, .LC3
        beqi    a2, 4, .L6
        l32r    a8, .LC4
        beqi    a2, 3, .L6
        l32r    a8, .LC1
        beqi    a2, 2, .L6
        l32r    a8, .LC0
        beqi    a2, 1, .L6
        l32r    a8, .LC2
        movi.n  a9, 0
        movnez  a8, a9, a2
.L6:
        mov.n   a2, a8
        retw.n
2 Likes

WOW! You gave me quite the mind opener! This will provide for many hours of testing & learning (i.e. joy). Thanks!

I found also another alternative, using attachInterruptArg() to identify which pin triggered the interrupt, thus allowing for a single ISR. Have you seen this?

Lastly:

I'm using SPDT switches, with the Input Terminal to an ESP32 Pin and the Output Terminals to GND & VCC respectively, thus ensuring a LOW or HIGH state on the ESP32 PIN being read. The main program loop() is monitoring a large matrix of capacitive electrodes (using several capacitive sensors in a I2C bus), so I want to avoid adding delays to the loop() by taking the monitoring of the SPDT switches out from the loop() . Interrupts looked like a good option?

Are you basically looking to pass an argument to the interrupt routine?

On ESP32 you can use

void attachInterruptArg(uint8_t pin, void (*)(void*), void * arg, int mode);

void IRAM_ATTR chkFS(void *arg) 
{
  fSwitch *fs = static_cast<fSwitch *>(arg);
  fs->changed = true; 
  fs->value = digitalRead(fs->PIN); 
  sFlag = true;
}

for (size_t i = 0; i < 5; ++i)
{
   attachInterruptArg(FS[i].PIN,chkFS,&FS[i],CHANGE);
}
3 Likes

you don't need delay() to read switches. Why do you think you need one? If you want do debounce you switches, see the "Digital / Debounce" example in the IDE or use a library like "OneButton" or similar.

and you don't need DT switches to get a stable high/low signal. For usual you would either use the internal pullups (and checking against LOW) or external pulldowns and checking against HIGH.

debounce example from Arduino.

And here is an example of the "debounce code" put into OOP to get a simple button class to reuse code for several buttons

I still don't see the need of any ISR to read a button

Yes!

Your code works like a charm! Thanks!

(I know you meant attachInterruptArg() instead of attachInterrupt() :wink:)

Full code:

typedef struct {
  const uint8_t PIN;
  bool changed;
  bool value;
} fSwitch;

fSwitch FS[5] = {{27,0,0},{33,0,0},{15,0,0},{32,0,0},{14,0,0}};

volatile bool sFlag = false;

void IRAM_ATTR chkFS(void *arg) {
  fSwitch *fs = static_cast<fSwitch *>(arg);
  fs->changed = true; 
  fs->value = digitalRead(fs->PIN); 
  sFlag = true;
}
  
void setup() {
  Serial.begin(115200);
  Serial.println("Start");
  for (size_t i=0; i<5; i++) {
    pinMode(FS[i].PIN,INPUT); 
    FS[i].value = digitalRead(FS[i].PIN); // initial state of each switch
    attachInterruptArg(FS[i].PIN,chkFS,&FS[i],CHANGE);
  } 
}

void loop() {
  if (sFlag) {  // if any PIN has changed
    sFlag = false;
    for (byte i=0; i<5; i++)
      if (FS[i].changed) { // determine which PIN changed
        Serial.println("FS" + String(i) + "=" + String(FS[i].value)); 
        FS[i].changed = false;
      }
  }
}

Yes of course, just copy pasted your code and forgot to change

1 Like

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