-
Notifications
You must be signed in to change notification settings - Fork 5
Better Callbacks
This tutorial demonstrates how to use std::function<>
instead of the usual function pointer approach to implement a callback mechanism.
- Traditional Callbacks
- Using std::function instead of function pointers
- Improved Frequency Generator
The usual way to provide a callback mechanism is to store the address of a user supplied callback function in a member variable of the callback provider. The provider can then invoke the callback by simply calling it using the stored address. While this works of course, it has some nasty drawbacks. E.g., passing context, or using a member function as a callback can only be achieved by some ugly workarounds.
In the course of this tutorial we will use the following simple software timer as an example of a class providing a callback mechanism.
Here the class declaration. Complete sketch can be found here.
#pragma once
#include "Arduino.h"
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;
};
Class definition:
#include "SimpleTimer.h"
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 use the timer class as shown below. Here we define the callback function onTimer()
and attach it to the timer which will call it every 100ms.
#include "SimpleTimer.h"
SimpleTimer timer;
void setup()
{
timer.begin(100'000, onTimer);
}
void loop()
{
timer.tick(); // update
}
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
Called at 800 ms
Called at 900 ms
...
(Complete sketch can be downloaded here)
Now, let's use our SimpleTimer class to implement a frequency generator. Of course the timer, which will generate the output signal, can be regarded as implementation detail. Thus, we try to hide it away from the user and encapsulate it in the frequency generator class.
Here the declaration: (FrequencyGenerator.h)
#pragma once
#include "SimpleTimer.h"
class FrequencyGenerator
{
public:
FrequencyGenerator(unsigned pin);
void setFrequency(float Hz);
protected:
SimpleTimer timer;
unsigned pin;
void onTimer();
};
and the definition: (FrequencyGenerator.cpp)
#include "FrequencyGenerator.h"
FrequencyGenerator::FrequencyGenerator(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()
{
digitalWriteFast(pin, !digitalReadFast(pin));
}
The idea of this implementation is to define a member function onTimer()
which simply toggles the output pin and attach this function as a callback to the timer in setFrequency(float)
.
Unfortunately, the compiler doesn't like our smart idea 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, invisible, compiler generated parameter which is needed to identify the actual object the member function belongs to. Thus, the signature of the onTimer
function doesn't fit to the required type (void function returning 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.
Opposed to a traditional function pointer a std::function typed variable accepts pretty much any object which can be called. (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++?)
Before implementing this scheme for the frequency generator class, we'll have a look at some examples demonstrating what we can do with our new toy.
Functors are normal c++ classes with an overridden function call operator. Here a very simple example:
(Complete code can be downloaded here)
class PulseGenerator
{
public:
PulseGenerator(unsigned _pin, unsigned _pulseLength)
: pin(_pin), pulseLength(_pulseLength)
{
pinMode(pin, OUTPUT);
}
void operator()()
{
digitalWriteFast(pin, HIGH);
delayMicroseconds(pulseLength);
digitalWriteFast(pin, LOW);
}
protected:
unsigned pin, pulseLength;
};
The PulseGenerator constructor just stores the passed in pin number and pulse length. The actual pulse generation is done in the overloaded operator(). Since std::function<>
variables accept functors we can, without any change in the SimpleTimer class, use a PulseGenerator object as callback. Whenever the timer invokes its callback the overloaded operator() of the pulse generator will called.
SimpleTimer timer;
void setup()
{
timer.begin(10'000, PulseGenerator(0,2000)); // use a functor as callback and call it every 10ms
}
void loop()
{
timer.tick();
}
As expected, this sketch produces a train of 2ms pulses with 10ms period.
That's nice, but we can do even better.
TBD
TBD
TBD
WIP WIP WIP
So, let's improve our timer example by using a std::function<>. Actually, all we have to do is to exchange the type of the callback_t alias at the top of SimpleTimer.h from a function pointer to a std::function<>
:
...
#include <functional>
using callback_t = std::function<void(void)>; // define a "void foo(void)" typed std::function
//using callback_t = void (*)(void); // function pointer
class SimpleTimer
{
public:
...
Work in progress...
Teensy is a PJRC trademark. Notes here are for reference and will typically refer to the ARM variants unless noted.