Tasks: A Time-Triggered Scheduler for Arduino

This has been a long time coming. A few years ago (back in 2012!) I wrote a basic TT scheduler which was really more of a ‘proof of concept’ and not really friendly for an everyday Arduino user. It relied on a strong knowledge of C or C++ and needed Eclipse with an Arduino plugin to get it to work. I’ve promised myself since then that I’d  write a proper Arduino library to demonstrate time-triggered scheduling to Arduino IDE users as well as learning some C++ skills for myself. I’ve followed the Arduino API Style Guide in order to make the library as familiar as possible but if you do find something counter-intuitive, or you have any suggestions or feature requests, please let me know in the comments.

The main idea behind time-triggered scheduling is to perform multiple functions without an operating system. It has been said that you should choose a Raspberry Pi (or a similar OS-based platform) over Arduino if you need to do more than two things. This is not always the case – by chopping your sketch up into small, predictable, rapidly-repeating chunks of code (or ‘Tasks’) we can do much more.

Time-triggered scheduling is not a new thing and has been used for a long time in safety-critical systems due to the inherently deterministic behaviour. There are also several other libraries for Arduino for task scheduling but these all work in slightly different ways.

This post will be a beginners’ tutorial for Arduino users. For those interested in how it works, you can see the code here. I will write  a separate ‘under the hood’ post describing how it works soon (although the operation is not dissimilar to my original code).

PLEASE NOTE: I’ve only had chance to test this on an Arduino Uno. Please get in touch if you have problems running it on another board.

Installation

Download the library from here

Extract the  Tasks library folder from the zip file into Arduino/libraries/

Restart the Arduino IDE if you have it open.

Getting Started with Tasks_Blink_example

Examples are included in Tasks/examples. You can load the example sketch from within the Arduino IDE by going to File -> Examples -> Tasks (If you can’t see this, make sure you extracted the library to the libraries folder in your Arduino installation).

The example sketch, Tasks_Blink_example.ino, supplied with the library demonstrates everything you need to do to get a simple schedule up and running. When you Upload the sketch you should see the LED on the board blinking in exactly the same way as the Blink example that comes with Arduino. The difference is that during the time in between blinking the LED, the Arduino isn’t waiting in a delay. Instead, there is another task scheduled to output the LED state to the serial port. You can see this status information using the Serial Monitor.

This demonstrates how you can use Tasks to do multiple things at the same time

It’s common practice in the embedded systems world to write a ‘blink’ program as the first thing in a new program, and keep it running in the background throughout development and into production – this way you can easily see if the timing of your software isn’t behaving properly.

There are three tasks:

  • ledOn() switches the LED on.
  • ledOff() switches the LED (you guessed it) off.
  • statusOut() reads the LED pin and outputs a status message.

Of course, the two LED tasks could be combined into a ‘LED toggle’ task, but I wanted to demonstrate how task offsets work.

Task timing is controlled by the Schedule.addTask() lines in setup() and Schedule.startTicks(1) tells the scheduler that the ticks are 1 millisecond long.

ledOn() has an offset of 0 ticks and a period of 2000 ticks (2 seconds). ledOff() has an offset of 1000 ticks (1 second) and also has a period of 2000 ticks (2 seconds). This means these two tasks will run 1000 ticks apart resulting in the LED turning on for 1 second, then off for 1 second then back on, and so on.

statusOut() has a period of 100 ticks, but because it has an offset of 1 tick, it will never run in the same tick as the other two tasks.

Check the example works, play around with the timing and see what happens, then read on to find out how to add your own tasks.

Task Creation

Create your tasks as separate void functions (see https://www.arduino.cc/en/Reference/FunctionDeclaration):

void task_1_function() {  
  /* [Repeated ('looped') code for Task 1] */  
}

/* ... */

void task_n_function() {  
  /* [Repeated ('looped') code for Task n] */  
}

Task functions can have any name and can call any other functions you like but the following rules apply.

  • while loops should be avoided where possible. If they are used, a timeout mechanism is required to prevent the program getting stuck. NEVER use an infinite while loop in a task.
  • Avoid long for loops: use counters and if(x == y) statements in tasks if you have lots of repeating code.
  • Some things will take longer than 1 tick to execute, for example, writing to an LCD screen. This can be overcome by buffering characters and writing fewer characters at once.

Scheduler configuration

Include the library in your sketch:

Sketch –> Include Library –> Tasks

or

#include <Tasks.h>

Configure the scheduler in setup():

void setup() {
    /* Tell the scheduler how many tasks there are: */
    Schedule.begin(n);

    /* Tell the scheduler which functions are tasks, and your desired timing (see note 1). 
     * Also include your setup code here as normal: */

    /* ['run-once' setup code for 1st task] */
    Schedule.addTask("Task 1 Name", task_1_function, task_1_offset, task_1_period);
    /* The name can be anything you like, but must be 20 characters or less */ 

    /* ... */

    /* ['run-once' setup code for nth task] */
    Schedule.addTask("Task n Name", task_n_function, task_n_offset, task_n_period);

    /* Start the scheduler with a tick length, t ms (see note 2):*/
    Schedule.startTicks(t);
}

Note 1:

Control the task timing using:
task_offset: The time in ‘ticks’ between start-up and the first execution of the task.
task_period: The time in ‘ticks’ between executions of the task.

These two properties allow tasks to be spaced out in the timeline to provide reliable timing.

Note 2:

The tick length, t, determines how long task_offset and task_period are.

Task Reports

You can check that a task has been added successfully by using Schedule.lastAddedTask() after you add a task.

Configure the serial port as normal:

Serial.begin(9600);

Print the last task report to the serial port:

Schedule.addTask("Test Task", task_A_function, 0, 2000);
Serial.print(Schedule.lastAddedTask());

If all is well, you will see this in the terminal:

---------------------------------------------
Added Task 0: "Test Task"
---------------------------------------------
Offset:     0 ms
Period:     2000 ms
Timing:     NORMAL
T Analysis: disabled
---------------------------------------------

If there are no tasks in the schedule you’ll see this:

*** No tasks in schedule ***

If there isn’t enough space in the schedule, you’ll see this:

*** We're going to need a bigger schedule ***

If you see this message, you need to increase the number in Schedule.begin to match the number of tasks you want.

Execution

Run the scheduler in loop():

void loop() {
    Schedule.runTasks();
}

All other code should be in the task functions, don’t put anything else in loop().

The scheduler will automatically space the tasks out according to the configured schedule.

Examples:

Basic schedule

Consider two tasks, Task A and Task B configured as follows:

Schedule.begin(2);

Schedule.addTask("Task A", taskA, 0, 5);
Schedule.addTask("Task B", taskB, 1, 10);

Schedule.startTicks(10);

These tasks can be represented on a timeline as follows:Capture

Task A will run every 50 ms starting at tick 0. Task B will run every 100 ms, but has an offset of 1, so it will first run in tick 1. It doesn’t matter that Task B takes longer than 1 tick, because there is enough space for it to complete before Task A is run again.

Prioritising tasks

If you have multiple tasks that are expected to run in the same tick, it’s useful to know that the tasks will run in the order that you add them to the schedule in setup():

Two tasks, Task C and Task D configured as follows:

Schedule.begin(2);

Schedule.addTask("Task C", taskC, 0, 1);
Schedule.addTask("Task D", taskD, 0, 5);

Schedule.startTicks(10);

Task C is a very short task, which will run in every tick. Task D is configured to run every 5 ticks. Because Task C is added to the schedule first, it will run before Task D.

Capture2

Now, suppose it is more important that Task D runs exactly every 50 ms, and the timing of Task C can be sacrificed. The priority can be changed in setup:

Schedule.begin(2);

Schedule.addTask("Task D", taskD, 0, 5);
Schedule.addTask("Task C", taskC, 0, 1);

Schedule.startTicks(10);

Capture3

It depends on the application to decide which task requires the most precise timing. Generally controlling (setting pins) or reading tasks need a higher priority than reporting tasks.

ADVANCED

Task Preemption

In the situation below, Task E takes longer than 2 ticks and is a lower priority than task C, which is required to run in every tick.

Schedule.begin(2);

Schedule.addTask("Task C", taskC, 0, 1);
Schedule.addTask("Task E", taskE, 0, 10);

Schedule.startTicks(10);

Using standard (cooperative) scheduling, this will result in undesirable behaviour (priority inversion). Task C has to wait for Task E to complete, so even though it has a higher priority, it misses execution slots and runs late when Task E does complete.

Capture4

This can be overcome by allowing Task C to interrupt Task E when it needs to run. This is known as ‘task preemption’ and is acheived by setting the TIMING_FORCED option when adding the task:

Schedule.begin(2);

Schedule.addTask("Task C", taskC, 0, 1, TIMING_FORCED);
Schedule.addTask("Task E", taskE, 0, 10);

Schedule.startTicks(10);

Now the tasks execute as required, with Task C maintaining its higher priority:

Capture5

NOTE: Tasks added with TIMING_FORCED must be shorter than 1 tick, otherwise ticks will be missed

Timing Analysis

It is possible to check the timing of tasks using an oscilloscope and configuring an analysis pin for a task. To do this, include the desired pin as a fourth argument when you add the task to the schedule:

Schedule.addTask(task_function_name, task_offset, task_period, pin_number);

or

Schedule.addTask(task_function_name, task_offset, task_period, TIMING_FORCED, pin_number);

With an analysis pin enabled, the scheduler will set this pin high just before the task is run, and set it low again when the task completes (Note there are some overheads involved in setting and clearing the pin, so the timing shown on the oscilloscope wont be spot-on, but it’ll give you an idea of when tasks are colliding). See below for an example using Tasks_Blink_example:

Oscilloscope Connections – Timing Analysis on Pin 10, LED on Pin 13

IMAG1677

ledOn() Timing (Task timing in yellow, LED state in blue)

IMAG1678

ledOff() Timing (Task timing in yellow, LED state in blue)

IMAG1679

statusOutput() Timing

IMAG1680

Conclusion

I hope this has helped understand the basics required to build your own schedules. My long-term plans for this are to write some plugins for the scheduler to control things like LCD screens, servos, etc with deterministic behaviour. These will consist of some TT-friendly libraries, and background tasks that will run alongside your custom tasks. Stay tuned for these – hopefully I will find the time to do them this decade!

Advertisements

6 thoughts on “Tasks: A Time-Triggered Scheduler for Arduino

  1. Pingback: Further task scheduling with Arduino / AVR | chris barlow

  2. Pingback: Reliable task scheduling with Arduino / AVR | chris barlow

  3. I just tried you example (Tasks_Blink_example) with a Genuine Uno, IDE 1.8.3, and your’s today library and it seems like runTasks() doesn’t do anything (no LED, no Serial output).

    • Hi,

      Thanks for the comment, I haven’t tried this for ages. It works with IDE 1.6.7, but not 1.8 – it looks like the avr/sleep.h file that I rely on is different. I’ll have a look over the next couple of days and figure something out

Tell me what you think...

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s