Skip to content

Using the new callback interface

luni64 edited this page Apr 7, 2023 · 6 revisions

Introduction

Teensyduino, starting with version 1.59, will replace the traditional function pointer interface for attaching callbacks with a more modern interface using the inplace_function facility.

inplace_function is a version of the std::function type designed to work without dynamic memory allocation. It allows the user to create a function object that can store pretty much any callable object in a statically allocated buffer, making it particularly suitable for embedded systems.

Why bother?

In the Arduino ecosystem, callbacks are typically attached to providers (e.g. IntervalTimer) by passing the address of the callback using simple void(*)(void) pointers. However, this can be limiting when the callback requires state or when the providing class needs to be embedded into another class. To address this issue, a common approach is to pass an additional pointer to the callback which points to additional information. While this method can solve the problems, it can be challenging to understand for users who are not familiar with low-level programming.

Modern c++ approaches this well known issue by not requiring a simple pointer to the callback but to accept a much broader range of callable objects which includes

  • Traditional callbacks, i.e. pointers to void functions
  • Functors as callback objects
  • Static and non static member functions
  • Lambda expressions

You find more basic information here Fun with modern cpp - Callbacks and here TeensyTimerTool-Callbacks.

FAQ and Examples

The following chapters show some use cases and worked out examples.

I don't need that stuff, I want to do it as I'm used to

Of course the new interface accepts exactly the same pointers to callback functions as the traditional interface did. I.e., the following code will work as usual:

IntervalTimer t;

void onTimer() // typical callback
{
    digitalToggleFast(LED_BUILTIN);
}

void setup()
{
    pinMode(LED_BUILTIN, OUTPUT);
    t.begin(onTimer, 500'000);  // pass the address of the callback to the provider, invoke callback every 500ms
}

void loop()
{
}

Do I really need to write a callback function to do something simple as toggling a pin?

No, these days compilers are smart enough to do the work for you.

IntervalTimer t;

void setup()
{
    pinMode(LED_BUILTIN, OUTPUT);
    t.begin([] { digitalToggle(LED_BUILTIN); }, 500'000);
}

void loop()
{
}

The expression [] { digitalToggle(LED_BUILTIN); } is a so called lambda expression. It tells the compiler: Please generate a function with the body given between the braces. The function shall not take parameters and shall return void. I don't need a name for this function but please return a pointer to it.

So, it will generate the same function as we wrote manually in the example above. Since the lambda returns a pointer to the generated function it can be directly placed in the begin function of the IntervalTimer.

I need to write a lot of identical callback functions can this be done smarter?

Lets assume you need to handle pin-interrupts for a lot of pins in a similar way. Instead of writing a dedicated callback for each and every pin you might prefer to have one callback handling all of them.

void onPinChange(int pinNr)
{
    Serial.printf("Pin %d changed\n", pinNr);
}

void setup()
{
    attachInterrupt(0, [] { onPinChange(0); }, FALLING);
    attachInterrupt(1, [] { onPinChange(1); }, FALLING);
    attachInterrupt(2, [] { onPinChange(2); }, FALLING);
    attachInterrupt(3, [] { onPinChange(3); }, FALLING);
    attachInterrupt(4, [] { onPinChange(4); }, FALLING);
    attachInterrupt(5, [] { onPinChange(5); }, FALLING);
    attachInterrupt(6, [] { onPinChange(6); }, FALLING);
    attachInterrupt(7, [] { onPinChange(7); }, FALLING);
}

void loop()
{
}

We use lambda expressions to have the compilier generating small functions which will be attached to the pin interrupt. Those functions will then invoke onPinChange(int pinNr) and pass the pin number to it.

Looking at the code above naturally leads to question "couldn't we do all these attachInterrupt calls in a loop?" Sure we can:

Note that the following code needs attachInterrupt to have the new interface, which it currently (2023-04-07) does not.

void onPinChange(int pinNr)
{
    Serial.printf("Pin %d changed\n", pinNr);
}

void setup()
{
    for (int pin = 0; pin < 8; pin++)
    {
        attachInterruptEx(pin,[pin]{onPinChange(pin);},FALLING);
    }
}

void loop()
{
}

Please note that the lambda expression now has the variable pin between the square brackets. The variables listed between those square brackets are captured. I.e., the compiler generated function will contain a variable pin which is preset to the value it had when the compiler evaluated the lambda expression. E.g., for the third iteration the compiler will translate the lambda expression into something equivalent to

void someUnknownName()
{
    int pin = 3;
    onPinChange(pin);
}

and returns the address of it.

How can I use the new interface to embedd an IntervalTimer in my class?

Using the traditional method of attaching callbacks this task used to be involved and ugly. With the new interface it gets very simple. Assume we want to do a frequency generator which generates a simple square signal on a pin.

class FrequencyGenerator
{
 public:
    void begin(uint8_t _pin, float frequency)
    {
        unsigned period = 1E6f / frequency / 2.0f;
        pin = _pin;
        pinMode(pin, OUTPUT);

        t.begin([this] {this->onTimer(); }, period);
    }

 protected:
    void onTimer() // non static callback, has access to class members
    {
        digitalToggleFast(pin);
    }

    IntervalTimer t;
    uint8_t pin;
};

//--------------------------------------------------------
// User code:

FrequencyGenerator fgA, fgB;

void setup()
{
    fgA.begin(0, 10'000);
    fgB.begin(1, 50'000);
}

void loop()
{
}

The example generates a 10kHz signal on pin0 and a 50kHz signal on pin1;

Again, we use a lambda expression to generate the actual callback. This time we capture the this pointer, i.e., the address of the actual object on which we called begin. The compiler generates somthing equivalent to

void someFunction()
{
    FrequencyGenerator* f = ... //address of the object on which begin was called
    f->onTimer();               // calls
}

and attaches it to the IntervalTimer. Whenever the callback is invoked it calls the onTimer() function of the captured address.

What else can I attach as callback?

Functors

Functors are classes with an overridden function call operator. Before lambda expressions where available functors where often used to generate functions with state. Let's have a look at an example how to use functors in the context of the new callback system.

// Functor, i.e. class with overridden operator()
// Generates pulses with configurable output pin and duration
class PulseGenerator
{
 public:
    PulseGenerator(uint8_t _pin, unsigned _duration)
    {
        pin      = _pin;
        duration = _duration;
        pinMode(pin, OUTPUT);
    }

    void operator()(void) // this will be called by our timer
    {
        digitalWriteFast(pin, HIGH);
        delayMicroseconds(duration); // for the sake of simplicity, avoid in real code
        digitalWriteFast(pin, LOW);
    }

 protected:
    uint8_t pin;
    unsigned duration;
};

//--------------------------------------------------------------
// User code

PulseGenerator g1(0, 100); // generates 100µs pulses on pin 0;
IntervalTimer timer;

void setup()
{
    timer.begin(generator, 10'000); //we can directly attach the functor, timer will call its operator()
}


void loop()
{
}

The IntervalTimer directly accepts functors and uses its operator() as callback. The example generates a 100µs pulse on pin 0 every 10ms.

Non-Static Member Functions

The interface also accepts static and nonstatic member functions. Static member functions can be used in the same way as free functions as shown in the first example. The syntax for non static member functions is a bit arkward but it works.

class TestClass
{
 public:
    TestClass(const char* _id)
    {
        strlcpy(id, _id, 8);
    }

    void doSomething()
    {
        Serial.print(id);
        Serial.println(" called");
    }

    protected:
       char id[8];
};

//--------------------------------------------------------------
// User code

TestClass m1("first"), m2("second");

IntervalTimer t1, t2;

void setup()
{
    t1.begin(std::bind(&TestClass::doSomething, m1), 100'000); // calls the doSomething member of m1 every 100ms
    t2.begin(std::bind(&TestClass::doSomething, m2), 250'000); // calls the doSomething member of m1 every 250ms
}


void loop()
{
}

However, since using a lambda expression is much simpler one doesn't see this syntax often.

// same test class as in the last example....


//--------------------------------------------------------------
// User code

TestClass m1("first"), m2("second");

IntervalTimer t1, t2;

void setup()
{
    t1.begin([] { m1.doSomething(); }, 100'000); // calls the doSomething member of m1 every 100ms
    t2.begin([] { m2.doSomething(); }, 250'000); // calls the doSomething member of m1 every 250ms
}

void loop()
{
}

To be continued ....

Clone this wiki locally