Dynamic lambdas pass to attachInterrupt

Hello,

I would like to have a common function that receives any PIN number as argument, then create a common ISR using attachInterrupt on that pin.

I decided to use lambda for dynamic purpose. However, I got stucked at how to pass the pin number to callback function.

For more details, please see the code below, function LocalRegisterButton , I would like to simplize that function so that I can call attachInterrupt on any pin input.

Currently I cannot do that because attachInterrupt does not accept a captured-by-value lambda. In the code, you can see line 18 where lambda starts. I want to refactor that code to utilize pin variable.

Any tries, you can test your code on my Wokwi project for this code here:

Thank you.

static uint8_t _pin_pressed = 0;

void btnRisingIsrHandler(uint8_t btn_pin)
{
  _pin_pressed = btn_pin;
  /* Do other stuff for specific pin */
}

void LocalRegisterButton(uint8_t pin)
{
  /* I would like to simplize this function based on *pin* variable */
  pinMode(pin, INPUT_PULLUP);
  
  switch ( pin )
  {
    case 4:
      attachInterrupt(4,
        /* Lambda start */
        []() IRAM_ATTR {
          /* The problem is that I must hard-code the number 4 here */
          /* I want to use *pin* variable to remove "case 5:" below */
          btnRisingIsrHandler(4);
        }
        /* Lambda end */
        ,
        RISING);
    break;

    case 5:
      attachInterrupt(5, []() IRAM_ATTR {
          btnRisingIsrHandler(5);
        },
        RISING);
    break;

    /* Other cases for all possible PIN numbers */
  }

}

void setup() {
  Serial.begin(115200);
  Serial.println("Hello, ESP32-S3!");
  LocalRegisterButton( 4 );
  LocalRegisterButton( 5 );
}

void loop() {
  if (_pin_pressed > 0) {
    Serial.printf("Pin pressed: %d\n", _pin_pressed);
    _pin_pressed = 0;
  }

  delay(10); // this speeds up the simulation
}

Perhaps some templates could be of use here?

static uint8_t _pin_pressed = 0;

template <uint8_t pin>
void btnRisingIsrHandler() {
  _pin_pressed = pin;
  /* Do other stuff for specific pin */
}

template <uint8_t pin>
void LocalRegisterButton() {
  pinMode(pin, INPUT_PULLUP);
  attachInterrupt(pin, btnRisingIsrHandler<pin>, RISING);
}

void setup() {
  Serial.begin(115200);
  Serial.println("Hello, ESP32-S3!");
  LocalRegisterButton<4>();
  LocalRegisterButton<5>();
}

void loop() {
  if (_pin_pressed > 0) {
    Serial.printf("Pin pressed: %d\n", _pin_pressed);
    _pin_pressed = 0;
  }

  delay(10); // this speeds up the simulation
}

Thank you very much. I also found the same solution as yours, that is using template.

However, another issue comes. That is, ISR should be placed in IRAM using IRAM_ATTR. Unfortunatelly, IRAM_ATTR does not work with template. I tried this but got compile error.

template <uint8_t pin> 
void IRAM_ATTR btnRisingIsrHandler()  {
  _pin_pressed = pin;
  /* Do other stuff for specific pin */
}

FYI, in ESP32, it is ok to use ISR without IRAM_ATTR in general but ISR will not work in some special cases. Therefore IRAM_ATTR is a must.

In ESP8266, without IRAM_ATTR , esp8266 reports "ISR not in IRAM!" and crashes.

What error?

I think this is a sketch preprocessing bug. Removing one newline seems to fix the problem.

template <uint8_t pin> void IRAM_ATTR btnRisingIsrHandler() {
  _pin_pressed = pin;
  /* Do other stuff for specific pin */
}

Three other points before answering your question:

  • Your _pin_pressed variable must be declared volatile.
  • The btnRisingIsrHandler() function should also be noted as IRAM_ATTR.
  • Your code structure could mishandle multiple button presses if the second one occurs just before the _pin_pressed = 0; line in the if statement is executed.

The code below fixes the first two of these issues and shows how to use the lambda as you want. It takes advantage of the "FunctionalInterrupt.h" header available for ESP32 and ESP8266.

#include "FunctionalInterrupt.h"

volatile static uint8_t _pin_pressed = 0;

IRAM_ATTR void btnRisingIsrHandler(uint8_t btn_pin) {
  _pin_pressed = btn_pin;
  /* Do other stuff for specific pin */
}

void LocalRegisterButton(uint8_t pin) {
  pinMode(pin, INPUT_PULLUP);
  attachInterrupt(pin, [pin]() IRAM_ATTR {btnRisingIsrHandler(pin);}, RISING);
}

void setup() {
  Serial.begin(115200);
  Serial.println("Hello, ESP32-S3!");
  LocalRegisterButton( 4 );
  LocalRegisterButton( 5 );
}

void loop() {
  if (_pin_pressed > 0) {
    Serial.printf("Pin pressed: %d\n", _pin_pressed);
    _pin_pressed = 0;
  }
  delay(10); // this speeds up the simulation
}

To fixed the third problem that I noted, you might consider using a FreeRTOS queue with ESP32. For ESP8266 you'll have come up with a better way in the ISR to note the individual buttons being pressed.

Finally, the standard advice here .... using interrupts for buttons is almost always a bad idea.

Thank you very much for your very detailed answer. The key is "FunctionalInterrupt.h" , it helps me to use lambda with variable capturing. I've never known this before. Thanks again.

I honestly don't understand this. Why it is a bad idea using interrupts for buttons? And do you suggest any other technique to due with buttons other than RTOS?
Actually, the code in the #1 post is just the minimal code, the purpose is to focus on how to use lambda within attachInterrupt. My full code is of cousre aware of debouncing.

Could you please explain more details on this point? Isn't it distinguished using button number that is passed as argument, the pin value?

Interrupts almost always bring extra complications (see below) that simply aren't worth the trouble when dealing with the slow (compared to processor) speed of buttons pressed by humans. You're already polling the _pin_pressed variable in the loop() function anyway. Just poll each button's GPIO instead. Also, contact bounce is harder to deal with when button presses are processed using interrupts.

Suppose the button on GPIO 4 is pressed. So, the ISR sets _pin_pressed to 4. The loop() code will then enter the if statement, but:

  if (_pin_pressed > 0) {
    Serial.printf("Pin pressed: %d\n", _pin_pressed);  // <--------- Prints "Pin Pressed: 4"
                                                       // <--- Button 5 pressed here. Interrupt occurs and ISR sets '_pin_pressed' to 5.
    _pin_pressed = 0;                                  // <------- '_pin_pressed' set to 0 here so the Button 5 press is lost!!!
  }

That's just one example of how your code could go sideways.

Thank you for pointing me out. However, as I said, that is the minimal code to focus on lambda usage only. My entire code has a way to handle with multi buttons.

Besides, even though I polled _pin_pressed in the loop, interrupts are still useful. This is just a minimal code so the polling can capture the pin pressed event in time. However, in a complex application, the loop may handle some big jobs which requires extra time of work. In that case, the pressed event may be missed.

Thank you for that. Sorry I didn't mention in the first post, I'm using esp32 and esp8266, in ESP8266, it still reports that "ISR is not in IRAM"
I think esp8266 may not handle this case.