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 long
dö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 > t1
anlam 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:
later_timestamp - earlier_timestamp
bir 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.
timestamp ± duration
baş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 - t2
imzası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 signed
gereksizdir (bir ova long
her zaman imzalanır), ancak amacı netleştirmeye yardımcı olur. İmzalı bir uzun LONG_ENOUGH_DURATION
zamana 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.