ptScheduler : A Minimal Cooperative Task Scheduler for Arduino

ptScheduler is a non-preemptive task scheduler for Arduino enabled microcontrollers that helps to write non-blocking periodic tasks easily.
ptScheduler-Feature-Image-1_2
Background image by Anton Maksimov juvnsky

When was the last time you used the delay() function? Must be not too long ago. We need delay functions for blinking lights, activating buzzers, push button debouncing and so on. But you would know this too – using delay functions also prevents the microcontroller from doing anything useful. This is because ordinary delay functions are implemented using NOP or No Operation assembly instructions. When you specify a delay time in some unit such as milliseconds, a certain number of NOP operations are executed to skip an equivalent amount of clock cycles. The MCU does nothing else in this period. That is precious clock cycles or computing time wasted. Such a delay creates a blocking code.

So what is a better alternative? What if we could do other things while we wait for a delay time to elapse? Some of you might already know to implement this using millis() or micros() functions. These functions are implemented using timer based interrupts. For example, the millis() function increments a global counter variable when a timer is overflown every millisecond. That means, your application code is interrupted every millisecond for a very short amount of time to increment the counter. The value of this counter variable can be used to determine if a certain amount of time has elapsed. An example code would look like this,

int entryTime = 0;

void setup() {
  Serial.begin(9200);
}

void loop() {
  if ((millis() - entryTime) >= 1000) {  //if a second is elapsed
    Serial.println("Time elapsed");  //prints every second
    entryTime = millis();  //save the next entry point
  }
}

If you want multiple such tasks, you have to create a unique variable such as entryTime for each task. Also, if you want to change the time during execution, you also need a variable for that. This can bloat your code and make it complicated and difficult to understand. ptScheduler is a library that does everything you could do with millis() functions, while keeping your code simple and intuitive.

Let’s start with a basic “Hello World” example.

include "ptScheduler.h"

//create tasks
ptScheduler sayHello = ptScheduler(1000);

//setup function runs once
void setup() {
  Serial.begin(9600);
}

//infinite loop
void loop() {
  if (sayHello.call()) {  //executed every second
    Serial.println("Hello World");
  }
  //add other tasks and non-blocking code here
}

To use the library, you have to first add the header file to your code. Installation will be explained further below. The sayHello is a new task created to run once every second. Then, in the loop function, we can invoke the task using the call() function. It checks if the preset time has elapsed. The if statement repeatedly calls this function in the loop. Whenever the time is elapsed one second, the code under the if clause is executed. Otherwise it is skipped.

You can add other non-blocking code or other tasks in the same loop function. Any number of tasks can be run at any intervals, given you have enough resources. Let’s have a look at an example with a few more functions.

include "ptScheduler.h"

//create tasks
ptScheduler sayHello = ptScheduler(1000);
ptScheduler blinkLed = ptScheduler(500);

//setup function runs once
void setup() {
  Serial.begin(9600);
  pinMode(LED_BUILTIN, OUTPUT);
}

//infinite loop
void loop() {
  if (sayHello.call()) {  //executed every second
    Serial.println("Hello World");
  }
  
  if (blinkLED.call()) {
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));  //toggle LED
  }
  
  Serial.println(analogRead(A0));  //continuous task
}

We have three tasks here, of which two are ptScheduler tasks. sayHello prints “Hello World” once every second. blinkLed will toggle the inbuilt LED of an Arduino every half second. The analogRead function is not timed and executed every time loop() function is executed.

How it works?

Under the hood, ptScheduler uses the native millis() implementation of Arduino. The millis() function is a timer based ISR (Interrupt Service Routine) that increments a global counter variable (32-bit unsigned integer) every millisecond. Every time you invoke the call function, it checks whether the millis() count has been overflown.

When you create a new ptScheduler object, you can specify the time intervals and execution modes. All the class member variables and functions are public and that gives you full control over your tasks, allowing dynamically changing the behavior of the task.

Advantages

  1. Write non-blocking periodic tasks with any number of custom length intervals and iterations.
  2. Custom pre-delay before task execution.
  3. There exists many task schedulers and RTOSs for microcontroller platforms. Some of them are hard to use, and consume a good amount of your flash and RAM. But ptScheduler is a low footprint library without any advanced features but allows you to write periodic tasks easily.
  4. Write clean and intuitive code.
  5. There is no overhead of context switching, callbacks, or intensive dynamic memory management. ptScheduler uses just enough state variables to run your tasks, and is executed by invoking a single call() function.
  6. The order of execution of tasks is fixed and deterministic. For example if two tasks get their interval times elapsed around the same time, the task that appears first on your code will be executed first. There is no concept of dynamic queues or priorities.
  7. Dynamically change the behavior of a task by manipulating the state variables and counters.
  8. ptScheduler can be stripped down to remove all unwanted modes and thus reduce code size.
  9. ptScheduler tasks can coexist with preemptive RTOS tasks (such as FreeRTOS tasks).
  10. ptScheduler is safe from millis() overflow issues.

Limitations

  1. ptScheduler is not a replacement for a proper RTOS and doesn’t have advanced features.
  2. There is no guarantee your task will be executed at exact intervals. If another task takes a long time to complete, it can delay all other tasks.
  3. You have to poll the call() function continuously to determine when to run your task.
  4. Does not work when interrupts are disabled globally, because doing so will also prevent the millis() function from running.

Installation

To install ptScheduler to your computer,

  • Method 1 : Using git, clone this repository to your default Arduino libraries folder in your root workspace folder.
  • Method 2 : Download the repository as a ZIP file and extract it to the libraries folder.
  • Method 3 : Install directly from Arduino IDE’s library manager.

ptScheduler is now part of the official Arduino library list. If you have Arduino IDE open, restart it. Under Sketch > Include Library you will see the ptScheduler library in the Contributed libraries category. That means the library is installed correctly.

To use the examples, go to File > Examples > ptScheduler and open an example sketch you like. The basic operation is demonstrated in the Hello-World.ino and Basic.ino sketches.

Modes

There are three properties that determine the task mode.

  1. Whether a task is executed once every interval or executed repeatedly during the interval.
  2. Whether a task has equal or unequal intervals.
  3. Whether a task is periodic or iterated (stops after a few cycles).

1. Oneshot or Spanning

There are two types of tasks – oneshot and spanning. A oneshot task is executed once every recurring interval. See the graph below.

ptScheduler-Task-Types-Oneshot_1
Equal interval, oneshot task

For example, every time a oneshot task is executed, you can toggle the state of an LED. This will blink the LED at every interval.

spanning task is a one that is executed repeatedly during an interval. Such a task could be sampling a sensor continuously over a period of time. See the graph below.

ptScheduler-Task-Types-Equal-Initerval_1
Equal interval, spanning task

During the Active time, the call() function will return true, and will return false during the inactive time. Both the tasks shown above have a single interval value of Ti

2. Equal or Unequal Intervals

Tasks are periodic or recurring and their intervals can be equal or unequal. For example, below is a spanning task with two unequal intervals.

ptScheduler-Task-Types-Unequal-Interval_1
Unequal interval, spanning task

The active time and inactive time intervals are different. pTschdeuler allows you to set any number of intervals of any length. For example, something like below is also possible with 6 interval values of different lengths. Every interval set is called an iteration. This iteration or pattern can repeat as many times you want.

ptScheduler-Task-Types-Unequal-Interval_2
Unequal interval, spanning task with 6 different interval values

3. Periodic/Recurring or Iterated

A task can run for either an indefinite period of time or stop after a number of iterations. You can set the number of iteration for a task using setIteration() function. If no iteration was set (itearations = 0), it will be a periodic indefinite task.

Depending on these properties, there can be 8 modes.

1. EPO – Equal, Periodic, Oneshot

ptScheduler-Task-Types-EPO_1
EPO task

This example has a single interval value of Ti.

Call setTaskMode(PT_MODE_EPO) to use this mode.

2. EIO – Equal, Iterated, Oneshot

ptScheduler-Task-Types-EIO_1
EIO task

This example also has single interval value but it is iterated. After 5 iterations, the task will be either disabled or suspended.

Call setTaskMode(PT_MODE_EIO) to use this mode.

3. UPO – Unequal, Periodic, Oneshot

ptScheduler-Task-Types-UPO_1
UPO task

This example has two intervals T1 and T2.

Call setTaskMode(PT_MODE_UPO) to use this mode.

4. UIO – Unequal, Iterated, Oneshot

ptScheduler-Task-Types-UIO_1
UIO task

This example has two intervals and the iteration is set to 2. After the two iterations, the task is suspended until you manually reactivate it.

Call setTaskMode(PT_MODE_UIO) to use this mode.

5. EPS – Equal, Periodic, Spanning

ptScheduler-Task-Types-EPS_1
EPS task

This is a spanning task with equal intervals. Iteration is not set and so it repeats indefinitely.

Call setTaskMode(PT_MODE_EPS) to use this mode.

6. EIS – Equal, Iterated, Spanning

ptScheduler-Task-Types-EIS_1
EIS task

This has iterations set to 4. Once four iterations are completed, the task will be suspended until you reactivate it again.

Call setTaskMode(PT_MODE_EIS) to use this mode.

7. UPS – Unequal, Periodic, Spanning

ptScheduler-Task-Types-UPS_1
UPS task

This spanning task has two unequal intervals T1 and T2.

Call setTaskMode(PT_MODE_UPS) to use this mode.

8. UIS – Unequal, Iterated, Spanning

ptScheduler-Task-Types-UIS_1
UIS task

This spanning task has two unequal intervals T1 and T2 and is also iterated.

Call setTaskMode(PT_MODE_UIS) to use this mode.

In addition to these, you can set a skip time or a pre-task delay. You can either express this delay in milliseconds, number of intervals or number of iterations. Ts is the skip time.

ptScheduler-Task-Types-Skipped_1
Skipped interval

Macro Constants

Here we have all the modes and constants defined.

#define debugSerial   Serial  //for debugging

//task modes
#define PT_MODE_EPO     1   //Equal, Periodic, Oneshot
#define PT_MODE_EIO     2   //Equal, Iterated, Oneshot
#define PT_MODE_UPO     3   //Unequal, Periodic, Oneshot
#define PT_MODE_UIO     4   //Unequal, Iterated, Oneshot
#define PT_MODE_EPS     5   //Equal, Periodic, Spanning
#define PT_MODE_EIS     6   //Equal, Iterated, Spanning
#define PT_MODE_UPS     7   //Unequal, Periodic, Spanning
#define PT_MODE_UIS     8   //Unequal, Iterated, Spanning

//sleep modes
#define PT_SLEEP_DISABLE     1    //self-disable mode
#define PT_SLEEP_SUSPEND     2    //self-suspend mode

Typedefs

typedef uint64_t time_ms_t;  //time in milliseconds

There is only one custom type defined. time_ms_t is for storing millisecond values.

Class

class ptScheduler;

There is only one class that wraps all the functions, state variables and counters for every task.

Member Functions

ptScheduler (time_ms_t interval_1); //sets the initial interval for the task
ptScheduler (uint8_t _mode, time_ms_t interval_1);
ptScheduler (uint8_t _mode, time_ms_t interval_1, time_ms_t interval_2);
ptScheduler (uint8_t _mode, time_ms_t* listPtr, uint8_t listLength);
~ptScheduler();
void reset(); //disable + enable
void enable();  //enabling a task to run at each intervals
void disable();  //block a task from running and reset all state variables and counters
void suspend();  //block a task from running but without resetting anything. interval counter will still run.
void resume();  //resume a suspended task
bool call();  //the task invokation call
bool setInterval (time_ms_t value);  //dynamically set task interval
bool setInterval (time_ms_t value_1, time_ms_t value_2);  //update two interval values. only for tasks instantiated with >= intervals
bool setIteration (int32_t value);  //no. of iterations you want to execute for each activation
bool setSkipInterval (uint32_t value);  //intervals to wait before executing the task
bool setSkipIteration (uint32_t value); //iterations to wait before executing the task
bool setSkipTime (time_ms_t value); //time to wait before executing the task
bool setTaskMode (uint8_t mode);  //set execution mode
bool setSleepMode (uint8_t mode); //set what happens after an iteration is complete
bool isInputError();
void printStats();  //prints all the statuses and counter to debug port

Constructors & Destructor

ptScheduler (time_ms_t interval_1);

This is the simplest method to create a ptScheduler object.

Parameters :

  1. interval_1 : Time in milliseconds. The task mode will be set to PT_MODE_EPO and other values to their defaults. An array will be dynamically allocated to hold the interval value and the pointer intervalList is updated with the address of this location. intervalLength will be set to 1.

Return : Nothing

ptScheduler (uint8_t _mode, time_ms_t interval_1);

This is the same as the previous one but we can explicitly specify the task mode here. For a single interval, only PT_MODE_EPOPT_MODE_EIOPT_MODE_EPS and PT_MODE_EIS modes are supported.

Parameters :

  1. _mode : Any of the 8 supported modes.
  2. interval_1 : Time in milliseconds.

Return : Nothing

ptScheduler (uint8_t _mode, time_ms_t interval_1, time_ms_t interval_2);

Create a task with two intervals. The interval values can be same or different. Same as before, a list will be allocated and intervalList will be pointed to that location. intervalCount will be 2 in this case.

Parameters :

  1. _mode : Any of the 8 supported modes.
  2. interval_1 : Time in milliseconds.
  3. interval_2 : Time in milliseconds.

Return : Nothing

ptScheduler (uint8_t _mode, time_ms_t* listPtr, uint8_t c);

Create a task with an arbitrary number of intervals. You first have to create an array of interval values something like below,

time_ms_t intervalArray[] = {1000, 1000, 2000, 2500, 3000};

then pass the address to the constructor like below,

ptScheduler uisTask = ptScheduler(PT_MODE_UIS, intervalArray, 2);  //unequal, iterated, spanning

Parameters :

  1. _mode : Any of the 8 supported modes.
  2. listPtr : Address of array of intervals.
  3. listLength : Number of intervals in the array. This value should be less than or equal to the number of values in the array.

Return : Nothing

~ptScheduler();

The destructor does nothing.

Parameters : None

Return : Nothing

reset()

void reset();

This function resets all status variables and counters to their default values. But no user defined values will be reset, such as the intervals, iterations, skip time etc. After resetting, it will also enable the task allowing you to start fresh.

Parameters : None

Return : Nothing

enable()

void enable();

This function enables a task by setting the enabled status variable to true. Only enabled tasks can run.

Parameters : None

Return : Nothing

disable()

void disable();

This function disables a task by setting the enabled status variable to false. Only enabled tasks can run.

Parameters : None

Return : Nothing

suspend()

void suspend();

Suspending a task means it will no longer run, but some of the counter variables will continue to increment. When a task is suspended, it is said to be in sleep. A task remains in sleep mode until we manually activate it again. In sleep mode, intervalCounter will continue to increment for every interval passed. If you have multiple intervals, each of them will cause intervalCounter to increment in a round-robin fashion. sleepIntervalCounter will keep track of how many intervals have passed after entering sleep mode. You can use either of these counters to conditionally activate the task at later point using the resume() function.

Parameters : None

Return : Nothing

resume()

void resume();

Resumes a suspended task.

Parameters : None

Return : Nothing

call()

bool call();

This is the main task invocation function. You should enclose this function inside any conditional statements to run the block of code under it. If it is time to run a task, call() will return true, and otherwise false.

Parameters : None

Return :

  1. true : Run the code block
  2. false : Skip the code block

setInterval()

bool setInterval (time_ms_t value);

Use this function if you need to change the interval after creating the object, or dynamically. If you created the object with more than one intervals, then only the first value will be updated.

Parameters :

  1. value : Time in milliseconds

Return :

  1. true : Setting interval was successful.
  2. false : Setting interval was unsuccessful. It can only fail if the intervalCount was 0.
bool setInterval (time_ms_t value_1, time_ms_t value_2);

This updates the first two interval values in the interval list. If the interval count is less than 2, this operation will fail.

Parameters :

  1. value_1 : Time in milliseconds
  2. value_2 : Time in milliseconds

Return :

  1. true : Setting interval was successful.
  2. false : Setting interval was unsuccessful. It can only fail if the intervalCount was less than 2.

setIteration()

bool setIteration (int32_t value);

This creates an iterated task. After the specified number of iterations has executed, the task will enter to sleep. The sleep mode is determined by what you have set with setSleepMode().

Parameters :

  1. value : Number of iterations (positive integer)

Return :

  1. true : Setting iterations was successful.
  2. false : Setting iteration was unsuccessful. This can only fail if the task mode is invalid.

setSkipInterval()

bool setSkipInterval (uint32_t value);

This calculates the skip time from the interval values. For example, if the interval is 1000 ms, setting setSkipInterval(5) will produce a skip time of 5 * 1000 = 5000 ms. If you have multiple intervals, each of the intervals will be added up to value times. The final value is assigned to skipTime and the flags skipIntervalSet and skipTimeSet are set to true.

Parameters :

  1. value : Number of intervals to skip (positive integer)

Return :

  1. true : Setting skip interval was successful.
  2. false : Setting skip interval was unsuccessful. This can only fail if intervalCount was 0.

setSkipIteration()

bool setSkipIteration (uint32_t value);

This calculates the skip time from the iteration values. Each iteration is a set of one or more interval values. The skip time is calculated as value * (sum of intervals in the interval list). The final value is assigned to skipTime and the flags skipIterationSet and skipTimeSet are set to true.

Parameters :

  1. value : Number of iterations to skip (positive integer)

Return :

  1. true : Setting skip iteration was successful.
  2. false : Setting skip iteration was unsuccessful. This can only fail if intervalCount was 0.

setSkipTime()

bool setSkipTime (time_ms_t value);

This directly sets the skipTime and the skipTimeSet flag.

Parameters :

  1. value : Time in milliseconds

Return :

  1. true : Setting skip time was successful.
  2. false : Setting skip time was unsuccessful. This can only fail if intervalCount was 0.

setTaskMode()

bool setTaskMode (uint8_t mode);

Sets the task mode.

Parameters :

  1. mode : One of the 8 supported modes.

Return :

  1. true : Setting task mode was successful.
  2. false : Setting task mode was unsuccessful. This can only fail if the input value was invalid.

setSleepMode()

bool setSleepMode (uint8_t mode);

Sets the sleep mode. A task enters into sleep mode after the specified number of iterations has completed. That means, sleep mode only applies to iterated tasks. The values can be, PT_SLEEP_DISABLE for disabling the task (reset all states and counters) or PT_SLEEP_SUSPEND for suspending the task.

Parameters :

  1. mode : One of the 2 supported modes – PT_SLEEP_DISABLEPT_SLEEP_SUSPEND

Return :

  1. true : Setting sleep mode was successful.
  2. false : Setting sleep mode was unsuccessful. This can only fail if the input value was invalid.

isInputError()

bool isInputError();

If any of the user inputs were invalid, the inputError flag is set. This function will return the value of inputError and also reset it.

Parameters : None

Return :

  1. true : inputError is true.
  2. false : No input error.

printStats()

void printStats();

Prints all the status variables, flags and counters to the serial port. Useful for debugging.

Parameters : None

Return : Nothing

Member Variables

I think these are self-explanatory.

time_ms_t entryTime = 0;  //the entry point of a task, returned by millis()
time_ms_t exitTime = 0; //the exit point of a task, returned by millis()
time_ms_t elapsedTime = 0;  //elapsed time since the last task execution
uint64_t intervalCounter = 0; //how many intervals have been passed
uint64_t sleepIntervalCounter = 0; //how many intervals have been passed after disabling/suspending the task
uint64_t executionCounter = 0; //how many times the task has been executed
uint64_t iterationCounter = 0; //how many times the iteration set has been executed

uint32_t iterations = 0;  //how many times a task has to be executed, for each activation
uint32_t iterationsExtended = 0;  //how many times a task has to be executed, for each activation
uint8_t taskMode = PT_MODE_EPO;  //the execution mode of a task
uint8_t sleepMode = PT_SLEEP_DISABLE; //default is disable
uint8_t intervalCount;  //how many intervals have been passed
time_ms_t* intervalList;  //a pointer to array of intervals
uint8_t intervalIndex = 0;  //the position in the interval list
uint32_t skipInterval = 0;  //number of intervals to skip
uint32_t skipIteration = 0; //number of iterations to skip
time_ms_t skipTime = 0; //time to skip before running a task

bool enabled = true;  //a task is allowed to run or not
bool taskStarted = false; //a task has started an execution cycle
bool cycleStarted = false; //a task has started an interval cycle
bool suspended = false; //a task is prevented from running until further activation
bool iterationEnded = false;  //end of an iteration set
bool running = false; //a task is running
bool runState;  //the current execution state of a task

bool inputError = false;  //if any user input parameters are wrong
bool skipIntervalSet = false; //if skip interval was set
bool skipIterationSet = false;  //if skip iteration was set
bool skipTimeSet = false; //if skip time was set
Share to your friends
Default image
Vishnu Mohanan
Founder and Chief Editor at CIRCUITSTATE Electronics
Articles: 36

Leave a Reply