millis()ロールオーバーをどのように処理できますか?


73

5分ごとにセンサーを読み取る必要がありますが、スケッチには他のタスクもあるためdelay()、読み取りと読み取りの間だけではできません。これらの行に沿ってコーディングすることを提案する遅延なし点滅チュートリアルがあります:

void loop()
{
    unsigned long currentMillis = millis();

    // Read the sensor when needed.
    if (currentMillis - previousMillis >= interval) {
        previousMillis = currentMillis;
        readSensor();
    }

    // Do other stuff...
}

問題は、millis()約49.7日後にゼロにロールバックすることです。私のスケッチはそれよりも長く実行することを目的としているため、ロールオーバーによってスケッチが失敗しないようにする必要があります。ロールオーバー状態(currentMillis < previousMillis)を簡単に検出できますが、どうすればよいかわかりません。

したがって、私の質問:millis()ロールオーバーを処理するための適切/最も簡単な方法は何でしょう か?


5
編集者注記:これは私の質問ではなく、質問/回答形式のチュートリアルです。このトピックについては、インターネット(ここを含む)で多くの混乱を目撃しており、このサイトは答えを探すのに明らかな場所のようです。これが、このチュートリアルをここで提供している理由です。
エドガーボネット

2
特定の頻度の結果previousMillis += intervalpreviousMillis = currentMillis必要な場合は、代わりに行います。
ジェイセン

4
@Jasen:そうです!previousMillis += interval一定の周波数が必要で、処理にかかる時間が未満であることが確実な場合interval、ただしpreviousMillis = currentMillisの最小遅延を保証しますinterval
エドガーボネット

このようなことには、よくある質問が必要です。

私が使用する「トリック」の1つは、間隔を含む最小のintを使用して、arduinoの負荷を軽減することです。たとえば、最大1分間隔で、次のように記述しますuint16_t previousMillis; const uint16_t interval = 45000; ... uint16_t currentMillis = (uint16_t) millis(); if ((currentMillis - previousMillis) >= interval) ...
frarugi87

回答:


95

簡単な答え:ミリ秒のロールオーバーを「処理」しようとせず、代わりにロールオーバーセーフコードを記述します。チュートリアルのコード例は問題ありません。修正措置を実装するためにロールオーバーを検出しようとする場合、何か間違ったことをしている可能性があります。ほとんどの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つの計算のみに制限することです。

  1. later_timestamp - earlier_timestamp期間、つまり、前の瞬間と後の瞬間の間の経過時間を求めます。これはタイムスタンプを含む最も便利な算術演算です。
  2. 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ビットを維持するために、時間分解能を下げる価値があるかもしれません。


2
だから、あなたは質問で書かれたコードが実際に正しく動作すると言っていますか?
-Jasen

3
@Jasen:その通り!そもそも存在しなかった問題を「修正」しようとしている人がいるようです。
エドガーボネット

2
これを見つけてうれしいです。以前にこの質問がありました。
セバスチャンフリーマン

1
StackExchangeの最良かつ最も有用な回答の1つです!どうもありがとう!:)
ファルコ

これは、質問に対する驚くべき答えです。基本的には年に1回この答えに戻ってきます。なぜなら、私はロールオーバーを台無しにすることを嫌がるからです。
ジェフリーキャッシュ

17

TL; DRショートバージョン:

An unsigned longは0〜4,294,967,295(2 ^ 32-1)です。

たとえばpreviousMillis、4,294,967,290(ロールオーバーの5ミリ秒前)とcurrentMillis10(ロールオーバーの10 ミリ秒後)であるとします。その後、currentMillis - previousMillis実際のである16(非-4294967280)結果は次のように計算されるので、符号なしの(それ自体の周りロールであろうように、負することができない)長いです。これは次の方法で簡単に確認できます。

Serial.println( ( unsigned long ) ( 10 - 4294967290 ) ); // 16

したがって、上記のコードは完全に機能します。コツは、常に2つの時間値を比較するのではなく、時間差を計算することです。


どの程度15msのロールオーバー前と10msのロールオーバー後(すなわち49.7日)。15> 10ですが、15msのスタンプは1か月半近くです。15-10> 0および10-15> 0 unsignedロジックなので、ここでは使用できません!
ps95

@ prakharsingh95 10ms-15msは〜49.7日-5msになりますが、これは正しい違いです。数学millis()は2回ロールオーバーするまで機能しますが、問題のコードで発生する可能性はほとんどありません。
BrettAM

言い換えさせてください。200msと10msの2つのタイムスタンプがあるとします。どちらがロールオーバーされているかをどのように確認しますか?
ps95

@ prakharsingh95に保存されpreviousMillisているものはcurrentMillis、前に測定されている必要currentMillisがあるためpreviousMillis、ロールオーバーが発生した場合よりも小さい場合。2つのロールオーバーが発生しない限り、数学について考える必要はありません。
BrettAM

1
ああ、わかった。行う場合t2-t1、および保証できる場合t1は前に測定t2 (t2-t1)% 4,294,967,295、それはsignedと同等であるため、自動ラップアラウンドです。いいね!しかし、2つのロールオーバーがある場合、またはinterval4,294,967,295を超える場合はどうでしょうか。
ps95

1

millis()クラスでラップ!

論理:

  1. millis()直接ではなくIDを使用します。
  2. IDを使用して反転を比較します。これはクリーンでロールオーバーに依存しません。
  3. 特定のアプリケーションの場合、2つのIDの正確な差を計算するには、反転とスタンプを追跡します。差を計算します。

反転の追跡:

  1. ローカルスタンプを定期的に更新しますmillis()。これmillis()は、オーバーフローしたかどうかを調べるのに役立ちます。
  2. タイマーの周期が精度を決定します
class Timer {

public:
    static long last_stamp;
    static long *stamps;
    static int *reversals;
    static int count;
    static int reversal_count;

    static void setup_timer() {
        // Setup Timer2 overflow to fire every 8ms (125Hz)
        //   period [sec] = (1 / f_clock [sec]) * prescale * (255-count)
        //                  (1/16000000)  * 1024 * (255-130) = .008 sec


        TCCR2B = 0x00;        // Disable Timer2 while we set it up

        TCNT2  = 130;         // Reset Timer Count  (255-130) = execute ev 125-th T/C clock
        TIFR2  = 0x00;        // Timer2 INT Flag Reg: Clear Timer Overflow Flag
        TIMSK2 = 0x01;        // Timer2 INT Reg: Timer2 Overflow Interrupt Enable
        TCCR2A = 0x00;        // Timer2 Control Reg A: Wave Gen Mode normal
        TCCR2B = 0x07;        // Timer2 Control Reg B: Timer Prescaler set to 1024

        count = 0;
        stamps = new long[50];
        reversals = new int [10];
        reversal_count =0;
    }

    static long get_stamp () {
        stamps[count++] = millis();
        return count-1;
    }

    static bool compare_stamps_by_id(int s1, int s2) {
        return s1 > s2;
    }

    static long long get_stamp_difference(int s1, int s2) {
        int no_of_reversals = 0;
        for(int j=0; j < reversal_count; j++)
        if(reversals[j] < s2 && reversals[j] > s1)
            no_of_reversals++;
        return stamps[s2]-stamps[s1] + 49.7 * 86400 * 1000;       
    }

};

long Timer::last_stamp;
long *Timer::stamps;
int *Timer::reversals;
int Timer::count;
int Timer::reversal_count;

ISR(TIMER2_OVF_vect) {

    long stamp = millis();
    if(stamp < Timer::last_stamp) // reversal
        Timer::reversals[Timer::reversal_count++] = Timer::count;
    else 
        ; // no reversal
    Timer::last_stamp = stamp;    
    TCNT2 = 130;     // reset timer ct to 130 out of 255
    TIFR2 = 0x00;    // timer2 int flag reg: clear timer overflow flag
};

// Usage

void setup () {
    Timer::setup_timer();

    long s1 = Timer::get_stamp();
    delay(3000);
    long s2 = Timer::get_stamp();

    Timer::compare_stamps_by_id(s1, s2); // true

    Timer::get_stamp_difference(s1, s2); // return true difference, taking into account reversals
}

タイマークレジット


9
コードを編集して、コンパイルを妨げるmaaaaanyエラーを削除しました。これには、約232バイトのRAMと2つのPWMチャネルが必要です。また、get_stamp()51回使用するとメモリが破損し始めます。タイムスタンプの代わりに遅延を比較する方が確かに効率的です。
エドガーボネット

1

私はこの質問と、それが生み出した素晴らしい答えが大好きでした。最初に、以前の回答に対する簡単なコメント(私は知っていますが、私は知っていますが、まだコメントする担当者がいません。:-)。

エドガー・ボネットの答えはすばらしかった。私は35年間コーディングを続けてきましたが、今日新しいことを学びました。ありがとうございました。とはいえ「非常に長い期間を追跡する必要がある場合はどうすればよいですか?」ロールオーバー期間ごとに少なくとも1回millis64()を呼び出さない限り、中断します。本当にきちんとした、そして実際の実装では問題になりそうにないが、そこに行く。

さて、本当に正しい時間範囲をカバーするタイムスタンプが必要な場合(64ビットミリ秒は私の計算では約5億年)、既存のmillis()実装を64ビットに拡張するのは簡単に見えます。

attinycore / wiring.c(これらはATTiny85で作業しています)に対するこれらの変更は機能しているようです(他のAVRのコードは非常に似ていると仮定しています)。// BFBコメントのある行と、新しいmillis64()関数を参照してください。明らかに大きく(98バイトのコード、4バイトのデータ)遅くなり、Edgarが指摘したように、符号なし整数の数学をよりよく理解することでほぼ確実に目標を達成できますが、それは興味深い演習でした。

volatile unsigned long long timer0_millis = 0;      // BFB: need 64-bit resolution

#if defined(__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
ISR(TIM0_OVF_vect)
#else
ISR(TIMER0_OVF_vect)
#endif
{
    // copy these to local variables so they can be stored in registers
    // (volatile variables must be read from memory on every access)
    unsigned long long m = timer0_millis;       // BFB: need 64-bit resolution
    unsigned char f = timer0_fract;

    m += MILLIS_INC;
    f += FRACT_INC;
    if (f >= FRACT_MAX) {
        f -= FRACT_MAX;
        m += 1;
    }

    timer0_fract = f;
    timer0_millis = m;
    timer0_overflow_count++;
}

// BFB: 64-bit version
unsigned long long millis64()
{
    unsigned long long m;
    uint8_t oldSREG = SREG;

    // disable interrupts while we read timer0_millis or we might get an
    // inconsistent value (e.g. in the middle of a write to timer0_millis)
    cli();
    m = timer0_millis;
    SREG = oldSREG;

    return m;
}

1
あなたが正しいです、millis64()それはロールオーバー期間よりも頻繁に呼び出された場合にのみ動作します。この制限を指摘するために回答を編集しました。お使いのバージョンにはこの問題はありませんが、別の欠点があります。割り込みコンテキストで64ビット演算を行うため、他の割り込みに応答する際のレイテンシが増加する場合があります。
エドガーボネット
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.