Millis () rollover'ını nasıl kullanabilirim?


73

Her beş dakikada bir sensör okumam gerekiyor, ancak taslağımın başka işleri de olduğundan, sadece delay()okumalar arasında yapamam . Bu satırlar boyunca kod yazacağımı öneren gecikme olmadan öğreticinin gösterdiği Blink var :

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

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

    // Do other stuff...
}

Sorun şu ki millis(), yaklaşık 49.7 gün sonra sıfıra geri dönecek. Çizimimin bundan daha uzun süre çalışması gerektiği için, rollover'ın çizimimi başarısız yapmadığından emin olmam gerekiyor. Devrilme durumunu ( currentMillis < previousMillis) kolayca tespit edebiliyorum , ancak ne yapacağımdan emin değilim.

Bu yüzden benim sorum: Devrimi ele almanın doğru / en basit yolu ne olurdu millis()?


5
Editoryal not: Bu tam olarak bir soru değil, bir soru / cevap formatında bir öğretici. İnternette (burada da dahil olmak üzere) bu konuyla ilgili çok fazla kargaşaya şahit oldum ve bu site cevap aramaya açık bir yer gibi görünüyor. Bu yüzden bu dersi burada veriyorum.
Edgar Bonet

2
Ben yapacağını previousMillis += intervalyerine previousMillis = currentMillisben sonuçlarının belli frekansını isteseydim.
Jasen

4
@ Jasen: Bu doğru! previousMillis += intervalSabit frekans istiyorsanız ve işleminizin daha az sürdüğünden emin olun interval, ancak previousMillis = currentMillisminimum gecikmeyi garanti etmek için interval.
Edgar Bonet

Böyle şeyler için gerçekten bir SSS'a ihtiyacımız var.

Kullandığım "püf noktalarından" arduino üzerindeki yükü, aralığı içeren en küçük int kullanarak hafifletmektir. Örneğin, en fazla 1 dakikalık aralıklarla yazarımuint16_t previousMillis; const uint16_t interval = 45000; ... uint16_t currentMillis = (uint16_t) millis(); if ((currentMillis - previousMillis) >= interval) ...
frarugi87

Yanıtlar:


95

Kısa cevap: millis rollover'ını “ele almaya” çalışmayın, bunun yerine rollover-safe kodunu yazın. Öğreticiden örnek kodunuz iyi. Düzeltici önlemler uygulamak için devir sayısını tespit etmeye çalışırsanız, şansınız yanlış bir şey yapıyorsunuz demektir. Arduino programlarının çoğu, yalnızca 50 ms boyunca bir düğmeyi açmak veya bir ısıtıcıyı 12 saat süreyle açmak gibi, nispeten kısa süreleri kapsayan olayları yönetmek zorundadır ... O zaman, ve program bir yıllarca çalışacak olsa bile, millis devri bir endişe olmamalı.

Devrilme problemini yönetmenin (veya yerine yönetmekten kaçınmanın) doğru yolu , modüler aritmetik açısından unsigned longdöndürülen sayıyı düşünmektir . Matematiksel olarak eğimli olarak, bu kavram ile ilgili biraz bilgi sahibi olmak programlama sırasında çok yararlıdır. Matematiği Nick Gammon'un millis () taşması ... kötü bir makalesinde görebilirsiniz. . Hesaplama detaylarına bakmak istemeyenler için, burada düşünmenin alternatif (umarım daha basit) bir yolunu sunuyorum. Instantlar ve süreler arasındaki basit farklara dayanmaktadır . Testleriniz sadece süreleri karşılaştırmayı içerdiği sürece, iyi olmalısınız.millis()

Mikrosta Not () : Burada bahsettiğimiz her şey, her 71.6 dakikada bir devrildiği ve aşağıda verilen fonksiyonun etkilemediği durumlar için millis()aynı şekilde geçerlidir .micros()micros()setMillis()micros()

Örnekler, zaman damgaları ve süreleri

Zamanla uğraşırken, en az iki farklı kavram arasında ayrım yapmalıyız: instantlar ve süreler . Anlık, zaman eksenindeki bir noktadır. Süre, bir zaman aralığının uzunluğudur, yani aralığın başlangıcını ve sonunu tanımlayan instantlar arasındaki zaman aralığıdır. Bu kavramlar arasındaki fark her zaman günlük dilde çok keskin değildir. Diyorum Örneğin, “ Beş dakika içinde olacak o zaman”, “ beş dakika ” tahmini olan süre benim olmaması “oysa, beş dakika içinde ” dir anlık Tahmininin geri gelmesi. Ayrımı göz önünde bulundurmak önemlidir, çünkü devrilme probleminden tamamen kaçınmanın en basit yolu budur.

Bunun dönüş değeri millis()bir süre olarak yorumlanabilir: programın başlangıcından bugüne kadar geçen süre. Bununla birlikte, bu yorum, milis taşar taşmaz bozulur. Genellikle millis()bir zaman damgası , yani belirli bir anı tanımlayan bir “etiket” döndürmek olarak düşünmek çok daha yararlıdır . Her 49.7 günde bir tekrar kullanıldıklarından, bu yorumlamanın bu etiketlerin belirsiz olmalarından muzdarip olduğu söylenebilir. Bununla birlikte, bu nadiren bir sorundur: gömülü uygulamaların çoğunda, 49.7 gün önce olan herhangi bir şey umursamadığımız eski tarihtir. Bu nedenle, eski etiketlerin geri dönüşümü sorun olmamalıdır.

Zaman damgalarını karşılaştırmayın

İki zaman damgası arasında hangisinin diğerinden daha büyük olduğunu bulmaya çalışmak bir anlam ifade etmiyor. Örnek:

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

Doğal olarak kişi durumunun if ()daima doğru olmasını bekler . Ancak, eğer milis sırasında taşma olursa aslında yanlış olacaktır delay(3000). T1 ve t2'yi geri dönüştürülebilir etiketler olarak düşünmek, hatayı önlemenin en kolay yoludur: t1 etiketi t2'den önce bir an için açıkça tanımlanmıştır, ancak 49.7 gün sonra gelecekteki bir anına atanacaktır. Böylece, t1, t2'den önce ve sonra gerçekleşir . Bu, ifadenin bir t2 > t1anlam ifade etmediğini açıkça belirtmelidir .

Ancak, bunlar yalnızca etiketlerse, açık soru şudur: Onlarla herhangi bir faydalı zaman hesaplamasını nasıl yapabiliriz? Cevap: Kendimizi zaman damgalarına duyarlı olan sadece iki hesaplamayla sınırlayarak:

  1. later_timestamp - earlier_timestampbir zaman verir, yani önceki anlık ile sonraki anlık arasındaki geçen zaman miktarı. Bu, zaman damgalarını içeren en kullanışlı aritmetik işlemdir.
  2. timestamp ± durationbaşlangıç ​​zaman damgasından bir süre sonra (+ kullanılıyorsa) veya öncesinde (eğer -) bir zaman damgası verir. Göründüğü kadar kullanışlı değil, çünkü ortaya çıkan zaman damgası yalnızca iki hesaplama türünde kullanılabiliyor ...

Modüler aritmetik sayesinde, her ikisinin de, hadis geçişi boyunca en azından ilgili gecikmeler 49,7 günden daha kısa olduğu sürece iyi çalışacağı garanti edilir.

Süreleri karşılaştırmak iyidir

Bir süre, belirli bir zaman aralığı boyunca geçen milisaniye miktarıdır. 49.7 günden daha uzun sürelerle işlem yapmamız gerekmediği sürece, fiziksel olarak mantıklı olan herhangi bir işlem de hesaplama açısından mantıklı olmalıdır. Örneğin, bir süreyi sıklık olarak almak için süreyi sıklık ile çarpabiliriz. Veya hangisinin daha uzun olduğunu bilmek için iki süreyi karşılaştırabiliriz. Örneğin, işte iki alternatif uygulama delay(). İlk önce, adamcağız:

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;
    }
}

Ve işte doğru olanı:

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;
    }
}

Çoğu C programcısı yukarıdaki döngüler gibi kısa bir formda yazardı.

while (millis() < start + ms) ;  // BUGGY version

ve

while (millis() - start < ms) ;  // CORRECT version

Her ne kadar aldatıcı bir şekilde benzer görünseler de, zaman damgası / süre ayrımı hangisinin arabası ve hangisinin doğru olduğunu açıkça belirtmelidir.

Zaman damgalarını gerçekten karşılaştırmam gerekirse ne olur?

Durumdan kaçınmaya çalışın. Kaçınılması mümkün değilse, ilgili taraftarların yeterince yakın olduğu biliniyorsa hala umut vardır: 24.85 günden daha yakın. Evet, maksimum 49.7 gün yönetilebilir gecikme süremiz ikiye katlandı.

Açık bir çözüm, zaman damgası karşılaştırma sorunumuzu bir süre karşılaştırma sorununa dönüştürmektir. Anında t1'in t2'den önce mi yoksa sonra mı olduğunu bilmemiz gerektiğini söyleyin. Ortak geçmişlerinde bir miktar referans anında seçeriz ve bu referanstaki süreleri hem t1 hem de t2'ye kadar karşılaştırırız. Referans anlık, t1 veya t2'den yeterince uzun bir süre çıkarılarak elde edilir:

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

Bu basitleştirilebilir:

if (t1 - t2 + LONG_ENOUGH_DURATION < LONG_ENOUGH_DURATION)
    // t1 is before t2

Daha da basitleştirmek için cazip geliyor if (t1 - t2 < 0). Açıkçası, bu işe yaramıyor, çünkü t1 - t2imzasız bir sayı olarak hesaplanmak negatif olamaz. Bu, ancak, taşınabilir olmasa da, işe yarıyor:

if ((signed long)(t1 - t2) < 0)  // works with gcc
    // t1 is before t2

Yukarıdaki anahtar kelime signedgereksizdir (bir ova longher zaman imzalanır), ancak amacı netleştirmeye yardımcı olur. İmzalı bir uzun LONG_ENOUGH_DURATIONzamana dönüştürme, 24.85 güne eşit ayarlara eşdeğerdir . Hile taşınabilir değildir, çünkü C standardına göre sonuç tanımlanır . Fakat gcc derleyicisi doğru olanı yapmayı vaat ettiğinden, Arduino'da güvenilir bir şekilde çalışır. Uygulama tarafından tanımlanmış davranışlardan kaçınmak istiyorsak, yukarıda işaretli karşılaştırma buna matematiksel olarak eşdeğerdir:

#include <limits.h>

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

Karşılaştırma geriye dönük görünüyor tek sorunla. Ayrıca, bu tek bitlik sınama için, 32 bit olduğu sürece, eşdeğerdir:

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

Son üç test aslında aynı makine koduna gcc tarafından derlenir.

Çizimimi millis geçişine karşı nasıl test ederim?

Yukarıdaki prensipleri izlerseniz, hepiniz iyi olmalısınız. Yine de test etmek istiyorsanız, bu işlevi çiziminize ekleyin:

#include <util/atomic.h>

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

ve şimdi programınızı arayarak zaman yolculuğu yapabilirsiniz setMillis(destination). Eğer değirmenciler arasında tekrar tekrar taşma yapmasını istiyorsanız, Phil Connors'ın Groundhog Day'ı yeniden yaşadığı gibi, bunu içine koyabilirsiniz loop():

// 6-second time loop starting at rollover - 3 seconds
if (millis() - (-3000) >= 6000)
    setMillis(-3000);

Yukarıdaki negatif zaman damgası (-3000), derleyici tarafından rollover'dan önce 3000 milisaniyeye karşılık gelen imzasız bir uzunluğa örtük olarak dönüştürülür (4294964296'ya dönüştürülür).

Ya gerçekten çok uzun süreler izlemem gerekirse?

Bir röleyi açmanız ve üç ay sonra kapatmanız gerekirse, gerçekten milis taşmalarını izlemeniz gerekir. Bunu yapmanın birçok yolu var. En basit çözüm, yalnızca millis() 64 bite kadar uzatmak olabilir :

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;
}

Bu, esasen rollover olaylarını sayıyor ve bu sayımı 64 bit milisaniyelik sayının en önemli 32 bit olarak kullanmak. Bu saymanın doğru çalışması için, fonksiyonun her 49.7 günde en az bir kez çağrılması gerekir. Ancak, 49.7 günde yalnızca bir kez çağrılırsa, bazı durumlarda kontrolün (new_low32 < low32)başarısız olması ve kodun bir sayıyı kaçırması mümkündür high32. Tek bir millis sargısında (belirli bir 49.7 günlük pencere) bu koda ne zaman karar verileceğine karar vermek için millis () kullanmak, zaman dilimlerinin nasıl sıralandığına bağlı olarak çok tehlikeli olabilir. Güvenlik için, millis64 () işlevine yalnızca aramaların ne zaman yapılacağını belirlemek için millis () kullanıyorsanız, her 49.7 günde bir pencerede en az iki arama yapılması gerekir.

Yine de, 64 bit aritmetiğin Arduino için pahalı olduğunu unutmayın. 32 bit'te kalabilmek için zaman çözünürlüğünü azaltmak faydalı olabilir.


2
Yani, soruda yazılı olan kodun gerçekten işe yarayacağını mı söylüyorsunuz?
Jasen

3
@Jasen: Kesinlikle! İnsanların ilk başta olmayan problemi “düzeltmeye” çalışan bir kereden fazla gözüküyorum.
Edgar Bonet

2
Bunu bulduğuma sevindim. Bu soruyu daha önce de yaşadım.
Sebastian Freeman

1
StackExchange'in en iyi ve en kullanışlı cevaplarından biri! Çok teşekkürler! :)
Falko

Bu soruya inanılmaz bir cevap. Temelde yılda bir kez bu cevaba geri dönüyorum çünkü rollover'ları karıştırmak paranoyaklığım var.
Jeffrey Cash

17

TL; DR Kısa versiyon:

An unsigned long0 ila 4,294,967,295 (2 ^ 32-1) 'dir.

Diyelim ki previousMillis4,294,967,290 (rollover'dan 5 ms önce) ve currentMillis10 (rollover'dan 10 ms sonra). O zaman currentMillis - previousMillisgerçek 16'dır (-4,294,967,280 değil) çünkü sonuç işaretsiz bir uzun olarak hesaplanır (bu negatif olamaz, bu yüzden kendi kendine döner). Bunu basitçe kontrol edebilirsiniz:

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

Bu yüzden yukarıdaki kod kusursuz çalışacaktır. İşin püf noktası, zaman farkını her zaman hesaplamak ve iki zaman değerini karşılaştırmamaktır.


Nasıl yaklaşık 15ms rollover öncesi ve bir 10ms devrilme sonra (yani 49.7 gün sonra ). 15> 10 , fakat 15ms'lik pul neredeyse bir buçuk ay eski. 15-10> 0 ve 10-15> 0 unsigned mantık, bu yüzden burada kullanım yok!
ps95

@ prakharsingh95 10ms-15ms ~ 49.7 gün olacak - 5ms, ki bu doğru fark. Matematik millis()iki kez geçene kadar çalışır , ancak söz konusu kodda gerçekleşmesi pek olası değildir.
BrettAM

Tekrar ifade etmeme izin ver. İki zaman damgası 200ms ve 10ms olduğunu varsayalım. Hangisinin yuvarlandığını nasıl söylersin?
ps95,

@ prakharsingh95 Saklananın previousMillisdaha önce ölçülmüş olması gerekiyor currentMillis, yani bir rollover oluşmuşsa currentMillisdaha küçükse previousMillis. Matematik, iki rollover olmadıkça, düşünmeniz bile gerekmediğini ortaya çıkarır.
BrettAM

1
Ah tamam. yaparsanız t2-t1ve garanti ederseniz daha t1önce ölçülmüşse t2, imzalanmaya eşdeğerdir (t2-t1)% 4,294,967,295, bu nedenle otomatik sarma. Güzel!. Peki ya iki rollover varsa, ya intervalda> 4,294,967,295 ise?
ps95

1

millis()Bir sınıfa sar !

Mantık:

  1. millis()Doğrudan yerine kimlikleri kullanın .
  2. Kimlikleri kullanarak terslerini karşılaştırın. Bu temiz ve devrilme bağımsızdır.
  3. Özel uygulamalar için, iki kimlik arasındaki kesin farkı hesaplamak için, ters çevrmeleri ve pulları takip edin. Farkı hesapla.

Geri dönüşlerin kaydını tutmak:

  1. Yerel bir pulu periyodik olarak daha hızlı güncelleyin millis(). Bu millis()taşma olup olmadığını öğrenmenize yardımcı olacaktır .
  2. Zamanlayıcının süresi doğruluğu belirler
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
}

Zamanlayıcı kredisi .


9
Kodu derlemesini engelleyen maaaaany hatalarını kaldırmak için kodu düzenledim. Bu malzeme size yaklaşık 232 byte RAM ve iki PWM kanalına mal olacak. Ayrıca get_stamp()51 kez sonra belleği bozmaya başlayacaktır . Zaman damgaları yerine gecikmeleri karşılaştırmak kesinlikle daha verimli olacaktır.
Edgar Bonet

1

Bu soruyu sevdim ve yarattığı harika cevaplar. Öncelikle önceki bir cevapla ilgili hızlı bir yorum (biliyorum, biliyorum, ama henüz yorum yapacak kadar cevabım yok. :-).

Edgar Bonet'in cevabı şaşırtıcıydı. 35 yıldır kodlama yapıyorum ve bugün yeni bir şey öğrendim. Teşekkür ederim. Bu, "Gerçekten çok uzun süreler izlemem gerekirse ne olur?" millis64 () işlevini her rollover dönemi için en az bir kez aramazsanız kırılır. Gerçekten nitpicky ve gerçek dünyadaki bir uygulamada bir sorun olması pek mümkün değil, ama işte gidiyorsunuz.

Şimdi, herhangi bir aklı başında zaman aralığını kapsayan zaman damgaları gerçekten istediyseniz (64 bit milisaniye, benim hesaplamamla yaklaşık yarım milyar yıldır), mevcut millis () uygulamasının 64 bit'e genişletilmesi basit görünüyor.

Attinycore / wiring.c'deki (ATTiny85 ile çalışıyorum) yapılan değişiklikler çalışıyor gibi görünüyor (diğer AVR kodlarının çok benzer olduğunu varsayıyorum). // BFB yorumlarına ve yeni millis64 () işlevine sahip satırlara bakın. Açıkçası hem daha büyük (98 bayt kod, 4 bayt veri), hem de daha yavaş olacak ve Edgar'ın belirttiği gibi, neredeyse kesinlikle imzasız tamsayı matematiğini daha iyi anlamakla hedeflerinizi gerçekleştirebilirsiniz, ancak ilginç bir alıştırma oldu. .

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
Haklısın, millis64()sadece işe alım devresinden daha sık çağrılırsa çalışır. Bu sınırlamaya işaret etmek için cevabımı düzenledim. Sürümünüzde bu sorun yoktur, ancak başka bir dezavantajı vardır: kesme bağlamında 64-bit aritmetik işlemi yapar , bu da bazen diğer kesintilere yanıt verme gecikmesini artırır.
Edgar Bonet
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.