64-Bit millis() overflows after half a billion years

I'm not sure if this has been done before, but anyway I have a custom breadboard made up here and I wanna torture test it for a very long time -- maybe even longer than 50 days. So I've written a 64-Bit millis() function that won't overflow until the microcontroller has been powered on for half a billion years:

inline long long unsigned millis64(void) noexcept
{
    static thread_local long unsigned timestamp[2u] = { 0u, 0u };
    static_assert( sizeof(timestamp) == sizeof(long long unsigned) );

    register long unsigned const tmp = millis();

#if BYTE_ORDER == BIG_ENDIAN
    if ( tmp < timestamp[1u] ) ++timestamp[0u];
    timestamp[1u] = tmp;
#else
    if ( tmp < timestamp[0u] ) ++timestamp[1u];
    timestamp[0u] = tmp;
#endif

    return *static_cast<long long unsigned*>(static_cast<void*>(timestamp));
}

On my ES32 using Arduino IDE with all the default compiler options, this gets compiled to:

400d2c10 <_Z8millis64v>:
400d2c10:       004136          entry   a1, 32
400d2c13:       15e1e5          call8   400e8a30 <millis>
400d2c16:       e3ce70          rur.threadptr   a12
400d2c19:       f51e81          l32r    a8, 400d0094 <_stext+0x74>
400d2c1c:       809c80          add     a9, a12, a8
400d2c1f:       0129b2          l32i    a11, a9, 4
400d2c22:       06bab7          bgeu    a10, a11, 400d2c2c <_Z8millis64v+0x1c>
400d2c25:       0029b2          l32i    a11, a9, 0
400d2c28:       bb1b            addi.n  a11, a11, 1
400d2c2a:       09b9            s32i.n  a11, a9, 0
400d2c2c:       88ca            add.n   a8, a8, a12
400d2c2e:       18a9            s32i.n  a10, a8, 4
400d2c30:       0828            l32i.n  a2, a8, 0
400d2c32:       0a3d            mov.n   a3, a10
400d2c34:       f01d            retw.n
400d2c36:       00              .byte 00
400d2c37:       00              .byte 00

So we're talking around about 15 more instructions than if we had just called the 32-Bit millis(). The CPU in the ESP32 runs at 240 MHz and therefore has a CPU cycle of 4 nanoseconds, and so 15 instructions should take less than a hundred nanoseconds.

Of course if this function gets expanded inline then you can take away two or three of those instructions (entry, retw.n, and also maybe the the final mov). Also on a single-core microcontroller, you can take out the rur.threadptr instruction.

No need to write a function. You can do that with millis()

I'm not a mind reader but I'll try to extrapolate on what you're saying here . . .

If you're saying that we can just do the following:

    if ( (millis() - timestamp) > 5000u ) return;

to ensure that the timing is still correct after millis() overflows, then that's fair enough.

What I need though is to keep count of how long my device has been powered on. When it's been powered on for 52 days, I need it to say 52 days (and not 2 days).

shouldn't that be a memcpy() into an uint64_t rather than a series of cast hacks?

const unsigned long MILLIS_PER_DAY = 1000UL * 60 * 1440;

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

void loop()
{
    unsigned long currentTime = millis();
    static unsigned long prevDay = 0;
    static byte dayCount = 0;
    if (currentTime - prevDay >= MILLIS_PER_DAY)
    {
        dayCount++;
        Serial.println(dayCount);
        prevDay = currentTime;
    }
}

I read somewhere that our galaxy is scheduled to collide with another one in about four billion years, so to keep track of that, consider using 9 bytes instead of int64_t.

Or, use seconds for the timestamp, instead of milliseconds.

A 64-bit millis() gives the safe feeling that millis() can be requested once a year. That is not possible if millis() is the base for the 64-bit.

There is a "uint64_t" that even works on a Arduino Uno, but Serial.println() can not print it.

In order for millis64() to remain accurate, it must be called at least once every 49 days. If the program goes 53 days without calling millis64(), and then calls millis64(), the figure returned will be inaccurate by 50 days.

UKHeliBob posted code above in which he counts the days. I want to have 64-Bit timestamps because I could have the following scenario:

Device powered on on 8th November 2023 at 2:00pm
Device received its first SMS text message on 10th November 2023
Device received its first phone call on 8th June 2024

I want to be able to easily subtract the SMS time from the phone call time to see how many days there were between them.

RTC "real time clock" modules are inexpensive and easy to use.

Using a running counter of whatever resolution to track long periods of time and figure time betweens this and that of large magnitude is not a good solution.

a7

I get my microcontroller to keep track of how long the superloop takes to iterate, and I print a histogram to the Serial comms:

Total superloop iterations since power on:    405203    Total Uptime: 18.49 seconds
     0ms -      1ms =     98%  |||||||||||||||||||||||||||||||||  (total = 396490 iterations) [no command], 
     1ms -      2ms =      2%  |                                  (total =   8706 iterations) [no command], 
     2ms -      4ms =      1%  |                                  (total =      1 iterations) [no command], 
     4ms -      8ms =      1%  |                                  (total =      1 iterations) [no command], 
     8ms -     16ms =      0%                                     (total =      0 iterations) 
    16ms -     32ms =      0%                                     (total =      0 iterations) 
    32ms -     64ms =      0%                                     (total =      0 iterations) 
    64ms -    128ms =      1%  |                                  (total =      2 iterations) [no command], 
   128ms -    256ms =      1%  |                                  (total =      3 iterations) [no command], 
   256ms -    512ms =      0%                                     (total =      0 iterations) 
   512ms -   1024ms =      0%                                     (total =      0 iterations) 
  1024ms -   2048ms =      0%                                     (total =      0 iterations) 
  2048ms -   4096ms =      0%                                     (total =      0 iterations) 
  4096ms - infinity =      0%                                     (total =      0 iterations) 

If my device is up for 2 months, then the count of superloop iterations, as well as the millisecond timestamps, must all be 64-Bit.

Please confirm that you need millisecond accuracy for events that play out over months.

Then I say the built in clock upon which you base these high precision measurements will only be as accurate as that clock - probably not as good as a modern RTC.

Even using the best (inexpensive) RTC you are not going to be able to rely on 0.001 second accuracy.

If you are srsly serious, you need to use some means of getting the actual time of day from some external high accuracy source like being served it from the internets.

Just sayin'.

a7

My priority here is to keep the monetary cost down. If I can remove a resistor then I'll remove a resistor. I want to design the cheapest circuit that can do what I want it to do. So I'm not putting extra chips like RTC's on the board.

I'm not even sure that I need wifi, as I also have a SIM module to receive txts and phone calls, and so I might swap out the ESP32 for a cheaper less-powerful microcontroller.

how will you know the date ?

You've heard the expression "Penny-wise but Pound-foolish"?

Stick with the ESP32, then you get esp_timer_get_time(). This will give you uptime as int64_t with microsecond resolution. That's good for > 290,00 years without rollover. There's no minimum call frequency required. As long as you call it more often then once every 290,00 years, you will be fine.

What day of the week will it be when our galaxies collide
.

The 'memcpy' would only be necessary if the alignment were incorrect. Actually you made me realise just now that I should use 'alignof' to ensure this. So instead of:

static thread_local long unsigned timestamp[2u] = { 0u, 0u };

I should have:

static thread_local alignas(long long unsigned) long unsigned timestamp[2u] = { 0u, 0u };

Also . . . to be hyperpedantic . . . if I don't want undefined behaviour here, I should probably use 'std::start_lifetime_as' but I don't think that's available yet on the Arduino compiler. Also I could use a union{ }.

The proper way to do this would be to have two separate 32-Bit values, and to combine them as follows:

    return (static_cast<long long unsigned>(rollover) << 32u) | current_millis;

but I didn't want the extra CPU cycles for the bit shift and bitwise OR.

Here's the code I'm currently using:

#ifdef ARDUINO_ARCH_ESP32
#   define millis64() (esp_timer_get_time() / 1000u)
#else
    inline long long unsigned millis64(void) noexcept
    {
        static thread_local alignas(long long unsigned) long unsigned timestamp[2u] = { 0u, 0u };
        static_assert( sizeof(timestamp) == sizeof(long long unsigned) );
        register long unsigned const tmp = millis();
#   if BYTE_ORDER == BIG_ENDIAN
        if ( tmp < timestamp[1u] ) ++timestamp[0u];
        timestamp[1u] = tmp;
#   else
        if ( tmp < timestamp[0u] ) ++timestamp[1u];
        timestamp[0u] = tmp;
#   endif
        return *static_cast<long long unsigned*>(static_cast<void*>(timestamp));
    }
#endif

You might want replace the crystal with a multiple of 32,768, You will get more accurate time.

I'm scraping every penny off the price tag for this device. If I could sell it for $5 and only make $1 profit then that would be okay. So if anything can be done in software instead of having extra components, then I'll code it rather than wire it.

To my understanding of such type punning issue, no it's not enough to be compliant with the C++ specification (even if GCC will likely do what you expect)

When it is needed to interpret the bytes of an object as a value of a different type, std::memcpy or std::bit_cast(since C++20) can be used

see
➜ reinterpret_cast conversion - cppreference.com
➜ What is Strict Aliasing and Why do we Care? · GitHub