millis() rollover

Been searching around to find out what exactly happens to the millis() output after rollover, but I'm not finding anything.

This is the active part of my code, using the DS1307 library to talk to a RTC which currently it's data requested once a second, but since I will be doing more with the program I can't really use a delay, that and I don't like to use delays if at all possible.
The easiest way I can see is to detect the 50 day millis() rollover, but not knowing what happens to the stored number makes that difficult ::slight_smile:

void doClock(){                           // Currently the only thing called from loop()
  if (millis() - checkRTCmillis > 1000) {
    checkRTCmillis = millis();
    
    RTC.get(rtc,true);

    if(rtc[2] < 10){LED.print("0");}
    LED.print(rtc[2]);                    // LED.print uses newsoftserial to talk to a serial LED display

    if(rtc[1] < 10){LED.print("0");}
    LED.print(rtc[1]);
  }
}

Also, since I have a RTC it would be very easy to wait for a bit less than the millis() rollover, then trigger a function to detect when it actually happens, not sure if that would be necessary though.

I too have followed the millis() rollover thing for sometime now. I get how it was changed to a long, so the rollover was increased to 50+ days or so.

What I don't get is the several statements made in various postings that now a programs flow can operate OK through a roll-over? Can some explain this in simple terms, how the edge condition of roll-over can continue without blowing up if statements using it.

Typically one saves a copy of the millis() count as the 'current time' and that is used later when compared to a new millis() count to see if a specific time has elapsed or not. How does the roll-over effect that use of logic?

Lefty

It has to do with the way twos-complement arithemetic works. After my belly is full (it's nearly dinner time), I'll make an attempt at a description.

But, knowing the incredible level of information and support that's provided here, someone will very likely beat me to it...

Glad I'm not the only one. :slight_smile:

Assuming standard operating procedure is that you have a main loop executing, and you want to perform some action periodically.

After you perform the action (straight away the first time through the loop), you save the current millis() and continue. Next time around the loop you read the value of millis() again and subtract the value obtained last time you performed the action. If the subtraction results in a number that is larger than your period then you perform the action again and get the new version of millis(), otherwise you don't.

Assume for the sake of this explanation that millis() is only an 8-bit number (the concept extends to 16- and 32-bit numbers equally well).
Also assume you want your periodic function to run every 30 ms, and that the loop takes 10 ms.

First time through the loop you perform your action and get the value of millis():

unsigned char last = millis()

Let's say last = 8. Next time around the loop millis() returns 18, so you do the subtraction:

 00010010 (18: current)
-00001000 (8: last)
---------
 00001010 (10)

this is repeated twice more until millis() returns 38 and then:

 00100110 (38: current)
-00001000 (8: last)
---------
 00011110 (30)

now we would perform the action and last would become 38.
Eventually (after the action had been performed 8 times) last would become 248, then the interesting bits start to happen, the next time through the loop, 10 ms have elapsed so millis() should be 258. 258 cannot be represented in 8 bits, so with rollover millis() would become 2. So now we to the subtraction:

 00000010 (2: current)
-11111000 (248: last)
---------
 00001010 (10)

The arithmetic unit "borrows" a bit (bit 9) and treats the minuend (the number being subtracted from) as if its value was 258 (0x102, 100000001 binary). In other words, inside the CPU the calculation actually looks like this:

 100000010 (258: current)
-011111000 (248: last)
---------
 000001010 (10)

Twice more around the loop and you get this:

 00010110 (22: current)
-11111000 (248: last)
---------
 00011110 (30)

Then you perform your action, set last to 22 and off you go.

Thanks Crimony;

So:
"258 cannot be represented in 8 bits, so with rollover millis() would become 2."
This results in setting an overflow bit in the status register, correct?

And:
"The arithmetic unit "borrows" a bit (bit 9) and treats the minuend (the number being subtracted from) as if its value was 258"Does that mean the current (overflowed) mills() long variable gets to include the overflow bit as a next higher order bit (33rd bit) and therefore can still perform a valid compare calculation?

If so, that makes sense and of course once through this last loop, the new last assumes the new overflowed millis() value, but only 32 bits worth, not the overflow bit?

Lefty

"258 cannot be represented in 8 bits, so with rollover millis() would become 2."
This results in setting an overflow bit in the status register, correct?

Yes. The carry bit is also set. The carry bit is conceptually where the extra bit comes from.

This is how an 8-bit processor can subtract (or add) a 32-bit value from another 32-bit value. The first bytes are subtracted from each other. The second bytes are subtracted from each other including the carry bit. The third bytes are subtracted from each other including the carry bit. Etctera. It's very similar to the way a human would subtract a four digit value from another four digit value; we borrow from the higher order digit when necessary.

"The arithmetic unit "borrows" a bit (bit 9) and treats the minuend (the number being subtracted from) as if its value was 258"
Does that mean the current (overflowed) mills() long variable gets to include the overflow bit as a next higher order bit (33rd bit) and therefore can still perform a valid compare calculation?

Yes. The idea is extended to any number of bytes.

If so, that makes sense and of course once through this last loop, the new last assumes the new overflowed millis() value, but only 32 bits worth, not the overflow bit?

Exactly.

What works for me is to think of integer values as pegs on a "wheel of fortune". Subtracting one moves the wheel one peg in the negative direction. Subtracting two moves the wheel two pegs. Ectectera. The difference between two values is the number of pegs between the two values; even if the zero peg is between the two values. For an 8-bit wheel, adding one is (almost) the same as subtracting 255.

Does that help?

great to know info ;D

Oh I see now, very clever.

Thanks for typing that :slight_smile:

Oh I see now, very clever.

So in response to your original post:

Since your code is using a subtraction, it will work correctly across the millis() rollover without modification.

Since your code is using a subtraction, it will work correctly across the millis() rollover without modification.

When doesn't arithmetic work across the millis() rollover?

When doesn't arithmetic work across the millis() rollover?

The arithmetic always works. Sometimes the results are not what the developer expected.

unsigned long Target;

void setup( void )
{
  Target = millis() + 1000;
}

void loop( void )
{
  if ( millis() >= Target )
  {
    /* Do our thing */
    Target = millis() + 1000;
  }
}

As millis approaches the rollover, there are three possible outcomes....

  • The code works as expected (1:1000 chance + loop takes more than 1ms)
  • The if fires every pass through loop for up to 1 second (999:1000 chance)
  • The if stops firing (1:1000 chance + loop takes more than 1ms) until the next rollover

Just to avoid the potential for confusion in the future, Target should be declared as an unsigned long since this is the type returned by the millis() function.

Target should be declared as an unsigned long

Dang it! I changed the post. Thanks for catching that.

Obviously I didn't get enough sleep. Me thinks it's time for a bit more caffeine...