Go Down

Topic: A more elegant way of using millis() or micros() for non blocking timers (Read 3757 times) previous topic - next topic

femmeverbeek

Decades ago I wrote a tiny non-blocking timer function in Pascal, which resulted in a lot of likes.
Here's my Arduino adaptation. I think it's great for learning purposes.
There's just a single function chkTimer which needs to be included in your sketch.

For each independent running timer you must declare an unsigned long (=4 bytes) variabele to store the expiration time. The function returns true when the expiration time was reached. When that happens a new expiration time is stored in the variabele by adding the value of loopTime to the expireTime variabele. So continuously polling the function will result in a repetitive loop that checks true every loopTime.

It can do milliseconds or microseconds depending on which line containing the currentTime declaration is commented out.

The maximum single shot duration is 232-1 ~ 50 days for ms or 72 min for µs.
The maximum loopTime duration is 231-1 ~ 25 days for ms or 36 min for µs .

Here's the function


Code: [Select]

/* Function chkTimer resets and returns true when the timer expired.
 * It stores the end time in the unsigned long variabele passed to &expireTime.  
 * When expired the value of loopTime is added to the expireTime so continuously
 * polling this function checks true every loopTime.
 * The time scale can either be in milliseconds or in microseconds, depending
 * on which line containing currentTime is commented out.
 * All information the timer needs is stored in the variable, so to have more
 * independent timers, just declare more variabeles.
 * Written by Femme Verbeek 30-4-2020
 * Use for free
*/
boolean chkTimer (unsigned long &expireTime, unsigned long loopTime) {
unsigned long currentTime = millis();       // use this for milliseconds
//unsigned long currentTime = micros();       // use this for microseconds
  if (currentTime >= expireTime)            //check the timer
  { if ( (bitRead(currentTime,31) == 1) &&
         (bitRead(expireTime,31) == 0) )    //check overflow
      return false;                         //integer overfow but not expired
      else            {                     //expired        
        expireTime += loopTime;            //set new expireTime
        return true;  }                     //OK, chkTimer was triggered
  } else return false;                      //not expired
}



Unlike most polling timers, this one will keep running fine when the integer value overflows after 50 days of running. So it can be used as the basis for longer running counters. It checks for the transition of bit 31. Assuming that the polling frequency is much faster than the timer loop, expireTime will overflow first, followed by currentTime within the loopTime duration.

Here's a test program.

Code: [Select]

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

unsigned long mytimer1 = 0;       //start timing loop immediately
unsigned long mytimer2 = 5000;    //wait 5 seconds before starting timing loop  
unsigned long mytimer3 = 10000;   //wait 10 seconds before starting timing loop

void loop() {                  
  if (chkTimer(mytimer1,377)) Serial.print("Hello ");
  if (chkTimer(mytimer2,610)) Serial.println("world ");
  if (chkTimer(mytimer3,1597)) Serial.println("and the universe ");
}



The initial value of the timers equals the one shot time in ms before the repetitive loop starts.
The loopTime values of 377, 610, 1597 are Fibonacci numbers for maximum chaos.

Comments welcome.

westfw

Most of the "polled timer" code uses ((millis() - starttime) > delaytime), which works fine at wraparound with less complexity than your method...


femmeverbeek

This is a solution looking for a problem.

The problem is when you want to make a timed event that runs indefinitely.

Quote
Most of the "polled timer" code uses ((millis() - starttime) > delaytime), which works fine at wraparound...
It makes no difference. After appr. 50 days the millis() timer overflows its maximum integer value and jumps back to zero. When that happens starttime is never updated anymore since (millis() - starttime) is always < delaytime. Effectively your timer stops.

Something similar happens to  millis() > expiretime. In that case it's the expiretime that is the first to overflow the maximum integer value and jump back to zero. When that happens the timer goes berserk as it will trigger every poll. This is the reason for adding the first bit check.

The same happens when using micros() but the problem starts at appr. 7 hours.

Quote
with less complexity than your method...
One declaration of a variabele, and one call to a function is a simpler than your method.

Robin2

It makes no difference. After appr. 50 days the millis() timer overflows its maximum integer value and jumps back to zero. When that happens starttime is never updated anymore since (millis() - starttime) is always < delaytime. Effectively your timer stops.
If you use this type of code what you say is not true
Code: [Select]
if (millis() - startTime >= interval) {
   startTime = millis();

  // other code

}


...R
Two or three hours spent thinking and reading documentation solves most programming problems.


Robin2

OK,  :smiley-confuse: I had to see this for myself.
Write a short test program where you use millis() to increment a byte variable 2 times per second and treat the byte variable as if it was millis() and see what happens. Define all the variables as byte so they overflow at 255

Code: [Select]
if (millis() - prevMillis >= 500) {
     prevMillis = millis();
     byteTime ++;
}

if (byteTime - byteStartTime >= byteInterval) {
   byteStartTime = byteTime;

  Serial.println(byteTime);

}


...R
Two or three hours spent thinking and reading documentation solves most programming problems.

femmeverbeek

The idea was actually to have the wraparound in a single function. Originally it was for a realtime clock where the date and time were reduced to a single number which could be stored in a longint.

The function now reduces to

Code: [Select]
boolean chkTimer (unsigned long &startTime, unsigned long loopTime)
{ if (millis()-startTime >= loopTime)  {      
        startTime = millis();          
        return true;  }                
  else return false;  
}


You loose the functionality of a timed single shot without triggering at t=0.

femmeverbeek

Write a short test program where you use millis() to increment a byte variable 2 times per second and treat the byte variable as if it was millis() and see what happens. Define all the variables as byte so they overflow at 255

What I used was this.
Code: [Select]

void setup() { Serial.begin(9600);
while (!Serial);}
unsigned long dummillis = 0xFFFFFFFA;
unsigned long startTime = dummillis;
int interval = 4;
void loop() {
  if (dummillis - startTime >= interval) {
   startTime = dummillis;
   Serial.println(" Timer triggered ");}
  else  Serial.println(" Still waiting ");
  dummillis++;
  Serial.print(" Time = ");
  Serial.print(dummillis);
  Serial.print(" StartTime = ");
  Serial.println(startTime);
  delay(200);
  //  Serial.println(dummillis=10   );
  while(dummillis==10);
}

Go Up