millis()のオーバーフロー
UNOESP32

概要

Arduinoで、millis()delay()の代わりに待ち時間を経過したかを確認するために利用する際、millis()がオーバーフローしたときの挙動に関する実験です。Arduino UnoとESP-WROOM-32について試してみました。

Arduinoのmillis()は、プログラムを起動してから経過した時間をミリ秒単位で返す関数です。返される値はunsigned long(32ビットの符号無し整数型)なので、約49日(もう少し正確には、232ミリ秒=4294967296ミリ秒、4294967296/1000/60/60/24≒49.71日)でオーバーフローします。ネット上では、約49日や50日で、millis()はオーバーフローするので対策が必要という意見を見かけます。実際にオーバーフローしたらどうなるのかを確認し、どういう対策が必要なのかを考えてみました。

と言っても、実際に約49日動かし続けたわけではなく、unsigned longの変数の最大値付近の動作を軽く試してみただけです。

結論は、タイミングをとるために、現在のmillis()と、過去のある時点でのmillis()の値との差分をとり、一定時間(ただしunsigned logに収まる範囲)を経過したかどうかを調べるだけなら、何も考えなくてもOK(対策は不要)、です。

実験

2パターンのタイミングの取り方を考えました。

millis()は、Arduinoを起動してからの時間をミリ秒単位で返す関数です。ここでは、millis()が返す値を、「時刻」と呼びます。

考え方

millis()を使って、タイミングをとる際の考え方は、大きく以下の2パターンあるようです。

  1. 現在時刻から、基準時刻を減算して経過時間を計算し、その値と待ち時間を比較する。
    ⇒ (現在時刻 ー 基準時刻) > 待ち時間 ?
  1. 基準時刻に、待ち時間を加算して目的時刻を計算し、現在時刻と比較する。
    ⇒ (基準時刻 + 待ち時間) < 現在時刻 ?

上記2つの考え方について、オーバーフローがどう影響するかを考えます。

経過時間と待ち時間を比較する

スケッチ

以下のスケッチを、Arduino UnoとESP-WROOM-32で動かしてみました。

  • 現在時刻を表すtimerというunsigned longの変数に、4294967290を代入
  • 基準時刻を表すprevというunsigned longの変数に、4294967285を代入
  • 現在時刻を1ずつ増やしながら、経過時間を表すdiffという変数に、timer - prev を代入
  • diff を表示
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void setup() {
  unsigned long timer;
  unsigned long prev;
  unsigned long diff;
 
  // put your setup code here, to run once:
  Serial.begin(115200);
  timer = 4294967290;
  prev = 4294967285;
 
  for (int i = 0; i < 10; i++, timer++) {
    diff = timer - prev;
    Serial.print("timer = ");
    Serial.print(timer);
    Serial.print(", prev = ");
    Serial.print(prev);
    Serial.print(", timer - prev = ");
    Serial.print(diff);
    Serial.print("\n");
  }
 
  Serial.print("\n");
 
  timer = 4294967290;
  for (int i = 0; i < 10; i++, timer++) {
    diff = timer - prev;
    Serial.print("timer = ");
    Serial.print(timer, BIN);
    Serial.print(", prev = ");
    Serial.print(prev, BIN);
    Serial.print(", timer - prev = ");
    Serial.print(diff, BIN);
    Serial.print("\n");
  }
}
 
void loop() {
  // put your main code here, to run repeatedly:
 
}

結果

Arduino UnoとESP-WROOM-32で試した結果を、以下に示します。COM3がUnoで、COM4がESP-WROOM-32です。

Arduino UnoでもESP-WROOM-32でも、同じ結果となりました。

  • 現在時刻を表すtimerは、4294967295になった後、0に戻る。
  • 経過時間を表す(timer - prev)は、timerが0に戻った後も、単調に増加している。

このため、ある時点でのmillis()の値を保存しておき、その後、現在のmillis()からその値を引いて、時間の差分を取得する処理では、オーバーフローを考えなくてもOKです。もちろん、約49日以上待つことはできませんが。

目的時刻と現在時刻を比較する

スケッチ

以下のスケッチを、Arduino UnoとESP-WROOM-32で動かしてみました。

  • 現在時刻を表すtimerというunsigned longの変数に、4294967290を代入
  • 基準時刻を表すprevというunsigned longの変数に、4294967285を代入
  • 目的時刻を表すtargetというunsigned longの変数に、prev+15を代入(15ミリ秒待つ)
  • 現在時刻を1ずつ増やしながら、現在時刻が目的時刻より大きいかを確認
  • 確認結果を表示
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void setup() {
  unsigned long timer;
  unsigned long prev;
  unsigned long target;

  // put your setup code here, to run once:
  Serial.begin(115200);
  timer = 4294967290;
  prev = 4294967285;
  target = prev + 15;

  for (int i = 0; i < 15; i++, timer++) {
    Serial.print("timer = ");
    Serial.print(timer);
    Serial.print(", target = ");
    Serial.print(target);
    Serial.print(", timer > target ? ");
    if (timer > target) {
      Serial.print("yes.");
    } else {
      Serial.print("no.");
    }
    Serial.print("\n");
  }
}

void loop() {
  // put your main code here, to run repeatedly:

}

結果

Arduino UnoとESP-WROOM-32で試した結果を、以下に示します。COM3がUnoで、COM4がESP-WROOM-32です。

Arduino UnoでもESP-WROOM-32でも、同じ結果となりました。

  • 目的時刻を表すtarget(=prev+15)は、オーバーフローを起こし、4になる。
  • 現在時刻を表すtimerは、オーバーフローを起こすまでは、targetよりも大きいので、すでに待ち時間を過ぎたことになってしまう。

このため、ある時点でのmillis()の値に、待ち時間を加算し、その後、現在時刻と比較する処理では、オーバーフローの影響を受けます。

結論

Arduinoで、millis()delay()の代わりに待ち時間を経過したかを確認するために利用する際には、現在時刻と基準時刻の差分を計算し、その差分と待ち時間を比較すれば、オーバーフローの影響を受けません。

パターン 比較方法 オーバーフローの影響
経過時間と待ち時間を比較する (現在時刻 ー 基準時刻) > 待ち時間 ? なし
目的時刻と現在時刻を比較する (基準時刻 + 待ち時間) < 現在時刻 ? あり

オーバーフローの動作は、たまたまこうなるのではなく、C++/C言語の符号なし整数に関する規格に従った動作です。

数学上は同じ不等式なのに、計算順序が違うだけで異なる結果となるのはおもしろいのと同時に、プログラムを作成する際には注意が必要ですね。

i

以下には注意が必要です。millis()の結果を格納する変数は、

  • unsigned long型とする
  • loop()の中で使う場合は多くの場合、静的記憶域期間を持つオブジェクト(グローバル変数や関数内でのstatic変数)とする

バージョン

Hardware:Arduino Uno/ESP-WROOM-32
Software:Arduino 1.8.12/Arduino core for the ESP32

最終更新日

February 6, 2022

inserted by FC2 system