Using seconds instead of millis() for 136 year rollover

I was looking at some very awkward code that was trying to use elapsed times longer than 49 days. It had millis() rollover detection and millis() resetting with adjustments to timer0_millis, along with a ton of delay(1000)s. I thought that a seconds variable would solve many of the workarounds this code used.

Consider:

// https://wokwi.com/projects/387510129380013057
// code for creating a "seconds" variable from millis()
// that won't roll over for 136 years

const long SecondsInDay = 3600L * 24;
const long SecondsInYear = SecondsInDay * 365;

// time variables to track elapsed seconds for
// 2^32/3600/24/365=136 years before secondsrollover
unsigned long seconds = 0, lastSecondMs = 0;
unsigned long currentMillis = 0;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
}

void loop() {

  if (timeStuff()) { // 1ms tick
    report();
    blinkLedQuarterly();
  }
}

bool timeStuff(void) {
  bool retval = false;
  const unsigned int interval = 1;
  static unsigned long last = 0;
  currentMillis = millis();
  if (currentMillis - last >= interval) {
    retval = true;
    last += interval;
    if (currentMillis - lastSecondMs >= 1000) {
      ++seconds;
      lastSecondMs += 1000;
    }
  }
  return retval;
}

void report(void) {
  const unsigned long interval = 500;
  static unsigned long last = 0;
  if (currentMillis - last >= interval) {
    last += interval;
    Serial.print("ms:");
    Serial.print(currentMillis);
    Serial.print(" Sec:");
    Serial.print(seconds);

    Serial.print(" min:");
    Serial.print(seconds / 60);

    Serial.println();

  };
}

void blinkLedQuarterly(void) {
  static unsigned long last = 0;
  unsigned long interval = 3600L * 24 * 365 / 4;
  if (seconds - last >= interval) {
    last = seconds;
    digitalWrite(LED_BUILTIN, digitalRead(LED_BUILTIN) == HIGH ? LOW : HIGH);
  }
}

... with the commented Wokwi demo at:

image
https://wokwi.com/projects/387510129380013057

By maintaining a seconds variable, you can do low resolution timing with the same BWOD-like mechanism as with millis(), but with periods and intervals up to 136years rather than millis()'s ~49 day limit. By using the lastSecondsMs variable you can get millisecond precision resolution over 136 years if you like, although the Arduino clocks aren't accurate enough to make that resolution meaningful.

With seconds you can do something like:

void blinkLedQuarterly(void) {
  static unsigned long last = 0;
  unsigned long interval = 3600L * 24 * 365 / 4;
  if (seconds - last >= interval) {
    last = seconds;
    digitalWrite(LED_BUILTIN, digitalRead(LED_BUILTIN) == HIGH ? LOW : HIGH);
  }
}

If you find yourself coding around millis() for intervals exceeding 49 days, consider working in seconds instead of millis()--It's 1000x better/simpler for long durations.

(In the other code I was looking at, using seconds will simplify lots of ugliness.)

This solution might be useful for events longer than 49 days, but I wonder how often, or an example of a single event that needs to run uninterrupted for longer than 49 days without any attention,

If you’re using the crystal based cpu timing, you might be better off with an RTC based calendar to trigger these long term events.

1 Like

Maybe extend to 64 bit millis counter like this:

uint64_t millis64bit()
{
  static uint32_t top32bits = 0;
  static uint32_t prev = millis();
  uint32_t now = millis();
  if (now < prev)
  {
    top32bits ++;
  }
  prev = now;
  return (uint64_t(top32bits) << 32) | now;
}

However you have to call millis64bit() at least once every 49 days for it to work...

1 Like

a bit more generic. adjusting MsecPeriod within tics() can provide whatever resolution you want

unsigned long
tics ()
{
    static unsigned long tic;
    const  unsigned long MsecPeriod = 1000;
    static unsigned long msec0;
           unsigned long msec = millis ();
    if (msec >= msec0) {
        msec0 += MsecPeriod;
        tic++;
    }
    return tic;
}

const unsigned long TicPeriod = 5;
      unsigned long tic0;

void
loop (void)
{
    unsigned long tic = tics ();
    if (tic > tic0) {
        tic0 += TicPeriod;

        Serial.println ("tic");
    }
}

void
setup (void)
{
    Serial.begin (9600);
}
1 Like

Most of the purpose for the awkward code and its workarounds is measuring accumulated hours for maintenance. A 1/100th hour or MsecPeriod=36000; tic counter would be efficient.

@MarkT Yes, extending the 32 but timer to 64 bits is a good solution too, but 64 bit numbers could be overkill for the accuracy required or implied. I thought seconds fit well with the micros(), millis() and schemes.

@lastchancename -- Even if an event ran longer than longer than 49 days uninterrupted, one could always break it up into <49day sub-events for time-keeping tasks. Yes, a RTC would be much better for keeping things synchronized with a calendar or day/night cycle and especially across power outages.

64- bit numbers won’t affect the accuracy, only the maximum duration.
The biggest challenge to accuracy over those longer durations will the processor clock

I meant that microsecond resolution interval math for a +/-0.5% resonator in the Arduino is overkill for day-to-month-sized intervals. I haven't done the math until now, but the +/-0.5% is about 8 bits worth of accuracy, so if you're using bit 33 of the 64 bits, 0.5% of that is about bit 25, or about +/-6 hours.

Unsigned long longs are more expensive for arithmetic and storage, and are awkward to output, and the precision doesn't get you much more accuracy than about ~8 bits.

Use a mains frequency clock.

1 Like

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.