Zbotic Logo Zbotic Logo
  • Home
  • Shop
  • Sale
  • 3D Print Service
  • PCB Service
  • B2B
  • Blogs
  • Contact Us
0 0

View Wishlist Add all to cart

0 0
0 Shopping Cart
Shopping cart (0)
Subtotal: ₹0.00

View cartCheckout

  • Shop
  • About Us
  • Contact Us
  • Reseller
  • Blogs
020 69134444
1800 209 0998
[email protected]
Help Desk
Facebook Twitter Instagram Linkedin YouTube
Zbotic Logo Zbotic Logo
0 0

View Wishlist Add all to cart

0 0
0 Shopping Cart
Shopping cart (0)
Subtotal: ₹0.00

View cartCheckout

All departments
  • 3D Print Service
  • 3D Printer
  • Batteries & Chargers
  • Development Boards
  • Drone Parts
  • EBike parts
  • Sensor Modules
  • Electronic Components
  • Electronic Modules
  • IoT and Wireless
  • Mechanical Parts and Workbench Tools
  • Motors & Drivers & Pumps & Actuators
  • DIY and Robot Kits
  • Show more
  • Home
  • Shop
  • Sale
  • 3D Print Service
  • PCB Service
  • B2B
  • Blogs
  • Contact Us
Return to previous page
Home Arduino & Microcontrollers

Arduino Multitasking Guide: millis() Instead of delay()

Arduino Multitasking Guide: millis() Instead of delay()

March 11, 2026 /Posted byJayesh Jain / 0

The single biggest step-up in Arduino programming skill is moving from delay() to millis()-based timing. While delay() is fine for simple blink sketches, it completely blocks the microcontroller — no button reads, no sensor polling, no serial communication, nothing else can happen while your code sits inside a delay() call. This arduino multitasking millis guide shows you how to run multiple independent tasks simultaneously using timestamp-based timing, giving your Arduino the ability to blink an LED, read a sensor, respond to button presses, and drive a display all at the same time — with no RTOS required.

Table of Contents

  • Why delay() Is the Wrong Tool
  • How millis() Works
  • The Basic millis() Non-blocking Pattern
  • Running Multiple Independent Tasks
  • State Machines for Complex Sequences
  • Button Debouncing Without delay()
  • millis() Pitfalls and Overflow Handling
  • FAQ

Why delay() Is the Wrong Tool

delay(1000) calls the AVR’s _delay_ms() function, which executes a busy-wait loop. The CPU is alive and counting clock cycles — it simply throws them away doing nothing useful. During that one second:

  • A button press is missed if it happens at the wrong time
  • Serial incoming data fills the 64-byte hardware buffer and overflows
  • A PIR sensor fires but the interrupt is handled too late
  • An LCD display cannot be updated
  • A second LED cannot blink at a different rate

The real-world consequence: any project that needs to respond to more than one input or output at once becomes impossible to implement cleanly with delay(). Beginners who try work around it end up nesting delays, duplicating code, and building fragile sketches that break whenever a requirement changes.

The millis()-based approach solves this by replacing blocking waits with comparisons: instead of waiting, you record when something last happened, and check whether enough time has passed every time loop() runs. Since loop() runs thousands of times per second, you can check dozens of independent timers without any of them blocking the others.

Recommended: Arduino Uno R3 Beginners Kit — includes LEDs, buttons, and components to practice all the millis() multitasking patterns in this guide with real hardware from day one.

How millis() Works

millis() returns the number of milliseconds elapsed since the Arduino last reset, as an unsigned long (32-bit, 0–4,294,967,295). It is driven by Timer 0, which is configured by the Arduino core to generate an interrupt every ~1.024 ms (the actual resolution). The overflow interrupt increments an internal counter that millis() reads.

Key properties:

  • Non-blocking: calling millis() takes just a few microseconds
  • 32-bit unsigned: counts up to ~49.7 days before rolling over to zero
  • Resolution: 1 ms (not µs — use micros() for sub-millisecond timing)
  • Frozen during interrupts: within an ISR, millis() does not advance
  • Disabled by Timer 0 shutdown: using power management (power_timer0_disable()) stops millis()

micros() works identically but returns microseconds and overflows after ~70 minutes. Use it for timing pulse widths, encoder pulses, or other sub-millisecond events.

The Basic millis() Non-blocking Pattern

Here is the canonical non-blocking blink, the “Hello World” of millis()-based programming:

const int LED_PIN = 13;
const unsigned long BLINK_INTERVAL = 500; // milliseconds

unsigned long lastBlinkTime = 0;
bool ledState = false;

void setup() {
pinMode(LED_PIN, OUTPUT);
}

void loop() {
unsigned long now = millis();

if (now - lastBlinkTime >= BLINK_INTERVAL) {
lastBlinkTime = now;
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
}

// Any other code here runs without delay!
}

The critical pattern has three parts:

  1. Store the timestamp of when the event last happened (lastBlinkTime)
  2. Every loop iteration, compute now - lastBlinkTime
  3. When the elapsed time exceeds the interval, execute the action and update the timestamp

Why now - lastBlinkTime instead of now >= lastBlinkTime + BLINK_INTERVAL? Subtraction handles the millis() overflow correctly (more on this in the pitfalls section). Always subtract, never add.

Recommended: Multifunction Shield for Arduino Uno/Leonardo — features 4 LEDs, 4 buttons, a 4-digit 7-segment display, and a buzzer all in one shield; perfect for practicing multitasking with multiple independent outputs running simultaneously.

Running Multiple Independent Tasks

The real power emerges when you add more tasks. Each needs its own timestamp variable and interval constant. Here is a sketch running four completely independent tasks:

// --- Task 1: Blink LED on pin 13 every 500ms ---
const int LED1 = 13;
const unsigned long LED1_INTERVAL = 500;
unsigned long led1Last = 0;
bool led1State = false;

// --- Task 2: Blink LED on pin 12 every 1200ms ---
const int LED2 = 12;
const unsigned long LED2_INTERVAL = 1200;
unsigned long led2Last = 0;
bool led2State = false;

// --- Task 3: Read temperature sensor every 2000ms ---
const unsigned long SENSOR_INTERVAL = 2000;
unsigned long sensorLast = 0;

// --- Task 4: Print uptime every 5000ms ---
const unsigned long PRINT_INTERVAL = 5000;
unsigned long printLast = 0;

void setup() {
Serial.begin(9600);
pinMode(LED1, OUTPUT);
pinMode(LED2, OUTPUT);
}

void loop() {
unsigned long now = millis();

// Task 1
if (now - led1Last >= LED1_INTERVAL) {
led1Last = now;
led1State = !led1State;
digitalWrite(LED1, led1State);
}

// Task 2
if (now - led2Last >= LED2_INTERVAL) {
led2Last = now;
led2State = !led2State;
digitalWrite(LED2, led2State);
}

// Task 3
if (now - sensorLast >= SENSOR_INTERVAL) {
sensorLast = now;
int temp = analogRead(A0); // replace with real sensor read
// process temp...
}

// Task 4
if (now - printLast >= PRINT_INTERVAL) {
printLast = now;
Serial.print("Uptime: ");
Serial.print(now / 1000);
Serial.println(" seconds");
}
}

All four tasks run independently. LED1 blinks at 500ms, LED2 at 1200ms — they immediately fall out of sync, producing the characteristic async blink pattern that is impossible with nested delay() calls. The sensor reads every 2 seconds and Serial prints every 5 seconds, all without any task blocking any other.

Encapsulating Tasks in Functions

For cleaner code, encapsulate each task’s logic and state in its own function:

void taskBlinkLED() {
static unsigned long lastTime = 0;
static bool state = false;
unsigned long now = millis();

if (now - lastTime >= 500) {
lastTime = now;
state = !state;
digitalWrite(13, state);
}
}

void taskReadSensor() {
static unsigned long lastTime = 0;
unsigned long now = millis();

if (now - lastTime >= 2000) {
lastTime = now;
// read sensor...
}
}

void loop() {
taskBlinkLED();
taskReadSensor();
// more tasks...
}

The static keyword inside a function makes the variable retain its value between calls (like a global, but scoped to the function). This pattern keeps all task state encapsulated and makes adding/removing tasks as easy as adding/removing one function call in loop().

Recommended: Arduino Mega 2560 R3 Board — with 54 digital I/O pins and 16 analog inputs, the Mega is ideal for multitasking projects driving many sensors, relays, and displays simultaneously without pin conflicts.

State Machines for Complex Sequences

Simple on/off tasks are straightforward with millis(). For sequences — like a traffic light that cycles Green→Yellow→Red with different durations — state machines are the right approach:

enum TrafficState { GREEN, YELLOW, RED };
TrafficState currentState = GREEN;
unsigned long stateStart = 0;

const unsigned long GREEN_TIME = 5000;
const unsigned long YELLOW_TIME = 1500;
const unsigned long RED_TIME = 4000;

void taskTrafficLight() {
unsigned long now = millis();
unsigned long elapsed = now - stateStart;

switch (currentState) {
case GREEN:
digitalWrite(2, HIGH); digitalWrite(3, LOW); digitalWrite(4, LOW);
if (elapsed >= GREEN_TIME) { currentState = YELLOW; stateStart = now; }
break;
case YELLOW:
digitalWrite(2, LOW); digitalWrite(3, HIGH); digitalWrite(4, LOW);
if (elapsed >= YELLOW_TIME) { currentState = RED; stateStart = now; }
break;
case RED:
digitalWrite(2, LOW); digitalWrite(3, LOW); digitalWrite(4, HIGH);
if (elapsed >= RED_TIME) { currentState = GREEN; stateStart = now; }
break;
}
}

The state machine transitions between states based on elapsed time, with each state having its own duration. A pedestrian walk button or emergency vehicle sensor can be incorporated by adding additional state transitions — none of which require delay().

Button Debouncing Without delay()

Mechanical buttons bounce — they generate multiple transitions over a period of ~10–50 ms when pressed. delay(50) is a common but blocking debounce approach. The non-blocking version:

const int BUTTON_PIN = 7;
const unsigned long DEBOUNCE_DELAY = 50;

bool lastButtonState = HIGH;
bool buttonState = HIGH;
unsigned long lastDebounceTime = 0;
bool stableState = HIGH;

void taskButton() {
unsigned long now = millis();
bool reading = digitalRead(BUTTON_PIN);

if (reading != lastButtonState) {
lastDebounceTime = now; // Reset debounce timer on any change
}

if (now - lastDebounceTime >= DEBOUNCE_DELAY) {
// Signal has been stable for DEBOUNCE_DELAY ms
if (reading != stableState) {
stableState = reading;
if (stableState == LOW) { // Button pressed (active LOW)
// Handle button press event
Serial.println("Button pressed!");
}
}
}

lastButtonState = reading;
}

This debounce logic only reports a state change after the signal has been stable for 50 ms, eliminating bounce noise — and it runs entirely non-blocking inside loop().

millis() Pitfalls and Overflow Handling

Always Use unsigned long

Never store a millis() value in an int or long (signed). Signed 32-bit integers overflow at ~24 days and produce negative numbers, breaking all timing comparisons. unsigned long overflows at ~49.7 days and wraps correctly with subtraction.

The Subtraction Trick and Overflow Safety

When millis() overflows from 4,294,967,295 back to 0, what happens to now - lastTime?

Example: lastTime = 4,294,967,200, now = 100 (after overflow). With unsigned long subtraction: 100 - 4,294,967,200 = 96 (unsigned arithmetic wraps correctly). The comparison 96 >= INTERVAL works perfectly. This is why the subtraction pattern is overflow-safe and the addition pattern is not.

Never Modify millis() Variables Inside ISRs

If you access millis() timestamps from both loop() and an interrupt service routine, declare them volatile and use atomic reads on AVR (wrap access in noInterrupts() / interrupts() pairs, or use ATOMIC_BLOCK from util/atomic.h).

Task Blocking Still Breaks Everything

If any code inside loop() blocks — even a Wire.requestFrom() that takes 10 ms, or a Serial.print() of a large string — all other tasks miss their deadlines by that amount. For strict timing requirements, use interrupts for time-critical tasks and millis() only for coarse scheduling.

Recommended: Arduino Nano 33 IoT with Header — for projects that outgrow single-threaded millis() scheduling, the Nano 33 IoT’s SAMD21 supports FreeRTOS via the Arduino_FreeRTOS library, enabling true preemptive multitasking with separate task stacks and priorities.

Frequently Asked Questions

Is millis() accurate enough for timing sensors?

millis() has 1 ms resolution and is accurate to within ±0.5% over temperature (limited by the crystal accuracy). For most sensor polling (temperature every 2 seconds, button debounce at 50 ms), this is more than adequate. For precise pulse timing, use micros() (4 µs resolution on 16 MHz boards) or input capture mode on Timer 1 for sub-microsecond accuracy.

Can I use millis() in an interrupt service routine?

You can call millis() inside an ISR, but the value will not advance while inside the ISR (Timer 0 interrupt is disabled during ISR execution). For very short ISRs, this is negligible. For longer ISRs, millis() may be slightly stale. A better pattern: set a volatile bool flag in the ISR and handle the event in loop().

What is the difference between millis() and micros()?

millis() returns elapsed time in milliseconds (1 ms resolution, 49.7-day overflow). micros() returns elapsed time in microseconds (4 µs resolution on 16 MHz, 70-minute overflow). Both are non-blocking. Use millis() for anything 10 ms or longer; use micros() for pulse measurement, encoder reading, or precise signal timing under 10 ms.

My tasks drift over time — how do I fix that?

The common mistake: lastTime = lastTime + INTERVAL vs lastTime = now. Setting lastTime = now causes drift because now is slightly later than lastTime + INTERVAL (by however long your task code took to execute). Setting lastTime = lastTime + INTERVAL (or lastTime += INTERVAL) maintains exact period timing regardless of task execution duration. Use the latter for applications where long-term accuracy matters (real-time clock, metronome, data logging at fixed rates).

Can millis() replace a RTOS for complex projects?

For most Arduino projects with 3–10 independent tasks that are not time-critical (tolerating ±1–10 ms jitter), millis()-based scheduling is perfectly adequate and far simpler than an RTOS. When you need strict real-time guarantees, preemptive task switching, or inter-task communication with synchronization, consider FreeRTOS (available via the Arduino_FreeRTOS library for AVR) or upgrade to an ARM-based board where FreeRTOS is mainstream.

Mastering millis()-based multitasking unlocks the full potential of the Arduino loop and makes your projects more responsive, reliable, and maintainable. Explore all Arduino boards and accessories at zbotic.in Arduino & Microcontrollers — from the classic Uno to the powerful Nano 33 IoT — and start building projects that truly run in parallel.

Tags: arduino concurrent tasks, arduino loop, arduino millis, arduino multitasking, arduino no delay, Arduino programming, arduino timing
Share Post
  • Facebook
  • Linkedin
  • Whatsapp
Arduino Mega Based Home Automa...
blog arduino mega based home automation with 8 relays oled 595168
blog best raspberry pi cases and enclosures cooling and style 595175
Best Raspberry Pi Cases and En...

Related posts

Svg%3E
Read more

Arduino Batch Programming: Flash Multiple Boards Quickly

April 1, 2026 0
Table of Contents Introduction Components and Hardware Setup Wiring Diagram and Connections Complete Code with Explanation Customization and Improvements Troubleshooting... Continue reading
Svg%3E
Read more

Arduino Based Radar System with Ultrasonic Sensor

April 1, 2026 0
Table of Contents Introduction Components and Hardware Setup Wiring Diagram and Connections Complete Code with Explanation Customization and Improvements Troubleshooting... Continue reading
Svg%3E
Read more

Arduino Automatic Plant Monitor: Sunlight, Moisture, Temperature

April 1, 2026 0
Table of Contents Introduction Components and Hardware Setup Wiring Diagram and Connections Complete Code with Explanation Customization and Improvements Troubleshooting... Continue reading
Svg%3E
Read more

Arduino Lie Detector: GSR Sensor Polygraph Project

April 1, 2026 0
Table of Contents Introduction Components and Hardware Setup Wiring Diagram and Connections Complete Code with Explanation Customization and Improvements Troubleshooting... Continue reading
Svg%3E
Read more

Arduino Metal Detector: Build a Treasure Finder

April 1, 2026 0
Table of Contents Introduction Components and Hardware Setup Wiring Diagram and Connections Complete Code with Explanation Customization and Improvements Troubleshooting... Continue reading

Add comment Cancel reply

Your email address will not be published. Required fields are marked

Facebook Twitter Instagram Pinterest Linkedin Youtube

Get the latest deals and more.

Download on Google Play Download on the App Store

Call us: 020 69134444 / 1800 209 0998

Monday - Saturday 09:30 AM - 06:00 PM
For Technical Supports Email: [email protected]
For Sales / Enquiries Email: [email protected]

  • My Account

    • Cart

    • Wishlist

    • Checkout

    • My Orders

    • Track Order

    • My Account

  • Information

    • FAQs

    • Blogs

    • Career

    • About Us

    • Contact Us

    • Payment Options

  • Policies

    • Privacy Policy

    • Terms & Conditions

    • GST Input Tax Credit

    • Shipping Return Policy

    • E-Waste Collection Points

    • Our Sitemap

© Zbotic.in is registered trademark of Moxie Supply Pvt Ltd – All Rights Reserved
Login
Use Phone Number
Use Email Address
Not a member yet? Register Now
Reset Password
Use Phone Number
Use Email Address
Register
Already a member? Login Now