Learn

Arduino millis(): timers, and several things at once

Start building free

Or see every lesson.

  • 455parts on the bench
  • 43boards running now
  • 1.00xreal time, on every board
Arduino Uno: three lamps at three rates, no delay()live0.000 s 0.00x
Press Run and watch the three lamps
Three timers in one loop, each checked against millis() on every pass.

millis() returns how many milliseconds have passed since the board started, as an unsigned long. It does not wait for anything. To do something every second, remember when you last did it, and each time around loop() ask whether a second has gone by: if (millis() - previous >= 1000). That one line is how an Arduino does several things at once.

The circuit above is the code bench with three lamps. Press Run. The red lamp flips every 200 ms, the yellow every 330 ms and the green every 1100 ms, all from one loop() with no delay() in it. The serial monitor prints "worst pass through loop: 2 ms" every two seconds: no pass of loop() ever took longer than that, so every lamp is on time.

Below is what millis() returns, the timer pattern, a sketch running three tasks at once, millis() against delay(), and the mistakes that make a timer drift, stop or go negative, each run on an Uno.

Want the step-by-step version? The lesson "Ask the time, do not wait for it" walks through this with checkpoints.

Open the lesson

What millis() returns

A count of milliseconds since power-up or reset, starting at 0. This sketch prints it once a second and printed 0, 999, 1999, 3000 and 3999. The count is 0 at the start of setup() because the board starts from nothing every time.

It is an unsigned long, 32 bits, so it counts up to 4294967295 and then wraps back to 0, after about 49.7 days. The millis() overflow page shows why the subtraction in the timer pattern survives the wrap.

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

void loop() {
  Serial.println(millis());
  delay(1000);
}

A millis() timer

Here is the pattern. previous holds when the timer last fired. Each pass, now - previous >= INTERVAL asks whether the interval has gone by, and if so the sketch does its job and moves previous on. The delay(7) stands in for the rest of a real sketch, which takes time too.

It printed "tick at 1003", "tick at 2001", "tick at 3006", "tick at 4003" and "tick at 5001". Each tick can be a few milliseconds late, because the loop only looks every 7 ms, but the lateness does not add up: the fifth tick is still at 5001.

A timer every second
const unsigned long INTERVAL = 1000;
unsigned long previous = 0;

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

void loop() {
  unsigned long now = millis();
  if (now - previous >= INTERVAL) {
    previous += INTERVAL;
    Serial.print("tick at ");
    Serial.println(now);
  }
  delay(7);  // stands in for the rest of a busy loop
}

Several tasks at once

Each task gets its own timer, and loop() checks them all on every pass. This sketch blinks pin 13 every 250 ms, flips a lamp on pin 12 each time the button is pressed, and prints a report every second, all at the same time.

We pressed the button three times, a quick 150 ms tap each. It printed "1 s, presses: 1", "2 s, presses: 3" and "3 s, presses: 3", the lamp on pin 12 changed at every tap, and the blink on pin 13 never missed a beat. Add a fourth task the same way: one more timer, one more if.

Three tasks, one loop
const int BLINK_LED = 13;
const int LAMP = 12;
const int BUTTON = 2;

unsigned long lastBlink = 0;
unsigned long lastChange = 0;
unsigned long lastReport = 0;
int lastReading = HIGH;
bool blinkOn = false;
bool lampOn = false;
int presses = 0;

void setup() {
  Serial.begin(9600);
  pinMode(BLINK_LED, OUTPUT);
  pinMode(LAMP, OUTPUT);
  pinMode(BUTTON, INPUT_PULLUP);
}

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

  // Task 1: blink pin 13 every 250 ms.
  if (now - lastBlink >= 250) {
    lastBlink = now;
    blinkOn = !blinkOn;
    digitalWrite(BLINK_LED, blinkOn ? HIGH : LOW);
  }

  // Task 2: each press flips the lamp on pin 12.
  int reading = digitalRead(BUTTON);
  if (reading != lastReading && now - lastChange > 50) {
    lastChange = now;
    lastReading = reading;
    if (reading == LOW) {
      presses++;
      lampOn = !lampOn;
      digitalWrite(LAMP, lampOn ? HIGH : LOW);
    }
  }

  // Task 3: report once a second.
  if (now - lastReport >= 1000) {
    lastReport = now;
    Serial.print(now / 1000);
    Serial.print(" s, presses: ");
    Serial.println(presses);
  }
}

millis() against delay()

Here is the same blink and button written with delay(). The sketch spends all its time inside delay(), so it only reads the button for a moment once a second. The same three 150 ms taps printed "pressed" once. Two of the three came and went while the sketch was waiting.

delay() is fine when the board has one thing to do. As soon as it has two, switch to millis().

The same, with delay()
const int BUTTON = 2;

void setup() {
  Serial.begin(9600);
  pinMode(13, OUTPUT);
  pinMode(BUTTON, INPUT_PULLUP);
}

void loop() {
  digitalWrite(13, HIGH);
  delay(500);
  digitalWrite(13, LOW);
  delay(500);
  if (digitalRead(BUTTON) == LOW) {
    Serial.println("pressed");
  }
}

Why millis() sometimes jumps by 2

The timer behind millis() ticks every 1024 µs, not every 1000. So millis() falls behind by 24 µs a tick, and every 41 or 42 ticks it catches up by adding 2 at once. This sketch watches for it in the first 200 ms and printed "41 -> 43", "84 -> 86", "126 -> 128" and "169 -> 171". A real Uno does exactly the same.

That is why the code bench above can report a worst pass of 2 ms: a pass that lands on a jump sees the clock move by 2. It is also why a timer should test >= and never ==. A test for exactly 42 would never be true.

Catch millis() jumping
void setup() {
  Serial.begin(9600);
  unsigned long last = millis();
  while (millis() < 200) {
    unsigned long now = millis();
    if (now - last > 1) {
      Serial.print(last);
      Serial.print(" -> ");
      Serial.println(now);
    }
    last = now;
  }
}

void loop() {
}

Common mistakes

Storing millis() in an int. An int on an Uno only reaches 32767. The sketch below printed 0, 7999, 16000, 23999, 32000, then -25536 at 40 seconds, and any timer built on it breaks. Use unsigned long.

Setting previous = now instead of previous += INTERVAL. Each tick is a little late, and with = now the lateness adds up: the same timer printed 1003, 2009, 3013, 4018 and 5022. Use += when the ticks must stay on the beat, and = now only when a late tick should push the next one back.

Testing millis() == target. millis() can skip a value, so the test may never be true. Test >= on a difference.

Adding instead of subtracting. if (millis() >= previous + INTERVAL) breaks when millis() wraps; millis() - previous >= INTERVAL does not.

A delay() hidden somewhere else. One delay() in any task holds up all the others.

millis() in an int
void setup() {
  Serial.begin(9600);
}

void loop() {
  int now = millis();  // should be unsigned long
  Serial.println(now);
  delay(8000);
}

Questions

What does millis() do in Arduino?

It returns the number of milliseconds since the board started, as an unsigned long. It does not pause anything, so it is how a sketch keeps time while doing other work.

How do I run multiple tasks with millis()?

Give each task its own unsigned long for when it last ran, and in loop() check each one with if (millis() - last >= interval). Keep delay() out of the loop.

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

delay() stops the sketch until the time is up. millis() only reports the time and lets the sketch carry on. With delay(), three quick button taps here caught one; with millis(), all three.

Why does my millis() timer go negative?

It is stored in an int, which wraps to negative after 32767 ms: -25536 at 40 seconds here. Use unsigned long for millis() and every time worked out from it.

Sources

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.