簡単な答え:ミリ秒のロールオーバーを「処理」しようとせず、代わりにロールオーバーセーフコードを記述します。チュートリアルのコード例は問題ありません。修正措置を実装するためにロールオーバーを検出しようとする場合、何か間違ったことをしている可能性があります。ほとんどのArduinoプログラムは、ボタンを50ミリ秒デバウンスしたり、ヒーターを12時間オンにするなど、比較的短い期間にわたるイベントを管理するだけで済みます。ミリ秒のロールオーバーは問題になりません。
ロールオーバーの問題を管理する(または管理する必要がない)正しい方法は、モジュラー演算の観点からunsigned long
返される数値
を考えることです。数学的には、プログラミングの際にこの概念にある程度精通していることが非常に役立ちます。Nick Gammonの記事millis()overflowで数学の動作を見ることができます...悪いことですか?。計算の詳細を知りたくない人のために、ここで代替の(できればより単純な)考え方を提供します。これは、インスタントと期間の単純な区別に基づいています。テストに期間の比較のみが含まれている限り、問題はありません。millis()
micros()に関する注意:ここで述べられていることmillis()
はすべてに等しく適用されますがmicros()
、micros()
71.6分ごとにロールオーバーするという事実を除き、setMillis()
以下に提供される機能はに影響しませんmicros()
。
インスタント、タイムスタンプ、および期間
時間を扱う場合、少なくとも2つの異なる概念、インスタントと期間を区別する必要があります。インスタントは時間軸上のポイントです。期間とは、時間間隔の長さ、つまり間隔の開始と終了を定義するインスタント間の時間の距離です。これらの概念の区別は、日常言語では必ずしも非常に明確ではありません。たとえば、「5分後に戻る」と言う場合、「5分」は不在の推定
期間であり、「5分」はインスタントです
私の予測の帰りの。区別を念頭に置くことが重要です。これは、ロールオーバーの問題を完全に回避する最も簡単な方法だからです。
の戻り値はmillis()
期間として解釈できます。これは、プログラムの開始から現在までの経過時間です。ただし、この解釈は、ミリ秒がオーバーフローするとすぐに壊れます。一般にmillis()
、タイムスタンプ、つまり特定の瞬間を識別する「ラベル」を返す
と考える方がはるかに便利です。これらのラベルは49.7日ごとに再利用されるため、この解釈はこれらのラベルがあいまいであることに苦しんでいると言えます。ただし、これが問題になることはほとんどありません。ほとんどの組み込みアプリケーションでは、49.7日前に発生したことは、私たちが気にしない古代史です。したがって、古いラベルのリサイクルは問題になりません。
タイムスタンプを比較しない
2つのタイムスタンプのどちらが他方よりも大きいかを見つけようとしても意味がありません。例:
unsigned long t1 = millis();
delay(3000);
unsigned long t2 = millis();
if (t2 > t1) { ... }
単純に、条件if ()
が常に真であることが期待されます。しかし、の間にミリスがオーバーフローすると、実際にはfalseになります
delay(3000)
。エラーを回避する最も簡単な方法は、t1とt2をリサイクル可能なラベルと考えることです。ラベルt1はt2よりも前のインスタントに明確に割り当てられていますが、49.7日後に将来のインスタントに再割り当てされます。したがって、t1はt2の前と後の両方で発生します。これは、式t2 > t1
が意味をなさないことを明確にする必要があります。
しかし、これらが単なるラベルである場合、明らかな疑問は、どのようにして有用な時間計算を行うことができるのかということです。答えは、タイムスタンプに意味のある2つの計算のみに制限することです。
later_timestamp - earlier_timestamp
期間、つまり、前の瞬間と後の瞬間の間の経過時間を求めます。これはタイムスタンプを含む最も便利な算術演算です。
timestamp ± duration
初期タイムスタンプの後(+を使用する場合)または前(-を使用する場合)のタイムスタンプを生成します。結果のタイムスタンプは2種類の計算でしか使用できないため、見た目ほど便利ではありません...
モジュラー演算のおかげで、これらの両方は、少なくとも関与する遅延が49.7日より短い限り、ミリロールオーバーで正常に動作することが保証されています。
期間の比較は問題ありません
期間は、ある時間間隔中に経過したミリ秒の量です。49.7日より長い期間を処理する必要がない限り、物理的に意味のある操作も計算上意味があります。たとえば、期間に頻度を掛けて、複数の期間を取得できます。または、2つの期間を比較して、どちらが長いかを知ることができます。たとえば、次の2つの代替実装がありdelay()
ます。まず、バグのあるもの:
void myDelay(unsigned long ms) { // ms: duration
unsigned long start = millis(); // start: timestamp
unsigned long finished = start + ms; // finished: timestamp
for (;;) {
unsigned long now = millis(); // now: timestamp
if (now >= finished) // comparing timestamps: BUG!
return;
}
}
そして、これが正しいものです:
void myDelay(unsigned long ms) { // ms: duration
unsigned long start = millis(); // start: timestamp
for (;;) {
unsigned long now = millis(); // now: timestamp
unsigned long elapsed = now - start; // elapsed: duration
if (elapsed >= ms) // comparing durations: OK
return;
}
}
ほとんどのCプログラマは、上記のループを次のような簡潔な形式で記述します。
while (millis() < start + ms) ; // BUGGY version
そして
while (millis() - start < ms) ; // CORRECT version
一見似ているように見えますが、タイムスタンプ/期間の区別により、どちらがバグで、どちらが正しいかが明確になります。
タイムスタンプを本当に比較する必要がある場合はどうなりますか?
状況を回避するようにしてください。やむを得ない場合でも、それぞれの瞬間が十分に近いことがわかっていれば、24.85日よりも近いという希望があります。はい、49.7日間の管理可能な最大遅延は半分になりました。
明らかな解決策は、タイムスタンプ比較問題を期間比較問題に変換することです。インスタントt1がt2の前か後かを知る必要があるとします。共通の過去の参照インスタントを選択し、この参照からt1とt2の両方までの期間を比較します。基準インスタントは、t1またはt2から十分に長い期間を減算することによって取得されます。
unsigned long reference_instant = t2 - LONG_ENOUGH_DURATION;
unsigned long from_reference_until_t1 = t1 - reference_instant;
unsigned long from_reference_until_t2 = t2 - reference_instant;
if (from_reference_until_t1 < from_reference_until_t2)
// t1 is before t2
これは次のように簡略化できます。
if (t1 - t2 + LONG_ENOUGH_DURATION < LONG_ENOUGH_DURATION)
// t1 is before t2
さらに簡略化するのは魅力的if (t1 - t2 < 0)
です。明らかに、これは機能しません。なぜならt1 - t2
、が符号なしの数値として計算されるため、負の値にできないからです。ただし、これは移植性はありませんが機能します。
if ((signed long)(t1 - t2) < 0) // works with gcc
// t1 is before t2
上記のキーワードsigned
は冗長です(プレーンlong
は常に署名されます)が、意図を明確にするのに役立ちます。符号付きlongに変換することはLONG_ENOUGH_DURATION
、24.85日に設定することと同等です。C標準によると、結果は実装定義であるため、このトリックは移植性がありません。しかし、gccコンパイラーは正しいことを行うことを約束しているため、 Arduinoでも確実に動作します。実装定義の動作を回避したい場合、上記の符号付き比較は数学的にこれと同等です。
#include <limits.h>
if (t1 - t2 > LONG_MAX) // too big to be believed
// t1 is before t2
比較が後方に見えるという唯一の問題があります。また、longが32ビットである限り、このシングルビットテストと同等です。
if ((t1 - t2) & 0x80000000) // test the "sign" bit
// t1 is before t2
最後の3つのテストは、gccによって実際にまったく同じマシンコードにコンパイルされます。
ミリスロールオーバーに対してスケッチをテストするにはどうすればよいですか
上記の教訓に従うなら、あなたはすべて良い人でなければなりません。それでもテストしたい場合は、この関数をスケッチに追加します。
#include <util/atomic.h>
void setMillis(unsigned long ms)
{
extern unsigned long timer0_millis;
ATOMIC_BLOCK (ATOMIC_RESTORESTATE) {
timer0_millis = ms;
}
}
を呼び出すことで、プログラムをタイムトラベルできます
setMillis(destination)
。フィルコナーズがグラウンドホッグデイを追reするように、ミリ秒のオーバーフローを何度も繰り返してやりたい場合は、次のように入力できますloop()
。
// 6-second time loop starting at rollover - 3 seconds
if (millis() - (-3000) >= 6000)
setMillis(-3000);
上記の負のタイムスタンプ(-3000)は、ロールオーバーの前の3000ミリ秒に対応する符号なしlongにコンパイラによって暗黙的に変換されます(4294964296に変換されます)。
非常に長い継続時間を本当に追跡する必要がある場合はどうなりますか?
3か月後にリレーをオンにしてオフにする必要がある場合、ミリ秒のオーバーフローを実際に追跡する必要があります。これを行うには多くの方法があります。最も簡単な解決策は、単純millis()
に64ビットに拡張することです。
uint64_t millis64() {
static uint32_t low32, high32;
uint32_t new_low32 = millis();
if (new_low32 < low32) high32++;
low32 = new_low32;
return (uint64_t) high32 << 32 | low32;
}
これは、基本的にロールオーバーイベントをカウントし、このカウントを64ビットミリ秒カウントの上位32ビットとして使用します。このカウントが適切に機能するためには、49.7日ごとに少なくとも1回関数を呼び出す必要があります。ただし、49.7日に1回しか呼び出されない場合、場合によってはチェック(new_low32 < low32)
が失敗し、コードがのカウントを逃す可能性がありますhigh32
。millis()を使用して、ミリ秒の単一の「ラップ」(この特定の49.7日のウィンドウ)でこのコードへの唯一の呼び出しをいつ行うかを決定することは、時間枠の並び方によっては非常に危険です。安全のために、millis()を使用していつmillis64()を呼び出すかを決定する場合は、49.7日ごとに少なくとも2つの呼び出しが必要です。
ただし、Arduinoでは64ビット演算が高価であることに注意してください。32ビットを維持するために、時間分解能を下げる価値があるかもしれません。