Bird
Raised Fist0
Arduinoprogramming~15 mins

millis() for non-blocking timing in Arduino - Deep Dive

Choose your learning style10 modes available

Start learning this pattern below

Jump into concepts and practice - no test required

or
Recommended
Test this pattern10 questions across easy, medium, and hard to know if this pattern is strong
Overview - millis() for non-blocking timing
What is it?
millis() is a function in Arduino that returns the number of milliseconds since the board started running the current program. It helps you measure time without stopping your program. Instead of waiting and pausing, you can check how much time has passed and do other tasks at the same time. This is called non-blocking timing.
Why it matters
Without millis(), you would often use delay(), which stops everything and makes your program wait. This means your Arduino can't do anything else during that wait. millis() lets your program keep running and check time in the background, making your projects faster and more responsive. Imagine trying to blink a light while also reading a sensor; millis() makes this possible.
Where it fits
Before learning millis(), you should understand basic Arduino programming and how delay() works. After millis(), you can learn about timers, interrupts, and more advanced multitasking techniques on microcontrollers.
Mental Model
Core Idea
millis() is like a stopwatch that keeps running in the background, letting your program check elapsed time without stopping anything else.
Think of it like...
Imagine you have a kitchen timer that keeps counting seconds while you cook multiple dishes. You don’t stop cooking to watch the timer; you just glance at it when you need to know how much time passed.
┌───────────────┐
│ Arduino Loop  │
│  ┌─────────┐  │
│  │ millis()│──┼─> Stopwatch keeps running
│  └─────────┘  │
│  ┌─────────┐  │
│  │ Check   │  │
│  │ elapsed │  │
│  └─────────┘  │
│  ┌─────────┐  │
│  │ Do other│  │
│  │ tasks   │  │
│  └─────────┘  │
└───────────────┘
Build-Up - 6 Steps
1
FoundationUnderstanding millis() basics
🤔
Concept: millis() returns the time in milliseconds since the Arduino started running.
In Arduino, calling millis() gives you a number that keeps increasing every millisecond. For example, right after starting, millis() might return 0, then 1000 after one second, 2000 after two seconds, and so on. This number resets only when the Arduino resets or powers off.
Result
You get a continuously increasing number representing elapsed time in milliseconds.
Knowing millis() gives you a running clock inside your program without stopping anything.
2
FoundationWhy delay() blocks your program
🤔
Concept: delay() pauses the whole program for a set time, stopping all other actions.
When you use delay(1000), the Arduino stops and waits for 1000 milliseconds before moving on. During this wait, it cannot check sensors, update displays, or respond to inputs. This blocking behavior limits what your program can do at once.
Result
Program pauses completely during delay, no other code runs.
Understanding delay() blocking helps you see why millis() is needed for multitasking.
3
IntermediateUsing millis() for non-blocking timing
🤔Before reading on: do you think millis() pauses your program like delay(), or lets it run freely? Commit to your answer.
Concept: You can check elapsed time by comparing current millis() with a saved start time, without stopping the program.
To do something every second without delay(), save the current millis() in a variable (startTime). Then, in your loop, check if millis() - startTime is greater than or equal to 1000. If yes, do your task and update startTime to the current millis(). This way, your program keeps running other code while waiting.
Result
Your task runs every second, but the program never stops or waits.
Knowing how to compare millis() values lets you create timers that don’t block your program.
4
IntermediateHandling millis() overflow safely
🤔Before reading on: do you think millis() can overflow and cause timing errors? Commit to yes or no.
Concept: millis() resets to zero after about 50 days; your code must handle this to avoid bugs.
Because millis() is stored in an unsigned long (32-bit), it overflows roughly every 49.7 days. When it resets to zero, subtracting times can give wrong results if not handled properly. Using subtraction like (currentMillis - previousMillis) works correctly even after overflow because of how unsigned math works in Arduino.
Result
Your timing code continues working correctly even after millis() resets.
Understanding overflow and unsigned math prevents subtle bugs in long-running programs.
5
AdvancedCreating multiple timers with millis()
🤔Before reading on: can you use millis() to run several timed tasks independently? Commit to yes or no.
Concept: You can manage multiple independent timers by storing separate start times for each task.
For example, to blink two LEDs at different intervals, keep two variables: led1PreviousMillis and led2PreviousMillis. Check each timer separately by comparing millis() with each stored time. This way, each task runs on its own schedule without blocking others.
Result
Multiple timed events run smoothly and independently in the same program.
Knowing to track separate start times unlocks complex multitasking with millis().
6
ExpertLimitations and alternatives to millis() timing
🤔Before reading on: do you think millis() timing is perfect for all timing needs? Commit to yes or no.
Concept: millis() is great for many tasks but has limits; hardware timers or interrupts may be better for precise timing.
millis() depends on the Arduino clock and can be affected by interrupts or heavy processing, causing slight delays. For very precise timing (microseconds or exact intervals), hardware timers or interrupts provide better accuracy. Also, millis() timing can drift over long periods. Knowing when to switch to these methods is key for advanced projects.
Result
You understand when millis() is enough and when to use more precise timing tools.
Recognizing millis() limits helps you choose the right timing method for your project’s needs.
Under the Hood
millis() reads a hardware timer that counts clock cycles since the Arduino started. This timer increments every millisecond using the microcontroller's internal clock and timer registers. The function returns this count as an unsigned long integer. Because it uses hardware timers, millis() runs independently of your code and does not block execution.
Why designed this way?
millis() was designed to provide a simple, non-blocking way to track elapsed time using existing hardware timers. It avoids the complexity of setting up hardware timers manually for most users. The unsigned long type and subtraction method were chosen to handle timer overflow gracefully, ensuring long-running programs work correctly without extra code.
┌───────────────┐
│ Hardware Timer│
│ (counts ms)   │
└──────┬────────┘
       │
       ▼
┌───────────────┐
│ millis() func │
│ returns count │
└──────┬────────┘
       │
       ▼
┌───────────────┐
│ User Program  │
│ compares time │
│ without delay │
└───────────────┘
Myth Busters - 4 Common Misconceptions
Quick: Does millis() stop your program like delay()? Commit to yes or no.
Common Belief:millis() pauses the program like delay(), just in smaller steps.
Tap to reveal reality
Reality:millis() does not pause or block; it only returns the elapsed time, letting your program run freely.
Why it matters:Believing millis() blocks leads to misuse and missed opportunities for multitasking.
Quick: Does millis() overflow cause your timing to break? Commit to yes or no.
Common Belief:When millis() overflows, your timing code will fail and cause bugs.
Tap to reveal reality
Reality:Proper subtraction of unsigned longs handles overflow automatically, so timing continues correctly.
Why it matters:Not understanding overflow handling causes unnecessary code complexity or bugs in long-running projects.
Quick: Can you use one millis() variable for multiple timers? Commit to yes or no.
Common Belief:One saved millis() value can manage all timed events in a program.
Tap to reveal reality
Reality:Each timed event needs its own saved millis() value to track its independent timing.
Why it matters:Using one variable for multiple timers causes timing conflicts and incorrect behavior.
Quick: Is millis() timing perfectly precise for all tasks? Commit to yes or no.
Common Belief:millis() timing is exact and suitable for all timing needs.
Tap to reveal reality
Reality:millis() has limited precision and can be affected by interrupts or processing delays; hardware timers are better for precise timing.
Why it matters:Relying on millis() for precise timing can cause errors in critical applications like motor control or communication.
Expert Zone
1
millis() timing depends on the Arduino clock speed; changing clock speed affects timing accuracy.
2
Heavy interrupt use or long-running code sections can delay millis() updates, causing timing drift.
3
Using unsigned long subtraction for timing comparisons is a subtle but crucial trick to handle overflow safely.
When NOT to use
Avoid millis() for tasks needing microsecond precision or exact timing intervals. Use hardware timers, interrupts, or real-time operating systems (RTOS) for those cases.
Production Patterns
In real projects, millis() is often combined with state machines to manage complex behaviors without blocking. Multiple millis() timers track different events like sensor reads, LED blinking, and communication timeouts simultaneously.
Connections
Event Loop (JavaScript)
Both use non-blocking timing to handle multiple tasks smoothly.
Understanding millis() timing helps grasp how event loops manage asynchronous tasks without stopping the program.
Stopwatch in Sports
millis() acts like a stopwatch measuring elapsed time continuously.
Knowing how a stopwatch works clarifies how millis() tracks time without interrupting other activities.
Multitasking in Operating Systems
millis() timing enables simple multitasking by letting tasks check time and run independently.
Seeing millis() as a lightweight multitasking tool connects embedded programming to broader OS concepts.
Common Pitfalls
#1Using delay() for timing causes program to freeze and miss other tasks.
Wrong approach:void loop() { digitalWrite(LED_BUILTIN, HIGH); delay(1000); // blocks everything digitalWrite(LED_BUILTIN, LOW); delay(1000); }
Correct approach:unsigned long previousMillis = 0; const long interval = 1000; void loop() { unsigned long currentMillis = millis(); if (currentMillis - previousMillis >= interval) { previousMillis = currentMillis; digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); } // other code runs here }
Root cause:Misunderstanding that delay() stops all code, blocking multitasking.
#2Using one saved millis() variable for multiple timers causes timing conflicts.
Wrong approach:unsigned long timer = 0; void loop() { if (millis() - timer >= 1000) { timer = millis(); // do task 1 } if (millis() - timer >= 500) { // do task 2 } }
Correct approach:unsigned long timer1 = 0; unsigned long timer2 = 0; void loop() { unsigned long currentMillis = millis(); if (currentMillis - timer1 >= 1000) { timer1 = currentMillis; // do task 1 } if (currentMillis - timer2 >= 500) { timer2 = currentMillis; // do task 2 } }
Root cause:Not realizing each timed event needs its own independent timer variable.
#3Ignoring millis() overflow leads to incorrect timing after ~50 days.
Wrong approach:if (millis() > previousMillis + interval) { previousMillis = millis(); // do something }
Correct approach:if (millis() - previousMillis >= interval) { previousMillis = millis(); // do something }
Root cause:Using greater-than comparison instead of subtraction causes errors when millis() resets.
Key Takeaways
millis() provides a running count of milliseconds since Arduino started, enabling non-blocking timing.
Using millis() instead of delay() lets your program do many things at once without stopping.
Comparing current millis() with a saved start time using subtraction handles timer overflow safely.
Each timed event needs its own saved millis() value to run independently.
For very precise timing, hardware timers or interrupts are better than millis().

Practice

(1/5)
1. What does the millis() function in Arduino return?
easy
A. The current date and time
B. The number of milliseconds since the Arduino board started running the current program
C. The number of microseconds since the last reset
D. The current time in seconds

Solution

  1. Step 1: Understand what millis() measures

    millis() returns the time in milliseconds since the Arduino started running the program.
  2. Step 2: Compare options with the definition

    Only The number of milliseconds since the Arduino board started running the current program correctly describes this behavior; others mention seconds, microseconds, or date/time which are incorrect.
  3. Final Answer:

    The number of milliseconds since the Arduino board started running the current program -> Option B
  4. Quick Check:

    millis() = milliseconds since start [OK]
Hint: Remember millis() counts milliseconds since start [OK]
Common Mistakes:
  • Confusing millis() with delay()
  • Thinking millis() returns seconds
  • Assuming millis() gives current date/time
2. Which of the following is the correct way to store the current time using millis() in Arduino?
easy
A. char currentTime = millis();
B. int currentTime = millis();
C. float currentTime = millis();
D. unsigned long currentTime = millis();

Solution

  1. Step 1: Identify the correct data type for millis()

    millis() returns an unsigned long value representing milliseconds.
  2. Step 2: Match the data type with variable declaration

    Only unsigned long currentTime = millis(); uses unsigned long, which can hold large millisecond values without overflow.
  3. Final Answer:

    unsigned long currentTime = millis(); -> Option D
  4. Quick Check:

    Use unsigned long for millis() values [OK]
Hint: Use unsigned long to store millis() time [OK]
Common Mistakes:
  • Using int which can overflow quickly
  • Using float or char which are incorrect types
  • Not declaring variable before assignment
3. What will the following Arduino code print to the Serial Monitor?
unsigned long previousMillis = 0;
const long interval = 1000;

void setup() {
  Serial.begin(9600);
}

void loop() {
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;
    Serial.println("Tick");
  }
}
medium
A. Prints "Tick" once and stops
B. Prints "Tick" continuously without delay
C. Prints "Tick" every 1000 milliseconds without stopping the program
D. Causes a compile error due to variable scope

Solution

  1. Step 1: Understand the timing logic

    The code checks if 1000 milliseconds have passed since last print using millis() and updates previousMillis accordingly.
  2. Step 2: Analyze the output behavior

    When 1000 ms pass, it prints "Tick" and continues looping without blocking, so it prints every second repeatedly.
  3. Final Answer:

    Prints "Tick" every 1000 milliseconds without stopping the program -> Option C
  4. Quick Check:

    Non-blocking timing prints "Tick" every second [OK]
Hint: Check millis() difference to print repeatedly [OK]
Common Mistakes:
  • Thinking it prints only once
  • Confusing with delay() causing blocking
  • Assuming compile error due to variable scope
4. Identify the error in this Arduino code using millis() for timing:
unsigned long previousMillis;
const long interval = 2000;

void setup() {
  Serial.begin(9600);
}

void loop() {
  if (millis() - previousMillis >= interval) {
    Serial.println("Hello");
  }
}
medium
A. previousMillis is never updated, so "Hello" prints continuously
B. interval should be unsigned long, not long
C. Serial.begin() is missing in setup()
D. millis() cannot be used in loop()

Solution

  1. Step 1: Check how previousMillis is used

    The code checks the time difference but never updates previousMillis after printing.
  2. Step 2: Understand the effect of missing update

    Without updating, the condition stays true, so "Hello" prints repeatedly without delay.
  3. Final Answer:

    previousMillis is never updated, so "Hello" prints continuously -> Option A
  4. Quick Check:

    Update previousMillis to avoid continuous printing [OK]
Hint: Always update previousMillis after action [OK]
Common Mistakes:
  • Forgetting to update previousMillis
  • Thinking interval type causes error
  • Assuming Serial.begin() is missing
5. You want to blink an LED every 500 milliseconds without stopping other code from running. Which code snippet correctly uses millis() for this non-blocking timing? A)
unsigned long previousMillis = 0;
const long interval = 500;
void loop() {
  if (millis() - previousMillis >= interval) {
    previousMillis = millis();
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
  }
  // other code runs here
}
B)
void loop() {
  delay(500);
  digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}
C)
unsigned long previousMillis = 0;
const long interval = 500;
void loop() {
  if (millis() >= previousMillis + interval) {
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
    previousMillis = millis();
  }
}
D)
unsigned long previousMillis = 0;
const long interval = 500;
void loop() {
  if (millis() - previousMillis > interval) {
    digitalWrite(LED_BUILTIN, HIGH);
    delay(500);
    digitalWrite(LED_BUILTIN, LOW);
  }
}
hard
A. Correct non-blocking blink using millis() and toggling LED
B. Uses delay(), which blocks other code from running
C. Correct logic but may cause overflow issues with addition
D. Uses delay() inside if, causing blocking and incorrect blink

Solution

  1. Step 1: Identify non-blocking timing usage

    Correct non-blocking blink using millis() and toggling LED uses millis() difference and updates previousMillis correctly, toggling LED without delay.
  2. Step 2: Compare other options for blocking or logic issues

    The other snippets either use delay(), which blocks other code from running, or use addition in the condition, which can cause overflow issues with large millisecond values.
  3. Final Answer:

    Correct non-blocking blink using millis() and toggling LED -> Option A
  4. Quick Check:

    Use millis() difference and update previousMillis [OK]
Hint: Toggle LED using millis() difference, avoid delay() [OK]
Common Mistakes:
  • Using delay() causing blocking
  • Not updating previousMillis properly
  • Using addition risking overflow bugs