Millis() instead of delay and loop() instead of for-loop

One of the most frequently asked questions by beginners is how to handle delays and for-loops in loop() without blocking other functions.

In this thread I like to publish some small sketches do demonstrate the use of millis() instead of delay() and how to implement the functionality of a for-loop in loop() without blocking.

The examples are ment just to improve understanding of the methods not to solve specific problems. They may not even be "best practice" in all aspects ...

Improvements and amendments are welcome; the purpose is only to support beginners.

P.S.: I like also to point out that there is already a comprehensive tutorial on millis() made by
@UKHeliBob which I recommend to beginners!

Another very well documented tutorial has been provided by @LarryD on Feb 2023:

2 Likes

So here is the first set of examples that can be checked on Wokwi:

Short Millis() Function Example

Selfmade For Loop I (Explaining how to create your "own" for loop in loop() )

Selfmade For Loop II (Demonstration why for-loop with delay() usually blocks loop() )

Selfmade For Loop III (showing the use of the Selfmade For Loop to achieve a non-blocking loop())

Selfmade For-Loop IV (like III but with separate functions to increase understanding and maintainability)

1 Like

Here the sketches behind the Wokwi links:

Millis() Function Example

/*
   ============================
   Millis() Function Example
  ============================
   
   This is an example of a non-blocking millis() function replacing delay() 

   First Edition 2023-04-02
   ec2021

   Simulation: https://wokwi.com/projects/360903822433974273
   Discussion: https://forum.arduino.cc/t/millis-instead-of-delay-and-loop-instead-of-for-loop/1110044/7

   Changed 2023-07-26 

*/

constexpr byte LEDPin = 13;

void setup() {
   Serial.begin(115200);
   pinMode(LEDPin, OUTPUT);
}

unsigned long lastChange = 0;    // Variable to store the time in ms when ledState was changed 
unsigned long delayTime  = 300;  // Delay in [ms] when then next change shall take place 
boolean firstTime = true;        // Boolean that ensures that the if-clause will be performed immediately
                                 // in the first call

void loop() {

  if (millis()-lastChange >= delayTime || firstTime){ 
                                         // As "firstTime" has been set to true when the sketch started
                                         // it will definitely perform what's inside the curly brackets
                                         // immediately in the first loop()
                                         // The greater in ">="" is required as we cannot always be sure
                                         // that this comparison is performed exactly "delayTime" later ...

    lastChange = millis();               // Stores this time so that it takes in mininimum delayTime for
                                         // the next entry to this bracket procedures  

    boolean ledState = digitalRead(LEDPin);  // Read the actual status of the ledPin, so we do not have
                                             // to memorize and handle it in a variable

    digitalWrite(LEDPin, !ledState);         // Change the status by inverting ledState with the ! operator
                                             // !ledState provides the opposite of ledState

    firstTime = false;                       // "firstTime" is set to false to make sure    
                                             // that in future the entry into the if-clause only depends
                                             // on the comparison of the time difference between "millis()"
                                             // and "lastChange" with "delayTime"
   }
 
}


This is a flowchart of the millis() function example:

  • The statements marked grey are only performed once after start of the sketch.
  • The green parts are performed every loop().
  • The blue path is performed when the condition in the "diamant" symbol is true otherwise the sketch follows the yellow path.

So most of the loops the sketch follows the green and yellow path. Only on the first occasion and then every 300 msec the blue statements are performed.

Selfmade For-Loop I

/*
  ============================
   Selfmade For-Loop I
  ============================

   This is an example of a "selfmade" for-loop
   which is usually required where a a non-blocking
   loop is required

   First Edition 2023-04-02
   ec2021

   Simulation: https://wokwi.com/projects/360901842970872833
   Discussion: https://forum.arduino.cc/t/millis-instead-of-delay-and-loop-instead-of-for-loop/1110044

  Changed 2023-07-26

*/

void setup() {
   Serial.begin(115200);

   // Standard for-loop
   Serial.println("Standard for-loop");
   for (int i = 0; i < 10;i++){
     Serial.print(i);
     Serial.print("\t"); // prints tabs
   }
   Serial.println();
   Serial.println("Selfmade for-loop");
}

// Selfmade for-loop with the same result as the Standard for-loop coded in setup()

// The next line replaces the (int i = 0) in for (int i = 0; i < 10;i++)
 //                                               ---------
int i = 0;

void loop() {
   // This replaces the condition in  for (int i = 0; i < 10;i++)
   //                                                 ------
   if (i < 10) { 
     Serial.print(i);
     Serial.print("\t"); // prints tabs
     i++;
   }
}


Selfmade For-Loop II

/*
  ============================
   Selfmade For-Loop II
  ============================

   This is an example of a "selfmade" for-loop
   which is usually required where a a non-blocking
   loop is required e.g. to blink an LED

   This is an example of 

   a non-blocking millis() function to blink a LED 
   but counting a variable in a for loop with delay()

   The LED will not blink in the correct timing as the for-loop incl. 
   the delay() blocks loop()

   First Edition 2023-04-02
   ec2021

   Simulation: https://wokwi.com/projects/360902704451617793
   Discussion: https://forum.arduino.cc/t/millis-instead-of-delay-and-loop-instead-of-for-loop/1110044/3

   Changed 2023-07-26


*/

constexpr byte LEDPin = 13;

void setup() {
   Serial.begin(115200);
   pinMode(LEDPin, OUTPUT);
}

unsigned long lastChange = 0;    // Variable to store the time in ms when ledState was changed 
unsigned long delayTime  = 300;  // Delay in [ms] when then next change shall take place 
unsigned long delayCount = 500;

int iMax = 9;

void loop() {

  if (millis()-lastChange >= delayTime){ 
     lastChange = millis();               
     boolean ledState = digitalRead(LEDPin);
     digitalWrite(LEDPin, !ledState); 
   }

  for (int i = 0; i <= iMax;i++){
      Serial.print(i);
      Serial.print("\t"); // prints tabs
      delay(delayCount);  
      if (i == iMax) Serial.println();
   }
}   

This is the flowchart of Selfmade For-Loop II (if you watch carefully you will see that the for-loop is broken up into an initialization, an incrementation statement and the condition statement that ends the for-loop):

While the green and blue parts are performed very quickly, the red loop contains a delay of 500 msec and is performed 10 times (iMax+1 times). Therefore the red loop consumes 10 x 0.5 s = 5 sec (plus a little bit for the other functions inside the red loop which can be neglected if compared with the delay).

That is the reason why the green if condition is only checked about every 5 sec. This is a far greater interval than the intended 0.3 sec to toggle the led state. Obviously the delay() function makes it impossible to combine the two functionalities in a sequence. The function delay() has to be replaced by a different method ...

A possible solution is to use a second millis() function to control the timing of the counting which leads to the next example:

Selfmade For-Loop III

/*
  ============================
   Selfmade For-Loop III
  ============================

   This is an example of a "selfmade" for-loop
   which is usually required where a a non-blocking
   loop is required e.g. to blink an LED

   This is an example of a non-blocking millis() function replacing delay() 
   to blink a LED and counting a variable in given time intervals 

   First Edition 2023-04-02
   ec2021

   Simulation: https://wokwi.com/projects/360905127753897985
   Discussion: https://forum.arduino.cc/t/millis-instead-of-delay-and-loop-instead-of-for-loop/1110044

   Changed 2023-07-26

*/

constexpr byte LEDPin = 13;

void setup() {
   Serial.begin(115200);
   pinMode(LEDPin, OUTPUT);
}

unsigned long lastChange = 0;    // Variable to store the time in ms when ledState was changed 
unsigned long delayTime  = 300;  // Delay in [ms] when then next change shall take place 
unsigned long lastCount  = 0;
unsigned long delayCount = 500;

boolean firstBlink       = true;  // Added firstBlink and firstCount to make sure
boolean firstCount       = true;  // that the millis()-if-clauses are already entered 
                                  // in the first loop()

int i = 0;
int iMax = 9;

void loop() {

  if (millis()-lastChange >= delayTime || firstBlink){ 
     lastChange = millis();               
     boolean ledState = digitalRead(LEDPin);
     digitalWrite(LEDPin, !ledState); 
     firstBlink = false;
   }

  if (millis()-lastCount >= delayCount || firstCount){ 
     lastCount = millis();
     firstCount = false;
     if (i <= iMax) {                                                            
         Serial.print(i);
         Serial.print("\t"); // prints tabs
         i++;
      } else { 
         Serial.println();
         i = 0;
      }
   }
}   




Selfmade For-Loop IV

/*
  ============================
   Selfmade For-Loop IV
  ============================

   This is an example of a "selfmade" for-loop
   which is usually required where a a non-blocking
   loop is required e.g. to blink an LED

   This is an example of a non-blocking millis() function replacing delay() 
   to blink a LED and counting a variable in given time intervals 

   It is more or less identical to Selfmade For-Loop III but
   the handling of the LED and the counting are taken out of loop()
   into separate functions to "reduce complexity"; it is easier to understand 
   and test functions which are short and at least do not require scrolling
   of the screen 

   First Edition 2023-04-02
   ec2021

   Simulation: https://wokwi.com/projects/360905879808368641
   Discussion: https://forum.arduino.cc/t/millis-instead-of-delay-and-loop-instead-of-for-loop/1110044

   Changed 2023-07-26

*/

constexpr byte LEDPin = 13;
constexpr int MaxCount = 9;

void setup() {
   Serial.begin(115200);
   pinMode(LEDPin, OUTPUT);
}


void loop() {
  handleLED();
  doCountingUpTo(MaxCount);
}   

void handleLED(){
  static unsigned long lastChange = 0;    // Variable to store the time in ms when ledState was changed 
  static unsigned long delayTime  = 300;  // Delay in [ms] when then next change shall take place 
  static boolean firstChange      = true; // To make sure that the if-clause is entered in the first call
                                          // of this function
  if (millis()-lastChange >= delayTime || firstChange){ 
     lastChange = millis();               
     firstChange = false;
     boolean ledState = digitalRead(LEDPin);
     digitalWrite(LEDPin, !ledState); 
   }
}

void doCountingUpTo(int iMax){
   static unsigned long lastCount  = 0;
   static unsigned long delayCount = 500;
   static boolean firstCount = true;
   static int i = 0;
  if (millis()-lastCount >= delayCount||firstCount){ 
     lastCount = millis();
     firstCount = false;
     if (i <= iMax) {                                                            
         Serial.print(i);
         Serial.print("\t"); // prints tabs
         i++;
      } else { 
         Serial.println();
         i = 0;
      }
   }
}

The simple wiring for the blink functions

image

4 Likes

Many thanks for this great job.

Hi @ec2021,

very good introductional tutorial.
Adding the WOKWI-Simulations is a great idea.

<== click on the arrow to unfold. Some more ideas and thoughts:

I want to suggest to add some pictures that visualise the "looping" of the loops.
The code on the left and on the right circular arrows.
As a rare exception to the common code-sections a screenshot of the code with added circular arrows.
Just to make very clear what looping means.

What do you think about making the blocking for-loop / while-loop and the non-blocking equivalents as similar as possible.

A demo-code that has one single for-loop but inside function loop() (not inside setup()
and the non-blocking equivalent.
Blocking and non-blocking code as screenshots side by side to make it even more easier to see the equivalent functionality and the differencies in the coding.

Of course not all but estimated around 80% of all "how to combine two things"-questions
have a very similar pattern.

Code-function A:
read sensor1 - print measurement - delay(5000) - repeat

Code-function B:
Check if a button is pressed - when pressed - increment a counter - print value of counter

Again as a screenshot of the code and added vertical arrows that visualise code-execution with timestamps with easy to follow numbers.
Fast code-execution 1 millisecond per line of code
delayed code-execution simply the number of milliseconds of the delay

To make it easy to understand how much time passes by from line to line
and to see OH ! at the delay(5000) 5 seconds before checking the button.
Checking the button a single millisecond again 5 seconds long no checking.
All as code-screenshots with vertical arrows representing the execution-time/time to finish that line of code.

another basic example a count-down
left-side blocking with for-loop / right-side non-blocking with added button stop count-down

And I was thinking about taking all the basic examples of the Arduino-IDE-Example-folder and to re-write each example in a non-blocking way.

So what do you think?
best regards Stefan

1 Like

Hi @StefanL38,

thanks, I consider your input valuable and constructive, therefore I tried to convert them into a kind of a "task list":

Task list
  • Provide a demo code with one single for-loop inside function loop() (not inside setup()) and the non-blocking equivalent.
  • Show examples of the most common pattern of "how to combine two things":
    • Example 1: Reading a sensor and reading a button:
      • Code-function A: read sensor1 - print measurement - delay(5000) - repeat
      • Code-function B: Check if a button is pressed - when pressed - increment a counter - print value of counter
      • Combine function A and B
        • Blocking
        • Non-Blocking solution
    • Example2: Count down and reading a button
      • Code function A: Count down blocking with for-loop ight-side non-blocking with added button stop count-down
      • Code-function B: Check if a button is pressed - when pressed - increment a counter - print value of counter
      • Combine function A and B
        • Blocking
        • Non-Blocking solution
  • Re-write each basic sketch of the Arduino-IDE-Example-folder in a non-blocking way
  • For all examples:
    • Make blocking for-loop / while-loop and the non-blocking equivalents as similar as possible
    • Blocking and non-blocking code side by side to make it even more easier to see the equivalent functionality and the differencies in the coding
    • Add vertical arrows that visualise code-execution with timestamps with easy to follow numbers to make it easy to understand how much time passes by from line to line
    • Fast code-execution 1 millisecond per line of code
    • Delayed code-execution simply the number of milliseconds of the delay
    • Use vertical arrows representing the execution-time/time to finish that line of code.
    • Add some pictures that visualise the "looping" of the loops (e.g. code on left side, drawings on the right)

Hope I still met your objectives as I tried to separate your suggestions for

  • Examples and for
  • Presentation of the tutorial

Please have a look and you are heartly invited to commit to it as the whole job may take significant time :wink:

Maybe one of the "easier" and quicker tasks is to rewrite some of the basic Arduino examples from blocking to non-blocking? Any suggestions where to start?

Regards
ec2021

1 Like

I simply took the first sketch in the list which is analogRead

Maybe this is overloaded with extras and with comments.
What do you think?

/*
  AnalogReadSerial
  Reads an analog input on pin 0, prints the result to the Serial Monitor.
  reading is done very often. Additionally a variable is counted up very fast
  incremented by 1 with each iteration of loop
  printing the value to the serial monitor is done only once per second
  the delay is realised WITHOUT function delay() with non-blocking timing
  Graphical representation is available using Serial Plotter (Tools > Serial Plotter menu).
  Attach the center pin of a potentiometer to pin A0, and the outside pins to +5V and ground.

  This example code is in the public domain.
*/

unsigned long fastCounter = 0; // variable fastCounter does demonstrate the fast looping of function loop()
unsigned long countSeconds = 0;

// the setup routine runs once when you press reset:
void setup() {
  // initialize serial communication at 115200 bits per second:
  // make sure to adjust the serial monitor to 115200 baud
  Serial.begin(115200);
  Serial.println( F("Setup-Start") );
}


// the loop routine runs over and over again forever:
void loop() {
  // increment variable fastCounter with every iteration of loop
  // for demonstration of how fast loop is looping
  fastCounter++;  

  doMeasuringPrintFromTimeToTime(); // call this function 
}


void doMeasuringPrintFromTimeToTime() {
  static unsigned long timeStamp;  // attribute static makes the variable persistant
  int sensorValue;

  sensorValue = analogRead(A0); // read the input on analog pin 0:

  // check if 1001 milliseconds have passed by since last measuring
  if (millis() - timeStamp >= 1001) {
    // when 1001 milliseconds REALLY have passed by
    timeStamp = millis(); // store actual time as new timestamp

    countSeconds++;       // increment variable countSeconds

    Serial.print( F("sec:") );    
    Serial.print(countSeconds);   
    Serial.print( F(" Value=") ); // print out the value you read:
    Serial.print(sensorValue);
    Serial.print( F(" counter counts up very fast=") );
    Serial.println(fastCounter);
  }
}

best regards Stefan

It might be nice to add a comment in the code of the Wokwi implementations that points back here to this thread. For example:

/*
   ============================
   Millis() Function Example
  ============================
   
   This is an example of a non-blocking millis() function replacing delay() 

   2023-04-02
   ec2021 

   Simulation: https://wokwi.com/projects/360903822433974273
   Discussion: https://forum.arduino.cc/t/millis-instead-of-delay-and-loop-instead-of-for-loop/1110044/2

*/

...then if people save a copy of the code, they can find their way back to your sources.

1 Like

This comment isn't correct, in that the "it will definitely perform what's inside the curly brackets immediately in the first loop()" won't happen immediately. It will wait for delayTime after the first loop().

You can prove this by adding a Serial.print() and increasing the delay. Drop this in the Wokwi:

/*
   ============================
   Millis() Function Example
  ============================
   
   This is an example of a non-blocking millis() function replacing delay() 

   2023-04-02
   ec2021

*/

constexpr byte LEDPin = 13;

void setup() {
   Serial.begin(115200);
   pinMode(LEDPin, OUTPUT);
}

unsigned long lastChange = 0;    // Variable to store the time in ms when ledState was changed 
unsigned long delayTime  = 3000; // Delay in [ms] when then next change shall take place 

void loop() {

  if (millis()-lastChange >= delayTime){ // As lastChange has been set to zero when the sketch started
                                         // it will definitely perform what's inside the curly brackets
                                         // immediately in the first loop()
                                         // The greater in ">="" is required as we cannot always be sure
                                         // that this comparison is performed exactly "delayTime" later ...
    Serial.print('.');
    lastChange = millis();               // Stores this time so that it takes in mininimum delayTime for
                                         // the next entry to this bracket procedures  

    boolean ledState = digitalRead(LEDPin);  // Read the actual status of the ledPin, so we do not have
                                             // to memorize and handle it in a variable

    digitalWrite(LEDPin, !ledState);         // Change the status by inverting ledState with the ! operator
                                             // !ledState provides the opposite of ledState
   }
 
}

If you want the described behavior of an immediate trigger on the first iteration near millis()==0, you need to roll lastChange before zero with an initialization like:

unsigned long delayTime  = 3000; // Delay in [ms] when then next change shall take place 
unsigned long lastChange = -delayTime;    // Variable to store the time in ms when ledState was changed 

Hi @DaveX,

thanks for your constructive review! Highly appreciated!

  • Added the Simulation and Discussion links to the Wokwi Examples
  • Changed the relevant parts of the sketches so that the millis()-if-clause is entered in the first call due to a boolean instead of the delay time.

Setting "unsigned long lastChange" to "-delayTime" would do it also of course but might require some more explanations ... :wink:

Regards
ec2021

Thanks @StefanL38,

I will have a look as soon as possible ...

Regards
ec2021

Yes. Many use-cases are short enough that missing a zeroth trigger isn't a concern, so the extra work of adding a one-shot or messing with the lastChange arithmetic to advance (or retard) the timing of the first trigger is awkward.

I didn't dig through the assembly, but efficiency-wise, I'd think the boolean oneshot scheme would add an additional test per loop() versus the scheme of backdating the lastChange.

If you are looking for simplicity and ease of explanation it might be better to have kept the common coding paradigm and to have changed the comment to explain that the first trigger comes at the end of the first interval rather than code for the uncommonly seen "immediately in the first loop()" trigger.

Understood! The pattern I liked to replace is

void loop(){
    doSomething();
   delay(waitTime);
}

That's the reason for "immediately in the first loop()" so that doSomeThing() is done before waitTime is active.

The boolean will quite likely add the additional test per loop() and also an additional "= false;" everytime the if clause is performed but that might be acceptible in most cases.

I actually thought of your suggestion to explain that the first trigger comes at the end of the first interval. That might sometimes even be wanted. If one uses global variables for the millis() clause it would also be possible to make the "first" step in setup() including storing the time in the "lastChange" variable. That would also make much sense for a lot of applications.

Maybe variants of this would also find a nice place in the examples?

Add examples to Arduino docs?
GitHub?

That's a normal way that people design their processes.

If you need the action at the beginning of the sketch, that's probably the easiest scheme to explain, but its a bit messier than keeping the doSomething() in one place. But the other alternatives have their own potential messes.

Exactly, and then they are puzzled when they cannot integrate reading a sensor or button ... :wink:

Sequential coding - including delays - complies more with the human view of processes. The "trick" to split the processes in small, less time consuming parts that can be handled step by step by the controller is something most beginners have to learn.

I think the capability of the millis() function and also of state machines should be more promoted in the examples and the advantages but also the disadvantages of the delay function should be described more clearly to newbies ...

1 Like

Hi @StefanL38,

thanks for your suggestion! I linked @LarryD 's tutorial in post no 1.

One problem with naively translating sequential code into delay-free, non-blocking forms is making the steps into one-shot actions--you have to translate an order of operations that executes once per loop() into something that can handle thousands of iterations through loop() properly. The edge-detecting comparison of millis() is one way of handling it, but if you need coordination between multiple steps, using one or more variables (other than lineNumber within loop() and iteration of delay) to track and transition state across iterations of loop().

The key idea when you use millis() instead of delay() or loop() instead of for() is that instead of using using the lineNumber/instructionPointer for tracking program state, you need to start using other, more persistent of variables for managing program state.

1 Like

Hi @StefanL38,

coming from your suggestion I made a slightly different version, closer to the original:

/*
  AnalogReadSerial

  Reads an analog input on pin 0, prints the result to the Serial Monitor.
  Graphical representation is available using Serial Plotter (Tools > Serial Plotter menu).
  Attach the center pin of a potentiometer to pin A0, and the outside pins to +5V and ground.

  This example code is in the public domain.

  The original sketch 

  https://www.arduino.cc/en/Tutorial/BuiltInExamples/AnalogReadSerial

  has been modified avoiding the delay() function and using 115200 Bd as 
  state-of-the-art instead of 9600 Bd.
  
  2023-07-29
 
*/

// lastMeasurement stores the time in msec when a measurement takes place
unsigned long lastMeasurement =    0; // msec
// delayTime defines the time in msec between the last and the next 
// measurement
unsigned long delayTime       =    1; // msec

// Count counts the loops performed between two measurements
unsigned long Count           = 0;

// the setup routine runs once when you press reset:
void setup() {
  // initialize serial communication at 115200 bits per second:
  Serial.begin(115200);
}

// the loop routine runs over and over again forever:
void loop() {
  // Increment Count with every loop
  Count++;
  // check if the last measurement has been performed (at least) delayTime ago
  if (millis()-lastMeasurement >= delayTime){
    // if yes: Store the new measurement time
    lastMeasurement = millis();
      // read the input on analog pin 0:
    int sensorValue = analogRead(A0);
    // print out how many loops have been performed
    // between the last and the recent measurement
    Serial.print(Count);
    // print a TAB 
    Serial.print("\t");  
    // print out the value you read:
    Serial.println(sensorValue);
    // Reset Count
    Count = 0;
  }  
}

I copied your idea of a "fastCounter" but left the rest more or less as it was ...

Wokwi version: https://wokwi.com/projects/371605982919348225

It prints the sensorValues and the number of loops() performed between two measurements. So it should be very obvious to the observer that loop() is performed multiple times (in the simulation 150 to 200) between two measurements.

Hi @DaveX,

I think this is a very good analysis of the main challenges in converting pure sequential methods to "time sliced" and state dependent coding. Wouldn't that be a nice topic for a further tutorial?