Time Functions

Lesson 17 - Using millis() for Non-Blocking Timing

Learn how millis() enables timing without pausing your entire program, so multiple tasks can run smoothly.

Progress indicator

Lesson 17 of 17

Learning Objectives

  • Understand what millis() returns and how it is used for timing.
  • Write the basic non-blocking timing pattern with current and previous time.
  • Compare blocking delay() timing with non-blocking millis() timing.
  • Build a blink-without-delay style loop.
  • Handle millis() overflow safely using unsigned subtraction.
  • Design responsive loops that run timed actions and input handling together.

Concept Explanation

What is millis()

millis() returns the number of milliseconds since the board started running.

It gives you a continuously increasing time counter you can compare against.

millis() Syntax

unsigned long now = millis();

Use unsigned long for millis values.

How millis() Works

The value increases in the background while your loop runs. Your code checks elapsed time by subtracting a saved timestamp from the current time.

If enough time has passed, run the timed action and store a new timestamp. This gives regular timing without pausing all other loop work.

Milliseconds Counter Concept

Think of millis() as a stopwatch that starts at power-on and keeps counting.

You do not stop this counter. You only compare values from it.

Blocking vs Non-Blocking Timing

delay() blocks code execution. millis() checks time without pausing the entire loop.

This is why millis()-based code usually feels more responsive when buttons and sensors are read in the same loop.

Basic millis() Timing Pattern

Read current time, compare elapsed time, run action, update previous time.

current = millis();
if (current - previous >= interval) {
  previous = current;
  // timed action
}

Blink Without delay() Concept

You can blink an LED at intervals while still reading buttons, sensors, or serial input in the same loop.

When to Use millis()

  • Projects with multiple actions running at different intervals
  • Responsive button/sensor reading while timed tasks run
  • Any sketch where delay() makes behavior feel slow

Handling millis() Overflow

millis() eventually wraps around. Use subtraction like currentMillis - previousMillis >= interval with unsigned values. This pattern remains safe across overflow.

Avoid direct comparisons like currentMillis > previousMillis + intervalbecause those can fail near overflow.

Example Code

This example blinks an LED using millis() without blocking the loop.

const int ledPin = 2;
unsigned long previousMillis = 0;
const unsigned long interval = 500;
bool ledState = false;

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

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

  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;
    ledState = !ledState;
    digitalWrite(ledPin, ledState ? HIGH : LOW);
  }

  // other code can run here without waiting
}

What this example teaches

  • Non-blocking LED blink pattern using elapsed time
  • State toggling with ledState = !ledState
  • Why previousMillis must be updated only when action runs

Example Code Explanation

  1. previousMillis stores the last time the LED toggled.
  2. interval sets how often the toggle should happen.
  3. Each loop reads currentMillis = millis().
  4. If elapsed time reaches interval, the LED toggles and timestamp updates.
  5. Loop stays free for other tasks because there is no blocking delay.

Beginner reading tip

Focus on four variables first: currentMillis, previousMillis,interval, and ledState. Together they control timing logic.

Common Mistakes with millis()

  • Using int instead of unsigned long for millis values.
  • Resetting previous time incorrectly every loop.
  • Comparing absolute millis values without subtraction.
  • Updating previousMillis every loop (this prevents interval triggers).

Best Practices for millis()

  • Use unsigned long for time variables.
  • Use subtraction-based elapsed-time checks.
  • Keep loop responsive by avoiding unnecessary delays.
  • Use clear names like lastBlinkMs, readIntervalMs.

Practice Task

  1. Change interval from 500 ms to 250 ms and describe LED behavior.
  2. Add a second timed task that prints to Serial every 1000 ms.
  3. Explain why both tasks can run together without delay().
  4. Write one sentence on how subtraction-based checks handle overflow safely.

Try it now

Open simulator workspace and compare non-blocking timing behavior with delay-based logic.

Run in Simulator