2. hafta / 14
Arduino ile gömülü programlama
Araçlar: Bu platform (Uno laboratuvarı), Wokwi (ESP32)
Bu haftanın sonunda
- setup/loop yürütme modelini açıklamak
- Dijital ve analog girişleri okumak, çıkışları sürmek
- millis() ve sonlu durum makineleri (finite state machines) ile bloklamayan (non-blocking) kod yazmak
Ders notları
Mikrodenetleyici, tek bir yonga üzerindeki küçük bir bilgisayardır: CPU, programınız için flash bellek, RAM ve zamanlayıcılar, ADC'ler, seri portlar gibi çevre birimleri. Arduino Uno, 16 MHz'te çalışan, 32 KB flash ve 2 KB RAM'e sahip bir ATmega328P kullanır. İşletim sistemi yoktur: loop() fonksiyonunuz sonsuza dek çalışır.
Girişler ve çıkışlar
digitalRead/digitalWrite: mantık seviyeleri, 0 V veya 5 V.analogRead: 10 bitlik ADC 0 ile 1023 arasında değer döndürür, dolayısıyla gerilim raw × 5 / 1023 olur.analogWrite: PWM çıkışı, 0 ile 255 arası (5. hafta).Serial: ana hata ayıklama (debugging) aracınızdır; Serial Plotter canlı grafikler çizer.
Döngüyü asla bloklamayın
delay(1000) tüm robotu bir saniyeliğine dondurur: sensör okuyamaz, tepki veremez. Robotlar aynı anda pek çok iş yapar; bu yüzden geçen süreyi millis() ile kontrol eder, yalnızca zamanı geldiğinde işlem yaparız.
Sonlu durum makineleri
Robot davranışlarının çoğu bir durum makinesidir (state machine): bir durumlar kümesi, her durumda yapılan eylem ve geçişlere yol açan olaylar. Davranışı açık bir enum ve bir switch ile yazmak, onu okunabilir, test edilebilir ve kolay genişletilebilir kılar. Aynı kalıp dronlardaki görev mantığına kadar ölçeklenir.
Derinlemesine
Bloklamadan zaman: millis() aritmetiği
millis(), 32 bitlik bir unsigned long döndürür ve bu değer 232 ms, yani yaklaşık 49,7 gün sonra sıfıra döner; micros() ise yaklaşık 71,6 dakikada başa döner. if (millis() - last >= period) testi bu taşmadan sonra da doğru kalır, çünkü işaretsiz çıkarma 232 modunda yapılır. Cazip görünen if (millis() >= last + period) ise tam taşma anında bozulur, çünkü önce last + period taşar. Depo AGV'si gibi haftalarca çalışan robotlar bu hataya gerçekten yakalanır.
Çözümlü örnek: gerilim bölücüyle batarya izleme
2S bir LiPo (nominal 7,4 V, dolu 8,4 V) 5 V'luk ADC için fazla yüksektir; bu yüzden üstte R1 = 20 kΩ, toprağa R2 = 10 kΩ olan bir bölücü kullanırız:
ADC ham değer olarak 2,467 × 1023 / 5 ≈ 505 döndürür. Geri dönersek 505 × 5 / 1023 × 3 = 7,40 V. Bir adım (LSB) pinde 5 / 1023 ≈ 4,89 mV, batarya tarafında 14,7 mV'tur; hücre başına 3,5 V'luk kesme eşiğini (7,0 V) yakalamak için fazlasıyla yeterlidir. Tam dolulukta pin 2,8 V görür, 5 V'un güvenle altındadır. 3,3 V'luk bir ESP32'de aynı bölücü yine güvenlidir, ancak ADC'si aralığının uçlarında daha az doğrusaldır; bu yüzden bir multimetreye karşı kalibre edin.
Seri port bedava değildir
115200 baud'da UART her bayt için 10 bit (başlangıç, 8 veri, bitiş) gönderir, yani saniyede yaklaşık 11 520 bayt. 100 Hz'de 40 karakterlik bir hata ayıklama satırı saniyede 4 000 bayt ister: sorun yok. 9600 baud'da sınır saniyede 960 bayttır; 64 baytlık gönderme tamponu dolduğunda Serial.print bloklar ve döngü periyodunuzu sessizce uzatır. Döngüyü ölçün: loop() başında micros() değerini kaydedin ve en kötü periyodu saniyede bir yazdırın.
Durum makinesi tasarlamak
- Giriş eylemlerini (entry actions) (duruma girildiğinde bir kez çalışır: zamanlayıcıyı başlat, motorları ayarla) süregelen eylemlerden (during actions) (her geçişte çalışır) ayırın.
- Bir geçişin bir olayı (event) ve isteğe bağlı bir koşulu (guard) vardır: "butona basıldı ve en az 5 s yeşil yandı".
- Durum sayısı beşi aşınca bir geçiş tablosu (durum, olay, sonraki durum) iç içe
ififadelerinden daha kolay incelenir ve test edilir. - Her durumdan ulaşılabilen bir hata (fault) durumu mutlaka ekleyin, örneğin düşük batarya için.
Kıt kaynak RAM'dir
Uno'da yalnızca 2 KB SRAM vardır. Metin sabitleri F("...") içine alınmadıkça RAM'e kopyalanır ve Arduino String sınıfı yığını (heap) parçalayarak kartın rastgele görünen anlarda sıfırlanmasına yol açar. Sabit boyutlu char tamponları tercih edin ve kod büyüdükçe boş belleği izleyin.
Simülasyon
Engelleyen delay() ile millis() durum makinesi
İki trafik ışığı aynı döngüyü çalıştırır. Soldaki delay() kullanır ve yaya butonunu yalnız bir durum bittiğinde okuyabilir; sağdaki her döngüde okuyan engellemeyen bir durum makinesidir. Butona bas ve tepki sürelerini karşılaştır.
Kod
enum State { GREEN, YELLOW, RED };
State state = GREEN;
unsigned long t0 = 0;
const unsigned long DUR[] = {4000, 1000, 3000}; // durum başına ms
const int PIN[] = {11, 12, 13};
void setup() {
for (int i = 0; i < 3; i++) pinMode(PIN[i], OUTPUT);
Serial.begin(115200);
}
void loop() {
if (millis() - t0 >= DUR[state]) { // taşmaya (overflow) karşı güvenli karşılaştırma
t0 = millis();
state = (State)((state + 1) % 3);
Serial.print("state -> "); Serial.println(state);
}
for (int i = 0; i < 3; i++) digitalWrite(PIN[i], i == state);
// diğer görevler burada beklemeden çalışabilir
}Sık yapılan hatalar
- if (millis() >= last + period) yazmak; millis() yaklaşık 49,7 gün sonra başa döndüğünde bozulur.
- millis() değerini unsigned long yerine int içinde saklamak; zamanlayıcı 32,8 s sonra negatife düşer.
- 9600 baud'da uzun hata ayıklama satırları yazdırmak; Serial.print bloklar ve döngü sessizce yavaşlar.
- Butonu sıçrama giderme (debounce) olmadan okumak; tek basış birkaç olay olarak sayılır.
- Giriş eylemlerini (zamanlayıcı başlatma, sayaç sıfırlama) durumun her geçişte çalışan gövdesine koymak; her döngüde tekrarlanırlar.
- Uzun süre çalışan bir programda String sınıfını kullanıp Uno'nun 2 KB RAM'ini tüketmek.
Yazılım mühendisliği bağlantısı
Sonlu durum makinesi robotiğin dışında da en kullanışlı tasarım kalıplarından biridir: kullanıcı arayüzü akışları, ağ protokolleri ve sipariş iş akışları birer durum makinesidir. Onu açıkça yazmak, davranışı gözden geçirilebilir (review) ve test edilebilir hâle getirir.
next = step(state, event, now)fonksiyonunu saf (pure) yapın; böylece geçiş tablosunun her satırı bir birim testine dönüşür.- Zamanı dışarıdan verin: mantığın içinde
millis()çağırmak yerine şimdiki zamanı parametre olarak geçirin; böylece bir test 50 günü bir milisaniyede simüle edebilir. - Bir kod inceleme kuralı ekleyin: ana döngüde
delay()ve sınırsızwhiledöngüsü olmayacak.
Dönem projesi uygulaması: robotunuzun üst düzey davranışını bir durum diyagramı olarak çizin (Mermaid veya PlantUML, depoda saklanır), onu bir enum ve geçiş tablosuyla gerçekleyin ve hata durumuna giden geçişler dahil her geçiş için bir test yazın.
Laboratuvarda: Trafik ışığı durum makinesi (millis) + potansiyometre
Işıklar 4 / 1 / 3 saniyelik döngüde değişir; aynı anda potansiyometre gerilimi 4 Hz'de yazılır. delay() olmadığı için ikisi birbirini beklemez.
Laboratuvarda açLaboratuvar görevi
Wokwi'de trafik ışığı durum makinesini kurun. A0'a bir potansiyometre ekleyin ve ışıklar çalışmaya devam ederken gerilimini Serial Plotter'a yazdırın.
Kendini sına
millis() başa döndüğünde hangi zamanlama testi doğru kalır?
12 V'luk bir batarya, üstte 30 kΩ ve toprağa 10 kΩ olan bir bölücü üzerinden Uno'nun A0 pinine bağlı. analogRead ne döndürür?
9600 baud'da loop() her geçişte 48 bayt yazdırıyor. Seri portun bloklamadan kaldırabileceği en yüksek döngü hızı nedir?
Işık yeşile döndüğünde 5 s'lik geri sayımı başlatan kod nereye konmalıdır?
Ödev
Kırmızı ışık talep eden bir yaya düğmesi ekleyin. Düğmenin arkını (debounce) yazılımla giderin ve programın hiçbir yerinde delay() kullanmayın.