Getting rid of delay() and for loops; un-delay2 is out

edit: V3 is in post 44, a ways below.
edit 5/19/22: progress on comments & docs, still to go; state machine doc.
-------------------- wondering when it won't fit one post.

This is version 2 of a code technique demonstration sketch. A how-to.
It now shows how to repeat steps without blocking void loop().

A future version will have functions in loop() running as parallel tasks and that should open minds about easier ways to approach real world coding.

I'd also like to discuss what other blocking code the demo does not address so it can be covered.

// NoBlockUndelayDemo2 2022 by GoForSmoke @ Arduino.cc Forum
// Free for use, May 10, 2022 by GFS. Compiled on Arduino 2.1.0.5
// This sketch shows a general method to get rid of delays in code.
// You could upgrade code with delays to work with add-a-sketch.
// .. adding looped cases

// #include <avr/io.h>  ---  remove this line per Railroader, it's no longer needed
#include "Arduino.h"

const byte ledPin = 13;
unsigned long delayStart, delayWait;
const byte indexMax = 12;
byte index;


void setup()
{
  Serial.begin( 115200 );
  Serial.println( F( "\n\n\n  Un-Delay Example, free by GoForSmoke\n" ));
  Serial.println( F( "This sketch shows how to get rid of delays in code.\n" ));

  pinMode( ledPin, OUTPUT );
};


/* The section of the original sketch with delays:
 * 
 * digitalWrite( ledPin, HIGH );   --  0
 * delay( 500 );
 * digitalWrite( ledPin, LOW );    --  1
 * delay( 500 );
 * for ( i = 0; i < 12; i++ )
 * (
 *   digitalWrite( ledPin, HIGH );   --  2
 *   delay( 250 );
 *   digitalWrite( ledPin, LOW );    --  3
 *   delay( 250 );
 * }
 * digitalWrite( ledPin, HIGH );   --  4
 * delay( 1000 );
 * digitalWrite( ledPin, LOW );    --  5
 * delay( 1000 );
 */

byte blinkStep; // state tracking for BlinkPattern() below

void BlinkPattern()
{
  // This one-shot timer replaces every delay() removed in one spot.  
  // start of one-shot timer
  if ( delayWait > 0 ) // one-shot timer only runs when set
  {
    if ( millis() - delayStart < delayWait )
    {
      return; // instead of blocking, the undelayed function returns
    }
    else
    {
      delayWait = 0; // time's up! turn off the timer and run the blinkStep case
    }
  }
  // end of one-shot timer

  // here each case has a timed wait but cases could change Step on pin or serial events.
  switch( blinkStep )  // runs the case numbered in blinkStep
  {
    case 0 :
    digitalWrite( ledPin, HIGH );
    Serial.println( F( "Case 0 doing something unspecified here at " ));
    Serial.println( delayStart = millis()); // able to set a var to a value I pass to function
    delayWait = 500; // for the next half second, this function will return on entry.
    blinkStep = 1;   // when the switch-case runs again it will be case 1 that runs
    break; // exit switch-case

    case 1 :
    digitalWrite( ledPin, LOW );
    Serial.println( F( "Case 1 doing something unspecified here at " ));
    Serial.println( delayStart = millis());
    delayWait = 500;
    blinkStep = 2;
    break;

    case 2 :
    digitalWrite( ledPin, HIGH );
    Serial.println( F( "Case 2 doing something unspecified here at " ));
    Serial.println( delayStart = millis());
    delayWait = 250;
    blinkStep = 3;
    break;

    case 3 :
    digitalWrite( ledPin, LOW );
    Serial.println( F( "Case 3 doing something unspecified here at " ));
    Serial.println( delayStart = millis());
    delayWait = 250;
    // this replaces the for-loop in non-blocking code.
    if ( index++ < indexMax ) // index gets incremented after the compare
    {
      blinkStep = 2;
    }
    else
    {
      index = 0;
      blinkStep = 4;
    }  // how to for-loop in a state machine without blocking execution.
    break;

    case 4 :
    digitalWrite( ledPin, HIGH );
    Serial.println( F( "Case 4 doing something unspecified here at " ));
    Serial.println( delayStart = millis());
    delayWait = 1000;
    blinkStep = 5;
    break;

    case 5 :
    digitalWrite( ledPin, LOW );
    Serial.print( F( "Case 5 doing something unspecified here at " ));
    Serial.println( delayStart = millis());
    delayWait = 1000;
    blinkStep = 0;
    break;
  }
}


void loop()  // runs over and over, see how often
{            
  BlinkPattern();
}

3 Likes

Not bad. Not bad at all...
Why the #include <avr/io.h>?

The code compiles without it.

2 Likes

It used to be needed.

Are there other structures than for-next loops where replacing a delay isn't cut & dry?
Does timing and states make un-blocking code complete?

A for loop executing arrays, or similar, can be used. for loops, bumping on some time consuming activity, could be "chopped up", if the prolonged time can be accepted. This in order to serve other urgent tasks.

Going for while loops could be beneficial. If there's nothing to catch, move on and try next time....
"while data available".... Okey but when no data is available?... Move on.

The possibilities using switch/case can be shown. Lots of new members want to do "this" for some time and then "that1" and then "that2".....
Those sequences would be solved by switch/case constructions.

I go for processing 1 element per loop() to kick up loop frequency.

When my loop() frequency averages 67KHz, I can read a 100 button matrix 670 times per second along with a few light tasks, all running "at the same time".

What has to be done ASAP just has to be but mostly "soon" is plenty good.

Don't say "while". Say "if, else I'll get back to ya."

What do you think of the switch-case in the demo?

1 Like

Cool. I would suggest:

  1. Edit the description to explain a bit of why and how you are getting rid of delay.
  2. Edit the comment to lead folks back to the discussion here, or the authoritative source if it is elsewhere.

Maybe:

// NoBlockUndelayDemo2 2022 by GoForSmoke @ Arduino.cc Forum
// Free for use, May 10, 2022 by GFS. Compiled on Arduino 2.1.0.5
// This sketch shows a general method to replace blocking delays in code
// with millis() based state varaibles and a switch-case structure.
// You could upgrade code with delays to work with add-a-sketch.
// .. adding looped cases
//
// Discussion: https://forum.arduino.cc/t/getting-rid-of-delay-and-for-loops-un-delay2-is-out/990525

  1. Change some of the "Serial.println( F( "Case ..." to "Serial.print" so the reporting is smoother:
  Un-Delay Example, free by GoForSmoke

This sketch shows how to get rid of delays in code.

Case 0 doing something unspecified here at 6
Case 1 doing something unspecified here at 506
Case 2 doing something unspecified here at 1006
Case 3 doing something unspecified here at 1256
Case 2 doing something unspecified here at 1506
Case 3 doing something unspecified here at 1756
Case 2 doing something unspecified here at 2006
...

I really liked the round-robin analogRead() idea in Help with moving steppers at different speeds - #38 by GoForSmoke -- maybe maintaining an array of updated analog readings would be interesting.

It's great. Maybe it can be made more clear in the respect of for loops versus sequential tasks.
Once I was hired to make code for a satellite receiver prototype, strategy evaluation. I got 3 months. The heavy guys in the company, making and coding the engine control for the Adrianne rockets looked at me as a big piece of shit. In their world the task called for creating a real time OS, and that needed at least a year. I solved it by a timer tick every 5 mS. In the 4 stage case construction "emergency" messages were checked in every case and less important stuff was checked once or twice. It showed up to work!
Afterwards I sat most of the day staring into the wall. Was it this simple to beat the dragons? Thinking outside the box I will say....

1 Like

Great stuff

It’s as much an example code on using state machines than getting rid of delay.

some feedback (hopefully seen as constructive / personal thoughts)

If this is for beginners this include is not needed, the IDE injects that for you too.


blinkStep is a global variable hence auto initialized to 0 which happens to be your first step in your state machine. So instead of

byte blinkStep; // state tracking for BlinkPattern() below

For clarity you could spell it out

byte blinkStep = 0; // state tracking for BlinkPattern() below. We will start at step 0

Or initialize that in the setup to show it’s important to start at the right step

Same applies to delayStart which is not initialized and can cause some confusion as to what happens there. Your step 0 primes things up and may be this could deserve a comment.


Also I thin’k it’s not a great practice to mix your business logic with a debug statement when you do

I would prefer to spell things out with an appropriate comment for the state machine

delayStart = millis(); // record the moment of the last action
Serial.println( delayStart);

And trust the compiler to optimize things if possible (ie reuse the register(s) if it can which holds the result of the previous assignment rather than a memory fetch).


Some examples do include the

while (!Serial) {} // wait for Serial to be ready, needed on some architectures

Might be worth it

Also I’ve noticed many examples start by setting the pins mode before even touching Serial etc, just to ensure the board is setup in the right way from the get go.


Last comment is more generic. You rely on global variables for timing and this could create confusion for beginners if they need more tasks (states machines) running in parallel. The way you did set things up, BlinkPattern() is self sufficient and thus I would define the timing and step variables as static within the function.

Why is loop frequency so important to you?

What about, for example, looping to search for a certain character in a (reasonably short) string?
Or the subtraction loop to convert a date from year and day-of-year to year, month, and day-of-month?

I agree with JML - delayStart had me confused as I'd "filtered out" the serial prints.

@GoForSmoke The switch - case is such a powerful tool and your code is a clear example.

How good can they be if they needed to hire someone from outside?

Been there. They don't like being shown up.

That is how often inputs may be updated and outputs acted upon.
It is the smoothness that tasks may be interleaved.
And one task being a cycle-hog will slow, possibly stumble others and jerk the code up.

When I have to do extra before the next event happens, I have to. I have a keyword match finder made to work on the fly char by char as serial text streams in. It can keep up with 250000 baud text while, it averages 9 usecs per char matched in a match from flash speed test leaving time to do something else/more.

These little examples are to not have too many things distracting learners, and now I'm adding more.

Breaking the sketch into tasks can make it easier to debug than an all-in-one code block.

Okay except that those "debug prints" are to show the blink timing is correct.
New to millis() timing members will see that for the first time.

I wish that serial monitor had an ANSI mode or some cursor control chars, we could have static data screens with values updating in place.

They had no guy having time to do the job. Lack of capacity...

That's because they thought they needed a rocket scientist able to invent a new real time operating system :grimacing: and no one raised their hand !

You did not know it was impossible, so you solved it :slight_smile:

joke aside, there are specific constraints depending on what you develop and I've seen project where you need to demonstrate (mathematically prove) that the time constraints will be met. In such projects you have specific tools or libraries and they require a a lot of formal red tape that takes longer than the actual coding

They make an impossible X-Y problem and went after that.
First you hear about their problem+answer that somehow Must Be.
Along the way you find out what they wanted to do and solve that. Way Easier!

I never got the real hardware to verify the time constrains and told the guy taking over the project to check the response times. Some minor changes in the code made it, and the work was accepted by ESA.

Fun is that the time, 3 months assigned for my work, was enough!

one way to simulate that is doing 50 println(); to clean the screen and then print everything new. If you increase the baudrate to what an Arduino Uno is capable to bitbang the visualisation should be almost the same.

best regards Stefan

1 Like

Yes.


Maybe invest in some 1.3 or .96” OLED I2C displays.

image