Arduino multitasking: several tasks at once with millis()
Or see every lesson.
- 455parts on the bench
- 43boards running now
- 1.00xreal time, on every board
An Arduino Uno has one core and no operating system, so it never runs two things at the same moment. It can still look after several tasks at once, as long as each task takes a moment and hands back. The pattern is called cooperative multitasking: loop() runs every task once per pass, each task checks millis() to see whether its time has come, and nothing ever waits in delay().
The circuit above is the code bench from the C++ course: three LEDs on pins 13, 12 and 11 and a button on pin 2. Its sketch blinks the three LEDs at 200, 330 and 1100 ms and measures its own loop. The serial monitor prints "worst pass through loop: 2 ms" every two seconds, which is why all three rates stay right.
Below is why delay() cannot do this, four tasks written the millis() way, one class per task, what a task that blocks does to the others, and the TaskScheduler library, which runs the same pattern for you.
Want the step-by-step version? The lesson "Ask the time, do not wait for it" walks through this with checkpoints.
Open the lessonWhy delay() can only do one thing
delay() stops the whole sketch. While it counts, no other line runs, so a sketch that blinks with delay() reads its button once per blink at best. The sketch below blinks pin 13 with two one-second delays and checks the button after them.
We held the button from 0.5 s to 1.4 s, and nothing happened: the press began and ended while the sketch was inside a delay(). A second press held from 2.3 s was noticed only when the sketch got round to it, and the serial monitor printed "press seen at 3999", 1.7 seconds late. Making the delays shorter makes the lag smaller but never removes it.
const int BUTTON = 2;
void setup() {
Serial.begin(9600);
pinMode(13, OUTPUT);
pinMode(12, OUTPUT);
pinMode(BUTTON, INPUT_PULLUP);
}
void loop() {
digitalWrite(13, HIGH);
delay(1000);
digitalWrite(13, LOW);
delay(1000);
if (digitalRead(BUTTON) == LOW) {
digitalWrite(12, HIGH);
Serial.print("press seen at ");
Serial.println(millis());
}
}Four tasks in one loop
Give each task a function and its own "last time I ran" variable. Every pass, loop() reads millis() once and hands it to each task; a task that is not due returns at once. Here a slow blink on pin 13 runs every 1000 ms, a fast one on pin 12 every 150 ms, a button toggles pin 11, and a report prints every two seconds.
With the same two presses, the serial monitor printed "press seen at 499" and "press seen at 2298": each press was caught within a millisecond of starting, while both LEDs kept their rates. Then "presses so far: 1" and "presses so far: 2" came from the report task on time.
Two details matter. Keep every time in an unsigned long and compare now - last >= interval, so the sum still works when millis() wraps after 49.7 days. And the button task ignores any change within 20 ms of the last one, because a real button bounces and the simulated one does too; without it, one press counts several times.
const int BUTTON = 2;
unsigned long lastBlink = 0;
unsigned long lastFlash = 0;
unsigned long lastReport = 0;
unsigned long lastChange = 0;
bool blinkOn = false;
bool flashOn = false;
bool wasPressed = false;
int presses = 0;
void setup() {
Serial.begin(9600);
pinMode(13, OUTPUT);
pinMode(12, OUTPUT);
pinMode(11, OUTPUT);
pinMode(BUTTON, INPUT_PULLUP);
}
void blinkTask(unsigned long now) {
if (now - lastBlink >= 1000) {
lastBlink = now;
blinkOn = !blinkOn;
digitalWrite(13, blinkOn);
}
}
void flashTask(unsigned long now) {
if (now - lastFlash >= 150) {
lastFlash = now;
flashOn = !flashOn;
digitalWrite(12, flashOn);
}
}
void buttonTask(unsigned long now) {
bool pressed = digitalRead(BUTTON) == LOW;
if (pressed != wasPressed && now - lastChange >= 20) {
lastChange = now;
wasPressed = pressed;
if (pressed) {
presses++;
digitalWrite(11, presses % 2);
Serial.print("press seen at ");
Serial.println(now);
}
}
}
void reportTask(unsigned long now) {
if (now - lastReport >= 2000) {
lastReport = now;
Serial.print("presses so far: ");
Serial.println(presses);
}
}
void loop() {
unsigned long now = millis();
blinkTask(now);
flashTask(now);
buttonTask(now);
reportTask(now);
}One class per task
When three tasks do the same job with different numbers, write the job once as a class and make three of them. Each Blinker keeps its own pin, interval, last time and state, so adding a fourth LED is one line. This is the shape most multitasking libraries use underneath.
Run for three seconds, the sketch printed "after 3 s: 6, 15, 59": six toggles at 500 ms, fifteen at 200 ms and fifty-nine at 50 ms, the last one landing a moment after the report.
class Blinker {
public:
Blinker(int pin, unsigned long everyMs) : pin_(pin), everyMs_(everyMs) {}
void begin() {
pinMode(pin_, OUTPUT);
}
void update(unsigned long now) {
if (now - last_ >= everyMs_) {
last_ = now;
on_ = !on_;
digitalWrite(pin_, on_);
toggles_++;
}
}
unsigned long toggles() const {
return toggles_;
}
private:
int pin_;
unsigned long everyMs_;
unsigned long last_ = 0;
bool on_ = false;
unsigned long toggles_ = 0;
};
Blinker slow(13, 500);
Blinker mid(12, 200);
Blinker fast(11, 50);
bool reported = false;
void setup() {
Serial.begin(9600);
slow.begin();
mid.begin();
fast.begin();
}
void loop() {
unsigned long now = millis();
slow.update(now);
mid.update(now);
fast.update(now);
if (!reported && now >= 3000) {
reported = true;
Serial.print("after 3 s: ");
Serial.print(slow.toggles());
Serial.print(", ");
Serial.print(mid.toggles());
Serial.print(", ");
Serial.println(fast.toggles());
}
}One task that blocks slows them all
Cooperative means every task has to cooperate. The bench sketch above measures the longest pass through loop(), and with three well-behaved jobs it printed "worst pass through loop: 2 ms". We added delay(250); to the slowest job only, and it printed "worst pass through loop: 250 ms": while that one job waited, the 200 ms LED missed its turn and the button would have gone unread.
The usual culprits are delay(), a while loop that waits for a pin, pulseIn() with a long timeout, and a long print at a slow baud rate. Break a slow job into steps, with a variable that remembers which step comes next, and let loop() come round between them. That is a state machine, and it is the next page to read.
The TaskScheduler library
TaskScheduler does the bookkeeping: you make a Task with an interval, a count and a function, add it to a Scheduler and call runner.execute() in loop(). It is still cooperative, so the same rule holds, and a callback that blocks blocks every task. Mokxi has it built in; the sketch below ran on the Uno as written.
It printed "slow 1, fast 1" at once, because a task runs as soon as it is enabled, then "slow 4, fast 21" at two seconds and "slow 9, fast 41" at four.
#include <TaskScheduler.h>
void blinkSlow();
void blinkFast();
void report();
Scheduler runner;
Task slowTask(500, TASK_FOREVER, &blinkSlow);
Task fastTask(100, TASK_FOREVER, &blinkFast);
Task reportTask(2000, TASK_FOREVER, &report);
int slowRuns = 0;
int fastRuns = 0;
void blinkSlow() {
digitalWrite(13, !digitalRead(13));
slowRuns++;
}
void blinkFast() {
digitalWrite(12, !digitalRead(12));
fastRuns++;
}
void report() {
Serial.print("slow ");
Serial.print(slowRuns);
Serial.print(", fast ");
Serial.println(fastRuns);
}
void setup() {
Serial.begin(9600);
pinMode(13, OUTPUT);
pinMode(12, OUTPUT);
runner.addTask(slowTask);
runner.addTask(fastTask);
runner.addTask(reportTask);
slowTask.enable();
fastTask.enable();
reportTask.enable();
}
void loop() {
runner.execute();
}Questions
Can an Arduino Uno multitask?
Not truly: it has one core and runs one instruction at a time. It can look after many tasks at once by running each for a moment in every pass of loop(), using millis() instead of delay(), which is fast enough that the tasks seem to run together.
Why does my button not work while an LED blinks?
The blink uses delay(), and nothing else runs during a delay. A press that starts and ends inside one is missed, and one that lasts longer is seen late. Time the blink with millis() and check the button in every pass of loop().
What is the best Arduino multitasking library?
TaskScheduler is a common choice and works on the Uno; there are smaller ones as well. All of them do what the four-task sketch does by hand, so learning the pattern first makes any library easier to read.
Does an ESP32 multitask differently?
A real ESP32 runs FreeRTOS, so xTaskCreate() gives true tasks with their own stacks, on both cores of a classic ESP32. Mokxi runs the ESP32 sketch on one core with no scheduler: xTaskCreate() says once on the serial monitor that the task does not run. The millis() pattern on this page works the same on both.
How many tasks can loop() run?
As many as fit in memory, as long as each one returns quickly. Measure the longest pass through loop(), as the bench sketch does; if it stays at a few milliseconds, every task keeps its timing.
Keep going
Where the facts on this page come from
Build this for real
Open the editor, change a value and watch the number move with it. Nothing to install, and no account needed.