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 일 후에 0으로 롤오버되는 것입니다. 스케치가 그보다 오래 실행되도록하기 때문에 롤오버로 인해 스케치가 실패하지 않아야합니다. 롤오버 조건 ( currentMillis < previousMillis)을 쉽게 감지 할 수 있지만 어떻게해야할지 잘 모르겠습니다.

따라서 내 질문 : millis()롤오버 를 처리하는 가장 적절하고 간단한 방법은 무엇입니까?


5
편집 메모 : 이것은 내 질문이 아니라 질문 / 답변 형식의 튜토리얼입니다. 나는이 주제에 관해 인터넷 (여기를 포함하여)에서 많은 혼란을 목격했으며,이 사이트는 답을 찾을 수있는 명백한 장소 인 것 같습니다. 이것이 제가이 튜토리얼을 제공하는 이유입니다.
Edgar Bonet 2016 년

2
특정 빈도의 결과를 원한다면 previousMillis += interval대신 할 것입니다 previousMillis = currentMillis.
Jasen

4
@Jasen : 맞습니다! previousMillis += interval일정한 주파수를 원하고 처리 시간이 단축 interval되지만 previousMillis = currentMillis최소 지연 시간이 보장되는 경우 interval.
Edgar Bonet

우리는 이와 같은 것들에 대한 FAQ가 정말로 필요합니다.

내가 사용하는 "트릭"중 하나는 간격이 포함 된 가장 작은 정수를 사용하여 arduino의 부하를 줄이는 것입니다. 예를 들어, 최대 1 분 간격으로 다음과 같이 씁니다uint16_t previousMillis; const uint16_t interval = 45000; ... uint16_t currentMillis = (uint16_t) millis(); if ((currentMillis - previousMillis) >= interval) ...
frarugi87

답변:


95

짧은 대답 : millis 롤오버를 "처리"하지 말고 대신 롤오버 안전 코드를 작성하십시오. 튜토리얼의 예제 코드는 괜찮습니다. 수정 조치를 구현하기 위해 롤오버를 감지하려고하면 문제가있는 것일 수 있습니다. 대부분의 Arduino 프로그램은 50ms 동안 버튼을 수신 거부하거나 12 시간 동안 히터를 켜는 것과 같이 비교적 짧은 시간 동안 발생하는 이벤트 만 관리하면됩니다. Millis 롤오버는 문제가되지 않습니다.

롤오버 문제를 관리하는 (또는 오히려 관리 할 필요가없는) 올바른 방법 은 모듈 식 산술 측면에서 unsigned long반환 된 수 를 생각하는 것입니다 . 수학적으로 기울어 진 경우이 개념에 익숙하면 프로그래밍 할 때 매우 유용합니다. Nick Gammon의 기사 millis () overflow ... 나쁜 점 에서 수학이 실제로 작동하는 것을 볼 수 있습니다 . . 계산 세부 사항을 거치고 싶지 않은 사람들을 위해 여기에 대안에 대한 대안을 제시합니다. 그것은 순간지속 시간 의 단순한 구별에 기초한다 . 테스트에 기간 비교 만 포함되는 한 괜찮습니다.millis()

micros ()에 대한 참고 사항 : 여기에 언급 된 모든 내용은 71.6 분마다 롤오버 된다는 사실을 제외하고 millis()동일하게 적용 되며 아래 제공된 기능은 영향을 미치지 않습니다 .micros()micros()setMillis()micros()

순간, 타임 스탬프 및 지속 시간

시간을 다룰 때는 적어도 두 가지 다른 개념, 즉 순간지속 시간을 구분해야 합니다. 순간은 시간 축의 한 지점입니다. 기간은 시간 간격의 길이, 즉 간격의 시작과 끝을 정의하는 순간 사이의 거리입니다. 이 개념들 사이의 구별이 일상 언어에서 항상 매우 날카로운 것은 아닙니다. 내가 말할 예를 들어, " 내가 다시 5 분있을 것이다 다음", " 5 분이 "예상입니다 기간 내 결석이 "반면, 5 분 "는 것입니다 인스턴트 내 예측의 다시. 롤오버 문제를 완전히 피하는 가장 간단한 방법이므로 구별을 명심해야합니다.

의 반환 값은 millis()기간 : 프로그램 시작부터 지금까지 경과 한 시간으로 해석 될 수 있습니다. 그러나이 해석은 밀리 초가 넘치 자마자 분해됩니다. 타임 스탬프 , 즉 특정 순간을 식별하는 "라벨" millis()을 반환하는 것으로 생각하는 것이 일반적으로 훨씬 더 유용합니다 . 이 해석은 49.7 일마다 재사용되기 때문에 이러한 레이블이 모호하기 때문에 어려움을 겪을 수 있습니다. 그러나 이것은 거의 문제가되지 않습니다. 대부분의 임베디드 응용 프로그램에서 49.7 일 전에 일어난 일은 우리가 신경 쓰지 않는 고대 역사입니다. 따라서 오래된 라벨을 재활용하는 것은 문제가되지 않습니다.

타임 스탬프를 비교하지 마십시오

두 타임 스탬프 중 어느 것이 다른 타임 스탬프보다 큰지 알아 내려는 것은 의미가 없습니다. 예:

unsigned long t1 = millis();
delay(3000);
unsigned long t2 = millis();
if (t2 > t1) { ... }

순진하게,의 조건 if ()이 항상 참 이라고 기대할 것 입니다. 그러나 millis가 오버플로하면 실제로는 false delay(3000)입니다. t1과 t2를 재활용 가능한 레이블로 생각하는 것이 오류를 피하는 가장 간단한 방법입니다. 레이블 t1은 t2 이전의 순간에 명확하게 할당되었지만 49.7 일 후에 미래의 순간으로 다시 할당됩니다. 따라서, T1은 모두 발생 후에 T2. 이것은 표현 t2 > t1이 의미가 없다는 것을 분명히해야합니다 .

그러나 이것들이 단순한 레이블이라면, 분명한 질문은 : 어떻게 유용한 시간 계산을 할 수 있을까요? 정답은 다음과 같습니다. 타임 스탬프에 적합한 유일한 두 가지 계산으로 제한합니다.

  1. later_timestamp - earlier_timestamp지속 시간, 즉 이전 순간과 이후 순간 사이에 경과 된 시간의 양을 산출합니다. 타임 스탬프와 관련된 가장 유용한 산술 연산입니다.
  2. timestamp ± duration타임 스탬프를 생성합니다. 타임 스탬프는 초기 타임 스탬프 이후 (+를 사용하는 경우) 또는 이전 (-인 경우)의 시간입니다. 결과 타임 스탬프는 두 종류의 계산에만 사용할 수 있으므로 소리만큼 유용하지는 않습니다.

모듈 식 산술 덕분에 적어도 관련된 지연 시간이 49.7 일보다 짧으면 Millis 롤오버에서 두 가지 모두 제대로 작동합니다.

지속 시간을 비교하는 것은 좋습니다

지속 시간은 일부 시간 간격 동안 경과 된 밀리 초입니다. 49.7 일보다 긴 기간을 처리 할 필요가없는 한, 물리적으로 의미가있는 작업은 계산적으로도 의미가 있습니다. 예를 들어 기간에 빈도를 곱하여 여러 기간을 얻을 수 있습니다. 또는 두 기간을 비교하여 어느 기간이 더 긴지 알 수 있습니다. 예를 들어 다음은의 두 가지 대체 구현입니다 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_DURATION24.85 일로 설정하는 것과 같습니다. C 표준에 따르면 결과는 구현이 정의 되어 있기 때문에 트릭은 이식성이 없습니다 . 그러나 gcc 컴파일러 는 올바른 일을 약속하므로 Arduino에서 안정적으로 작동합니다. 구현 정의 동작을 피하려면 위의 서명 된 비교는 수학적으로 다음과 같습니다.

#include <limits.h>

if (t1 - t2 > LONG_MAX)  // too big to be believed
    // t1 is before t2

비교가 거꾸로 보이는 유일한 문제. 또한 32 비트 인 한이 단일 비트 테스트와 동일합니다.

if ((t1 - t2) & 0x80000000)  // test the "sign" bit
    // t1 is before t2

마지막 세 테스트는 실제로 gcc에 의해 정확히 동일한 머신 코드로 컴파일됩니다.

Millis 롤오버에 대한 스케치를 어떻게 테스트합니까?

위의 교훈을 따르면 모든 것이 좋을 것입니다. 그럼에도 불구하고 테스트하려면 스케치에이 기능을 추가하십시오.

#include <util/atomic.h>

void setMillis(unsigned long ms)
{
    extern unsigned long timer0_millis;
    ATOMIC_BLOCK (ATOMIC_RESTORESTATE) {
        timer0_millis = ms;
    }
}

이제 전화로 프로그램을 시간 여행 할 수 있습니다 setMillis(destination). Phil Connors가 Groundhog Day를 재현하는 것처럼 Millis 오버플로를 반복해서 반복하려면 다음을 입력하십시오 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 일마다 한 번씩 함수를 호출해야합니다. 그러나 49.7 일에 한 번만 호출되는 경우 검사 (new_low32 < low32)가 실패하고 코드 개수가 누락 될 수 high32있습니다. millis ()를 사용하여 시간 단위가 어떻게 정렬되는지에 따라 millis (특정 49.7 일 창)의 단일 "랩"에서이 코드를 호출 할시기를 결정하는 것은 매우 위험 할 수 있습니다. 안전을 위해 millis ()를 사용하여 millis64 ()에 대한 유일한 호출시기를 결정하는 경우 49.7 일마다 두 번 이상 호출해야합니다.

그러나 64 비트 산술은 Arduino에서 비싸다는 것을 명심하십시오. 32 비트를 유지하려면 시간 해상도를 줄이는 것이 좋습니다.


2
따라서 질문에 작성된 코드가 실제로 올바르게 작동한다고 말하고 있습니까?
Jasen

3
@Jasen : 맞아요! 사람들이 처음에는 존재하지 않는 문제를 "수정"하려고 한 번 이상 봤습니다.
Edgar Bonet

2
나는 이것을 발견해서 기쁘다. 나는이 질문을 전에했다.
Sebastian Freeman

1
StackExchange에서 가장 유용하고 유용한 답변 중 하나입니다! 고마워요! :)
Falko

이것은 질문에 대한 놀라운 대답입니다. 나는 롤오버를 망쳐 놓는 편집증이기 때문에 기본적으로 일년에 한 번이 대답으로 돌아옵니다.
Jeffrey Cash

17

TL; DR 짧은 버전 :

An unsigned long은 0 내지 4,294,967,295 (2 ^ 32-1)입니다.

따라서 previousMillis4,294,967,290 (롤오버 전 5ms)이고 currentMillis10 (롤오버 후 10ms ) 이라고 가정 하겠습니다 . 이어서 currentMillis - previousMillis실제 16 (-4,294,967,280없는) 그 결과가 다음과 같이 계산되기 때문에 부호 (자체 뒹굴 있도록, 음의 수없는) 긴. 다음과 같이 간단히 확인할 수 있습니다.

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

따라서 위의 코드는 완벽하게 작동합니다. 요령은 항상 시차를 계산하고 두 시간 값을 비교하지 않는 것입니다.


롤오버 전 15ms 와 롤오버 후 10ms (즉, 49.7 일 ) 는 어떻습니까 . 15> 10 이지만 15ms 스탬프는 거의 한 달 반이되었습니다. 15-10> 0 및 10-15> 0 unsigned 논리이므로 여기서는 사용되지 않습니다!
ps95

@ prakharsingh95 10ms-15ms ~ 49.7 일 ~ 5ms가되어 정확한 차이입니다. 수학은 millis()두 번 롤오버 될 때까지 작동 하지만 문제의 코드에서는 발생하지 않을 것입니다.
BrettAM

다시 말하겠습니다. 두 개의 타임 스탬프가 200ms와 10ms라고 가정합니다. 어느 것이 롤오버되었는지 어떻게 알 수 있습니까?
ps95

@ prakharsingh95에 저장된 previousMillis것이 이전에 측정 currentMillis되었으므로 롤오버 currentMillis보다 작은 경우 previousMillis롤오버가 발생했습니다. 두 번의 롤오버가 발생하지 않으면 계산할 필요가 없다는 것이 수학의 결과입니다.
BrettAM

1
그래. 당신이하고 t2-t1, 보장 할 수 있다면 그 t1전에 측정 된 t2것은 signed (t2-t1)% 4,294,967,295 와 같으므로 자동 랩 어라운드입니다. 좋은!. 그러나 두 개의 롤오버가 있거나 interval4,294,967,295보다 크면 어떻게됩니까?
ps95

1

millis()수업에 랩 !

논리:

  1. millis()직접 대신 id를 사용하십시오 .
  2. id를 사용하여 반전을 비교하십시오. 이것은 깨끗하고 롤오버 독립적입니다.
  3. 특정 애플리케이션의 경우 두 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 번 후에 메모리가 손상되기 시작합니다 . 타임 스탬프 대신 지연을 비교하는 것이 더 효율적일 것입니다.
Edgar Bonet 2016 년

1

나는이 질문을 좋아했고, 그 큰 답을 얻었습니다. 먼저 이전 답변에 대한 빠른 의견 (알고 있습니다.하지만 아직 언급 할 담당자가 없습니다. :-).

Edgar Bonet의 답변은 훌륭했습니다. 저는 35 년 동안 코딩을 해왔으며 오늘 새로운 것을 배웠습니다. 감사합니다. 그건 내가에 대한 코드를 믿는다 고 말했다 "내가 정말 매우 긴 기간을 추적해야하는 경우?" 롤오버 기간 당 적어도 한 번 millis64 ()를 호출하지 않으면 중단됩니다. 정말 까다 롭고 실제 구현에서는 문제가되지는 않지만 그럴 수 있습니다.

이제 제정신 시간 범위 (64 비트 밀리 초는 내 계산에 의해 약 50 억 년입니다)를 포괄하는 타임 스탬프를 원한다면 기존 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 비트 산술 을 수행 하여 다른 인터럽트에 대한 응답으로 대기 시간이 증가하는 경우가 있습니다.
Edgar Bonet
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.