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?
programming
time
millis
Edgar Bonet
sumber
sumber
previousMillis += interval
alih - alihpreviousMillis = currentMillis
jika saya ingin frekuensi hasil tertentu.previousMillis += interval
jika Anda menginginkan frekuensi konstan dan yakin bahwa pemrosesan Anda membutuhkan waktu kurang dariinterval
, tetapipreviousMillis = currentMillis
untuk menjamin penundaan minimuminterval
.uint16_t previousMillis; const uint16_t interval = 45000; ... uint16_t currentMillis = (uint16_t) millis(); if ((currentMillis - previousMillis) >= interval) ...
Jawaban:
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 long
angka yang dikembalikan olehmillis()
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 untukmicros()
, kecuali untuk kenyataan yangmicros()
bergulir setiap 71,6 menit, dansetMillis()
fungsi yang disediakan di bawah ini tidak mempengaruhimicros()
.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 menganggapmillis()
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:
Secara naif, orang akan mengharapkan kondisi
if ()
untuk selalu benar. Tapi itu akan benar-benar salah jika millis meluap selamadelay(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 itut2 > t1
tidak 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:
later_timestamp - earlier_timestamp
menghasilkan durasi, yaitu jumlah waktu yang berlalu antara instan sebelumnya dan instan selanjutnya. Ini adalah operasi aritmatika paling berguna yang melibatkan cap waktu.timestamp ± duration
menghasilkan 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:Dan ini yang benar:
Kebanyakan programmer C akan menulis loop di atas dalam bentuk terser, seperti
dan
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:
Ini dapat disederhanakan sebagai:
Sangat menggoda untuk menyederhanakan lebih jauh ke dalam
if (t1 - t2 < 0)
. Jelas, ini tidak berhasil, karenat1 - t2
, dihitung sebagai angka yang tidak ditandatangani, tidak boleh negatif. Namun, ini meskipun tidak portabel, berfungsi:Kata kunci di
signed
atas adalah mubazir (poloslong
selalu ditandatangani), tetapi membantu memperjelas maksudnya. Mengonversi ke waktu yang ditandatangani setara dengan pengaturan yangLONG_ENOUGH_DURATION
setara 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:dengan satu-satunya masalah yang perbandingannya terlihat mundur. Ini juga setara, asalkan rindu 32-bit, untuk uji bit tunggal ini:
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:
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 dalamloop()
: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: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 hitunganhigh32
. 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.
sumber
TL; DR Versi singkat:
An
unsigned long
adalah 0 hingga 4.294.967.295 (2 ^ 32 - 1).Jadi katakanlah
previousMillis
adalah 4.294.967.290 (5 ms sebelum rollover), dancurrentMillis
10 (10ms setelah rollover). MakacurrentMillis - previousMillis
aktual 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.
sumber
unsigned
logika, jadi tidak ada gunanya di sini!millis()
berguling dua kali, tetapi itu sangat tidak mungkin terjadi pada kode yang dimaksud.previousMillis
harus sudah diukur sebelumnyacurrentMillis
, jadi jikacurrentMillis
lebih kecil daripreviousMillis
rollover terjadi. Matematika menghitung bahwa kecuali dua rollover telah terjadi, Anda bahkan tidak perlu memikirkannya.t2-t1
, dan jika Anda dapat menjamint1
diukur sebelumnyat2
maka itu setara dengan yang ditandatangani(t2-t1)% 4,294,967,295
, maka sampul otomatis. Bagus!. Tetapi bagaimana jika ada dua rollover, atauinterval
> 4.294.967.295?Bungkus
millis()
dalam kelas!Logika:
millis()
langsung.Melacak pembalikan:
millis()
. Ini akan membantu Anda mengetahui apakahmillis()
telah meluap.Pengukur waktu kredit .
sumber
get_stamp()
51 kali. Membandingkan penundaan dan bukan cap waktu tentu akan lebih efisien.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 .
sumber
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.