Not just the signature, f is the wrong type entirely: it is a closure that stores a copy of the this pointer, it cannot be converted to a function pointer.
True, but this is not portable beyond Espressif boards.
Indeed.
Let's say you want to create a class InterruptCounter that simply counts edges using an interrupt (very similar to the Encoder class, but simpler).
Such a class would have a private member function incrementCount that is to be called from the ISR:
/// Private handler function that is called from the ISR.
void InterruptCounter::incrementCount() { ++counter; }
As established before, you cannot call this member function from the ISR/callback directly, because you don't have access to a pointer to the instance. The type of the callback function should be:
using isr_func_t = void (*)(); // no arguments, no return value
Therefore, you need to store the instance pointer associated with each interrupt handler in an array:
/// Array of pointers to all instances with active interrupts. Used to look
/// up the instance to call the handler for in the ISR.
static InterruptCounter *InterruptCounter::instance_table[max_num_interrupts];
Your interrupt handler would then look like this:
static void InterruptCounter::handler() {
instance_table[?]->incrementCount();
}
where [?] is the (unknown) index of the interrupt that caused the handler to be invoked. In an ideal world, you already know this, but the Arduino API doesn't expose it.
So instead of having one handler function, you're forced to make multiple, one for each interrupt index, and then attach the right one during setup.
static void InterruptCounter::handler0() { instance_table[0]->incrementCount(); }
static void InterruptCounter::handler1() { instance_table[1]->incrementCount(); }
static void InterruptCounter::handler2() { instance_table[2]->incrementCount(); }
// etc.
void InterruptCounter::begin() {
int interrupt_index = digitalPinToInterrupt(pin);
instance_table[interrupt_index] = this;
isr_func_t handler;
switch (interrupt_index) {
case 0: handler = handler0; break;
case 1: handler = handler1; break;
case 2: handler = handler2; break;
// etc.
}
attachInterrupt(interrupt_index, handler, FALLING);
}
The requirement to have a specific handler function for each interrupt is what causes the mess. Ideally, we'd like a function get_isr() such that get_isr(N) returns a pointer to a function with the same body as InterruptCounter::handlerN.
The get_isr function can be implemented without any repetitions or endless switch statements by using a lambda function inside of a recursive function template:
template <unsigned NumISR = max_num_interrupts>
auto InterruptCounter::get_isr(unsigned interrupt) -> isr_func_t {
return interrupt == NumISR - 1
? [] { instance_table[NumISR - 1]->incrementCount(); }
: get_isr<NumISR - 1>(interrupt); // Compile-time tail recursion
}
template <>
inline auto InterruptCounter::get_isr<0>(unsigned) -> isr_func_t {
return nullptr; // Base case
}
void InterruptCounter::begin() {
int interrupt_index = digitalPinToInterrupt(pin);
instance_table[interrupt_index] = this;
isr_func_t handler = get_isr(interrupt_index);
attachInterrupt(interrupt_index, handler, FALLING);
}
You don't need the lambda function here: Since it doesn't have any captures, it could have been replaced by a free function template, with the NumISR-1 as a template argument.
The main power here comes from the recursive template, which allows you to generate many (lambda) functions on demand.
I explored the generated code in this post: attachInterrupt using array of function pointer? - #2 by PieterP
Full implementation:
InterruptCounter.hpp
#pragma once
#include <Arduino.h>
// Use IRAM_ATTR for functions called from ISRs to prevent ESP8266 resets.
#if defined(ESP8266) || defined(ESP32)
#define IC_ISR_ATTR IRAM_ATTR
#else
#define IC_ISR_ATTR
#endif
// How many external interrupts does this board have?
#include "NumInterrupts.hpp"
/// The highest number returned by digitalPinToInterrupt().
constexpr size_t max_num_interrupts = CORE_NUM_INTERRUPT;
class InterruptCounter {
public:
/// Constructor: doesn't do anything until begin() is called.
InterruptCounter(uint8_t pin);
/// Destructor: calls end().
~InterruptCounter();
/// Copy constructor: meaningless, so it has been deleted.
InterruptCounter(const InterruptCounter &) = delete;
/// Copy assignment: meaningless, so it has been deleted.
InterruptCounter &operator=(const InterruptCounter &) = delete;
/// Move constructor: omitted for brevity.
InterruptCounter(InterruptCounter &&other) = delete;
/// Move assignment: omitted for brevity.
InterruptCounter &operator=(InterruptCounter &&other) = delete;
/// Attach the interrupt.
void begin();
/// Detach the interrupt.
void end();
/// Get the actual count. Use with interrupts enabled only.
unsigned read() const;
private:
/// Private handler function that is called from the ISR.
IC_ISR_ATTR void incrementCount();
/// Array of pointers to all instances with active interrupts. Used to look
/// up the instance to call the handler for in the ISR.
static InterruptCounter *instance_table[max_num_interrupts];
/// The type of a handler function.
using isr_func_t = void (*)();
/// Get a pointer to the interrupt handler function for the given interrupt.
template <unsigned NumISR = max_num_interrupts>
static isr_func_t get_isr(unsigned interrupt);
/// Register the interrupt handler for this instance.
void attachInterruptCtx(int interrupt);
/// Un-register the interrupt handler for this instance.
void detachInterruptCtx(int interrupt);
private:
uint8_t pin;
bool attached = false;
volatile unsigned counter = 0;
};
inline unsigned InterruptCounter::read() const {
noInterrupts();
auto tmp = counter;
interrupts();
return tmp;
}
InterruptCounter.cpp
#include "InterruptCounter.hpp"
#define FATAL_ERROR(msg) // customize this
InterruptCounter::InterruptCounter(uint8_t pin) : pin {pin} {}
InterruptCounter::~InterruptCounter() { end(); }
void InterruptCounter::begin() {
pinMode(pin, INPUT_PULLUP);
delayMicroseconds(2000);
attachInterruptCtx(digitalPinToInterrupt(pin));
}
void InterruptCounter::end() {
if (attached)
detachInterruptCtx(digitalPinToInterrupt(pin));
}
void InterruptCounter::incrementCount() { ++counter; }
template <unsigned NumISR>
auto InterruptCounter::get_isr(unsigned interrupt) -> isr_func_t {
return interrupt == NumISR - 1
? []() IC_ISR_ATTR { instance_table[NumISR - 1]->incrementCount(); }
: get_isr<NumISR - 1>(interrupt); // Compile-time tail recursion
}
template <>
inline auto InterruptCounter::get_isr<0>(unsigned) -> isr_func_t {
return nullptr;
}
void InterruptCounter::attachInterruptCtx(int interrupt) {
if (attached) {
FATAL_ERROR(F("This instance was attached already"));
return;
}
if (interrupt == NOT_AN_INTERRUPT) {
FATAL_ERROR(F("Not an interrupt-capable pin"));
return;
}
if (instance_table[interrupt] != nullptr) {
FATAL_ERROR(F("Multiple instances on the same pin"));
return;
}
instance_table[interrupt] = this;
attached = true;
attachInterrupt(interrupt, get_isr(interrupt), FALLING);
}
void InterruptCounter::detachInterruptCtx(int interrupt) {
detachInterrupt(interrupt);
attached = false;
instance_table[interrupt] = nullptr;
}
InterruptCounter *InterruptCounter::instance_table[] {};
NumInterrupts.hpp
https://github.com/tttapa/Control-Surface/blob/ed57f9b22f9e656a9cb7363c812ea9f56e92d5ae/src/Submodules/Encoder/NumInterrupts.hpp
sketch.ino
#include "InterruptCounter.hpp"
InterruptCounter ic2 {2};
InterruptCounter ic3 {3};
void setup() {
Serial.begin(115200);
ic2.begin();
ic3.begin();
}
void loop() {
Serial.print("2: ");
Serial.print(ic2.read());
Serial.print(", 3: ");
Serial.print(ic3.read());
Serial.println();
delay(5);
}