5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-03-17

(2021/3初稿。2023/12にESP32の最新状況を加筆(ラズパイ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年まで延命されますね。
    mktimetm 構造体に変換して確認しましょう。

  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年問題がどうなっているか解るでしょう。

検証プログラム



#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 次項加筆しました

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

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 実行すれば試せます。

プログラム
#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 );
}

実行結果

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() */

面倒だけど一応調べました(笑)

5
3
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?