2038 bug: any remedy in MCU world?

Can anything simple be done to avoid 2038 bug issues for the Arduino boards or ESP32 boards?

This post says that libraries have switched from int32_t to uint32_t so we have an extension to 2106, but my project is on ESP32 using the Arduino framework and I have confirmed that time_t is 4 bytes.

(I know 2038 is a ways away, but 14 years is not forever and I'd like the project I'm working on to last at least that long. More to the point, I don't want to have to put a to-do item on my calendar in 13 years to deal with the upcoming bug.)

It's not worth a huge painful workaround to me, but if there was a relatively simple way to do it, that'd be great. I haven't been able to google any answers on this.

You can use the 28-year rollback trick to buy yourself some time.

If you don't care about Easter, or holidays tied to Easter, or phases of the moon, tides, etc., then for all intents and purposes, the calendar repeats every 28 years (within the interval 1901~2099 at least). Leap years and days of the week match up perfectly after exactly 28 years.

Example: We are now in the year 2024, which is a leap year. Twenty-eight years ago, it was 1996, which was also a leap year. Today is Wednesday, January 3, 2024. Twenty-eight years ago today, it was January 3, 1996, which was also a Wednesday.

I suppose you could jerry-rig date get, set, and print routines to add or subtract 28 as necessary, as a workaround to buy you an extra 28 years.

3 Likes

Good tip, thanks!

Why the "but"? Do you think there is a problem? int32_t and uint32_t are both four bytes, just different ways of interpreting the binary content.

1 Like

Right, sorry, forgot to mention that I also tried to determine the type by inspecting the code and it seemed like it was signed... but repeating that effort now I don't know how I came to that conclusion.

Regardless, does this code demonstrate that it's using a signed int internally?

  time_t myt = 0;
  struct tm* tm = localtime(&myt);
  tm->tm_year = 2100-1900;
  Serial.printf("year is %d\n", tm->tm_year+1900);
  myt = mktime(tm);
  tm = localtime(&myt);
  Serial.printf("year is %d\n", tm->tm_year+1900);

output:

year is 2100
year is 1963

(edit: I note that 2100-2038 ~= 63)

I'm definitely not experienced in C date manipulation so maybe I'm doing something dumb there...

tm is a structure, with several members or components, like Second, Minute, Day, etc.

This won't work as you expect, because the members of tm are all unsigned 8 bits and the addition overflows:

  Serial.printf("year is %d\n", tm->tm_year+1900);

time_t is a "typedef", which will be either signed or unsigned, 32 bits.
It is the number of seconds since some arbitrary date or "epoch". Take a look at how that is defined in the library. Best if it is unsigned!

In addition, there are these conventions in Arduino TimeLib.h

//convenience macros to convert to and from tm years 
#define  tmYearToCalendar(Y) ((Y) + 1970)  // full four digit year 
#define  CalendarYrToTm(Y)   ((Y) - 1970)
#define  tmYearToY2k(Y)      ((Y) - 30)    // offset is from 2000
#define  y2kYearToTm(Y)      ((Y) + 30)   

@savel, can you give a full example and tell which Arduino board you use, which library and what you use to compile it.
There are many "time" libraries.
The most used libraries are the Adafruit RTClib and the PJRC TimeLib. The normal 'C' time library was not used on Arduino boards, but today it is possible to use that as well. Some Arduino boards have a RTC in the processor, so the Arduino Team made a "time" library for that.

TimeLib: unsigned long
avr-libc uint32_t
Adafruit RTClib: it does not have a "time_t", it uses a "uint32_t" for the epoch/unix time. That is unsigned.

In my opinion, 'time_t' should be "unsigned", but I think that there are still build-environments today which use "signed".
Wikipedia has a page about the year 2038. To be honest, it is not looking good: https://en.wikipedia.org/wiki/Year_2038_problem#Solutions


A few files on my computer have this:

#define _TIME_T_ long
typedef _TIME_T_ time_t;

All those files are in the "tools" section, for example:

  • arduino15\Packages\Arduino\Tools\arm-none-eabi-gcc\4.8.3-2014q1\arm-none-eabi\Include\Machine\Types.h
  • arduino15\Packages\Arduino\Tools\arm-none-eabi-gcc\7-2017q4\arm-none-eabi\Include\Sys\_types.h
  • arduino15\Packages\Esp32\Tools\riscv32-esp-elf-gcc\esp-2021r2-patch5-8.4.0\riscv32-esp-elf\Include\Sys\_types.h

It honestly might be less work to make your own library than to try searching for one that you are satisfied with.

Here's a start:

int days_in_month (int y, byte m) {
  // Fourth, eleventh, ninth, and sixth,
  // thirty days to each we fix.  
  if ((m==4)||(m==11)||(m==9)||(m==6)) return 30;  
  // Every other, thirty-one,
  // except the second month alone,
  if (m!=2) return 31;
  // which hath twenty-eight, in fine,
  // till leap-year give it twenty-nine.
  if ((y%400)==0) return 29; // leap year
  if ((y%100)==0) return 28; // not a leap year
  if ((y%4)==0)   return 29; // leap year
  return 28; // not a leap year  
}

If you dig through the typedefs in the source code for ESP32, you'll see that 'time_t' is indeed defined as a 'long' (aka int32_t). So, it's Posix time() function will roll over at UTC: Tuesday, January 19 2038 03:14:07. Immediately after roll over, the time will be UTC: Friday, December 13 1901 20:45:52:

void printTimeInfo (time_t epchoTime);

void setup() {
  time_t timeVal;

  Serial.begin(115200);
  delay(2000);

  timeVal = 1704373697;
  printTimeInfo(timeVal);
  Serial.printf("\n");

  timeVal = 0;
  printTimeInfo(timeVal);
  Serial.printf("\n");

  timeVal--;
  printTimeInfo(timeVal);
  Serial.printf("\n");

  timeVal = 0x7FFFFFFF;
  printTimeInfo(timeVal);
  Serial.printf("\n");

  timeVal++;
  printTimeInfo(timeVal);
  Serial.printf("\n");
}

void printTimeInfo (time_t epchoTime) {
  char timeString[100];
  tm *timeinfo;

  Serial.printf("Epcho Time = 0x%08X\n", static_cast<uint32_t> (epchoTime));
  timeinfo = gmtime(&epchoTime);
  strftime(timeString, 100, "UTC: %A, %B %d %Y %H:%M:%S %Z", timeinfo);
  Serial.printf("%s\n", timeString);
  Serial.printf("tm Struture Values:\n");
  Serial.printf("Year = %d, Mon = %d, Day = %d\n", timeinfo->tm_year, timeinfo->tm_mon, timeinfo->tm_mday);
  Serial.printf("Hour = %d, Min = %d, Sec = %d\n", timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec);
  Serial.printf("DoY = %d, DoW = %d, IsDST = %d\n", timeinfo->tm_yday, timeinfo->tm_wday, timeinfo->tm_isdst);
}

void loop() {
}
Epcho Time = 0x6596ADC1
UTC: Thursday, January 04 2024 13:08:17 GMT
tm Struture Values:
Year = 124, Mon = 0, Day = 4
Hour = 13, Min = 8, Sec = 17
DoY = 3, DoW = 4, IsDST = 0

Epcho Time = 0x00000000
UTC: Thursday, January 01 1970 00:00:00 GMT
tm Struture Values:
Year = 70, Mon = 0, Day = 1
Hour = 0, Min = 0, Sec = 0
DoY = 0, DoW = 4, IsDST = 0

Epcho Time = 0xFFFFFFFF
UTC: Wednesday, December 31 1969 23:59:59 GMT
tm Struture Values:
Year = 69, Mon = 11, Day = 31
Hour = 23, Min = 59, Sec = 59
DoY = 364, DoW = 3, IsDST = 0

Epcho Time = 0x7FFFFFFF
UTC: Tuesday, January 19 2038 03:14:07 GMT
tm Struture Values:
Year = 138, Mon = 0, Day = 19
Hour = 3, Min = 14, Sec = 7
DoY = 18, DoW = 2, IsDST = 0

Epcho Time = 0x80000000
UTC: Friday, December 13 1901 20:45:52 GMT
tm Struture Values:
Year = 1, Mon = 11, Day = 13
Hour = 20, Min = 45, Sec = 52
DoY = 346, DoW = 5, IsDST = 0

More tests.

C++ has probably better ways to determine the type of a variable, and I didn't use the code by gfvalvo, but this small test is good enough for me:

void setup() 
{
  Serial.begin(115200);
  Serial.print("sizeof(time_t) = ");
  Serial.println(sizeof(time_t));

  Serial.println("Testing time_t, a negative number is signed.");
  time_t x = 0xFFFFAAAACCCC3333;
  Serial.print("Result = ");
  Serial.println(x);
}

void loop() {}

Results:

Arduino Uno with TimeLib : unsigned 32 bit (Wokwi test)
Arduino Uno with avr-libc <time.h> : unsigned 32 bit (Wokwi test)
ESP32 : signed 32 bit (Wokwi test)
Raspberry Pi Pico : signed 64 bit (Wokwi test)

The ESP32 has a problem with the year 2038. I wonder if the Arduino UNO R4 has it as well.

BTW, it's not a bug. It's a design limitation.

1 Like

Thanks everyone. Not to beat a dead horse, but I found the same with this simple code:


#include <Arduino.h>
#include <time.h>
void setup()
{
  Serial.begin(115200);
  while (!Serial) delay(10);

  char tempstring[128];
  struct tm* tm = (struct tm*)malloc(sizeof(struct tm));
  tm->tm_year = 2038-1900;
  tm->tm_mon = 0;
  tm->tm_mday = 19;
  tm->tm_hour = 3;
  tm->tm_min = 14;
  tm->tm_sec = 0;
  struct timeval tv = {mktime(tm), 0};
  settimeofday(&tv, NULL);
  getLocalTime(tm, 1);
  strftime(tempstring,sizeof(tempstring), "%Y-%m-%d %H:%M:%S", tm);
  Serial.printf("Time: %s\n", tempstring);
  delay(5000);
  getLocalTime(tm, 1);
  strftime(tempstring,sizeof(tempstring), "%Y-%m-%d %H:%M:%S", tm);
  Serial.printf("Time: %s\n", tempstring);
  delay(5000);
  getLocalTime(tm, 1);
  strftime(tempstring,sizeof(tempstring), "%Y-%m-%d %H:%M:%S", tm);
  Serial.printf("Time: %s\n", tempstring);
}

void loop() {;}

Output:

Time: 2038-01-19 03:14:00
Time: 2038-01-19 03:14:05
Time: 1901-12-13 20:45:54

(This is an ESP32 board using arduino framework on PlatformIO/VSCode on linux.)

So if I want to work around this, am I correct that if I'm getting the system time I need to:

  • get system time into uint32_t, up-cast it to a 64bit int (unsigned?)
  • if system time claims to be earlier than say 01-01-2024, add max possible value of int32_t to it (==LONG_MAX for me)
  • use some more modern time library to compute an analog to tm that can handle 64bit time? And use that for whatever time comparisons and such that I may need to do?

...and if I want to set the system clock I can just use 32bit tm, set the values to some potentially >2038 time, and mktime and it will roll over when it's setting the internal clock.

Sound workable?

One concern is that I'm using NTP to set the system time and I'm wondering how the configTime() call will set things after 2038... i.e. will it just roll over in the same way when the NTP result comes back...

...sorry, I didn't quite follow this: in my IDE it looks like tm_year is an int? And even if it was uint8_t, wouldn't it automatically upcast? E.g. this prints 2024:

  uint8_t var = 4;
  Serial.printf("var is %d\n", var+2020);

I have been caught by the fact that C casts arithmetic between signed and unsigned into unisgned... that took me a while to figure out. :slight_smile:

In which library?

In the Arduino TimeLib.h to which I referred, the year element is an unsigned byte, with offset 1970.

typedef struct  { 
  uint8_t Second; 
  uint8_t Minute; 
  uint8_t Hour; 
  uint8_t Wday;   // day of week, sunday is day 1
  uint8_t Day;
  uint8_t Month; 
  uint8_t Year;   // offset from 1970; 
} 	tmElements_t, TimeElements, *tmElementsPtr_t;

I'm very unclear on which libraries are operating under the hood... as a beginner it has been hard to suss that out, but I'm doing "#include time.h" in platformIO with the arduino framework, for what that's worth. I hover over tm->year and it shows "int tm::tm_year". F12 into tm and it goes to toolchain-xtensa-esp32/xtensa-esp32-elf/sys-include/time.h

time.h in the ESP32 Arduino Core:

struct tm
{
  int	tm_sec;
  int	tm_min;
  int	tm_hour;
  int	tm_mday;
  int	tm_mon;
  int	tm_year;
  int	tm_wday;
  int	tm_yday;
  int	tm_isdst;
#ifdef __TM_GMTOFF
  long	__TM_GMTOFF;
#endif
#ifdef __TM_ZONE
  const char *__TM_ZONE;
#endif
};

Ah, found the ESP-IDF issue here.
...it was updated to 64bit in ESP-IDF v5.0.

Trying to figure out what version of the ESP-IDF is installed in platformio inside of vscode has been a bit of a rabbit hole... The PIO extension is using ESP32 platform v6.4.0 which according to this link should have the ESP-IDF v5.1.1 in it.

However, ~/platformio/platforms/espressif32/platform.json shows:

    "framework-espidf": {
      "type": "framework",
      "optional": true,
      "owner": "platformio",
      "version": "~3.50101.0",
      "optionalVersions": ["~3.40405.0"]
    },

...and of course my own testing shows the latter seems to be the more accurate indicator.

I'm working on sorting that out. But it seems like it might be easier to ignore the issue for now, and whenever they get ESP-IDF >5.0 in there, re-flash.

1 Like

I've never used Platformio and have no idea how the ESP32 IDF framework is incorporated into it. But if you use a different IDE, you'll have more options:

  1. You could, of course, switch your entire project over to ESP-IDF and choose the version you want.

  2. Arduino IDE 1.x, Arduino IDE 2.x, Eclipse / Sloeber, and probably others allow you to choose which Arduino ESP32 core is used to build your project. Here you can find the list of Arduino ESP32 core releases. Each entry indicates which version of ESP-IDF it's based on. The first release to use IDF v5.0 (or later) is v3.0.0 that is currently at the Alpha 3 release stage. It's actually based on ESP-IDF v5.1.

UPDATE:
I just noticed that both Arduino IDE and Eclipse / Sloeber only allow selecting up to Arduino ESP32 Core v2.0.14. That's the last released (non-Alpha) version. Not sure how to select beyond that.

Thanks! I heard similar from platformIO people on another forum: "by default PlatformIO still uses the 8.4.0 toolchain, as does the Arduino IDE with the latest stable 2.0.14 release." "Per https://github.com/platformio/platform-espressif32/issues/1211 you can use a third-party platform that uses the updated Arduino-ESP32 3.x package with the corresponding toolchain upgrade." " This will hopefully be back-integrated into the official Espressif32 at some point. However, Arduino-ESP32 3.x is not even out of Alpha yet."

Re: the version in use in vscode/pio, it's apparently 4.4.5, as revealed by ESP_IDF_VERSION_* constants, so neither the github link nor the .json file were the same data, for reasons that are currently beyond me.

I haven't the slightest clue what "8.4.0 toolchain" means.

I haven't the slightest idea what most of this means :slight_smile: but for now I'm personally summarizing it as "the layers of frameworks/platforms/boards/cores/libraries/toolchains/etc are complicated and the time_t upgrade is in the pipeline and it's probably not worth me messing with the default configuration because I don't understand it well enough to deal with unforeseen consequences."