Millis() shortcut

I recently found a simpler way of using millis() than described in blink without delay and most other tutorials as it only involves one line.

if (!(millis() % 500)) { // do your thing here}

Can someone explain why this way of using millis() isn't used more often instead of the more complicated version in 'blink without delay'.

What will happen if you miss reading the value of millis() at least every millisecond ?

millis() will skip a number from time to time and modulo is computational heavy (costly)

I'm using it for simple time delay, where the actually timing of the delay isn't critical, I just want to slow things down (taking an anologue reading every second or two, instead of dozens a second).

edit: removed debounce as an example

the amount of time you need to wait until the bouncing is over needs to be fixed to a minimum, not dependant on what was the millis() value when you pressed..

Yes I wasn't thinking. Not for debounce, just in a situation where I need a short delay.
i.e instead of the
delay(50);
line

If you don't care whether the delay is mostly 50, sometimes longer and that in your program you can afford the time to calculate the modulus then feel free to use it

I use this version that is based on a function.

You define an unsigned long as the timing variable.

This enables to have multiple timers without interferring with each other.

void PrintFileNameDateTime()
{
  Serial.println("Code running comes from file ");
  Serial.println(__FILE__);
  Serial.print("  compiled ");
  Serial.print(__DATE__);
  Serial.print(" ");
  Serial.println(__TIME__);  
}


boolean TimePeriodIsOver (unsigned long &periodStartTime, unsigned long TimePeriod) {
  unsigned long currentMillis  = millis();  
  if ( currentMillis - periodStartTime >= TimePeriod )
  {
    periodStartTime = currentMillis; // store new snapshot of time as the new periodStartTime
    return true;                // more time than TimePeriod) has elapsed since last time if-condition was true
  } 
  else return false;            // not expired
}

unsigned long MyTestTimer = 0;                   // variables MUST be of type unsigned long
const byte    OnBoard_LED = 2;


void BlinkHeartBeatLED(int IO_Pin, int BlinkPeriod) {
  static unsigned long MyBlinkTimer;
  pinMode(IO_Pin, OUTPUT);
  
  if ( TimePeriodIsOver(MyBlinkTimer,BlinkPeriod) ) {
    digitalWrite(IO_Pin,!digitalRead(IO_Pin) ); 
  }
}


void setup() {
  Serial.begin(115200);
  Serial.println("Setup-Start");
  PrintFileNameDateTime();
}

//#define debugTxtVar(abc, def) Serial.print("<  "#abc" = "); Serial.print(def); Serial.println('>')

// using the #define "statement" to make the compiler "write" code
// #define can be used to make the compiler replace text that describes something with (almost) anything else
// easy example 
// let's assume IO-pin number 4 shall be set to work as output 
// the command for defining an IO-pin to be configured as output is
//
// pinMode(4,OUTPUT); 
// the number "4" says nothing about he purpose of this IO-pin

// let's assume the IO-pin shall switch On/Off a buzzer

// you would have to add a comment to explain
// pinMode(4,OUTPUT); // IO-pin 4 is buzzer

// the compiler needs the number. To make the code easier to read and understand
// #define BuzzerPin 4
// So the command looks like this
// pinMode(BuzzerPin,OUTPUT);

// the descriptive word "BuzzerPin" gets replaced through the compiler by a "4"
// so what the compiler compiles is still 'pinMode(4,OUTPUT);'

// the #define can do much more than just replace a word by a single number
// it can even take parameters like in this example

#define debugTxtVar(myParameterText, variableName) \
        Serial.print(#myParameterText " " #variableName"="); \
        Serial.println(variableName); 
        
int myVar = 1234;
int mySecondVar = 999;

void loop() {
  BlinkHeartBeatLED(OnBoard_LED,250);

  if ( TimePeriodIsOver(MyTestTimer,1000) ) {
    debugTxtVar("first line " , myVar);
    mySecondVar++;
    debugTxtVar("second line" , mySecondVar);
  }  
}

best regards Stefan

So you find that while (millis() % 50) ; is easier to read than delay(50); ?
(not even mentioning the wait will be kinda random)

That first argument should be called "startTime" or something like that, since it's storing the beginning of your timing interval rather than the end of it. Naming is important!

if you look at the code, that parameter gets updated at the end of the delay with the time when the period was really over so whilst it's the start time when you call, it returns the expiration time.

That where my thoughts when I wrote the function.
After re-thinking about it I indeed renamed it to periodStartTime.

It is used in a continious repeated way so if it is "Start"-time or "expire"-Time depends on what you are looking at.

The name "periodStartTime" tries to catch both aspects.
if the value is handed over it is the Start of a Timeperiod
and at the end with

periodStartTime = currentMillis;

the variable gets assigned a new value. But the meaning is what the name says the Start of a new timeperiod.

best regards Stefan

Just providing a few points for thought:

  • the original example naming currentMillis and previousMillis is very elegant, two millis values are subtracted and compared to an interval
  • periodStartTime is not a classical time (what you read on a watch), I try to limit "time" in variable names for values containing actual time
  • the function TimePeriodIsOver has a return value and has side effects (changing periodStartTime) that are not obvious from the name. The function name suggests only the return parameter is "changed".
  • you used the Arduino naming convention for some of your code and your own style for other parts

I use something nearly identical to Stefan's code. ( Duino-hacks/delay_without_delay.ino at master · WestfW/Duino-hacks · GitHub )
It is essentially a C-style "object" (simplified by having only a single attribute, and therefore probably slightly more efficient than using a full C++ object?)

While we can argue details of name choices and such, I really don't understand why the "how to use millis()" tutorials seems to be so hesitant to include such a function instead of scattering the logic all around...

Really I don't really see what's the win doing

if (delay_without_delaying(startTime, duration) ) {
 ...
}

versus

if (millis() - startTime >= duration) {
  ...
  startTime = millis(); // or startTime += duration; (might not be needed if you don't want to re-trigger)
}

I think that all examples are written to make people think about the approach. If you would give people a function that could be copied blindfolded, only a few of them will take the time to learn how it works. And Arduino is all about learning :wink:

1 Like

Yes for using microcontrollers you have to learn. How much learning through going deeper and deeper into the code is the users decision. And to me there is only a gradual difference for each and every "going deeper".
@Klaus_K how deep did you go into libraries like "wire.h/.cpp", "lcdgfx.h/.cpp", "hardwareserial.h/.cpp" etc. etc. I guess there were many libraries yiu were just using. There are only a few really "hardcore" coders that go down to the last line of the core-code.

About keeping conventions: there seems to be an unwritten "convention" to reduce documentation in the source-code to a bare minimum. To me this seems to be an attitude of "if you want to use it learn it (almost) from scratch yourself". I did not include a full tutorial as a block comment but there is quite some comments in my function.
Adding more comments enables learning "on the fly"

best regards Stefan

If it directly involves the microcontroller, I do dig quite deep at occasion.

Those are the ones that write libraries for hardware like graphic displays :wink:

When I have time to spend on helping a user here on the forum, I try to document every line of code; that includes the obvious because one never knows who will find a thread or post. If I have to document for myself, I mostly document (in the code) complicated stuff where I might forget why it was implemented that way; this should actually be documented in a separate document, but OK.

Hi @StefanL38,
I listened to some compelling arguments against comments after using verbose comments myself for a long time. I found many arguments to be true for comments in the source code posted especially by beginners. Here are a few arguments I remember from a talk that convinced me:

  • many comments make the code harder to read

  • most comments are trivial

  • many comments are wrong, and the compiler does not check comments

  • comments get out of sync with the source code

  • comments are often used as storage for old code

  • many programmers use comments instead of rewriting the code in a way that it explains itself

  • because code is read more often than it is written, it is worth spending the time to make it easy to read

There are still good comments that can help others to understand the code. For instance, a high-level description of libraries, classes, interfaces ...

Well, "we" are a bit inconsistent on the idea. We seem happy to ask users to understand and implement pseudo-concurrency by using non-blocking code, or assemble strings as they come in one-byte at a time, but are also convinced that they'll never understand bitmasks well enough to exist without digitalRead()/etc.