Arduino setup() and loop() functions make writing simple programs easy. However, for complicated programs the code can become hard to organize and understand. The Scheduler and Publisher/Subscriber libraries improve code readability, maintainability, extensibility and performance.
Issue
As my projects grew in complexity, they became difficult to read, maintain and extend. I researched the schedulers available for Arduino and concluded that I needed something simple to avoid the use of delay(), minimize memory usage and provided better code organization. Therefore, I wrote my own task scheduler and publisher/subscriber libraries which you can download using the link in the top right corner.
Solution – Scheduler
The scheduler maintains a linked list of tasks. Each task has a function, a period between calls and its next run time. Each time the scheduler’s run() function is called, it loops thru the linked list and calls the function of each task who’s nextTime has past.
The scheduler design:
- Implements nonpreemptive (cooperative) round-robin scheduling.
- Eliminates the need to use delay() and thus maximizes CPU usage.
- Provides a mechanism to perform CPU intensive tasks outside the interrupt handler to avoid blocking the interrupt.
- Tasks are added and removed at runtime allowing a program to be more dynamic.
- Increases readability, maintainability, extensibility and performance.
- Minimal design makes it simple to understand and use.
Solution – Publisher/Subscriber
The pub/sub design pattern allows functions to be decoupled. Sensors are an excellent usage of this pattern. For instance you have a function that periodically reads data from a GPS, performs some calculations then posts a notifications to all subscribers that are interested in using the GPS data. Using the sub/pub pattern the GPS function has no relationship or knowledge of the functions that are consuming the data it is providing. Without the pub/sub pattern, the GPS function would need to personally call all the consumer functions leading to ugly code or the consumer function would need to poll for GPS data updates resulting in wasted CPU cycles.
The full benefit of the pub/sub design pattern is realized in operating systems that have real threads since the subscriber can run within a separate thread not blocking the publisher. For Arduino, the benefits are mostly improved readability since the relationships can be specified in the setup() function.
The publisher/subscriber design:
- Allows programs to be written uncoupled; without nested function call.
- Eliminates the need to do polling (such as reading a sensor) and thus maximizes CPU usage.
- Subscribers are added and removed at runtime allowing a program to be more dynamic.
- Used within a multi-threaded OS, the publisher could be modified to run each subscriber handler in a separate thread. Separate threads would allow the subscriber to perform CPU intensive tasks without blocking the publisher.
- Increases readability, maintainability, extensibility and performance.
- Minimal design makes it simple to understand and use.
Solution – Example
The following example demonstrates how simple it is to use these libraries and the improvement in readability. Without the scheduler, the interrupt handler function could not be as short as it is.
#include <SPI.h>
#include <Wire.h>
#include <JB_Publisher_Subscriber.h>
#include <JB_TaskScheduler.h>
using namespace JB;
/* ****************************************************************************************************
Global Variables
**************************************************************************************************** */
Scheduler* scheduler = new Scheduler();
Publisher* publisherLedOne;
Publisher* publisherLedTwo;
#define LED_1 0
#define LED_2 1
#define LED_3 2
#define BUTTON_1 3
/* ****************************************************************************************************
Setup
**************************************************************************************************** */
void setup()
{
/* -------------------------------------------------------------------------------
Initialize IO.
------------------------------------------------------------------------------- */
pinMode(LED_1, OUTPUT);
pinMode(LED_2, OUTPUT);
pinMode(LED_3, OUTPUT);
digitalWrite(LED_1, LOW);
digitalWrite(LED_2, LOW);
digitalWrite(LED_3, LOW);
pinMode(BUTTON_1, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(BUTTON_1), ButtonOnePressed, FALLING);
/* -------------------------------------------------------------------------------
Add tasks to the scheduler.
------------------------------------------------------------------------------- */
scheduler->Add(new Task(ToggleLedOne, 500));
/* -------------------------------------------------------------------------------
Specify the relations between publishers and subscribers.
------------------------------------------------------------------------------- */
publisherLedOne = new Publisher();
publisherLedOne->Add(new Subscriber(ToggleLedThree));
publisherLedTwo = new Publisher();
publisherLedTwo->Add(new Subscriber(ToggleLedThree));
}
/* ****************************************************************************************************
The loop() function only needs to call the Scheduler's run() function which then calls only
those tasks whose next run time has past.
**************************************************************************************************** */
void loop()
{
scheduler->Run();
}
/* ****************************************************************************************************
Toggle LED 1 is a scheduled task configured in setup().
**************************************************************************************************** */
bool isLedOneOn = false;
void ToggleLedOne()
{
isLedOneOn = !isLedOneOn;
if (isLedOneOn)
digitalWrite(LED_1, HIGH);
else
digitalWrite(LED_1, LOW);
publisherLedOne->Notify();
}
/* ****************************************************************************************************
Toggle LED 2 is a scheduled task configured in the button interrupt function ButtonOnePressed().
**************************************************************************************************** */
bool isLedTwoOn = false;
void ToggleLedTwo()
{
isLedTwoOn = !isLedTwoOn;
if (isLedTwoOn)
digitalWrite(LED_2, HIGH);
else
digitalWrite(LED_2, LOW);
publisherLedTwo->Notify();
}
/* ****************************************************************************************************
Toggle LED 3 is a subscriber to publisherLedOne and publisherLedTwo.
**************************************************************************************************** */
void ToggleLedThree()
{
if (isLedOneOn && isLedTwoOn)
digitalWrite(LED_3, HIGH);
else
digitalWrite(LED_3, LOW);
}
/* ****************************************************************************************************
Button 1 Interrupt Handler toggles adding/removing ToggleLedTwo() to/from the scheduler.
**************************************************************************************************** */
/* ---------------------------------------------------------------------------------------------------
We want to create one instance of the Task, toggleLedTwoTask, to avoid a memory leak.
Since it is created outside the scope of the interrupt handler function, we need to use the volatile
keyword to tell the compiler to not optimize out this instance (See:
https://www.keil.com/support/man/docs/armclang_intro/armclang_intro_chr1385110934192.htm).
Since the task parameters are defined once, we need to use a Volatile pointer to non-volatile data
(See: https://barrgroup.com/embedded-systems/how-to/c-volatile-keyword).
---------------------------------------------------------------------------------------------------- */
Task* volatile toggleLedTwoTask = new Task(ToggleLedTwo, 250);
bool isLedTwoScheduled = false;
unsigned long buttonOneDebounceTime = millis();
void ButtonOnePressed()
{
if (millis() > buttonOneDebounceTime)
{
isLedTwoScheduled = !isLedTwoScheduled;
if (isLedTwoScheduled)
{
scheduler->Add(toggleLedTwoTask);
}
else
{
scheduler->Remove(toggleLedTwoTask);
}
}
// Add time to wait before handing next call.
buttonOneDebounceTime = millis() + 5;
}
Conclusion
The Task Scheduler and Publisher/Subscriber libraries make code more readable, maintainable, extensible and performant.