A Very Simple Task Scheduler on Arduino
Another turn in my journey building the Stoic Display. I wanted to launch a number of tasks on some recurrence. The most basic one being showing a quote
.
But as I added more features, I realized the need to do many other things. Among them:
- Synchronize the internal clock using NTP
- Compute statistics on the quotes that have been shown (e.g. last 10 quotes displayed, most frequently displayed one, etc.)
- Send statistics and telemetry to an endpoint
In the spirit of keeping things simple, I came up with this architecture:
- A
Timer
(interrupt-driven) that sets a signal every predefined amount of time (for me this every minute). I am calling this event atick
. - A
Dispatcher
that reacts to eachtick
. - An array of registered
actions
.
This Arduino board comes with an RTC and there’s a cute library that allows you to set up cron-esque alarms.
Now, timer notifications run as an ISR (Interrupt Service Routine), and ISRs have a bunch of limitations. It is generally a good idea to keep ISRs short and as simple as possible to prevent any weird side effects.
Some good notes on ISRs here
In my implementation, the ISR simply sets a flag. Can’t think of anything simpler than that:
void init(unsigned long epoch, void (*tickHandler)()){
//Start the RTC
rtc.begin();
set(epoch);
//Default ticker is 1/min
rtc.setAlarmSeconds(1);
rtc.enableAlarm(rtc.MATCH_SS);
rtc.attachInterrupt(tickHandler);
}
Some place else (in my ..ino
typically):
int tick = 0;
void signalNewTick(){
tick = 1;
}
...
clock.init(epoch, signalNewTick);
Now the main loop, typical of all Arduino sketches looks like this now:
void loop(){
//Checks whether a new signal for a new action is due or not.
//tick is set every minute. The dispatcher will call all actions that are due
if(tick){
Debug("Ticker. tick");
tick = 0;
dispatcher.dispatch();
}
}
The Dispatcher
Let’s dissect the Dispatcher::dispatch
now.
#ifndef DISPATCHER_H
#define DISPATCHER_H
#define MAX_ACTIONS 10
typedef struct {
const char * name[MAX_ACTIONS]; //Name of the action
void (*actions[MAX_ACTIONS])(); //List of "Actions" to call on their "tick"
int ticks[MAX_ACTIONS]; //The number of 'ticks' after wich an action will be called on. 1 tick = 1 min. A value of 3, means the action will be called every 3 min
int count[MAX_ACTIONS]; //Used to keep track of the counts for the action.
int len; //Actual actions
} ACTIONS;
class Dispatcher{
static ACTIONS actions;
public:
int add(const char * name, void (*action)(), int _ticks){
if(actions.len == MAX_ACTIONS){
return -1;
}
actions.name[actions.len] = name;
actions.actions[actions.len] = action;
actions.ticks[actions.len] = _ticks;
actions.count[actions.len++] = 0;
return actions.len;
}
void updateActionTicks(int actionIndex, int _ticks){
//Ignore updates out of range
if(actionIndex >= 0 && actionIndex < actions.len ){
actions.ticks[actionIndex] = _ticks;
}
}
void dispatch(){
for(int x=0; x < actions.len; x++){
actions.count[x]++;
if(actions.count[x] >= actions.ticks[x]){
Debug("Dispatcher. Action " + String (x) + " ready");
actions.count[x] = 0;
(*actions.actions[x])();
}
}
}
const ACTIONS * getActions(){
return &actions;
}
};
ACTIONS Dispatcher::actions;
#endif
The data structure ACTIONS
keeps a list of:
- Names
- Pointers to handlers (the
action
) - The number of
ticks
at which theaction
will be called - A counter for the current
ticks
dispatcher::dispatch
(which runs on every timer tick
), simply iterates over all registered actions, checks if the counter for each has reached the predefined number, and if it has it calls the action
.
The other methods are various getters and setters to the ACTIONS
data structure.
Setup
Setup is trivial (usually in the setup
function of the sketch):
dispatcher.add("Show Quote", actions.showQuoteAction, 5); // Every 5 ticks
dispatcher.add("Save Stats", actions.saveStatsAction, 120); // Every 120
dispatcher.add("Synch Clock", actions.synchClockAction, 1440); // Once a day for a 1 min / tick frequency
dispatcher.add("Send Stats", actions.sendStatsAction, 480); // Every 8 hours
Features and limitations
Notice that this scheduler has no notion of precise time. All actions
run sequentially one after the other. Some might take longer than others. And because they all run in the main thread
(if we can call it that way), you are free to use any time limiting/manipulation function (e.g. delay
or millis
). The end result is that it is possible that some functions will not run exactly at the time you scheduled them. This is more of a cooperative scheduler. And needless to say, if an action
never returns, then nothing else will run! This is totally fine for this design where precision timing is not required (and ticks
are measured in minutes which is almost eternal time for a microprocessor).
An application like mine doesn’t really require the sophistication of an OS like task scheduler.
Also, notice the use of fixed arrays (e.g. MAX_ACTIONS
). In this project, there’s a well known list of actions, and there’s no need for any dynamic allocation. In small systems like this, with contrained memory, I like keeping things as bare bones as possible.