Bagaimana saya bisa menangani rollover millis ()?

73

Saya perlu membaca sensor setiap lima menit, tetapi karena sketsa saya juga memiliki tugas-tugas lain yang harus dilakukan, saya tidak bisa hanya delay()antara membaca. Ada tutorial Blink tanpa penundaan yang menyarankan saya kode di sepanjang baris ini:

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

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

    // Do other stuff...
}

Masalahnya adalah bahwa millis()akan berguling kembali ke nol setelah sekitar 49,7 hari. Karena sketsa saya dimaksudkan untuk berjalan lebih lama dari itu, saya perlu memastikan rollover tidak membuat sketsa saya gagal. Saya dapat dengan mudah mendeteksi kondisi rollover ( currentMillis < previousMillis), tetapi saya tidak yakin apa yang harus saya lakukan.

Jadi pertanyaan saya: apa cara yang tepat / paling sederhana untuk menangani millis()rollover?

Edgar Bonet
sumber
5
Catatan editorial: Ini bukan pertanyaan saya, melainkan tutorial dalam format pertanyaan / jawaban. Saya telah menyaksikan banyak kebingungan di Internet (termasuk di sini) tentang topik ini, dan situs ini sepertinya merupakan tempat yang jelas untuk mencari jawaban. Inilah mengapa saya memberikan tutorial ini di sini.
Edgar Bonet
2
Saya akan melakukan previousMillis += intervalalih - alih previousMillis = currentMillisjika saya ingin frekuensi hasil tertentu.
Jasen
4
@ Yasen: Benar! previousMillis += intervaljika Anda menginginkan frekuensi konstan dan yakin bahwa pemrosesan Anda membutuhkan waktu kurang dari interval, tetapi previousMillis = currentMillisuntuk menjamin penundaan minimum interval.
Edgar Bonet
Kami benar-benar membutuhkan FAQ untuk hal-hal seperti ini.
Salah satu "trik" yang saya gunakan adalah meringankan beban pada Arduino dengan menggunakan int terkecil yang berisi interval. Misalnya, untuk interval maksimum 1 menit, saya menulisuint16_t previousMillis; const uint16_t interval = 45000; ... uint16_t currentMillis = (uint16_t) millis(); if ((currentMillis - previousMillis) >= interval) ...
frarugi87

Jawaban:

95

Jawaban singkat: jangan coba-coba "menangani" millis rollover, sebagai gantinya tulis kode aman rollover. Contoh kode Anda dari tutorial baik-baik saja. Jika Anda mencoba mendeteksi rollover untuk menerapkan tindakan korektif, kemungkinan Anda melakukan sesuatu yang salah. Sebagian besar program Arduino hanya perlu mengatur acara yang memiliki rentang waktu yang relatif singkat, seperti mendebit tombol selama 50 ms, atau menyalakan pemanas selama 12 jam ... Lalu, dan bahkan jika program dimaksudkan untuk berjalan selama bertahun-tahun pada suatu waktu, rollover millis seharusnya tidak menjadi perhatian.

Cara yang benar untuk mengelola (atau lebih tepatnya, menghindari harus mengelola) masalah rollover adalah dengan memikirkan unsigned longangka yang dikembalikan oleh millis()dalam hal aritmatika modular . Untuk yang cenderung matematis, beberapa keakraban dengan konsep ini sangat berguna saat pemrograman. Anda dapat melihat matematika beraksi di artikel millis Nick Gammon () melimpah ... hal yang buruk? . Bagi mereka yang tidak ingin melalui rincian komputasi, saya menawarkan cara berpikir alternatif (semoga lebih sederhana) di sini. Ini didasarkan pada perbedaan sederhana antara instance dan durasi . Selama tes Anda hanya melibatkan membandingkan durasi, Anda harus baik-baik saja.

Catatan pada micros () : Segala sesuatu yang dikatakan di sini tentang millis()berlaku sama untuk micros(), kecuali untuk kenyataan yang micros()bergulir setiap 71,6 menit, dan setMillis()fungsi yang disediakan di bawah ini tidak mempengaruhi micros().

Instan, stempel waktu, dan durasi

Ketika berhadapan dengan waktu, kita harus membuat perbedaan antara setidaknya dua konsep yang berbeda: instan dan durasi . Instan adalah titik pada sumbu waktu. Durasi adalah panjang interval waktu, yaitu jarak waktu antara instance yang menentukan awal dan akhir interval. Perbedaan antara konsep-konsep ini tidak selalu sangat tajam dalam bahasa sehari-hari. Misalnya, jika saya mengatakan " Saya akan kembali dalam lima menit ", maka " lima menit " adalah perkiraan durasi ketidakhadiran saya, sedangkan " dalam lima menit " adalah instan prediksi saya akan kembali. Mempertahankan perbedaan dalam pikiran adalah penting, karena ini adalah cara paling sederhana untuk sepenuhnya menghindari masalah rollover.

Nilai kembali dari millis()dapat diartikan sebagai durasi: waktu berlalu sejak awal program hingga sekarang. Namun, interpretasi ini rusak segera setelah millis meluap. Secara umum jauh lebih berguna untuk menganggap millis()mengembalikan cap waktu , yaitu "label" yang mengidentifikasi instan tertentu. Dapat dikatakan bahwa penafsiran ini menderita dari label-label ini menjadi ambigu, karena mereka digunakan kembali setiap 49,7 hari. Ini, bagaimanapun, jarang menjadi masalah: di sebagian besar aplikasi embedded, apa pun yang terjadi 49,7 hari yang lalu adalah sejarah kuno yang tidak kita pedulikan. Dengan demikian, daur ulang label lama seharusnya tidak menjadi masalah.

Jangan bandingkan cap waktu

Mencoba mencari tahu di antara dua cap waktu yang lebih besar dari yang lain tidak masuk akal. Contoh:

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

Secara naif, orang akan mengharapkan kondisi if ()untuk selalu benar. Tapi itu akan benar-benar salah jika millis meluap selama delay(3000). Memikirkan t1 dan t2 sebagai label yang dapat didaur ulang adalah cara paling sederhana untuk menghindari kesalahan: label t1 telah jelas ditugaskan untuk instan sebelum t2, tetapi dalam 49,7 hari itu akan dipindahkan ke instan berikutnya. Jadi, t1 terjadi sebelum dan sesudah t2. Ini harus menjelaskan bahwa ungkapan itu t2 > t1tidak masuk akal.

Tetapi, jika ini hanya label, pertanyaan yang jelas adalah: bagaimana kita bisa melakukan perhitungan waktu yang berguna dengan mereka? Jawabannya adalah: dengan membatasi diri hanya pada dua perhitungan yang masuk akal untuk cap waktu:

  1. later_timestamp - earlier_timestampmenghasilkan durasi, yaitu jumlah waktu yang berlalu antara instan sebelumnya dan instan selanjutnya. Ini adalah operasi aritmatika paling berguna yang melibatkan cap waktu.
  2. timestamp ± durationmenghasilkan cap waktu yang beberapa waktu setelah (jika menggunakan +) atau sebelum (jika -) cap waktu awal. Tidak berguna seperti kedengarannya, karena cap waktu yang dihasilkan hanya dapat digunakan dalam dua jenis perhitungan ...

Berkat aritmatika modular, keduanya dijamin bekerja dengan baik di seluruh rollover millis, setidaknya selama penundaan yang terjadi lebih pendek dari 49,7 hari.

Membandingkan durasi tidak masalah

Durasi hanya jumlah milidetik yang berlalu selama beberapa interval waktu. Selama kita tidak perlu menangani durasi lebih lama dari 49,7 hari, operasi apa pun yang secara fisik masuk akal juga harus masuk akal secara komputasi. Kita dapat, misalnya, mengalikan durasi dengan frekuensi untuk mendapatkan sejumlah periode. Atau kita bisa membandingkan dua durasi untuk mengetahui mana yang lebih panjang. Sebagai contoh, berikut adalah dua implementasi alternatif dari delay(). Pertama, yang buggy:

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

Dan ini yang benar:

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

Kebanyakan programmer C akan menulis loop di atas dalam bentuk terser, seperti

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

dan

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

Meskipun mereka tampak serupa, perbedaan cap waktu / durasi harus memperjelas mana yang bermasalah dan mana yang benar.

Bagaimana jika saya benar-benar perlu membandingkan cap waktu?

Lebih baik coba menghindari situasi. Jika tidak dapat dihindari, masih ada harapan jika diketahui bahwa masing-masing instance cukup dekat: lebih dekat dari 24,85 hari. Ya, penundaan maksimum yang dapat dikelola kami selama 49,7 hari baru saja dikurangi setengahnya.

Solusi yang jelas adalah mengubah masalah perbandingan cap waktu kami menjadi masalah perbandingan durasi. Katakanlah kita perlu tahu apakah t1 instan sebelum atau sesudah t2. Kami memilih beberapa referensi instan di masa lalu mereka yang sama, dan membandingkan durasi dari referensi ini hingga t1 dan t2. Referensi instan diperoleh dengan mengurangi durasi yang cukup lama dari t1 atau 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

Ini dapat disederhanakan sebagai:

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

Sangat menggoda untuk menyederhanakan lebih jauh ke dalam if (t1 - t2 < 0). Jelas, ini tidak berhasil, karena t1 - t2, dihitung sebagai angka yang tidak ditandatangani, tidak boleh negatif. Namun, ini meskipun tidak portabel, berfungsi:

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

Kata kunci di signedatas adalah mubazir (polos longselalu ditandatangani), tetapi membantu memperjelas maksudnya. Mengonversi ke waktu yang ditandatangani setara dengan pengaturan yang LONG_ENOUGH_DURATIONsetara dengan 24,85 hari. Caranya tidak portabel karena, menurut standar C, hasilnya adalah implementasi yang ditentukan . Tetapi karena kompiler gcc berjanji untuk melakukan hal yang benar , ia bekerja dengan andal pada Arduino. Jika kami ingin menghindari implementasi perilaku yang ditentukan, perbandingan yang ditandatangani di atas secara matematis setara dengan ini:

#include <limits.h>

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

dengan satu-satunya masalah yang perbandingannya terlihat mundur. Ini juga setara, asalkan rindu 32-bit, untuk uji bit tunggal ini:

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

Tiga tes terakhir sebenarnya dikompilasi oleh gcc ke dalam kode mesin yang sama persis.

Bagaimana cara menguji sketsa saya terhadap rollover milis

Jika Anda mengikuti sila di atas, Anda harus baik-baik saja. Namun jika Anda ingin menguji, tambahkan fungsi ini ke sketsa Anda:

#include <util/atomic.h>

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

dan sekarang Anda dapat melakukan perjalanan waktu ke program Anda dengan menelepon setMillis(destination). Jika Anda ingin melewati millis overflow berulang-ulang, seperti Phil Connors yang menghidupkan kembali Groundhog Day, Anda dapat memasukkan ini ke dalam loop():

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

Stempel waktu negatif di atas (-3000) secara implisit dikonversi oleh kompiler ke panjang yang tidak bertanda yang sesuai dengan 3000 milidetik sebelum rollover (dikonversi menjadi 4294964296).

Bagaimana jika saya benar-benar perlu melacak durasi yang sangat lama?

Jika Anda perlu menyalakan relay dan mematikannya tiga bulan kemudian, maka Anda benar-benar perlu melacak millis overflow. Ada banyak cara untuk melakukannya. Solusi yang paling mudah adalah dengan memperpanjang millis() ke 64 bit:

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

Ini pada dasarnya menghitung peristiwa rollover, dan menggunakan penghitungan ini sebagai 32 bit paling signifikan dari hitungan 64 milidetik. Agar penghitungan ini berfungsi dengan baik, fungsi harus dipanggil setidaknya sekali setiap 49,7 hari. Namun, jika itu hanya dipanggil sekali per 49,7 hari, untuk beberapa kasus ada kemungkinan bahwa cek (new_low32 < low32)gagal dan kode melewatkan hitungan high32. Menggunakan millis () untuk memutuskan kapan harus membuat satu-satunya panggilan ke kode ini dalam satu "bungkus" millis (jendela 49,7 hari tertentu) bisa sangat berbahaya, tergantung pada bagaimana kerangka waktu berbaris. Untuk keamanan, jika menggunakan millis () untuk menentukan kapan membuat satu-satunya panggilan ke millis64 (), harus ada setidaknya dua panggilan di setiap jendela 49,7 hari.

Perlu diingat, bahwa aritmatika 64 bit mahal pada Arduino. Mungkin perlu untuk mengurangi resolusi waktu agar tetap pada 32 bit.

Edgar Bonet
sumber
2
Jadi, Anda mengatakan bahwa kode yang ditulis dalam pertanyaan akan benar-benar berfungsi dengan benar?
Jasen
3
@ Yasen: Tepat! Saya sudah tampak lebih dari sekali orang mencoba untuk "memperbaiki" masalah yang tidak ada di tempat pertama.
Edgar Bonet
2
Aku senang menemukan ini. Saya punya pertanyaan ini sebelumnya.
Sebastian Freeman
1
Salah satu jawaban terbaik dan paling berguna di StackExchange! Terima kasih banyak! :)
Falko
Ini adalah jawaban yang luar biasa untuk pertanyaan itu. Saya kembali ke jawaban ini pada dasarnya setahun sekali karena saya paranoid mengacaukan rollover.
Jeffrey Cash
17

TL; DR Versi singkat:

An unsigned longadalah 0 hingga 4.294.967.295 (2 ^ 32 - 1).

Jadi katakanlah previousMillisadalah 4.294.967.290 (5 ms sebelum rollover), dan currentMillis10 (10ms setelah rollover). Maka currentMillis - previousMillisaktual 16 (tidak -4.294.967.280) karena hasilnya akan dihitung sebagai panjang yang tidak ditandatangani (yang tidak bisa negatif, jadi itu sendiri akan berguling-guling). Anda dapat memeriksanya hanya dengan:

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

Jadi kode di atas akan berfungsi dengan baik. Caranya adalah dengan selalu menghitung perbedaan waktu, dan tidak membandingkan dua nilai waktu.

Gerben
sumber
Bagaimana dengan 15ms sebelum rollover dan 10ms setelah rollover (yaitu 49,7 hari setelah ). 15> 10 , tetapi perangko 15ms hampir satu setengah bulan. 15-10> 0 dan 10-15> 0 unsigned logika, jadi tidak ada gunanya di sini!
ps95
@ prakharsingh95 10ms-15ms akan menjadi ~ 49,7 hari - 5ms, yang merupakan perbedaan yang benar. Matematika bekerja sampai millis()berguling dua kali, tetapi itu sangat tidak mungkin terjadi pada kode yang dimaksud.
BrettAM
Biarkan saya ulangi. Misalkan Anda memiliki dua cap waktu 200 ms dan 10 ms. Bagaimana Anda tahu mana yang terguling?
ps95
@ prakharsingh95 Yang disimpan di previousMillisharus sudah diukur sebelumnya currentMillis, jadi jika currentMillislebih kecil dari previousMillisrollover terjadi. Matematika menghitung bahwa kecuali dua rollover telah terjadi, Anda bahkan tidak perlu memikirkannya.
BrettAM
1
Ah, baiklah. jika Anda melakukannya t2-t1, dan jika Anda dapat menjamin t1diukur sebelumnya t2maka itu setara dengan yang ditandatangani (t2-t1)% 4,294,967,295 , maka sampul otomatis. Bagus!. Tetapi bagaimana jika ada dua rollover, atau interval> 4.294.967.295?
ps95
1

Bungkus millis()dalam kelas!

Logika:

  1. Gunakan id daripada millis()langsung.
  2. Bandingkan pembalikan menggunakan id. Ini bersih dan bebas rollover.
  3. Untuk aplikasi tertentu, untuk menghitung selisih tepat antara dua id, catat pembalikan dan prangko. Hitung perbedaannya.

Melacak pembalikan:

  1. Perbarui prangko lokal secara berkala lebih cepat dari millis(). Ini akan membantu Anda mengetahui apakah millis()telah meluap.
  2. Periode timer menentukan akurasi
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
}

Pengukur waktu kredit .

ps95
sumber
9
Saya mengedit kode untuk menghapus kesalahan maaaaany yang mencegahnya untuk dikompilasi. Barang ini akan dikenakan biaya sekitar 232 byte RAM dan dua saluran PWM. Ini juga akan mulai merusak memori setelah Anda get_stamp()51 kali. Membandingkan penundaan dan bukan cap waktu tentu akan lebih efisien.
Edgar Bonet
1

Saya menyukai pertanyaan ini, dan jawaban-jawaban hebatnya dihasilkan. Pertama komentar cepat pada jawaban sebelumnya (saya tahu, saya tahu, tetapi saya belum memiliki perwakilan untuk berkomentar. :-).

Jawaban Edgar Bonet luar biasa. Saya telah mengkode selama 35 tahun, dan saya belajar sesuatu yang baru hari ini. Terima kasih. Yang mengatakan, saya percaya kode untuk "Bagaimana jika saya benar-benar perlu melacak durasi yang sangat lama?" istirahat kecuali jika Anda memanggil millis64 () setidaknya sekali per periode rollover. Benar-benar nitpicky, dan tidak mungkin menjadi masalah dalam implementasi dunia nyata, tapi begitulah.

Sekarang, jika Anda benar-benar ingin cap waktu yang mencakup rentang waktu yang waras (64-bit milidetik adalah sekitar setengah miliar tahun menurut saya), kelihatannya mudah untuk memperpanjang implementasi milis () yang ada menjadi 64 bit.

Perubahan-perubahan ini ke attinycore / wiring.c (saya bekerja dengan ATTiny85) tampaknya berfungsi (Saya mengasumsikan kode untuk AVR lain sangat mirip). Lihat baris dengan komentar // BFB, dan fungsi millis64 () baru. Jelas itu akan menjadi lebih besar (98 byte kode, 4 byte data) dan lebih lambat, dan seperti yang ditunjukkan Edgar, Anda hampir pasti dapat mencapai tujuan Anda hanya dengan pemahaman yang lebih baik tentang matematika bilangan bulat tanpa tanda, tetapi itu adalah latihan yang menarik .

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;
}
brainbarker
sumber
1
Anda benar, saya millis64()hanya berfungsi jika dipanggil lebih sering daripada periode rollover. Saya mengedit jawaban saya untuk menunjukkan batasan ini. Versi Anda tidak memiliki masalah ini, tetapi memiliki kelemahan lain: ia melakukan aritmatika 64-bit dalam konteks interupsi , yang kadang-kadang meningkatkan latensi dalam menanggapi interupsi lainnya.
Edgar Bonet