Arduino millis() looping after 45.5104 days

I am having a small issue with millis() rollover on one of my nodes that monitors its own uptime. On most nodes that do similar, the millis() rollover happens after approximately 49.7049 days.
49.7049 * 24hrs * 60mins * 60seconds = 4,294,503.36 which is pretty close to the max value of a uint32_t, so I'm sure its being hit if you multiply by 1000
45.5104 * 24hrs * 60mins * 60seconds = 3,932,098.56 which is not very close to the max value of a uint32_t.

I noticed 45.5104 * 24hrs * 60mins = 65,534.976 which is the max value of uint16_t.

My code to calculate minutes and log to MQTT is as follows:

    char str[4];
    itoa(ip[3], str, 10);
    char str1[] = "Node    :      \r\n";
    memcpy(&str1[5], &str, strlen(str));
    uint32_t seconds = millis() / 1000UL;
    uint32_t minutes = seconds / 60UL;
    char strMinutes[7];
    utoa(minutes, strMinutes, 10);
    memcpy(&str1[9], &strMinutes, strlen(strMinutes));
    client.publish("upTime", str1);

then on a NodeRed node, receiving the MQTT message, I have the following:

var myArray = msg.payload.split(":");
msg.topic = myArray[0];
msg.payload = myArray[1];
msg.payload = msg.payload / 60;
msg.payload = msg.payload / 24;
msg.payload = msg.payload.toFixed(4);
return msg;

Is my math wrong, or is this device really looping after 45.5104 days?

Working code from another node:

  uint32_t seconds = millis() / 1000UL;
  uint32_t minutes = seconds / 60UL;
  uint32_t hours = minutes / 60UL;
  uint8_t days = hours / 24UL;
  seconds %= 60;
  minutes %= 60;
  hours %= 24;

  sprintf_P(buffer, PSTR("%u days, %lu hours %lu minutes %lu"), days, hours, minutes, seconds);

The failing code is running on an Arduino Pro Micro. Working code is on Pro Micro, Mega and Nano.

This is hard for me to debug, because it takes about 45 days to see if any changes worked.

Replace millis() with micros() and you can debug every hour and 10 minutes.

Or replace millis() with a new millisWithOffset()

uint32_t millisWithOffset()
{
  //  offset = max uint32_t minus about 10 seconds. adjust to needs.
  return millis() + 4294960000;   
}
1 Like

In the past I have had a similar problem, I found this useful function:

int millisRollover() {
  // get the current millis() value for how long the microcontroller has been running
  //
  // To avoid any possiblity of missing the rollover, we use a boolean toggle that gets flipped
  //   off any time during the first half of the total millis period and
  //   then on during the second half of the total millis period.
  // This would work even if the function were only run once every 4.5 hours, though typically,
  //   the function should be called as frequently as possible to capture the actual moment of rollover.
  // The rollover counter is good for over 35 years of runtime. --Rob Faludi http://rob.faludi.com
  //
  static int numRollovers=0; // variable that permanently holds the number of rollovers since startup
  static boolean readyToRoll = false; // tracks whether we've made it halfway to rollover
  unsigned long now = millis(); // the time right now
  unsigned long halfwayMillis = 2147483647; // this is halfway to the max millis value (17179868 for earlier versions of Arduino)

  if (now > halfwayMillis) { // as long as the value is greater than halfway to the max
    readyToRoll = true; // you are ready to roll over
  }

  if (readyToRoll == true && now < halfwayMillis) {
    // if we've previously made it to halfway
    // and the current millis() value is now _less_ than the halfway mark
    // then we have rolled over
    numRollovers++; // add one to the count the number of rollovers
    readyToRoll = false; // we're no longer past halfway
  } 
  return numRollovers;
}

It could let you getting the real elapsed time, by adding the "rollover time" by "numRollovers" times.

Hope it helps.

2 Likes

Well damn, I should have thought of something like that. You DID, so thank you!

I am pretty sure now the line utoa(minutes, strMinutes, 10); just needed to be changed to ultoa(minutes, strMinutes, 10); because the max value of minutes is > 65,535.

It seems to be rolling over at the correct time now!

1 Like

Your code is just one step away from a uint64_t millis64() function

uint64_t millis64()
{
  static uint32_t numRollOvers = 0;
  static boolean readyToRoll = false; 
  const uint32_t halfwayMillis = 2147483647; 
  uint32_t now = millis();

  if (now > halfwayMillis) {
    readyToRoll = true;
  }

  if (readyToRoll  && now < halfwayMillis) {
    numRollOvers++;
    readyToRoll = false;
  } 

  //  convert numRollOvers + now to 64 bit timestamp.
  uint64_t _now64 = numRollOvers;
  _now64 <<= 32;    //  most significant bits
  _now64 |= now;    //  least significant bits
  return _now64;
}

Optimizations are possible.
Printing 64 bit can be done with - GitHub - RobTillaart/printHelpers: Arduino library to help formatting data for printing

1 Like

@docdoc

squeezed some micros out of it and added offset support (for testing etc).

uint64_t millis64(uint32_t offset = 0)
{
uint32_t now = 0;
static uint64_t now64 = 0;
static bool flag = false;
now = millis() + offset;
if ((now & 0x80000000) == 0)
{
if (flag)
{
flag = false;
now64 += 0x100000000; // 1 << 32;
}
}
else
{
flag = true;
}
return now64 | now;
}

micros64() idem


update:

found a bug
As the 64 part of now64 is static it can occur that if one changes the offset from large to small, that the static now64 is (1<<32) too high.

Solution: add the offset at the end.

uint64_t millis64(uint32_t offset = 0)
{
  static uint64_t now64 = 0;
  static bool flag = false;
  uint32_t now = millis();

  if ((now & 0x80000000) == 0)
  {
    if (flag)
    {
      flag = false;
      now64 += 0x100000000;  //  1 << 32;
    }
  }
  else
  {
    flag = true;
  }
  return (now64 | now) + offset;
}
1 Like