Callbacks - TeensyUser/doc GitHub Wiki


Tricks for legacy callbacks

In the Arduino ecosystem callbacks are often required to be of type void(*)(). I.e., simple pointers to void functions. If, for example, you want to attach callbacks to pin interrupts you'd do something like:

void myCallback_0() {
    Serial.println("pin0");
}

void myCallback_1(){
    Serial.println("pin1");
}

void setup(){
    pinMode(0, INPUT_PULLUP);
    pinMode(1, INPUT_PULLUP);

    attachInterrupt(0, myCallback_0, FALLING);
    attachInterrupt(1, myCallback_1, FALLING);
}

void loop(){
}

Back | Fun with modern c++

Passing constant information to callbacks

While the code shown above works, it might get tedious if we need to attach a lot of interrupts. Would be good to have one callback which would get information about the pin passed in. However, since such a callback requires a the pin as a parameter it is not compatible to the void(*)() functions required by attachInterrupt. Thus, it can not be attached to the pin interrupt directly. We can of course work around this by defining some relay functions:

void myCallback(int pin) {
    Serial.printf("pin %d\n", pin);
}

void relay0(){
    myCallback(0);
}

void relay1(){
    myCallback(1);
}

void setup(){
    pinMode(0, INPUT_PULLUP);
    pinMode(1, INPUT_PULLUP);

    attachInterrupt(0, relay0, FALLING);
    attachInterrupt(1, relay1, FALLING);
}

void loop(){
}

Hm, not really better than the first one. But, we can improve by using anonymous functions (aka lambda expressions) and let the compiler do all the work for us:

void myCallback(int pin) {
    Serial.print("pin");
    Serial.println(pin);
}

void setup(){
    pinMode(0, INPUT_PULLUP);
    pinMode(1, INPUT_PULLUP);

    attachInterrupt(0, [] { myCallback(0); }, FALLING);
    attachInterrupt(1, [] { myCallback(1); }, FALLING);
}

void loop(){
}

Basically, the lambda expression []{myCallback(0);} tells the compiler to

  • generate an anonymous function which takes no arguments
  • use the expression between the braces as function body and
  • return a pointer to this function.

Which is exactly what we did manually with our relay functions.

Back | Fun with modern c++

Here another example, showing how to blink an LED with an IntervalTimer using a lambda expression.

IntervalTimer t1;

void setup(){
    pinMode(LED_BUILTIN, OUTPUT);
    t1.begin([] { digitalToggleFast(LED_BUILTIN); }, 250'000);
}

void loop(){
}

In case you don't need to change or stop the timer later you can even dispose of the global reference and say:

void setup()
{
    pinMode(LED_BUILTIN, OUTPUT);
    (new IntervalTimer())->begin([] { digitalToggleFast(LED_BUILTIN); }, 250'000);
}

void loop(){
}

Which will construct the timer on the heap where it happily blinks the LED in the background forever.

Back | Fun with modern c++

Using modern callback concepts

A simple timer class as callback provider

We will use the following simple software timer as an example of how to develop a class providing a modern callback mechanism. Here the class declaration (the complete sketch can be found here).

Let's start with the usual void(*)() type callback

using callback_t = void (*)(void);   // c++ way to define a function pointer (since c++11)
//typedef void (*callback_t)(void);  // traditional C typedef way works as well

class SimpleTimer
{
 public:
    void begin(unsigned period, callback_t callback);
    void tick(); // call this as often as possible

 protected:
    unsigned period;
    elapsedMicros timer;
    callback_t callback;
};

and here the definition:

void SimpleTimer::begin(unsigned _period, callback_t _callback)
{
    period = _period;
    callback = _callback;
    timer = 0;
}

void SimpleTimer::tick()
{
    if (timer >= period){
        timer -= period;
        callback();
    }
}

How does it work?

  • The begin function takes and stores the timer period and the callback function and resets the elapsedMicros based timer variable.
  • The tick() function does the actual timing. You should call it as often as possible. It simply checks if the elapsed time is larger than the timer period and, in this case, invokes the callback function and resets the timer variable.

You can use the timer class as shown in the example below where we define the callback function onTimer() and attach it to the timer which will call it every 100ms.

SimpleTimer timer;

void setup(){
    timer.begin(100'000, onTimer);  // start timer and attach callback
}

void loop(){
    timer.tick();                   // update as often as possible
}

void onTimer(){                     // callback Function
    Serial.printf("Called at %d ms\n", millis());
}

Which will print out:

Called at 500 ms
Called at 600 ms
Called at 700 ms
...

Back | Fun with modern c++

Encapsulating the SimpleTimer

Now, let's use our SimpleTimer class to implement a frequency generator class. The fact that we use a SimpleTimer to generate the frequency can be considered an implementation detail. Thus, we would like to hide the SimpleTimer away from the user and encapsulate it in the frequency generator class instead. To achive this we define a member function onTimer() which simply toggles the output pin and try to attach this function as callback to the encapsulated SimpleTimer.

Here the declaration:

class FrequencyGenerator
{
 public:
    void begin(unsigned pin);
    void setFrequency(float Hz);

 protected:
    SimpleTimer timer;  // encapsulate the timer in the class
    unsigned pin;

    void onTimer();     // callback function
};

and here the corresponding definitions:

void FrequencyGenerator::begin(unsigned _pin)
{
    pin = _pin;
    pinMode(pin, OUTPUT);
}

void FrequencyGenerator::setFrequency(float frequency)
{
    unsigned period = 0.5f * 1E6f / frequency;
    timer.begin(period, onTimer); // attach callback
}

void FrequencyGenerator::onTimer()  // callback
{
    digitalWriteFast(pin, !digitalReadFast(pin));
}

Unfortunately, the compiler doesn't like our smart class at all and complains that we try to pass the address of a non-static member function to timer.begin, which expects an address of a void function returning void;

FrequencyGenerator.cpp: In member function 'void FrequencyGenerator::setFrequency(float)':
FrequencyGenerator.cpp:12:32: error: invalid use of non-static member function
     timer.begin(period, onTimer); // attach callback
                                ^

The root cause for this compiler error is, that all non static member functions carry an implicit and invisible, compiler generated parameter. This parameter is needed to identify the actual object the member function belongs to. Therefore, the real signature of the onTimer() function doesn't fit to the required type, i.e., void(*)(). We could of course fix this by making onTimer() static. But, this would make it impossible to use more than one FrequencyGenerator at the same time, since all objects would share the same static onTimer() callback.

Back | Fun with modern c++

Using std::function instead of plain function pointers

Opposed to a traditional function pointer, a std::function typed variable can hold pretty much any object which can be called using paranthesis semantics. (If you are interested in more details there is a nice writeup on stackoverflow Should I use std::function or a function pointer in C++?)

So, let's improve our timer example by using a std::function<>. Actually, all we have to do is to change the callback_t type alias. Instead of defining it as a void(*)() function pointer we redefine it to a std::function<void()> type.

...
#include <functional>

using callback_t = std::function<void(void)>; // define the callback type as std::function<void()>
//using callback_t = void (*)(void);          // plain function pointer

class SimpleTimer
{
 public:
 ...

Back | Fun with modern c++

... To be continued...

⚠️ **GitHub.com Fallback** ⚠️