Skip to content

Callbacks

luni64 edited this page Oct 17, 2020 · 2 revisions

By default, TeensyTimerTool uses callbacks of type std::function. This allows the user to attach pretty much all callable objects to a timer. Callable objects include e.g.

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

In case you prefer a plain vanilla function pointer interface you can configure TeensyTimerTool accordingly.

Traditional Callbacks

As usual you can simply attach a pointer to a parameter less void function.

PeriodicTimer t1;

void plainOldCallback()
{
  // do something
}

void setup()
{
    t1.begin(plainOldCallback, 1000);
}

Functors as Callback Objects

Functors are classes with an overridden function call operator and can be used as callback objects. The overridden operator() will be used as callback. The following example shows a functor which generates a pulse with adjustable pulse width.

#include "TeensyTimerTool.h"
using namespace TeensyTimerTool;

class PulseGenerator
{
public:
    PulseGenerator(unsigned _pin, unsigned _pulseLength)
        : pin(_pin), pulseLength(_pulseLength)
    {
    }

    inline void operator()()
    {
        digitalWriteFast(pin, HIGH);
        delayMicroseconds(pulseLength);
        digitalWriteFast(pin, LOW);
    }

protected:
    unsigned pin, pulseLength;
};

//==============================================================

OneShotTimer t1, t2;

void setup()
{
    pinMode(1, OUTPUT);
    pinMode(2, OUTPUT);

    t1.begin(PulseGenerator(1, 5));  // 5µs pulse on pin 1
    t2.begin(PulseGenerator(2, 10)); //10µs pulse on pin 2
}

void loop()
{
    t1.trigger(1'000);
    t2.trigger(500);
    delay(10);
}

Lambda Expressions and Callbacks with Context

Using lambda expressions as callbacks allows for some interesting use cases. If you are not familiar with lambdas, here a nice write up from Sandor Dago http://sandordargo.com/blog/2018/12/19/c++-lambda-expressions.

Let's start with a simple example:

#include "TeensyTimerTool.h"
using namespace TeensyTimerTool;

OneShotTimer t1;

void setup()
{
    while(!Serial);

    t1.begin([] { Serial.printf("I'm called at %d ms\n", millis()); });

    Serial.printf("Triggered at  %d ms\n", millis());
    t1.trigger(100ms);
}

void loop(){}

Output:

Triggered at  384 ms
I'm called at 484 ms

The example shows that there is no need to create a dedicated callback function. You can directly define it as a lambda expression in the call to beginOneShot. That's nice, but not very exciting.

It gets more interesting when you need to pass context to a callback function:

#include "TeensyTimerTool.h"
using namespace TeensyTimerTool;

elapsedMillis stopwatch;
OneShotTimer t1, t2, t3;

void callbackWithContext(unsigned pin)
{
    int callTime = stopwatch;
    Serial.printf("Do something with pin %u (%u ms)\n", pin, callTime);
}

void setup()
{
    while(!Serial);

     t1.begin([] { callbackWithContext(1); });
     t2.begin([] { callbackWithContext(2); });
     t3.begin([] { callbackWithContext(3); });

     stopwatch = 0;

     t1.trigger(20ms);
     t2.trigger(5ms);
     t3.trigger(50ms);
}

void loop()
{
}

Output:

Do something with pin 2 (5 ms)
Do something with pin 1 (20 ms)
Do something with pin 3 (50 ms)

It would be quite tedious to achieve the same with traditional function pointer callbacks.

A interesting use of this pattern is to call non static member functions. To demonstrate this let's assume you want to write a class which is supposed to blink on a specific pin. It takes the pin number in the constructor and toggles the pin whenever you call its blink function.

class Blinker
{
 public:
    Blinker(unsigned _pin)
    {
        pin = _pin;
        pinMode(pin, OUTPUT);
    }

    void blink() const
    {
        digitalWriteFast(pin, !digitalReadFast(pin));
    }

    protected:
       unsigned pin;
};

Then, simply use a lambda expression to call the blink member functions from the lambda callback of a timer object.

#include "TeensyTimerTool.h"
using namespace TeensyTimerTool;

// copy Blinker class definition here or use separate .h file

PeriodicTimer t1, t2, t3;

Blinker b1(1);           // blinks on pin 1
Blinker b2(7);           // blinks on pin 7
Blinker b3(LED_BUILTIN); // blinks the built in LED

void setup()
{
    t1.begin([] { b1.blink(); }, 1'000);
    t2.begin([] { b2.blink(); }, 2'000);
    t3.begin([] { b3.blink(); }, 50'000);
}

void loop() {}

How to Encapsulate a Timer and its Callback in a Class

The Blinker class from the previous example can be improved further. Since the timers are only needed to call the blink functions it would be much more elegant to directly encapsulate them in the class. Then, the timers could be seen as an implementation detail of the Blinker class and don't need to be known outside the class at all. The same applies for the blinker function which can be hidden (protected / private) as well.

So, lets try to get rid of the globally defined timers and their initialization in setup(). First move the blink function to the protected part of the class and add the timer as a member variable. The interesting part happens in beginPeriodic(). As callback to the timer we define a lambda expression which captures the this pointer and uses it to call our own blink() member function.

Here the complete code:

File blinker.h

#pragma once

class Blinker
{
 public:
    Blinker(unsigned _pin, unsigned _period) // add blink period to the constructor
    {
        pin = _pin;
        period = _period;
    }

    void begin()  // better not initalizie peripherals in constructors
    {
        pinMode(pin, OUTPUT);
        timer.begin([this] { this->blink(); }, period);
    }

 protected:
    void blink() const // this will be called by the timer
    {
        digitalWriteFast(pin, !digitalReadFast(pin));
    }

    unsigned pin, period;
    PeriodicTimer timer;
};

File someSketch.ino

#include "blinker.h"

Blinker b1(1, 1'000);            // blinks on pin 1, 1ms
Blinker b2(7, 2'000);            // blinks on pin 7, 2ms
Blinker b3(LED_BUILTIN, 50'000); // blinks the built in LED, 50ms

void setup()
{
    b1.begin();
    b2.begin();
    b3.begin();
}

void loop() {}

The new Blinker class has a very clean public interface. It completely encapsulates the timer and its callback which makes using the class much easier. The user code can simply define some Blinker objects and and doesn't need to know anything about timers, lambdas and other nerdy stuff.