Arduino'da buton debounce yapmak, butondan gelen değişimi hemen kabul etmeyip belirli bir süre kararlı kaldıktan sonra tek olay saymak demektir. Buton kontakları basış anında kısa süre açılıp kapanabildiği için digitalRead() tek basışı birden çok basış gibi görebilir. Çözüm, delay() ile beklemek yerine millis() ile geçen süreyi ölçüp değişimi ancak sinyal oturduktan sonra saymaktır.
Butona bir kez bastın, sayaç neden birden fazla arttı? Durum mu, olay mı?
Mekanik bir buton, basıldığı anda kontaklarını tek hamlede temiz biçimde birleştirmez. Metal yüzeyler birbirine çarpıp çok kısa süre açılıp kapanabilir; bu dalgalanmaya bounce (buton titreşimi) denir. Dalgalanmanın süresi butonun yapısına ve eskimişliğine göre değişir; üreticinin veri sayfası senin butonun için en güvenilir kaynaktır.
İnsan gözü bunu fark etmez ama Arduino fark eder. Çünkü digitalRead() bir olay bildirmez, o an pinin durumunu okur. loop() çok hızlı döner; bir basış boyunca çok sayıda tur geçer ve dalgalanma sırasında bazı turlar LOW, bazıları HIGH okur.
Debounce, düğmeye bir kez basıldığında oluşan kısa süreli sinyal dalgalanmalarını tek bir kararlı olaya dönüştürme işlemidir.
Sayaç hatalarının çoğu tek bir karışıklıktan doğar: durumu olay sanmak. Kodda şöyle bir satır varsa sorun buradadır:
if (digitalRead(2) == LOW) {
sayac++;
}
Bu kod "buton şu an basılı mı?" sorusuna bakar. Buton basılı kaldığı her turda sayaç artar; dalgalanma varsa artış daha da çoğalır. Oysa sayaç "buton basıldı" olayına bağlanmalıdır, yani durumun HIGH'dan LOW'a değiştiği ana.
Üç teşhis sorusu
Belirti "tek basışta birden çok artış" ise nedeni şu üç soruyla ara:
- Sayacı durum mu artırıyor, değişim mi? Koşul yalnızca pinin o anki değerine bakıyorsa her turda tekrar sayarsın. Önceki okumayla karşılaştırma yoksa olay yoktur, yalnızca durum vardır.
- Okuma çok mu sık?
loop()bir basış süresince çok kez döner. Tek bir basış bile yüzlerce okuma üretebilir; bu yüzden "okuma" ile "basış" aynı şey değildir. - Dalgalanma var mı? Değişim kontrolü ekledikten sonra hâlâ fazladan artış görüyorsan sinyal gerçekten dalgalanıyordur. Bu durumda zaman aralığı kontrolü gerekir.
İlk iki soru kodun mantığına, üçüncüsü fiziksel butona işaret eder. Sorunun hangi katmanda olduğunu ayırmak, gelişigüzel süre denemekten çok daha hızlı sonuç verir.
Önceki durum karşılaştırması ve millis ile debounce nasıl kurulur?

Aşağıdaki sketch iki sayacı yan yana çalıştırır. Ham sayaç yalnızca önceki okuma ile şimdiki okuma farkına bakar. Filtreli sayaç ise değişimi, son değişimden bu yana yeterli süre geçtiyse kabul eder. Buton 2 numaralı pin ile GND arasına bağlıdır; INPUT_PULLUP satırı iç direnci devreye alır, böylece buton basılıyken pin LOW okunur.
const int BUTTON_PIN = 2;
const unsigned long DEBOUNCE_MS = 30; // başlangıç değeri, ayarlanabilir
int lastReading = HIGH;
int stableState = HIGH;
unsigned long lastChangeTime = 0;
int rawCount = 0;
int filteredCount = 0;
void setup() {
Serial.begin(9600);
pinMode(BUTTON_PIN, INPUT_PULLUP);
}
void loop() {
int reading = digitalRead(BUTTON_PIN);
if (reading != lastReading) {
lastChangeTime = millis();
if (reading == LOW) {
rawCount++; // filtresiz: her LOW geçişi sayılır
}
}
if (millis() - lastChangeTime >= DEBOUNCE_MS && reading != stableState) {
stableState = reading;
if (stableState == LOW) {
filteredCount++;
Serial.print("ham: ");
Serial.print(rawCount);
Serial.print(" filtreli: ");
Serial.println(filteredCount);
}
}
lastReading = reading;
}
Bir butonla üç kez basıldığında seri port monitöründe şuna benzer bir çıktı görebilirsin:
ham: 3 filtreli: 1
ham: 4 filtreli: 2
ham: 7 filtreli: 3
Ham değerler butonun yapısına ve basış şekline göre değişir; temiz bir butonda iki sayı birbirine eşit çıkabilir. Filtreli sayaç ise her basışta bir artar.
Kod nasıl çalışıyor?
lastReading: bir önceki turda okunan ham değerdir. Değişimi bununla yakalarsın.stableState: yeterince uzun süre kararlı kalmış, kabul edilmiş durumdur.lastChangeTime: okumanın en son değiştiği andır. Her dalgalanma bu zamanı sıfırlar.
millis(), kart açıldığından beri geçen süreyi milisaniye olarak unsigned long türünde döndürür. Bu yüzden zaman değişkenini de unsigned long tanımlarsın. Süre kontrolünü millis() - lastChangeTime çıkarmasıyla yaparsan, sayaç en büyük değerine ulaşıp sıfırdan başlasa bile işaretsiz tamsayı aritmetiği sayesinde fark doğru hesaplanır. Zamanları doğrudan karşılaştıran koşullar (millis() > hedef gibi) bu taşma anında yanlış sonuç verebilir.
Neden delay(50) yerine millis?
Aklına gelen ilk çözüm, değişimi görünce delay(50) ile beklemek olabilir. Bu, basit bir sketch'te işe yarar ama bekleme boyunca işlemci başka bir şey yapamaz. Aynı programda ekranı güncelliyor, motoru sürüyor ya da başka bir butonu okuyorsan hepsi donar; bekleme sırasında gelen ikinci bir basış da kaçabilir. millis() yaklaşımında loop() durmadan dönmeye devam eder, yalnızca "yeterli süre geçti mi?" sorusunu sorar.
DEBOUNCE_MS değerini kesin bir ayar olarak değil, deneyerek ayarlayacağın bir başlangıç noktası olarak düşün. Kartın, butonun ve kablolamanın etkisi farklı olabileceği için aynı değer her kurulumda aynı sonucu vermeyebilir.
Basış ile bırakış nasıl ayrılır, sayaç hangi kenarda artmalı?

Kenar, kararlı durumun bir değerden diğerine geçtiği andır. INPUT_PULLUP düzeninde pinMode satırı pini dahili dirençle HIGH'a çeker, butona basınca pin LOW olur. Bu yüzden HIGH'dan LOW'a geçiş basış, LOW'dan HIGH'a geçiş bırakış kenarıdır.
Sayaç durumu değil olayı saymalıdır. "Pin şu an LOW mu?" diye sorarsan buton basılı kaldığı her turda sayaç artar. "Pin az önce HIGH'dı, şimdi LOW oldu mu?" diye sorarsan basış başına tek artış elde edersin.
Hangi kenarı seçeceğin, butonun ne iş yaptığına bağlıdır:
- Sayaç: basış kenarı, tepki hissini en hızlı veren seçenektir.
- Menüde ilerleme: basışta bir sonraki öğeye geçmek doğal geliştir.
- Onay ya da seçim: bırakış kenarı, kullanıcıya basıp vazgeçme şansı tanır.
- Uzun basış: basış anını kaydedip bırakışta geçen süreyi ölçersin.
Önceki bölümdeki sketch'te kenarı seçen yer, kararlı durumla önceki kararlı durumu karşılaştıran koşuldur:
if (oncekiDurum == HIGH && kararliDurum == LOW) {
sayac++;
}
Bu satır basış kenarını yakalar. Bırakışa geçmek için yalnızca değerlerin yerini değiştirirsin: oncekiDurum == LOW && kararliDurum == HIGH. Değişken adların farklıysa mantık aynıdır: önceki kararlı değer bir uçtaysa ve şimdiki kararlı değer diğer uçtaysa olay gerçekleşmiştir.
Önceki durumla mevcut durumu karşılaştırmak, küçük ama gerçek bir algoritmik düşünme alıştırmasıdır: bir değeri hafızada tutarsın, yeni değerle kıyaslarsın, farkı olaya çevirirsin. Durum, koşul ve değişken mantığında nerede durduğunu görmek istersen ücretsiz bilgi testleri merkezi dilden bağımsız bir başlangıç noktası sunar.
Titreşimi seri portta nasıl gözlemlersin? Filtresiz ve filtreli çıktı karşılaştırması
Titreşimi gözle göremezsin, ama sayarsan görürsün. Önceki sketch'in iki sayaç yazdırdığını varsay: ham sayaç filtresiz okumadaki her HIGH'dan LOW'a geçişi, filtreli sayaç debounce sonrası kabul edilen basışları sayar. Tek bir basış için örnek bir çıktı şöyle görünebilir:
Ham sayac: 0 | Filtreli sayac: 0
Ham sayac: 4 | Filtreli sayac: 1
Ham sayac: 4 | Filtreli sayac: 1
İkinci satırda ham sayaç dört basamak sıçrarken filtreli sayaç yalnızca bir artmıştır. Bu çıktı örnektir. Titreşim her butonda ve her basışta aynı olmaz; bazı basışlarda ham sayaç da yalnızca bir artar, yani titreşim hiç görünmeyebilir. Bu durum filtrenin gereksiz olduğunu göstermez: aynı butonun başka bir basışta ya da farklı koşulda dalgalanabilmesi mümkündür.
Kendi devrende gözlem yapmak için şu planı izle:
- Sketch'i karta yükle ve Arduino IDE'de seri monitörü aç.
- Monitördeki baud değerini koddaki
Serial.begin()değeriyle eşle; uyuşmazsa ekranda anlamsız karakterler görürsün. - Butona önce yavaş, kararlı bir şekilde bas ve bırak. Ham ve filtreli sayaçların farkını not et.
- Aynı butona bu kez hızlı ve sert bas. Ham sayacın daha fazla sıçrayıp sıçramadığına bak.
- Her denemede ham değerle filtreli değeri yan yana karşılaştır. Filtreli sayaç basış başına bir artıyorsa filtre işini yapıyor demektir.
DEBOUNCE_MSdeğerini değiştirip denemeyi tekrarla. Çok küçük bir değerde filtreli sayaç da fazla artabilir; değeri yükselttikçe farkın nasıl değiştiğini izle.
Süreyi tek bir doğru değere bağlamak yerine, kendi butonunda ayarladığın bir başlangıç değeri olarak düşün. Gözlemin sonucu, kullandığın buton ve devreye göre farklı çıkabilir.
Debounce'ta sık yapılan hatalar ve süre seçerken dikkat edilecekler
Sayaç hâlâ şaşıyorsa kodu şu altı noktadan geçir:
- delay ile bekleme:
delay()sürerken program başka bir şey okuyamaz. Basışları kaçırabilir, diğer işleri de durdurur. Beklemeyimillis()farkıyla yap. - Son değişim zamanını yanlış yerde güncelleme: Zamanı her döngüde güncellersen bekleme süresi hiç dolmaz. Yalnızca ham okuma bir önceki ham okumadan farklı çıktığında güncelle.
- Durumu olay sanma: Buton basılı kaldığı sürece durum "basılı"dır. Sayacı durumda artırırsan tek basış sürekli sayım yapar. Sayacı yalnızca durumun değiştiği kenarda artır.
- Çok uzun süre: Hızlı art arda basışlar tek basış gibi görünür.
- Çok kısa süre: Titreşimin bir kısmı filtreden geçer ve fazladan sayım sürer.
- Kablo ve bağlantı kaynaklı gürültü: Gevşek bir jumper ya da breadboard temassızlığı da sahte değişimler üretir. Yazılımı değiştirmeden önce bağlantıyı kontrol et.
Doğru süre butona, kullanım amacına ve bağlantına göre değişir. Ucuz ve eski bir butonun titreşimi uzun sürebilir. Sayaçta biraz bekleme sorun olmaz, ama hızlı tepki isteyen bir girişte aynı süre fark edilir. Bu yüzden süreyi herkes için geçerli sabit bir sayı olarak değil, ayarlanabilir bir başlangıç değeri olarak ele al: kısa bir değerle başla, seri porttaki çıktıya bak. Tek basışta fazladan sayım görüyorsan süreyi biraz uzat, hızlı basışların kaçtığını fark ediyorsan kısalt. Kendi butonunla yaptığın bu gözlem, tablolardaki genel değerlerden daha güvenilir bir ayar kaynağıdır.
Arduino ve gömülü sistemlerle ilgili yeni örneklere devam etmek istersen blog arşivindeki ilgili yazılara göz atabilirsin.