Using millis() Instead of delay() for Arduino Projects

Arduino, Programming
Share Tweet This! Email

So You Want To Learn How To Use millis() Instead Of delay(), Awesome!

When you first start our coding Arduino, you will no doubt be using a line of code like this:

delay(1000);

The delay function for Arduino programming is a remarkable piece of code, and there are places it needs to be used. However, more often than not, the delay function is being used where it shouldn't be. In this post, we are going to have an in-depth look at the use of delay, and show you how you can replace it with a different way to time Arduino events using the millis() function.

Firstly, Let's Understand Arduino Loops

Before we dive into why and why not use the delay() function. You need to understand how Arduino works, and in particular, what is a loop.

Below is some simple Arduino code from the 'Blink' tutorial. There are two main sections, setup and loop. The setup runs when the Arduino is turned on, and the loop continually loops itself. When all of the code within the loop has run, then it starts at the top of the loop section again.

// the setup function runs once when you press reset or power the board
void setup() {
// initialize digital pin LED_BUILTIN as an output.
pinMode(LED_BUILTIN, OUTPUT);
}

// the loop function runs over and over again forever
void loop() {
digitalWrite(LED_BUILTIN, HIGH); // turn the LED on (HIGH is the voltage level)
delay(1000); // wait for a second
digitalWrite(LED_BUILTIN, LOW); // turn the LED off by making the voltage LOW
delay(1000); // wait for a second
}

The code inside of the loop may be simple or complex, but it always loops. And these loops happen very quickly. Now that you understand how the Arduino code works, we can talk more about using millis() and not delay().

Ad

Why Use millis() Instead of delay()?

I guess the first question you probably have is 'Why?'. Why should I use millis() instead of delay()? That is a great question, and understanding it will give you a better idea of when you can use delay() and when it would be better to use millis().

The delay() function is known as 'Code Blocking'. What it does is pauses the code for a set amount of time. The time it pauses for is indicated within the brackets in milliseconds. EG: delay(1000) will pause the code for 1 second.

Using delay() to pause your code for 1 second is not horrible, and the function itself works well, but when you're in 'Pause' mode, no other code runs as you could imagine. The pause might not have any implications or problems when running a simple program like 'Blink', but you will run into a lot of issues with more complex projects.

The millis() function is nothing like delay(). It helps us time events without pausing the code. The Arduino Reference for millis() says it: Returns the number of milliseconds passed since the Arduino board began running the current program.

When you use millis() to time events instead of delay(), your code keeps on looping and allows it to do other tasks. Continuing to loop is very important if part of your project is 'listening' for events. EG: button presses, or wireless signals. If your code is paused, it can not be listening or calculating anything else.

When To Use millis() Instead of delay()?

I never use delay() for events. I think it is good practice to use millis() for event timing. Even if I am writing a small piece of code like the basic 'Blink' project, I will use millis() to time it, and not delay().

The reason I choose millis() is that I never know when I might want to add the small code into a more significant project, and if I used delay(), the larger project would not work.

However, I do use delay when I notice the Arduino is stuck performing tasks. For example, I recently created the Arduino Traffic Lights Without delay() project and it was not changing light colour status correctly. So, I used the delay() function after a changing colour function just to overcome this bug. Please note: I only delayed for 4 milliseconds, not for any noticeable period. 4 one-thousandths of a second will not generally have any major impact on a project.

How To Use millis() Instead of delay()?

After reading above, we have covered why and when you should use millis(), and now we get to the all-important how. How to use millis() to time your events instead of using delay() to pause your events.

Instead of pausing your program, what you need to do is write a small equation to check if the correct amount of time has passed, if the correct amount of time has passed, do the event, or else keep looping.

Let's rewrite the 'Blink' project, and convert it from using delay() to using millis(). After this code, we will break down what is going on.

int ledState = LOW;
unsigned long ledStarted = 0;
const long interval = 1000;

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

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

if (currentMillis - ledStarted >= interval) {
ledStarted = currentMillis;

if (ledState == LOW) {
ledState = HIGH;
} else {
ledState = LOW;
}

digitalWrite(LED_BUILTIN, ledState);
}
}

Variables

When using millis() we need to introduce a few variables, so let's take a look:

int ledState = LOW;
unsigned long ledStarted = 0;
const long interval = 1000;

First-line defines the state of the LED. We initially set this to 'Low' meaning the LED is not turned on.
Second-line defines the time in millis() that the LED started it's current 'State'. Because these variables are just setup when initialising, we can set it to 0.
Third-line defines the interval in which we want the LED to turn on and off. In the standard 'Blink' project, we used this number in the delay() function, but here we declare it as a variable.

Setup

The setup is the code that runs the first time the Arduino is turned on. Here is what we have inside the setup code:

pinMode(LED_BUILTIN, OUTPUT);

All we are doing is defining a pin as an output. In this case, we used the LED_BUILTIN as per the 'Blink' project.

The Loop

As we know, the loop is the part of the code that runs in a continuous loop, very quickly. Here is what we are doing inside the loop, and let's break it down:

unsigned long currentMillis = millis();

if (currentMillis - ledStarted >= interval) {
ledStarted = currentMillis;
if (ledState == LOW) {
ledState = HIGH;
} else {
ledState = LOW;
}
digitalWrite(LED_BUILTIN, ledState);
}

The first line: This is very important, we are getting the current millis() and storing it inside a variable called currentMillis.

At this point, we have three stored times saved as variables. We know what the current time is (currentMillis), we know when we last switched the LED either on or off (ledStarted) and we know how long we want the LED to stay in its current state (interval).

Using these three variables, we can create the equation to check if it is time to switch it either on or off. Here is the equation:

if (currentMillis - ledStarted >= interval) {
// leaving the content off this for now
}

This equation is an 'if' statement, and every time the loop runs, we check if the equation is true. If it 'is' true, then the code inside runs.

Here is the equation in a human-readable form: If the current time, minus when the LED last changed is greater than or equal to the interval we want than do this.

Let's use some figures in that equation. Let's say we just turned it on, and it has been running for just half a second, remembering that on startup we initiated the ledStarted to 0 millis. This is what the equation will see:

if(500 - 0 >= 1000)

The answer to this equation is false, so the code will not run. Half a second is NOT equal to, or greater than 1 second. The code will then continue to loop, at breakneck speed, and each time it loops, the equation gets tested. Now let's see what happens when the Arduino has been running for 1 second, and it gets to the equation.

if(1000 - 0 >= 1000)

Boom! Success! This statement now returns true. The current time is 1000, and this means that 1000 millis has passed since the LED state was switched. Now we run the code inside of this if statement:

ledStarted = currentMillis;

if (ledState == LOW) {
ledState = HIGH;
} else {
ledState = LOW;
}
digitalWrite(LED_BUILTIN, ledState);

What is this code doing? This code is quite simple and primarily does three things. The first thing it does (first-line) is to save the current time into the variable 'ledStarted'. This is the time in which the LED started it's present on/off state.

The second thing the code does is change the 'ledState' variable to the opposite of what it was. It does this with a couple of if statements. If it was off, make it on, if it was on, make it off.

The third thing to do is to write the state (LOW or HIGH) to the LED using digitalWrite. We write to it using the 'ledState' variable from above:

digitalWrite(LED_BUILTIN, ledState);

The Loop On Repeat

At this point, we actually have it all working, and you probably understand what's going on, but I just wanted to show you the equation in action again, but this time show you what it looks like after the first loop, and after it has already changed the first time.

After the LED changes it's status the first time, there are new variables. Here is what the variables used to be, and what they are after the first time the LED changes:

ledState: was 'LOW' but is now 'HIGH'.
ledStarted: was '0', but is now '1000'. The last time we flicked it on or off.
currentMillis: was 0 when we started, and we override it every single loop of the main code. For our next example, let's assume the machine has been running for 1.6 seconds.

With these new variables, let's take a look at the equation again:

if (currentMillis - ledStarted >= interval)

Here it is with our variables in place:

if(1600 - 1000 >= 1000)

This equation returns false. The current time in milliseconds is 1600, and the last time we switched the LED was at 1000. So, there has not been 1000 millis since we last switched it.

From here, it keeps looping, and every time the equation is true, the code inside executes and changes the state of the LED. After a while, your equation will look like this:

if(25731 - 25000 >= 1000)

In the above equation, the Arduino has been running for 25.731 seconds, and the last time we switched the LED was at 25 seconds. The equation will return false until the current time in millis is 26000.

Things to Keep in Mind

After reading the above, you should now have an understanding of how to use millis() instead of delay(), and the reason for doing so. There are, though, a couple of things to keep in mind.

It is NOT Precise

When you run this code, and if you debug it and print out the exact millisecond that the LED switches, you will notice it is not 100% accurate. The LED will not switch exactly on 1000, 2000, 3000, 4000, 5000, and so on. This is the reason we use >= (Greater than or equal to) in the equation. Sometimes it might take a thousandth of a second to trigger, and your ledStarted variable might save 1001 instead of 1000. You will not notice this in real-time, but something to keep in mind.

millis() Does Not Go On Forever

millis() returns the number of milliseconds the Arduino board has been powered up for. However, there is a limit. This number will overflow (go back to zero), after approximately 50 days.

This limit will not affect most projects, as they are turned on and off well inside the 50-day limit. However, you do need to keep this in mind. Here is an article that will help: Avoiding the millis() Overflow Issue.

We Hope This Helped

Hopefully, this article has helped you convert over from delay() to millis() and helps you to write even better code for projects that stand the test of time.

If you found this article useful, please share it with your friends, colleagues, family or students. It would mean a lot to us. If you have any questions, please comment below. We try to respond to all comments as soon as possible.

Jim
Terrific short and sweet to the point and very understandable.
Reply
User Comment
For more precise timekeeping instead of writing " ledStarted = currentMillis;" use " ledStarted += 1000;". You still have some jitter at each interval, but this makes your long term timekeeping as accurate as the processor crystal.
Reply
STEM Mayhem
This is actually a great tip. For this example, it would work great.

Currently, if the trigger happens at 1004 milliseconds, the next trigger will be looking for 2004 or greater. Over time, this could be out by a lot. Your solution allows for the variance but then sets the next trigger at the last, plus 1000 milliseconds. Thanks for the comment!

Read Next