4 / 6 tamamlandı
görev 4 · ~35 dk

49,7 gün

Windows 98 ve Boeing 787 — zaman sayacı dolunca ne olur

hedef

Zamanı sayan bir sayaç dolduğunda ne olduğunu görmek ve bundan etkilenmeyen kodu yazmak.

Ne oldu?

1999. Microsoft bir hata duyurdu: Windows 95 ve Windows 98 çalıştıran bilgisayarlar, hiç kapatılmadan 49,7 gün açık kalırsa kilitlenebiliyordu.

Neden 49,7?

Sistemin içinde, bilgisayar açıldığından beri geçen milisaniyeyi sayan bir sayaç vardı. 32 bitlik bir kutu. Bir 32 bitlik kutuya en fazla şu kadar sayı sığar:

kod
4.294.967.295

Bunu günlere çevirelim:

kod
4.294.967.295 ms ÷ 1000 = 4.294.967 saniye
             ÷ 60   =    71.582 dakika
             ÷ 60   =     1.193 saat
             ÷ 24   =     49,7 gün

49,7 günün sonunda sayaç doldu ve sıfıra döndü. Zamana bakan kodun bir kısmı bu dönüşü beklemiyordu.

Neden bu kadar geç fark edildi? O yıllarda bir bilgisayarı yedi hafta hiç kapatmadan çalıştırmak çok nadirdi. Hata, yeterince uzun bekleyen kimse olmadığı için gizli kaldı.

Bir de uçak

2015. Amerikan Havacılık İdaresi bir uyarı yayınladı: Boeing 787 uçaklarında, elektrik sistemi 248 gün hiç kapatılmadan çalışırsa, jeneratörleri kontrol eden üniteler kendini kapatabiliyordu. Sebep yine bir sayacın dolmasıydı.

Hiçbir uçak bu yüzden sorun yaşamadı — hata önceden bulundu. Yazılım düzeltilene kadar çözüm basitti: uçakların elektriği belli aralıklarla kapatılıp açılacaktı.

Ve senin kartında

Arduino'da millis() diye bir fonksiyon var: kart açıldığından beri geçen milisaniyeyi veriyor. Zamanı ölçmenin yolu bu — kitlerde "beş saniye sonra sula" diyen her satır ona bakıyor. Ve onun kutusu da 32 bit.

Senin Arduino'n da 49,7 günde sıfıra dönüyor. Windows 98'le aynı sayı, aynı gün.

49 gün bekleyemeyiz. O yüzden sayacı, dolmasına 3 saniye kala başlatıyoruz:

kod · .ino
void setup() {
  Serial.begin(9600);

  // 49,7. gun: sayacin dolmasina 3 saniye var
  unsigned long simdi     = 4294964296UL;
  unsigned long baslangic = simdi;

  // "5 saniye sonra" ne zaman?
  unsigned long bitis = baslangic + 5000;

  for (int adim = 0; adim <= 7; adim++) {
    bool bitti = (simdi >= bitis);

    Serial.print(adim);
    Serial.print(". saniye  sayac: ");
    Serial.print(simdi);
    Serial.print("  5 sn doldu mu: ");
    Serial.println(bitti ? "EVET" : "hayir");

    simdi = simdi + 1000;   // bir saniye gecti
  }
}

void loop() {
}

unsigned long 32 bitlik, eksi olmayan bir kutu — millis()'in kutusu tam olarak bu. Sondaki UL ise "bu sayıyı unsigned long olarak oku" demek.

Çalıştı mı?

kod
0. saniye  sayac: 4294964296  5 sn doldu mu: EVET
1. saniye  sayac: 4294965296  5 sn doldu mu: EVET
2. saniye  sayac: 4294966296  5 sn doldu mu: EVET
3. saniye  sayac: 0  5 sn doldu mu: hayir
4. saniye  sayac: 1000  5 sn doldu mu: hayir
5. saniye  sayac: 2000  5 sn doldu mu: EVET
6. saniye  sayac: 3000  5 sn doldu mu: EVET
7. saniye  sayac: 4000  5 sn doldu mu: EVET

Tamamen saçma.

Daha başlamadan "doldu" diyor. Üç saniye sonra fikrini değiştirip "dolmadı" diyor. İki saniye sonra yine "doldu".

Ne oldu? baslangic + 5000 toplaması kutuyu taşırdı ve başa sardı. bitis dev bir sayı olması gerekirken 2000 oldu. Sayaç 4 milyardan başladığı için 2000'den büyük, "doldu" dedi.

Kitlerde hep bir sulama, bir kilit, bir alarm "5 saniye sonra" bir şey yapıyordu. Bu kod 49,7. günde kilidi açık unutabilirdi.

Şimdi sen yaz

Karşılaştırmayı farklı yaz. "Şimdiki zaman bitiş zamanını geçti mi?" diye sorma. "Başladığımdan beri ne kadar zaman geçti?" diye sor.

kod · .ino
    // ŞİMDİ SEN YAZ:
    // bitti degiskenini toplama yerine CIKARMA ile hesapla.
    // Gecen sure = simdi - baslangic
🔍 İpucu 1

Geçen süreyi bul: simdi - baslangic. Sonra onu 5000 ile karşılaştır.

bitis değişkenine artık hiç ihtiyacın yok.

✅ Cevap
kod · .ino
    bool bitti = (simdi - baslangic >= 5000);

Şimdi sonuç:

kod
0. saniye  ...  hayir
1. saniye  ...  hayir
2. saniye  ...  hayir
3. saniye  ...  hayir
4. saniye  ...  hayir
5. saniye  ...  EVET
6. saniye  ...  EVET
7. saniye  ...  EVET

Tam beşinci saniyede "doldu" diyor. Sayaç arada sıfıra döndüğü halde.

Neden çalışıyor? Bir saat kadranı düşün. Saat 11'den saat 2'ye kaç saat geçti?

2 − 11 = −9 olmaz. Kadranda geriye sayarsan 3 saat. Saat kadranı başa sarıyor ama aradaki süre hâlâ doğru çıkıyor.

unsigned long da aynı kadran — sadece 12 yerine 4 milyar dilimli. Çıkarma da başa sarıyor, ve iki taşma birbirini götürüyor. Sonuç hep gerçekte geçen süre.

Toplama yaptığında ise "bitiş zamanı" kadranın öbür tarafına düşüyor ve karşılaştırma anlamını yitiriyor.

📄 Görevin tam kodu
kod · .ino
void setup() {
  Serial.begin(9600);

  unsigned long simdi     = 4294964296UL;
  unsigned long baslangic = simdi;

  for (int adim = 0; adim <= 7; adim++) {
    bool bitti = (simdi - baslangic >= 5000);

    Serial.print(adim);
    Serial.print(". saniye  sayac: ");
    Serial.print(simdi);
    Serial.print("  5 sn doldu mu: ");
    Serial.println(bitti ? "EVET" : "hayir");

    simdi = simdi + 1000;
  }
}

void loop() {
}

Olmadıysa

Ne görüyorsun Ne yap
Sayaç hiç sıfıra dönmüyor unsigned long yerine long ya da int yazmışsın. Kutu boyu önemli
Sayaç eksiye düşüyor unsigned kelimesini unutmuşsun. long işaretli, eksiye gider
integer constant is too large Sayının sonuna UL koymayı unutmuşsun
Düzeltmeden sonra hâlâ başta "EVET" Karşılaştırmada hâlâ bitis kullanıyorsun

Kendini test et

Soru: Windows 98'in hatası düzeltildi. Peki Arduino'nun millis() sayacı neden hâlâ 49,7 günde sıfıra dönüyor? Neden kimse düzeltmiyor?

Cevabı gör

Çünkü hata değil.

Her sayaç bir gün dolar. Kutuyu büyütsen de dolar — sadece daha geç. 64 bitlik bir sayaç yüz milyonlarca yıl dayanır ama sonsuz değildir. Kartın hafızası ise sınırlı; 32 bit makul bir seçim.

Hata sayacın dolması değil, dolacağını hesaba katmayan kod. O yüzden düzeltilmesi gereken yer kartın içi değil, senin yazdığın satır.

Kitlerde millis() - sonSulama >= SURE yazılmasının, hiçbir yerde millis() >= sonSulama + SURE yazılmamasının sebebi bu. millis()'i donmadan kullanmanın yolu ise ayrı bir konu — Yazılım temelleri Görev 6'da. İkisi 49 gün boyunca birebir aynı çalışır. Ellinci günde biri çalışmaya devam eder, öbürü saçmalar.

Bu hata nerede karşına çıkar

Akıllı sera kitinde pompa sulamadan sonra suyun toprağa inmesini beklerken, ev güvenlik kitinde kilit beş saniye sonra kendini kapatırken millis() kullanıyor. İkisinde de çıkarma ile yazdık. Orada "neden böyle?" diye sorduysan, cevabı buydu.


Takıldın, hata tablosu da yetmedi mi?

WhatsApp'tan sor — hangi görevdesin, ekranda ne yazıyor, devrenin fotoğrafı.