Embedded Systems
millis() vs delay() on Arduino
delay() stops the whole program on that line until the time is up, so nothing else runs. millis() returns the milliseconds since the board started, so loop() can glance at the clock every pass and do whichever job is due. Use delay() for one simple job; use millis() as soon as a sketch has to do two things at once.
The trouble with delay()
During delay(500) the board waits and does nothing else. Take a blink with two delay(500) lines and one button read per pass. A press just after the read waits through both delays:
A whole second is a long time to wait for a light switch. Two blinking lights fare no better: each delay() holds up the other's.
Live experiment
Two rhythms, two ways
Dot wants 500 ms on and 500 off; Pip wants 200 and 200. Run them with delay() and each waits for the other. Switch the program to millis() and each keeps its own rhythm.
“Timing without delay” is lesson 4 of 6 in The Core, Electronics World 8. Free with an account. Lessons unlock in order, so you’ll do the 52 before it first.
Glance at the clock
millis() counts milliseconds, 1000 a second: 12000 means 12 s. Note when a state began, then check how long it has lasted:
Times grow big, so store them in an unsigned long, a whole number from 0 to about 4.3 billion. This heartbeat is on for 100 ms and off for 900 ms, with no delay() at all:
unsigned long since = 0; // when this state began
bool on = false;
void setup() {
pinMode(9, OUTPUT);
}
void loop() {
unsigned long t = millis() - since;
if (on && t >= 100) {
on = false;
since = since + 100;
digitalWrite(9, LOW);
} else if (!on && t >= 900) {
on = true;
since = since + 900;
digitalWrite(9, HIGH);
}
}
Many jobs, one loop
Nothing in that loop() waits, so it runs thousands of times a second. To add another job, add more lines: read a button, check a sensor, blink a second light with its own since. Each pass does a little of every job. It still does one thing at a time; it just switches between them so often that they all seem to happen at once.
Rollover
millis() is a 32-bit counter, so it rolls over to 0 after 2³² ms, about 49.7 days. Unsigned subtraction wraps the same way, so millis() - since still gives the right gap across a rollover. Compare elapsed time with the interval, as above, rather than comparing millis() with a future deadline, and use unsigned long throughout.
Free lesson
Now build it yourself
Each lesson comes in three short parts: see the idea, try it on a live board, then build it yourself, with quick checks as you go.
“Timing without delay” is lesson 4 of 6 in The Core, Electronics World 8. Free with an account. Lessons unlock in order, so you’ll do the 52 before it first.
Or start from the very first lesson, no account neededPractice questions
Three quick questions from the lessons. Pick an answer to check it.
Show the answer
Answer: does nothing else. delay() stops the program on that line until the time is up. Nothing else runs, so a press in that half second goes unnoticed.
- Why not “keeps reading the button”? delay() holds the program on that one line, so no other line runs, including the one that reads the button. A press during the wait is missed.
- Why not “runs loop() twice”? loop() can't start again until delay() returns. The board sits on that line for the whole 500 ms.
Show the answer
Answer: 20–50 ms. The contacts chatter for a few milliseconds, so 20–50 ms outlasts the bounce, yet is still far quicker than a person can press again.
- Why not “a microsecond”? Bounce lasts a few milliseconds, thousands of times longer than a microsecond, so the chatter would still count as extra presses.
- Why not “a whole second”? A second is far longer than any bounce, and a person can press twice in that time, so real presses would be missed.
Show the answer
Answer: 3. Each HIGH → LOW edge looks like a new press, so the count jumps by 3. Ignoring the pin for 50 ms after the first edge makes it count 1.
- Why not “0”? Every falling edge looks like a press to the program, so a bouncing button is counted more than once, not zero times.
- Why not “1”? One is what a debounced counter adds. Without debouncing, each of the three falling edges counts as a separate press.
“Timing without delay” is lesson 4 of 6 in The Core, Electronics World 8. Free with an account. Lessons unlock in order, so you’ll do the 52 before it first.
Questions people ask
Why is delay() bad in Arduino?
During delay() your sketch does nothing but wait on that line (only background interrupts run). No button is read and no other job runs, so a press that comes and goes meanwhile is missed. It's fine for a sketch with one job; it gets in the way as soon as there are two.
How do I use millis() instead of delay()?
Store the time a state began in an unsigned long. Each pass of loop(), check whether millis() − since has reached your interval; if it has, do the job and move since on by the interval. loop() never waits, so it can check other jobs too.
What happens when millis() overflows?
millis() is a 32-bit counter, so it rolls over to 0 after 2³² ms, about 49.7 days. Unsigned subtraction wraps the same way, so millis() − since still gives the right gap across the rollover, as long as both are unsigned long.
Is millis() accurate?
It counts from the board's crystal or resonator, close enough for blinking, debouncing and most timing. Your loop checks a little after each deadline, so add the interval to since rather than setting since to millis(): that keeps the rhythm from slowly drifting.