How Do Smart Devices Perform Multiple Tasks?
Ever wonder how your Alexa, robot vacuum cleaner, and smartwatch seamlessly perform multiple tasks without getting stuck in the middle? How exactly are these devices programmed to handle it all?
You came to the right place to understand how it happens under the hood.
In this article, we will understand how these tasks are performed and how they work.
As all these devices use a microcontroller. I will be explaining using the simple microcontroller ESP8266.
How will the task be performed?
Every smart device consists of a microcontroller. These microcontrollers are programmed to perform specific tasks.
These tasks are performed using a single loop function.
void loop() {
// Perform Task
}
This loop function will run continuously.
In this loop, we can do multiple things.
Let's take the example of a weather monitoring device.
These operations are done using a single loop function. As there is nothing else to do.
- Start Device
- Start Loop
- Read sensor data
- Display data / Store data
- Repeat the process
void loop() {
// Read data from sensor
float temperature = dht.readTemperature();
float humidity = dht.readHumidity();
// Send data to cloud
sendDataToCloud(temperature, humidity);
}
But this comes with a problem.
This loop will be running with a microsecond delay.
If we run this loop function, it will run continuously. Means there will be no delay between the operations.
How will a normal loop cause a problem?
Let's assume the above operation takes 1 ms (which is not true in real-world use, as it depends on which microcontroller you use).
The DHT11 sensor should have a 1-second delay between readings.
That means the above loop will run 1000 times in 1 second to just see if the sensor data has changed.
Can you see the problem?
1000 times operation means 1000 times power consumption.
1000 times the power consumption means the battery will drain fast, or the embedded system will heat up.
To avoid this, we can use a delay function.
void loop() {
// Read data from sensor
float temperature = dht.readTemperature();
float humidity = dht.readHumidity();
// Send data to cloud
sendDataToCloud(temperature, humidity);
// Wait for 1 second
delay(1000);
}
By adding a delay, we are making sure that the loop function is not running continuously.
This is a good implementation if we have only one task to perform.
But if we want to do multiple operations to be performed in specific intervals.
Adding Other Operations in a loop
Like this, if we want to read the DHT11 sensor every 1 second and read the MQ2 sensor every 5 seconds. We can't use the delay function.
It will be like this.
void loop() {
// Read data from sensor
float temperature = dht.readTemperature();
float humidity = dht.readHumidity();
// Send data to cloud
sendDataToCloud(temperature, humidity);
// Read data from sensor
float gas = analogRead(MQ2_PIN);
// Send data to cloud
sendDataToCloud(gas);
// Wait for 5 seconds
delay(5000);
}
This will cause a 5-second no-data reading from the DHT11 sensor.
Hence, there will be data loss for 5 seconds.
Here is where the task scheduler comes into the picture.
What is Task Scheduler?
A task scheduler is a lightweight system that allows a processor to juggle multiple activities seemingly at the same time, a concept known as multitasking.
A Task Scheduler solves this problem using a "to-do list" approach.
Instead of freezing, the program loops continuously at top speed.
Every time it loops, the scheduler checks the clock and looks at its list of tasks to see if any are due to run right now.
If a task's time is up, the scheduler runs it and then immediately goes back to checking the clock.
To understand the internal clock, imagine the microcontroller holding a stopwatch that starts counting milliseconds the moment it powers on.
In the Arduino framework, this stopwatch is read using a function called millis().
Instead of pausing the whole system to wait, the scheduler constantly checks this stopwatch against a simple formula for each task:
Current Time - Last Time Executed >= Interval
Here is a simple example of how the task scheduler works in ESP8266.
Ex:
unsigned long previousDHTTime = 0; // Remembers the last time DHT ran
unsigned long previousMQ2Time = 0; // Remembers the last time MQ2 ran
const long dhtInterval = 1000; // 1000 milliseconds = 1 second
const long mq2Interval = 5000; // 5000 milliseconds = 5 seconds
void setup() {
// Initialization code goes here
}
void loop() {
// 1. Read the internal stopwatch
unsigned long currentMillis = millis();
// 2. Check the DHT Task
if (currentMillis - previousDHTTime >= dhtInterval) {
previousDHTTime = currentMillis; // Reset this task's timer
float temperature = dht.readTemperature();
float humidity = dht.readHumidity();
// Send data to cloud
sendDataToCloud(temperature, humidity);
}
// 3. Check the MQ2 Task
if (currentMillis - previousMQ2Time >= mq2Interval) {
previousMQ2Time = currentMillis; // Reset this task's timer
float gas = analogRead(MQ2_PIN);
// Send data to cloud
sendDataToCloud(gas);
}
}
This way, we can perform multiple operations in specific intervals without blocking the loop function.
Core Logic of Task Scheduler
I want to perform two operations in ESP8266.
- Blink the LED every 2.5 seconds 1 time.
- Blink the LED every 5 seconds 2 times.
Let's see how to do this without using the built-in task scheduler.
- Set up serial communication and the LED pin.
- Start Loop.
- Check if 2.5 seconds have passed since the last task activation.
- If yes, perform the blink.
- Check if 5 seconds have passed since the last task activation.
- If yes, perform the blink
#include <Arduino.h>
unsigned long t25, t50;
/**
* Performs a single blink on the built-in LED (Active Low).
* @param message Debug message to output to Serial.
*/
void performBlink(const char *message) {
Serial.println(message);
digitalWrite(LED_BUILTIN, 0); // LED ON
delay(80);
digitalWrite(LED_BUILTIN, 1); // LED OFF
delay(80);
}
void setup() {
Serial.begin(115200);
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, 1); // Start with LED OFF
}
void loop() {
// Task 1: Every 2.5s (triggers at 2.5s, 5.0s, 7.5s...)
if (millis() - t25 >= 2500) {
t25 = millis();
performBlink("[2.5s] 💡");
}
// Task 2: Every 5.0s (triggers at 5.0s, 10.0s...)
// At 5s, both tasks trigger, resulting in a double blink.
if (millis() - t50 >= 5000) {
t50 = millis();
performBlink("[5.0s] 💡");
}
}
Output:
[2.5s] 💡
[5.0s] 💡
[2.5s] 💡
[2.5s] 💡
[5.0s] 💡
See, as we defined, to use a 2.5 sec interval for the first task and a 5 sec interval for the second task.
This also means the 2nd time, a 2.5 sec blink will happen after 5 sec from the 1st blink.
This will create a 2-second blink in 5 sec.
Using Task Scheduler
We are going to use the task scheduler library to perform multiple operations in specific intervals.
Let's see how to do this using the task scheduler library.
- Include the task scheduler library.
- Create a scheduler object.
- Define tasks.
- Add tasks to the scheduler.
- Enable tasks.
- Run the scheduler.
#include <Arduino.h>
#include <TaskScheduler.h>
Scheduler runner;
/**
* Performs a single blink pulse.
* Note: NodeMCU V2 built-in LED is active LOW.
*/
void pulse() {
digitalWrite(LED_BUILTIN, 0); // ON
delay(80);
digitalWrite(LED_BUILTIN, 1); // OFF
delay(80);
}
void Callback(const char *msg) {
Serial.println(msg);
pulse();
}
// Task Definitions
Task t25(2500, TASK_FOREVER, []() { Callback("[2.5s] 💡"); });
Task t50(5000, TASK_FOREVER, []() { Callback("[5.0s] 💡"); });
void setup() {
Serial.begin(115200);
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, 1); // Start OFF
runner.init();
// Add tasks to the scheduler
runner.addTask(t25);
runner.addTask(t50);
// Enable tasks
t25.enable();
t50.enable();
}
void loop() { runner.execute(); }
In this way, we can perform multiple operations in specific intervals without blocking the loop function.
Output:
[2.5s] 💡
[5.0s] 💡
[2.5s] 💡
[5.0s] 💡
[2.5s] 💡
[2.5s] 💡
You can also see that the 5.0s blink is happening first, and sometimes the 2.5s blink is happening first.
This is because of the task scheduler library. It has its own algorithm to perform the tasks.
How real world devices use task scheduler
In real-world devices, task scheduling depends on the complexity of the system.
For simple devices (like basic IoT systems using ESP8266), developers use the same approach we discussed earlier — non-blocking scheduling using millis() or lightweight task schedulers.
For more advanced devices, a Real-Time Operating System (RTOS) such as FreeRTOS is commonly used.
An RTOS allows:
- Running multiple tasks with priorities
- Executing time-critical tasks without delay
- Managing communication between tasks
For example, a smart device can:
- Read sensors periodically
- Handle WiFi communication
- Update display
- Respond to user input
All these tasks run efficiently using a scheduler without blocking each other.
In short, real-world systems use more advanced schedulers, but the core idea remains the same as the simple loop-based scheduler explained earlier.
Conclusion
We understood how smart devices perform multiple tasks using the task scheduler and how to use it to perform multiple operations in specific intervals without blocking the loop function.