짧은 대답 : 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
이 의미가 없다는 것을 분명히해야합니다 .
그러나 이것들이 단순한 레이블이라면, 분명한 질문은 : 어떻게 유용한 시간 계산을 할 수 있을까요? 정답은 다음과 같습니다. 타임 스탬프에 적합한 유일한 두 가지 계산으로 제한합니다.
later_timestamp - earlier_timestamp
지속 시간, 즉 이전 순간과 이후 순간 사이에 경과 된 시간의 양을 산출합니다. 타임 스탬프와 관련된 가장 유용한 산술 연산입니다.
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_DURATION
24.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 비트를 유지하려면 시간 해상도를 줄이는 것이 좋습니다.