2038年問題有無確認 Arduino (AVR), esp32, nRF51822, ラズパイ(32bit)

(2021/3初稿。
　2023/12にESP32の最新状況を加筆、
　2025/2ラズパイ64bitはコメントに結果だけ記載しました）

# これまでのあらすじ
「え、Arduino って(time_t)0 が2000/1/1 なの？しかも time_t が uint32_t だと？2038年問題対応済み？」
そういう情報をネットで見たので確かめようと思ったが…


# 2038年問題とは
C言語(あるいは派生形である C++ やArduino言語) において、以下の3条件を満たしている場合
　1. `time_t` 型が 32bit 符号付き整数(int32_t等)で表現されており、
　2. `(time_t)0` が指す日時が UNIX EPOCH TIME(1970/01/01 00:00:00)であり、
　3. `time_t` が1増えると1秒増えるよう実装されている

その場合に、`time_t`の最大値が 0x7fffffff (2038/01/19 03:14:07に相当)になり、その先を表現できないという問題。
具体的には 0x7fffffff に1足すと 0x80000000 になるが、これが `int32_t` 的にはマイナスの値になるため、いきなり過去を表現した値に化けてしまう、というような問題が起こる。

たとえば2の`(time_t)0` が指す日時が2000/01/01 00:00:00 になっていれば、30年ずれるので、それだけで2038年問題は2068年問題になる。あとは1をunsigned にしたり64bitにしたりすることで未来を表現できる幅が拡がる。


# Arduino の確認はどうすれば？
`time_t` の型はヘッダのどこかに書いてあるはずだけど面倒だし、`(time_t)0` を渡した `mktime` が2000年1月1日を返すかどうかはライブラリ依存なので、ライブラリを呼ぶプログラムを書いみた方が早いでしょう。ついでにArduinoIDEで開発している esp32 や nRF51822(micro:bit や AE-TYBLE16)あたりも確認してしまいましょう。（おまけにラズパイのgcc with glibcやらでも）


# 確認したいこと
仕様確認という意味では4点。

1. `time_t` は何ビットの整数か
`sizeof(time_t)` でバイト数が解るので8倍しましょう。
本当に整数型かどうかはヘッダファイルを見るべきですが、面倒なのでfloat や double、long double に　1.1 を代入して、time_t にキャストしてみます。どれかの型であればキャスト前後で一致しますが、整数型なら 1 にされるので一致しません。

2. エポックタイム : `(time_t)0` がいつをさすか
1970/01/01 00:00:00 (UTC) が UNIX epoch ですが、
AVR Arduino では2000/01/01 00:00:00 (UTC)であるらしく、
`time_t` がsigned 32bit integer であっても、これだけで2068年まで延命されますね。
`mktime` で `tm` 構造体に変換して確認しましょう。

3. `time_t` は signed か unsigned　か
singed か unsigned か、は `(time_t)0` と `(time_t)-1` の比較で解ります。
マイナスが扱えるなら`(time_t)-1` は `(time_t)0` に対して過去になりますし、が未来になるなら unsigned となります。
（型としてはsignedなんだけどライブラリがunsignedに変換して処理、などとしているのは実質unsignedという結果主義で。そんな実装無いとは思いますが）

4. `time_t` は1秒刻みか？ 
・signed 32bit integer の最大値 0x7fffffff がいつを指すか
・signed 32bit integer の最大値から1を減じた 0x7ffffffe がいつを指すか
を見比べれば良いでしょうか。

さらに

・signed 32bit integer の最大値に1を加えた 0x80000000 がいつを指すか
・unsigned 32bit integer の最大値 0xffffffff （これはsigned 32bit だと-1と扱われる）がいつを指すか
・unsigned 32bit integer の最大値に1を加えた33bit値 0x100000000 を入れたらいつを指すか（内部64bitになっているかどうか）

なんてのも見てみれば、2038年問題がどうなっているか解るでしょう。


# 検証プログラム

```cpp


#include <time.h>

char buf[1000];

void test_tm(time_t t)
{
  struct tm *p;
  p = gmtime(&t);
  sprintf( buf, "%d/%02d/%02d %02d:%02d:%02d\n", p->tm_year + 1900, p->tm_mon +1, p->tm_mday, p->tm_hour, p->tm_min, p->tm_sec );
  Serial.print(buf);
}

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

  // time_t のバイト数、ビット数
  sprintf( buf, "sizeof(time_t) : %d bytes (%d bits) \n", sizeof(time_t), sizeof(time_t)*8 );
  Serial.print(buf);

  // time_t は整数型か浮動小数点型か
  float a = 1.1;
  double b = 1.1;
  long double c = 1.1;
  if ( a == (time_t)a || b == (time_t)b || c == (time_t)c ) {
    Serial.print( "floating point number type\n" );
  }
  else {
    Serial.print( "integer type\n" );
  }

  Serial.print( "(time_t) 0 :" ); // epoch 次第で結果が変わる
  test_tm( (time_t)0 );

  Serial.print("(time_t)-1 :");// time_t の定義次第で結果が変わる
  test_tm( (time_t)-1 );

  Serial.print("(time_t)0x ffff ffff :"); // unsigned 32bit int の正の最大値
  test_tm( (time_t)0xffffffff );

  Serial.print("(time_t)0x 7fff ffff :"); // signed 32bit int の正の最大値
  test_tm( (time_t)0x7fffffff );

  Serial.print("(time_t)0x 7fff fffe :"); // signed 32bit int の正の最大値 -1
  test_tm( (time_t)0x7ffffffe );

  Serial.print("(time_t)0x 8000 0000 :"); // signed 32bit int の正の最大値 +1
  test_tm( (time_t)0x80000000 );

  Serial.print("(time_t)0x10000 0000 :"); // 33bit を time_t にキャストしたら？
  test_tm( (time_t)0x100000000 );
}

void loop()
{
  delay(10000); //なにもしない
}
```

# Arduino (ATmega328P)での実行結果
```
sizeof(time_t) : 4 bytes (32 bits) 
integer type
(time_t) 0 :2000/01/01 00:00:00
(time_t)-1 :2136/02/07 06:28:15
(time_t)0x ffff ffff :2136/02/07 06:28:15
(time_t)0x 7fff ffff :2068/01/19 03:14:07
(time_t)0x 7fff fffe :2068/01/19 03:14:06
(time_t)0x 8000 0000 :2068/01/19 03:14:08
(time_t)0x10000 0000 :2000/01/01 00:00:00
```

1. time_t は 32bitで、整数です。
2. エポックはたしかに 2000/1/1 00:00:00 でした。
3. time_t は unsigned です。（-1がマイナスにならず未来を指しているので）
4. 0x7fff ffff と 0x7xfff fffe の差が1秒なので1秒刻み

0xffff ffff の2136/02/07 06:28:15 が表現できる最大値で（-1を uint32_t にキャストするとこれになります）
その1秒後は0x1 0000 0000 のはずが、32bitに収まっておらずキャストすると 0x0000 0000 に削られてしまうので、指す時刻も 2000/1/1 00:00:00 に戻ってしまいます。


2136年問題が発生しうるのですが、自分は流石に死んでると思うので気にしないことに。


# ESP32-DevkitC (Arduino-ESP32 v1.0.5) やmicro:bit (nRF51822) の実行結果
```

sizeof(time_t) : 4 bytes (32 bits) 
integer type
(time_t) 0 :1970/01/01 00:00:00
(time_t)-1 :1969/12/31 23:59:59
(time_t)0x ffff ffff :1969/12/31 23:59:59
(time_t)0x 7fff ffff :2038/01/19 03:14:07
(time_t)0x 7fff fffe :2038/01/19 03:14:06
(time_t)0x 8000 0000 :1901/12/13 20:45:52
(time_t)0x10000 0000 :1970/01/01 00:00:00
```

esp32 (Arduino-ESP32 v1.0.5)も nRF51822 も全く同じなので一緒に。
1. `time_t` は 32bitで、整数です。
2. エポックはUNIX EPOCH です。
3. `time_t` は signed です。（-1がマイナスになって過去を指しているので）
4. 0x7fff ffff と 0x7xfff fffe の差が1秒なので1秒刻み

2038年問題の条件が揃いきってるので、 0x7fffffff が 0x80000000 になった瞬間1901年に戻っています。
ライブラリが将来アップデートされる、なんてことはあるのでしょうか。→2023/12/1 次項加筆しました

<strong>同じArduinoIDE で同じコードをビルドしても、呼ばれるライブラリプログラムの実装が違えば結果が変わる</strong>ので、走らせてみて確認するので正しかった気がします。

# Arduino-ESP32 3.0.0 Alpha2 (2023/12加筆)
ArduinoIDEのボードマネージャで、esp32 by espressif Systems の v3.0.0-alpha2 が来ていました。esp32はesp-idf v5.x系からtime_tが64bit になり、それはArduino-ESP32は 3.xからidf v5系になると事前に聞いていたので、Alphaじゃなくなったら試そうかなと思っていたのですが、待ちきれずにAlpha2で試してしまいました。

ESP32-DevkitC (Arduino-ESP32 v3.0.0alpha2)の実行結果
```
sizeof(time_t) : 8 bytes (64 bits) 
integer type
(time_t) 0 :1970/01/01 00:00:00
(time_t)-1 :1969/12/31 23:59:59
(time_t)0x ffff ffff :2106/02/07 06:28:15
(time_t)0x 7fff ffff :2038/01/19 03:14:07
(time_t)0x 7fff fffe :2038/01/19 03:14:06
(time_t)0x 8000 0000 :2038/01/19 03:14:08
(time_t)0x10000 0000 :2106/02/07 06:28:16
```

64bitですので、0x100000000 まできっちり未来に向かっています(0xffffffffの1秒後)
記事を書いたときからずっとこれを待っていました！
signed 64bit であれば過去のほうも相当昔まで表現できそうですね。

# おまけ: raspberrypi (32bit)
ArduinoIDE 用プログラムは(必要なら `sprintf` してから) `Serial.print` してますが、全て `printf` にすればよくて（もちろん`#include <stdio.h>` もしましょう） test.c として保存、 gcc test.c でコンパイルして出来る a.out 実行すれば試せます。

<details><summary>プログラム</summary><div>

```c
#include <time.h>
#include <stdio.h>

char buf[1000];

void test_tm(time_t t)
{
  struct tm *p;
  p = gmtime(&t);
  printf( "%d/%02d/%02d %02d:%02d:%02d\n", p->tm_year + 1900, p->tm_mon +1, p->tm_mday, p->tm_hour, p->tm_min, p->tm_sec );
}

int main()
{

  // time_t のバイト数、ビット数
  printf( "sizeof(time_t) : %d bytes (%d bits) \n", sizeof(time_t), sizeof(time_t)*8 );

  // time_t は整数型か浮動小数点型か
  float a = 1.1;
  double b = 1.1;
  long double c = 1.1;
  if ( a == (time_t)a || b == (time_t)b || c == (time_t)c ) {
    printf( "floating point number type\n" );
  }
  else {
    printf( "integer type\n" );
  }

  printf( "(time_t) 0 :" ); // epoch 次第で結果が変わる
  test_tm( (time_t)0 );

  printf("(time_t)-1 :");// time_t の定義次第で結果が変わる
  test_tm( (time_t)-1 );

  printf("(time_t)0x ffff ffff :"); // unsigned 32bit int の正の最大値
  test_tm( (time_t)0xffffffff );

  printf("(time_t)0x 7fff ffff :"); // signed 32bit int の正の最大値
  test_tm( (time_t)0x7fffffff );

  printf("(time_t)0x 7fff fffe :"); // signed 32bit int の正の最大値 -1
  test_tm( (time_t)0x7ffffffe );

  printf("(time_t)0x 8000 0000 :"); // signed 32bit int の正の最大値 +1
  test_tm( (time_t)0x80000000 );

  printf("(time_t)0x10000 0000 :"); // 33bit を time_t にキャストしたら？
  test_tm( (time_t)0x100000000 );
}

```
</div></details>

実行結果 

```
sizeof(time_t) : 4 bytes (32 bits)
integer type
(time_t) 0 :1970/01/01 00:00:00
(time_t)-1 :1969/12/31 23:59:59
(time_t)0x ffff ffff :1969/12/31 23:59:59
(time_t)0x 7fff ffff :2038/01/19 03:14:07
(time_t)0x 7fff fffe :2038/01/19 03:14:06
(time_t)0x 8000 0000 :1901/12/13 20:45:52
(time_t)0x10000 0000 :1970/01/01 00:00:00
```

32bit版なのでそりゃそうかっていう結果。
64bit Linux PC でやると(time_t)0x100000000 のところがちゃんと0xffffffff の1秒後になるのでラズパイも64bit版を待ちましょう。2038年まではかからないはず。

# おまけ2
arduino-1.8.13\hardware\tools\avr\avr\include\time.h には
`    typedef uint32_t time_t;`
と書いてありますね。

esp32 なら
Arduino15\packages\esp32\hardware\esp32\1.0.5\tools\sdk\include\newlib\sys\timeb.h で
`typedef _TIME_T_ time_t;`
となっていて、

Arduino15\packages\esp32\hardware\esp32\1.0.5\tools\sdk\include\newlib\machine\types.h で
`#define	_TIME_T_	long			/* time() */`
となっています。(sdkでそうなので、ESP-IDFで開発しても一緒でしょう)

nRF51 なら、
Arduino15\packages\sandeepmistry\tools\gcc-arm-none-eabi\5_2-2015q4\arm-none-eabi\include\sys\timeb.h
`typedef _TIME_T_ time_t;`

Arduino15\packages\sandeepmistry\tools\gcc-arm-none-eabi\5_2-2015q4\arm-none-eabi\include\machine\types.h
`#define _TIME_T_ long 	/* time() */`

面倒だけど一応調べました（笑）