Bird
Raised Fist0
Arduinoprogramming~15 mins

Multiple timed events with millis() 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 - Multiple timed events with millis()
What is it?
In Arduino programming, millis() is a function that returns the number of milliseconds since the board started running. Using millis(), you can track time without stopping your program. Multiple timed events with millis() means managing several actions that happen at different times without pausing the whole program. This helps your Arduino do many things at once smoothly.
Why it matters
Without millis(), programmers often use delay(), which stops everything and makes the Arduino wait. This means the board can’t do other tasks during the wait, like reading sensors or controlling motors. Using multiple timed events with millis() lets your Arduino handle many tasks at once, making projects more responsive and efficient. Imagine a robot that can move, blink lights, and listen to commands all at the same time.
Where it fits
Before learning this, you should understand basic Arduino programming, how to write simple loops, and what functions like delay() do. After mastering multiple timed events with millis(), you can move on to more advanced topics like state machines, interrupts, or using real-time clocks for precise timing.
Mental Model
Core Idea
Using millis() lets you check the clock without stopping your program, so you can run many timed tasks side by side smoothly.
Think of it like...
It’s like having a stopwatch that you glance at to see if enough time has passed to do something, instead of stopping everything to wait.
┌─────────────────────────────┐
│ Arduino Program Loop        │
│ ┌─────────────────────────┐ │
│ │ Check millis() time      │ │
│ │ Compare with last event  │ │
│ │ If time passed → do task │ │
│ └─────────────────────────┘ │
│ Continue running other code │
└─────────────────────────────┘
Build-Up - 7 Steps
1
FoundationUnderstanding millis() basics
🤔
Concept: Learn what millis() does and how it counts time since the Arduino started.
millis() returns an unsigned long number showing milliseconds since the Arduino powered on. It keeps counting up and resets after about 50 days. You can use it to measure how much time has passed by saving a start time and subtracting it from the current millis() value.
Result
You can measure elapsed time without stopping your program.
Understanding millis() as a continuously running clock is the foundation for non-blocking timing.
2
FoundationWhy avoid delay() in Arduino
🤔
Concept: Learn why delay() stops your program and why that’s a problem for multiple tasks.
delay() pauses the whole program for a set time. During delay(), the Arduino can’t check sensors, update displays, or respond to inputs. This makes your project slow or unresponsive if you want to do many things.
Result
You see that delay() blocks all other actions, limiting multitasking.
Knowing delay() blocks helps you appreciate why millis() timing is better for multitasking.
3
IntermediateSingle timed event with millis()
🤔Before reading on: do you think you must reset millis() to start timing? Commit to your answer.
Concept: Use millis() to run one task after a set interval without stopping the program.
Save the current millis() in a variable called previousMillis. In each loop, check if (millis() - previousMillis) is greater than your interval. If yes, run your task and update previousMillis to the current millis(). This way, the task runs repeatedly at intervals without delay().
Result
Your task runs repeatedly every interval while the program keeps running other code.
Understanding how to compare current millis() with a saved time unlocks timed actions without blocking.
4
IntermediateManaging multiple timed events
🤔Before reading on: do you think one variable can track multiple timed events? Commit to your answer.
Concept: Track separate previousMillis variables for each event to run many timed tasks independently.
For each timed event, create its own previousMillis and interval variables. In the loop, check each event’s elapsed time separately. When an event’s interval passes, run its task and update its previousMillis. This lets multiple events run at different speeds without blocking each other.
Result
Multiple tasks run at their own intervals smoothly and independently.
Knowing to track each event’s timing separately is key to managing multiple timers.
5
IntermediateHandling millis() rollover safely
🤔Before reading on: do you think millis() rollover breaks timing calculations? Commit to your answer.
Concept: Use subtraction of unsigned longs to handle millis() rollover without errors.
millis() resets to zero after about 50 days. But subtracting unsigned longs like (millis() - previousMillis) still works correctly because of how numbers wrap around in Arduino. This means your timing code keeps working even after rollover.
Result
Your timed events continue working correctly even after millis() resets.
Understanding unsigned math prevents bugs from millis() rollover in long-running projects.
6
AdvancedStructuring timed events with arrays
🤔Before reading on: do you think arrays can simplify managing many timed events? Commit to your answer.
Concept: Use arrays to store previousMillis and intervals for many events, making code cleaner and scalable.
Create arrays to hold previousMillis and interval values for each event. Loop through these arrays in your main loop to check and run each event. This reduces repetitive code and makes it easier to add or remove timed events.
Result
Your code becomes easier to manage and extend for many timed events.
Knowing how to organize timers in arrays helps scale projects with many timed tasks.
7
ExpertAvoiding timing drift with millis()
🤔Before reading on: do you think updating previousMillis after task runs causes timing drift? Commit to your answer.
Concept: Update previousMillis by adding the interval instead of setting to current millis() to keep precise timing.
If you set previousMillis = millis() after each event, small delays in code execution add up, causing drift. Instead, add the interval to previousMillis (previousMillis += interval). This keeps events running on a steady schedule, even if some loops take longer.
Result
Timed events stay accurate over long periods without drifting.
Understanding timing drift and how to prevent it is crucial for precise, reliable timed events.
Under the Hood
millis() reads a hardware timer that counts milliseconds since the Arduino started. This timer runs independently of your code and increments a counter in the background. When you call millis(), it returns the current count. By saving previous counts and subtracting, you measure elapsed time without stopping the program. The timer uses an unsigned 32-bit integer, which rolls over to zero after about 50 days, but subtraction still works correctly due to unsigned arithmetic.
Why designed this way?
Arduino’s designers wanted a simple way to track time without blocking code execution. delay() was easy but limited. millis() provides a non-blocking timer using hardware counters, allowing multitasking. Using unsigned math for rollover handling avoids complex checks and keeps timing code simple and efficient.
┌───────────────┐
│ Hardware Timer│
│ (counts ms)   │
└──────┬────────┘
       │
       ▼
┌───────────────┐
│ millis() call │
│ returns count │
└──────┬────────┘
       │
       ▼
┌───────────────────────────────┐
│ User code subtracts previous   │
│ millis() value to get elapsed  │
│ time and decides if event runs │
└───────────────────────────────┘
Myth Busters - 4 Common Misconceptions
Quick: Does using millis() mean your program never stops or waits? Commit yes or no.
Common Belief:Using millis() means your Arduino never pauses or waits at all.
Tap to reveal reality
Reality:millis() itself doesn’t pause the program, but your code can still block if you use delay() or long loops. millis() just helps you avoid blocking by checking time instead of waiting.
Why it matters:Thinking millis() alone prevents all pauses can lead to mixing delay() and blocking code, causing unresponsive programs.
Quick: Can you use one previousMillis variable to time multiple events correctly? Commit yes or no.
Common Belief:One previousMillis variable is enough to manage all timed events.
Tap to reveal reality
Reality:Each timed event needs its own previousMillis variable to track its timing independently. Sharing one causes events to interfere and run incorrectly.
Why it matters:Using one variable for many events causes timing bugs and unpredictable behavior.
Quick: Does millis() rollover break timing calculations? Commit yes or no.
Common Belief:When millis() resets to zero, your timing calculations break and events stop working.
Tap to reveal reality
Reality:Because of unsigned subtraction, timing calculations continue to work correctly even after rollover without extra code.
Why it matters:Worrying about rollover unnecessarily complicates code and can cause premature fixes that introduce bugs.
Quick: Does setting previousMillis = millis() after each event keep timing perfectly accurate? Commit yes or no.
Common Belief:Resetting previousMillis to current millis() after each event keeps timing exact.
Tap to reveal reality
Reality:This causes timing drift because small delays add up. Adding the interval to previousMillis keeps timing steady.
Why it matters:Ignoring drift leads to events slowly shifting out of sync, which can break time-sensitive projects.
Expert Zone
1
When stacking many timed events, the order of checks can affect responsiveness; prioritizing critical events first improves performance.
2
Using unsigned long for all timing variables avoids subtle bugs from integer overflow or signed arithmetic.
3
Combining millis() timing with interrupts requires careful design to avoid conflicts and ensure accurate timing.
When NOT to use
Avoid millis() timing for extremely precise timing needs like microsecond-level control or real-time audio processing; use hardware timers or interrupts instead.
Production Patterns
Professionals use millis() timing in sensor polling, LED animations, motor control, and communication timeouts. They often encapsulate timers in classes or structs for cleaner code and use arrays or linked lists to manage many timers dynamically.
Connections
Event Loop (JavaScript)
Both use non-blocking timing to handle multiple tasks smoothly.
Understanding millis() timing helps grasp how event loops schedule tasks without stopping the whole program.
Real-Time Operating Systems (RTOS)
millis() timing is a simple form of task scheduling like RTOS timers but without full multitasking support.
Knowing millis() timing builds intuition for how RTOS manage multiple timed tasks with priorities and preemption.
Project Management Deadlines
Both involve tracking multiple deadlines independently to keep a project on schedule.
Managing multiple timed events in code is like juggling deadlines in real life; each needs its own tracking to avoid conflicts.
Common Pitfalls
#1Using delay() inside loop with millis() timing.
Wrong approach:void loop() { if (millis() - previousMillis >= interval) { delay(1000); // blocks everything // do task previousMillis = millis(); } }
Correct approach:void loop() { if (millis() - previousMillis >= interval) { // do task previousMillis = millis(); } // no delay, other code runs }
Root cause:Confusing millis() timing with delay(), forgetting delay() stops all code execution.
#2Using one previousMillis variable for multiple events.
Wrong approach:unsigned long previousMillis = 0; void loop() { if (millis() - previousMillis >= interval1) { // event 1 previousMillis = millis(); } if (millis() - previousMillis >= interval2) { // event 2 previousMillis = millis(); } }
Correct approach:unsigned long previousMillis1 = 0; unsigned long previousMillis2 = 0; void loop() { if (millis() - previousMillis1 >= interval1) { // event 1 previousMillis1 = millis(); } if (millis() - previousMillis2 >= interval2) { // event 2 previousMillis2 = millis(); } }
Root cause:Not realizing each event needs its own timer variable to track elapsed time independently.
#3Resetting previousMillis to millis() causing drift.
Wrong approach:if (millis() - previousMillis >= interval) { // do task previousMillis = millis(); // causes drift }
Correct approach:if (millis() - previousMillis >= interval) { // do task previousMillis += interval; // prevents drift }
Root cause:Not understanding how small delays accumulate and shift timing over many cycles.
Key Takeaways
millis() provides a non-blocking way to track elapsed time by counting milliseconds since Arduino start.
Using multiple previousMillis variables lets you manage many timed events independently and smoothly.
Avoid delay() to keep your Arduino responsive and able to handle multiple tasks at once.
Handling millis() rollover with unsigned subtraction keeps timing reliable even after long runtimes.
Prevent timing drift by adding intervals to previousMillis instead of resetting it to current millis().

Practice

(1/5)
1. What is the main advantage of using millis() for timing multiple events in Arduino instead of delay()?
easy
A. It resets the Arduino automatically.
B. It stops the program until the time passes.
C. It makes the program run slower.
D. It allows the program to run other tasks while waiting.

Solution

  1. Step 1: Understand delay() behavior

    delay() pauses the whole program, stopping all other actions.
  2. Step 2: Understand millis() advantage

    millis() returns elapsed time without stopping the program, so other tasks can run simultaneously.
  3. Final Answer:

    It allows the program to run other tasks while waiting. -> Option D
  4. Quick Check:

    millis() enables multitasking [OK]
Hint: Remember: delay() stops, millis() doesn't [OK]
Common Mistakes:
  • Thinking millis() pauses the program
  • Confusing delay() with non-blocking timing
  • Believing millis() resets Arduino
2. Which of the following is the correct way to declare a variable to store the last time an event occurred using millis()?
easy
A. unsigned long lastEventTime = 0;
B. int lastEventTime = 0;
C. float lastEventTime = 0.0;
D. char lastEventTime = '0';

Solution

  1. Step 1: Identify the correct data type for time

    Since millis() returns an unsigned long value, the variable must be unsigned long.
  2. Step 2: Check variable initialization

    Initializing to 0 is correct to mark the start time.
  3. Final Answer:

    unsigned long lastEventTime = 0; -> Option A
  4. Quick Check:

    Use unsigned long for millis() times [OK]
Hint: Use unsigned long for time variables with millis() [OK]
Common Mistakes:
  • Using int which can overflow quickly
  • Using float which is not precise for time
  • Using char which is for characters, not numbers
3. What will be the output of the following Arduino code snippet if millis() returns 5000 at the moment of checking?
unsigned long previousMillis = 3000;
unsigned long interval = 1500;

if (millis() - previousMillis >= interval) {
  Serial.println("Event triggered");
} else {
  Serial.println("Waiting");
}
medium
A. Waiting
B. No output
C. Event triggered
D. Compilation error

Solution

  1. Step 1: Calculate elapsed time

    Elapsed time = 5000 (current millis) - 3000 (previousMillis) = 2000 ms.
  2. Step 2: Compare elapsed time with interval

    Interval is 1500 ms. Since 2000 >= 1500, the condition is true, so "Event triggered" should print.
  3. Final Answer:

    Event triggered -> Option C
  4. Quick Check:

    Elapsed time 2000 >= 1500 triggers event [OK]
Hint: Subtract previousMillis from millis() to check elapsed time [OK]
Common Mistakes:
  • Mixing up >= and > operators
  • Forgetting to subtract previousMillis
  • Assuming output without calculation
4. Identify the error in this code snippet that tries to blink two LEDs at different intervals using millis():
unsigned long previousMillis1 = 0;
unsigned long previousMillis2 = 0;
const long interval1 = 1000;
const long interval2 = 2000;

void loop() {
  if (millis() - previousMillis1 >= interval1) {
    digitalWrite(LED1, !digitalRead(LED1));
    previousMillis1 = millis();
  }
  if (millis() - previousMillis1 >= interval2) {
    digitalWrite(LED2, !digitalRead(LED2));
    previousMillis2 = millis();
  }
}
medium
A. LED pins are not defined.
B. Second if condition uses previousMillis1 instead of previousMillis2.
C. Intervals should be unsigned long, not long.
D. digitalWrite cannot toggle LEDs.

Solution

  1. Step 1: Check timing variables in conditions

    The second if condition incorrectly uses previousMillis1 instead of previousMillis2, causing wrong timing for LED2.
  2. Step 2: Understand impact of error

    This mistake means LED2 timing depends on LED1 timing, so LED2 won't blink at its own interval.
  3. Final Answer:

    Second if condition uses previousMillis1 instead of previousMillis2. -> Option B
  4. Quick Check:

    Each event needs its own previousMillis variable [OK]
Hint: Use separate previousMillis for each timed event [OK]
Common Mistakes:
  • Reusing the same previousMillis variable for multiple events
  • Not updating previousMillis after event
  • Confusing interval variables
5. You want to control three LEDs blinking at 500ms, 1000ms, and 1500ms intervals respectively without using delay(). Which approach correctly manages all three timed events using millis()?
hard
A. Use three separate previousMillis variables and check each with its own interval inside loop().
B. Use one previousMillis variable and reset it after each LED toggles.
C. Use delay(500) and toggle all LEDs together.
D. Use millis() only once and toggle LEDs based on dividing millis() by intervals.

Solution

  1. Step 1: Understand independent timing needs

    Each LED needs its own timer to blink independently at different intervals.
  2. Step 2: Evaluate options

    Use three separate previousMillis variables and check each with its own interval inside loop(), allowing independent timing without blocking.
  3. Final Answer:

    Use three separate previousMillis variables and check each with its own interval inside loop(). -> Option A
  4. Quick Check:

    Separate timers for each event [OK]
Hint: Assign each event its own timer variable for independent control [OK]
Common Mistakes:
  • Using one timer for all events causing sync issues
  • Using delay which blocks other events
  • Trying to calculate toggles from one millis() value without state