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 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 > 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:
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 signed
atas adalah mubazir (polos long
selalu ditandatangani), tetapi membantu memperjelas maksudnya. Mengonversi ke waktu yang ditandatangani setara dengan pengaturan yang LONG_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:
#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.