Multitasking with Arduino – Millis(), RTOS & More!
Arduino microcontrollers are a beginner friendly and low cost platform for electronics and programming. They’re great for simple control tasks like blinking an LED, but how much can we stretch the potential of a single Arduino? In other words, is multitasking with Arduino possible? If you’ve learnt some basic Arduino Programming and want to take it to the next level, this article is definitely for you!
In our Multitasking with Arduino guide, we will cover:
- Issues with the Age-Old Delay()
- Keeping Time with Millis()
- Tutorial: Achieve Arduino Multitasking with Millis()
- How to Scale Multitasking with Object Oriented Programming
- Introduction to RTOS (Real Time Operating Systems)
- Tutorial: Achieve Arduino Multitasking with FreeRTOS
Issues with the Age-Old Delay()
Before we get into proper Arduino multitasking, we need to talk about different methods of keeping time, starting from the very popular delay() function. Let’s first take a look at the definition of the function from the official Arduino documentation.
delay(ms)
// Pauses the program for the amount of time (in milliseconds) specified as parameter. (There are 1000 milliseconds in a second.)
// ms: the number of milliseconds to pause. Allowed data types: unsigned long.
An unfortunate caveat of the delay() function is that it is a blocking delay. Let me explain. Through the duration of a delay() function call, the CPU of our Arduino is kept busy. This means it can’t respond to sensor inputs, perform any calculations, or send any outputs.
To better explain the implications of this behaviour, let’s look at an example. Taking the blinky example from the same page, our Arduino code to blink an LED at regular intervals with the use of the delay() function might look like this.
int ledPin = 13; // LED connected to digital pin 13
void setup() {
pinMode(ledPin, OUTPUT); // sets the digital pin as output
}
void loop() {
digitalWrite(ledPin, HIGH); // sets the LED on
delay(1000); // waits for a second
digitalWrite(ledPin, LOW); // sets the LED off
delay(1000); // waits for a second
}
Now, what if we wanted to blink two LEDs at different intervals? Perhaps you might try the code below.
int ledPin1 = 13; // LED 1 connected to digital pin 13
int ledPin2 = 14; // LED 2 connected to digital pin 14
void setup() {
pinMode(ledPin1, OUTPUT); // sets the digital pin as output
pinMode(ledPin2, OUTPUT); // sets the digital pin as output
}
void loop() {
digitalWrite(ledPin1, HIGH); // sets LED 1 on
delay(1000); // waits for a second
digitalWrite(ledPin1, LOW); // sets LED 1 off
delay(1000); // waits for a second
digitalWrite(ledPin2, HIGH); // sets LED 2 on
delay(2000); // waits for two seconds
digitalWrite(ledPin2, LOW); // sets LED 2 off
delay(2000); // waits for two seconds
}
On inspecting the code line by line, we’ll find that the LEDs are turned on one after another. However, it won’t do it simultaneously.
What is Millis()?
The solution to the problem we encountered previously is fundamentally simple. Instead of defining the delay duration directly, we constantly check the clock to determine if enough time has passed for the next action to be executed. To do this, the millis() function is most commonly used.
time = millis()
// Returns the number of milliseconds passed since the Arduino board began running the current program. This number will overflow (go back to zero), after approximately 50 days.
So, what we have here is a very useful function that will mark out references in time, so that we are able to program timing in our Arduino sketches! Let’s apply this to the blinking example by looking at the BlinkWithoutDelay sketch from Arduino.
const int ledPin = LED_BUILTIN; // the number of the LED pin
int ledState = LOW; // ledState used to set the LED
// Generally, you should use "unsigned long" for variables that hold time
// The value will quickly become too large for an int to store
unsigned long previousMillis = 0; // will store last time LED was updated
const long interval = 1000; // interval at which to blink (milliseconds)
void setup() {
pinMode(ledPin, OUTPUT);
}
void loop() {
// check to see if it's time to blink the LED; that is, if the difference
// between the current time and last time you blinked the LED is bigger than
// the interval at which you want to blink the LED.
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
// save the last time you blinked the LED
previousMillis = currentMillis;
// if the LED is off turn it on and vice-versa:
if (ledState == LOW) {
ledState = HIGH;
} else {
ledState = LOW;
}
// set the LED with the ledState of the variable:
digitalWrite(ledPin, ledState);
}
}
What’s so special about this sketch, you might ask? Well, the first thing you’ll notice is that there are no longer any delay() function calls in our code! Yet, we’re able to blink our LEDs all the same.
Our new sketch is made essentially of two continuously running parts that do the following:
- Checks the ledState variable and writes the LED state accordingly.
- Checks the time elapsed with millis() and toggles the ledState variable accordingly.
In fact, this structure is what is known as a State Machine. A state machine is a behaviour model where the machine performs predefined outputs depending on the current state.
To Summarise: Avoid the use of delay(), use millis() instead!
Arduino Microcontrollers: My Recommendations
Before we dive deeper into the tutorials on how to perform more advanced multitasking with Arduino, I want to share some of the Arduino-compatible microcontrollers that we have here at Seeed. After all, in electronics, the hardware is just as important as software!
Seeeduino XIAO
Seeeduino XIAO is the smallest Arduino board in the Seeeduino Family. Despite its small size, the Seeeduino XIAO is equipped with the powerful SAMD21 microchip and extensive hardware interfaces, coming in at an ultra-affordable price of under five dollars.
Product Features:
- ARM Cortex-M0+ 32bit 48MHz microcontroller (SAMD21G18) with 256KB Flash, 32KB SRAM
- Compatible with Arduino IDE & MicroPython
- Easy Project Operation: Breadboard-friendly
- Small Size: As small as a thumb(20×17.5mm) for wearable devices and small projects.
- Multiple development interfaces: 11 digital/analog pins, 10 PWM Pins, 1 DAC output, 1 SWD Bonding pad interface, 1 I2C interface, 1 UART interface, 1 SPI interface.
Keen to learn more about the Seeeduino XIAO? Visit its product page on our Seeed Online Store now!
Wio Terminal
The Wio Terminal is a complete Arduino development platform based on the ATSAMD51, with wireless connectivity powered by Realtek RTL8720DN. As an all-in-one microcontroller, it has an onboard 2.4” LCD Display, IMU, microphone, buzzer, microSD card slot, light sensor & infrared emitter. It’s the last Arduino you will need!
Product Features:
- Powerful MCU: Microchip ATSAMD51P19 with ARM Cortex-M4F core running at 120MHz
- Reliable Wireless Connectivity: Equipped with Realtek RTL8720DN, dual-band 2.4GHz / 5GHz Wi-Fi (supported only by Arduino)
- Highly Integrated Design: 2.4” LCD Screen, IMU and more practical add-ons housed in a compact enclosure with built-in magnets & mounting holes
- Raspberry Pi 40-pin Compatible GPIO
- Compatible with over 300 plug&play Grove modules to explore with IoT
- USB OTG Support
- Support Arduino, CircuitPython, Micropython, ArduPy, AT Firmware, Visual Studio Code
- TELEC Certified
If you’re interested to pick up a Wio Terminal, please visit its product page on the Seeed Online Store!
Achieve Arduino Multitasking with Millis()
The biggest advantage of using millis() over delay() is the removal of blocking. This opens up the possibility to run multiple operations at once! To show you how this can be done, let’s try to apply what we’ve learnt to fix our previous (not-so-successful) attempt to blink two LEDs at different intervals.
int ledPin1 = 13; // LED 1 connected to digital pin 13
int ledPin2 = 14; // LED 2 connected to digital pin 14
int ledState1 = LOW; // LED 1 is initially set to off
int ledState2 = LOW; // LED 2 is initially set to off
unsigned long millis1; // initialises millis marker for LED 1
unsigned long millis2; // initialises millis marker for LED 2
void setup() {
pinMode(ledPin1, OUTPUT); // sets the digital pin as output
pinMode(ledPin2, OUTPUT); // sets the digital pin as output
}
void loop() {
// Update LED States
digitalWrite(ledPin1, ledState1);
digitalWrite(ledPin2, ledState2);
// Toggle the LED states if the duration has passed
if ( (millis() - millis1) > 1000) {
millis1 = millis();
if (ledState1 == LOW) {
ledState1 = HIGH;
} else if (ledState1 == HIGH) {
ledState1 = LOW;
}
}
if ( (millis() - millis2) > 2000) {
millis2 = millis();
if (ledState2 == LOW) {
ledState2 = HIGH;
} else if (ledState2 == HIGH) {
ledState2 = LOW;
}
}
}
You’ll notice that this isn’t too different from the previous millis() example. We’ve essentially duplicated the lines of code that concern the specific LED, and adjusted the parameters so that the second LED toggles every 2 seconds – Yes, it’s truly that simple!
To provide a more practical example, I’ll refer to my Wio Terminal Customisable Timer project. In that sketch, I wanted to implement a countdown timer that essentially must do four things simultaneously.
- Display the time remaining in minutes and seconds
- Show progress in the form of a bar that is gradually filled
- Provide the ability to interrupt the countdown with a keypress
- Keep the time for eventually ending the timer
Now then, let’s take a look at the code.
long start_millis = millis();
int progress, t_min, t_sec;
long seconds_elapsed = 0;
long seconds_remain;
while(seconds_elapsed<duration && clicker_state == 0) {
seconds_elapsed = (millis() - start_millis)/1000;
seconds_remain = duration - seconds_elapsed;
t_min = seconds_remain/60;
t_sec = seconds_remain%60;
ttext.setCursor(0, 0);
ttext.setTextColor(0xFFE0, 0);
ttext.setTextSize(3);
ttext.printf("%02d:%02d", t_min, t_sec);
ttext.pushSprite(220,200);
progress = 200*seconds_elapsed/duration;
tft.fillRoundRect(10, 210, progress, 10, 4, TFT_WHITE);
if (digitalRead(WIO_5S_PRESS) == LOW) {
clicker_state = 1;
skipped_timer = true;
}
}
clicker_state = 0;
The two key things to note in the above is the use of millis() and clicker_state. While you could technically use delay(1000) and simply decrement the number of seconds remaining, the real problem comes in when you want to introduce the ability to interrupt the timer.
Remember, during a delay(), we can’t provide any inputs. Even if we wrote a statement to check for the button press at decrement, the exit condition would only be checked for a split second every second. In this case, it’s nearly impossible for the user to break out of the loop.
Scale Arduino Multitasking with Object Oriented Programming
In the previous section, I mentioned how the addition of another LED simply required us to duplicate some portions of the code. To add even more devices, we could simply copy and paste that code as many times as we needed. The downside is that our sketch would get fairly long, and programmers don’t like long code where it can be avoided. Cleaner code also means a smaller program, which translates to more precious space saved on our Arduino!
Fortunately, C++ based Arduino code supports Object Oriented Programming (OOP), which allows us to package all the code that we reuse for each LED into a paradigm or framework known as a class.
This is actually quite simple to do. To start write our own class, we will need:
- a class constructor,
- some member variables,
- and class methods (or functions).
First, we declare a class blinkingLED and include some variables. These are the same as the ones that we used previously.
class blinkingLED {
int ledPin;
int ledState = LOW;
long duration;
unsigned long millisMarker;
};
The next step is to create a class constructor. A constructor is a function that is called to instantiate or create an object of that class. It has the same name as our class, and typically carries several input parameters so that we can define some characteristics of the particular object that we are creating.
In the constructor below, we are defining the PIN and the duration of our ledFlasher object, while initialising its variables.
class blinkingLED {
int ledPin;
int ledState = LOW;
long duration;
unsigned long millisMarker;
public:
blinkingLED(int pin, long interval){
ledPin = pin;
pinMode(ledPin, OUTPUT);
duration = interval;
millisMarker = 0;
};
};
Still with me? Good. The next and final step of building our class is to define a method that toggles LED states and updates PIN outputs!
While we’re at it, we’ll also update our setup() and loop() functions to take advantage of our hard work! First, we add the declarations for each LED by instantiating a class for each of them, then we’ll let the Update() method for each of them run continuously in our loop!
class blinkingLED {
int ledPin;
int ledState = LOW;
long duration;
unsigned long millisMarker;
public:
blinkingLED(int pin, long interval){
ledPin = pin;
pinMode(ledPin, OUTPUT);
duration = interval;
millisMarker = 0;
};
void Update() {
digitalWrite(ledPin, ledState);
if ( (millis() - millisMarker) > duration) {
millisMarker = millis();
if (ledState == LOW) {
ledState = HIGH;
} else if (ledState == HIGH) {
ledState = LOW;
}
}
}
};
blinkingLED LED1(12, 1000);
blinkingLED LED2(13, 2000);
void setup() {
}
void loop() {
LED1.Update();
LED2.Update();
}
And there we have it – code that does the exact same thing as before! Except, if we wanted to add a third, fourth or fifth LED, each of them would only require us to add just two more lines of code!
Achieve Arduino Multitasking with RTOS
Using millis() will suffice for keeping time in most Arduino programs. However, there are other options to explore as well – one being RTOS.
What is RTOS and How does It Work?
RTOS stands for Real Time Operating System, and is one of the most important components of today’s embedded systems. RTOS is designed to provide predictable execution of programs, and is usually very lightweight.
RTOS works through a kernel, which is a core component in operating systems like Linux. Each program being executed is a task (or thread) that is controlled by the operating system. If an operating system can perform multiple tasks this way, it can be said to be multitasking.
Multitasking & Scheduling
Traditionally, processors can only execute one task at a time, but an operating system can make each task appear to execute simultaneously by quickly switching between them.
For an operating system to decide how to switch between tasks, it uses a scheduler, which is responsible for deciding which tasks to execute at any given time. It is also important to note that a given task can be paused and resumed multiple times throughout its life cycle.
Tutorial: Multitasking an Arduino with FreeRTOS
In this section, I’m going to show you how you can achieve multitasking with FreeRTOS. FreeRTOS is a well known operating system in the IoT RTOS scene and has been extensively developed for more than a decade. Specially developed for microcontrollers, FreeRTOS features a low memory footprint and power optimisation features.
Required Materials
To follow along, you can use any of the microcontroller boards developed by Seeed that are based on the SAMD microchip, including:
- Wio Terminal
- Seeeduino XIAO
- Seeeduino Zero Series:
- Seeeduino LoRaWAN
Quick Start with FreeRTOS For Arduino
Step 1: Download the Seeed_Arduino_FreeRTOS repository as a ZIP file.
Step 2: Install the ZIP file as a library through the Arduino IDE. Please check here for detailed instructions.
Step 3: Copy the following code into a new sketch and upload it to your Arduino board.
#include <Seeed_Arduino_FreeRTOS.h>
TaskHandle_t Handle_aTask;
TaskHandle_t Handle_bTask;
static void ThreadA(void* pvParameters) {
Serial.println("Thread A: Started");
while (1) {
Serial.println("Hello World!");
delay(1000);
}
}
static void ThreadB(void* pvParameters) {
Serial.println("Thread B: Started");
for (int i = 0; i < 10; i++) {
Serial.println("---This is Thread B---");
delay(2000);
}
Serial.println("Thread B: Deleting");
vTaskDelete(NULL);
}
void setup() {
Serial.begin(115200);
vNopDelayMS(1000); // prevents usb driver crash on startup, do not omit this
while(!Serial); // Wait for Serial terminal to open port before starting program
Serial.println("");
Serial.println("******************************");
Serial.println(" Program start ");
Serial.println("******************************");
// Create the threads that will be managed by the rtos
// Sets the stack size and priority of each task
// Also initializes a handler pointer to each task, which are important to communicate with and retrieve info from tasks
xTaskCreate(ThreadA, "Task A", 256, NULL, tskIDLE_PRIORITY + 2, &Handle_aTask);
xTaskCreate(ThreadB, "Task B", 256, NULL, tskIDLE_PRIORITY + 1, &Handle_bTask);
// Start the RTOS, this function will never return and will schedule the tasks.
vTaskStartScheduler();
}
void loop() {
// NOTHING
}
Step 4: Open the Serial Monitor on the Arduino IDE and watch the magic happen!
This Hello World Example creates two threads that print different strings to the Serial Monitor at a different rate.
- Thread A prints “Hello World”,
- while Thread B prints “—This is Thread B—”!
And that concludes this quick tutorial!
FreeRTOS For Arduino Multitasking: More Examples
You can also do similarly exciting Arduino multitasking with FreeRTOS, such as blinking and fading different LEDs simultaneously, or update elements on an LCD display separately! For more details on these examples, I strongly encourage you to visit the full demonstration and code on the Seeed Wiki page!
Wrapping Up
Thanks for reading this Multitasking with Arduino guide! To summarise, we’ve covered the pitfalls of delay() and how multitasking can be achieved by using millis() and the concept of state machines. In addition, we’ve also gotten our toes wet with a little bit of FreeRTOS!
Nonetheless, these are no more than tools and methods to be used in your projects. Why not pick up an Arduino project for yourself to put your newfound skills to the test?
For more resources, be sure to check out the following links!
- 20 Awesome Arduino Projects That You Must Try 2021!
- 13 Arduino LED Projects you need to try!
- Learn TinyML using Wio Terminal and Arduino IDE #1 Intro
- Wio Terminal: Arduino Customisable Timer (with Code!)