In this post we will look at different approaches to multitasking in a processor like Arduino or, as it is commonly known, the blink without delay problem.
First, let’s lower our expectations regarding this “asynchronous” behavior. In a small-sized processor like Arduino, with a single core and no operating system, the execution of two simultaneous tasks is impossible.
When we refer to “multitasking” or “asynchronous behavior” we are actually referring to the ability to time tasks in a non-blocking manner. That is, executing one or more tasks at certain times, without preventing us from doing anything else.
We will understand this much better if we illustrate it with the example of blink without delay, so let’s stop talking and get to the point (in the code, rather).
Blink with delay
Let’s start by recalling the well-known Blink, the equivalent of “Hello World” in the Arduino world, which simply makes the LED on the board blink every second.
void setup()
{
pinMode(LED_BUILTIN, OUTPUT);
}
void loop()
{
digitalWrite(LED_BUILTIN, HIGH);
delay(1000);
digitalWrite(LED_BUILTIN, LOW);
delay(1000);
}
As we know, we use the delay function to mark the times. The problem is that delay is a blocking wait, meaning the processor stops the main control loop during this time.
What if we want to perform other functions while the LED is blinking, such as reading a sensor, receiving data by serial port, or activating a motor? Do I have to stop the entire board just to make the LED blink?
Well, if this were the case, processors would not be very interesting. We have several mechanisms to deal with this. Among them are interrupts and timers, but these are intended for more specific functions.
In addition, in a processor like Arduino, timers and interrupts are valuable and scarce resources. Not to mention that modifying them can cause conflicts with other functions and libraries.
Blink without delay
If we don’t have strict timing requirements, if all we need is to time a series of functions that execute at a certain time, the simplest (and most common) approach is based on the time elapsed between events.
First of all, since we don’t feel like staring at a LED, let’s modify the Blink example so that instead of blinking an LED, it shows “ON”, “OFF” through the serial port. This way we can also show the trigger times.
We will also change the LED on and off to a toggleLed() function, which simply changes the state of the LED from on to off. We do this only to make the examples simpler, because we have a call to a single action.
The code would be as follows, which is basically identical in functionality to the previous Blink with some minor changes to make the example easier to illustrate.
bool state = false;
// Display values for debugging
void debug(char* text)
{
Serial.print(millis());
Serial.print('\t');
Serial.println(text);
}
void setup()
{
Serial.begin(115200);
}
void loop()
{
toggleLed()
delay(1000);
}
// Change the state of the LED
void toggleLed()
{
state = !state;
if (state) debug("ON");
if (!state) debug("OFF");
}
And here is the system output.
Now let’s see how to do the same as the previous code but without using the delay() function, that is, the expected blink without delay.
The general idea is that, instead of stopping the execution for a certain time, we will let it run normally. At a certain point we check the time elapsed between triggers. If the elapsed time is greater than the desired interval, we will perform the specified action. In this example, change the LED state with toggleLed().
//Blink without delay
unsigned long interval = 1000;
unsigned long previousMillis;
bool state = false;
void debug(String text)
{
Serial.print(millis());
Serial.print('\t');
Serial.println(text);
}
void setup()
{
Serial.begin(115200);
// Capture the first millis
previousMillis = millis();
}
void loop()
{
// This is the important part of the example
unsigned long currentMillis = millis();
if ((unsigned long)(currentMillis - previousMillis) >= interval)
{
switchLed();
previousMillis = millis();
}
}
void switchLed()
{
state = !state;
if (state) debug("ON");
if (!state) debug("OFF");
}
And this is the system response.
Of course, this has several consequences, such as higher energy consumption because the control loop runs continuously, without entering a low-energy state.
On the other hand, if we have tasks with high execution time, it may cause our timed task to be delayed because the processor is busy at the time it should trigger our action.
This would lead us to reflect on how we want the system to behave if there is a delay. Whether it is better to maintain the time between actions (millis() + interval) or the time between triggers (previousMillis + interval).
Multitasking in Arduino
What if we have to time more than one action with different intervals? Well, the code is very similar, we simply have two identical code fragments, each with its interval, previousMillis, and action() for each task.
Here is an example where we have two timed tasks that simply display Action1 and Action2 through the serial port, at intervals of 1000 and 800ms, respectively.
//Blink without delay multitarea
unsigned long interval1 = 1000;
unsigned long interval2 = 800;
unsigned long previousMillis1;
unsigned long previousMillis2;
void debug(String text)
{
Serial.print(millis());
Serial.print('\t');
Serial.println(text);
}
void setup() {
Serial.begin(115200);
previousMillis1 = millis();
previousMillis2 = millis();
}
void loop() {
unsigned long currentMillis = millis();
// Manage overflow
if ((unsigned long)(currentMillis - previousMillis1) >= interval1)
{
action1();
previousMillis1 = millis();
}
if ((unsigned long)(currentMillis - previousMillis2) >= interval2)
{
action2();
previousMillis2 = millis();
}
}
void action1()
{
debug("Action1");
}
void action2()
{
debug("Action2");
}
Here is the system output.
Multitasking in a class
The previous example allows us to see that the code for timing a task has the same structure, and is susceptible to encapsulation in a class.
Here are the libraries AsyncTask, MultiTask, StoryBoard, which allow simple addition of timed tasks. In addition, we have AsyncServo and AsyncStepper to move a servo and a stepper motor following the same philosophy.
For example, with AsyncTask the blink without delay example would look like this.
#include "AsyncTaskLib.h"
AsyncTask asyncTask1(1000, []() { digitalWrite(LED_BUILTIN, HIGH); });
AsyncTask asyncTask2(2000, []() { digitalWrite(LED_BUILTIN, LOW); });
void setup()
{
pinMode(LED_BUILTIN, OUTPUT);
asyncTask1.Start();
}
void loop()
{
asyncTask1.Update(asyncTask2);
asyncTask2.Update(asyncTask1);
}
Which we can agree is much more compact and convenient.
FreeRTOS
The last option we are going to see for achieving multitasking in a microprocessor is to use an operating system for embedded systems like FreeRTOS.
FreeRTOS is a real-time micro operating system designed for embedded systems, free to use, written in C, and compatible with over 30 processors.
The general philosophy of FreeRTOS is similar to what we have seen in this post, in which the main loop becomes a scheduler in which we have registered tasks, and FreeRTOS takes care of triggering them at the right time.
FreeRTOS, of course, adds many more functions, such as task prioritization or passing parameters to actions. In addition, by completely controlling timings, it allows taking advantage of low-energy states and achieving high efficiency.
However, although it is very lightweight, in a board like Arduino Nano it occupies more than 20% of the memory. But, for example, on an ESP32 it fits perfectly, and in fact is the basis of many boards based on this SoC.
Its use is more complex than what we have seen in this post, and it deserves its own post in the future.
Download the code
All the code in this post is available for download on Github.