MODÜL 07 DERS 3 / 6 · 60 DK

Faz makinesi: ileri, dön, dur

Önceki derste robotuna zamanla iş yaptırmayı öğrendin: millis ile "2 saniye ileri git" diyebiliyordun. Ama gerçek bir rutin tek adımdan ibaret değildir. Bir robotun kendi başına "önce ilerle, sonra dön, sonra dur" gibi bir sıra izlemesi gerekir. Peki robot bir adımı bitirdiğini ve sıranın bir sonrakine geldiğini nereden bilecek? Bu derste robota adımları saydıracaksın. Ona küçük bir hafıza vereceksin: şu an hangi adımdayım? İşte bu hafızaya faz diyeceğiz.

Kanca: robot sırayı nasıl hatırlıyor? (9 dk)

🎬 VİDEO · kanca · 45 sn · robot banda konur, kimse kumandaya dokunmaz; Start'a basınca kendi başına ileri gider, durup sola döner ve durur; her adım değişimi yavaş çekimle vurgulanır

Videoda kumanda kimsenin elinde değil. Robot üç şeyi sırayla yaptı: ileri gitti, döndü, durdu. Şimdi dikkatli düşün: robot ileri giderken, "artık yeter, dönme zamanı" kararını nasıl verdi? Ve döndükten sonra tekrar ileri gitmeye kalkmadı, durdu. Yani bir yerlerde "ben şu an ikinci adımdayım" bilgisini tutuyor olmalı.

Sınıfa sor, önce tahmin etsinler:

Cevabı hemen vermiyoruz. Kodu açınca robotun bu hafızayı tek bir sayıyla tuttuğunu göreceksin.

Ana etkinlik: faz makinesini çalıştır (20 dk)

Sıfırdan yazmayacaksın. Aşağıdaki kod, tanıdığın diferansiyel sürüş iskeletinin üstüne kurulu. Yeni olan tek şey en alttaki otonom kısmı. Önce olduğu gibi yükle, sonra ne yaptığını satır satır çözeceğiz.

🎬 VİDEO · ekran-kaydi · 180 sn · kod yüklenir, robot ileri-dön-dur yapar, Driver Station'da "faz: 0 → 1 → 2" değişimi izlenir; sonra sıfırlama satırı silinip tuzak gösterilir
#define PROBOT_WIFI_AP_SSID     "RobotAdi"
#define PROBOT_WIFI_AP_PASSWORD "sifre1234"
#define PROBOT_WIFI_AP_CHANNEL  1
#include <probot.h>

const int SOL_RPWM = 16, SOL_LPWM = 17;   // sol surus motoru
const int SAG_RPWM = 18, SAG_LPWM = 19;   // sag surus motoru

// hazir yardimci: motoru hizina gore surer (-1 geri, 0 dur, +1 ileri)
void surMotor(int rpwm, int lpwm, float hiz) {
  hiz = constrain(hiz, -1.0f, 1.0f);
  if (hiz > 0) { analogWrite(rpwm, (int)(hiz * 255));  analogWrite(lpwm, 0); }
  else         { analogWrite(rpwm, 0);  analogWrite(lpwm, (int)(-hiz * 255)); }
}
// hazir yardimci: sasiyi ileri hizi ve donusle birlikte surer
void surSasi(float ileri, float donus) {
  surMotor(SOL_RPWM, SOL_LPWM, ileri + donus);
  surMotor(SAG_RPWM, SAG_LPWM, ileri - donus);
}

int faz = 0;                 // robot su an kacinci adimda? (hafiza)
unsigned long fazBasi = 0;   // bu adim ne zaman basladi?

void robotInit() {
  pinMode(SOL_RPWM, OUTPUT); pinMode(SOL_LPWM, OUTPUT);
  pinMode(SAG_RPWM, OUTPUT); pinMode(SAG_LPWM, OUTPUT);
}
void robotEnd() { surSasi(0.0f, 0.0f); }
void teleopInit() {}
void teleopLoop() {
  auto js = probot::io::joystick_api::makeDefault();
  surSasi(js.getLeftY(), js.getRightX());
  delay(20);
}

void autonomousInit() {
  faz = 0;               // adimi basa sar
  fazBasi = millis();    // sayaci simdi sifirla
}
void autonomousLoop() {
  probot::clearTelemetry();
  probot::printf("faz: %d\n", faz);        // hangi adimdayiz? ekranda gor

  unsigned long gecen = millis() - fazBasi;
  if (faz == 0) {                          // 2 saniye ileri
    surSasi(0.6f, 0.0f);
    if (gecen > 2000) { faz = 1; fazBasi = millis(); }
  } else if (faz == 1) {                   // 0.8 saniye sola don
    surSasi(0.0f, -0.5f);
    if (gecen > 800) { faz = 2; fazBasi = millis(); }
  } else {                                 // dur ve bekle
    surSasi(0.0f, 0.0f);
  }
  delay(20);
}

Robotu bandın üstüne, önü açık zemine bak edecek şekilde koy. Bağlantı her zamanki gibi: telefonu robotun WiFi ağına bağla, tarayıcıda http://192.168.4.1 adresini aç, Driver Station çıkınca kumandayı bağlayıp bir tuşuna bas. Şimdi ekranda otonomu aç ve süresini (varsayılan 30 saniye yeter) gör. Init'e, sonra Start'a bas. Start'a basınca robot autonomousInit'i bir kez, autonomousLoop'u saniyede yaklaşık 50 kez çalıştırır.

Robotu izle: ileri gider, sonra sola döner, sonra durur. Aynı anda telefonun sağ panelinde faz: 0 yazısının önce 1'e, sonra 2'ye atladığını gör. İşte robotun hafızası orada, gözünün önünde değişiyor. Faz 2'ye geçtiği an robot durmuştu; çünkü 2 senin "dur" adımın.

Kavram köprüsü: faz bir hafızadır (7 dk)

Çalışan şeyi gördün. Şimdi adını koyalım.

Faz, robotun o anki adımını tutan bir sayıdır. int faz satırı bir kutu açar ve içine bir tam sayı koyar: 0, 1, 2. Bu sayı, robotun "şu an neredeyim" bilgisidir. Bu tür bir kurguya faz makinesi (ya da durum makinesi) denir: robot her an tek bir fazda olur ve şartlar oluşunca bir sonrakine geçer.

if / else if zinciri fazı okur. Her turda robot önce "faz kaç?" diye bakar. faz == 0 ise ileri sürer, faz == 1 ise döner, değilse durur. Zincirde yalnız bir dal çalışır: o anki faza uyan dal. Diğerleri atlanır. Bu yüzden robot aynı anda hem ilerleyip hem dönmez; her turda tek bir işi yapar.

Fazı ileri atlatan şey zamandır. Her fazın içinde bir çıkış şartı var: if (gecen > 2000). gecen, o adımın başından beri kaç milisaniye geçtiğidir (millis() - fazBasi). Süre dolunca iki şey birden yaparız: faz'ı bir artırırız ve fazBasi = millis() ile sayacı sıfırlarız. Sayacı sıfırlamayı unutursak bir sonraki faz, en baştan değil, yarısından ölçmeye başlar; adımlar yanlış uzunlukta olur. Bu yüzden faz değiştiren her satırda sayaç da yenilenir.

Belki merak ettin: neden koca bir bekleme, delay(2000), yazıp öyle beklemedik? Çünkü autonomousLoop iki saniye boyunca kilitlenirse robotun beyni o süre boyunca hiçbir şey yapamaz. Robotun güvenliği "beyin donmuş" der ve robotu durdurabilir. Bu yüzden asla uzun delay yazmayız; beklemeyi hep millis farkıyla ölçeriz. Loop akmaya devam eder, robot canlı kalır.

Uygulama: sıfırlama tuzağı ve kendi rutinin (15 dk)

Deney: sıfırlamayı sil, ikinci kez çalıştır

Şimdi bilerek bir hata yapıp ne olduğunu göreceğiz. autonomousInit içindeki faz = 0; satırını sil (ya da başına // koyup yorum satırı yap). fazBasi = millis(); satırı kalsın:

void autonomousInit() {
  // faz = 0;            <-- bu satiri sildik
  fazBasi = millis();
}

Kodu yükle ve otonomu bir kez çalıştır. Robot ileri-dön-dur rutinini sorunsuz yapar; hiçbir şey bozulmuş gibi görünmez. Rutin bitince robotu durdur (Stop). Şimdi bir daha Init ve Start'a bas.

İkinci çalıştırmada robot hiç kıpırdamaz. Ekrana bak: faz: 2 yazıyor ve orada takılı. Ne oldu?

faz global bir değişken; yani turlar arası değil, çalıştırmalar arası bile değerini korur. İlk rutin bittiğinde faz en son 2 olmuştu (dur adımı). İkinci Start'ta autonomousInit çalıştı ama içinden faz'ı sıfırlayan satırı sildiğimiz için faz hâlâ 2. Robot ikinci turda daha ilk andan "ben dur adımındayım" sanıyor ve hiç başlamıyor. Onu başa saran tek şey faz = 0; satırıydı. Sen onu silince robot geçen seferden kalan hafızayla uyandı.

Şimdi sildiğin satırı geri koy ve tekrar iki kez çalıştır: her seferinde baştan başlar. Aldığın ders şu: otonomun kullandığı her değişkeni autonomousInit içinde başa sarmak zorundasın. Yoksa robot "önceki çalışmadan kalan" değerle işe başlar.

Üret: kendi 4 fazlı rutinin

Şu anki rutin üç fazlı: ileri, dön, dur. Sen buna bir faz daha ekleyip 4 fazlı kendi rutinini kur. Örneğin: ileri git, sağa dön, tekrar ileri git, dur. Fikir, her yeni adım için bir else if bloğu daha koymak ve son fazı bir büyük yapmak:

if (faz == 0) {                    // 1.5 sn ileri
  surSasi(0.6f, 0.0f);
  if (gecen > 1500) { faz = 1; fazBasi = millis(); }
} else if (faz == 1) {             // 0.7 sn saga don
  surSasi(0.0f, 0.5f);
  if (gecen > 700) { faz = 2; fazBasi = millis(); }
} else if (faz == 2) {             // 1.5 sn tekrar ileri
  surSasi(0.6f, 0.0f);
  if (gecen > 1500) { faz = 3; fazBasi = millis(); }
} else {                           // faz 3: dur
  surSasi(0.0f, 0.0f);
}

Dikkat et: dönüşü artı değere (0.5f) çevirdim, robot bu kez sağa döner; eksi (-0.5f) sola döndürüyordu. Süreleri ve dönüş işaretini kendi zeminine göre değiştir. Robot çok az ya da çok dönüyorsa dönüş süresini (700) oynat. Bir sonraki derste bu süreleri tahminle değil, ölçerek doğru bulmayı öğreneceksin; şimdilik deneyerek yaklaş.

Küçük bir dürüstlük notu: bu yöntem zamana güvenir, mesafeye değil. Pil zayıfladıkça ya da zemin değiştikçe robot aynı sürede farklı yol alır ve her adımdaki küçük sapma bir sonrakine eklenerek birikir. Yani rutinin sonu her seferde tıpatıp aynı yere düşmez. Bu, tahmine dayalı otonomun doğal sınırıdır.

Pekiştirme ve değerlendirme (6 dk)

Kodu kapat, sözlü ya da kâğıda cevapla:

Günlük hayat bağı: bir çamaşır makinesi de bir faz makinesidir. Önce su alır, sonra yıkar, sonra durular, sonra sıkar, sonra durur. Her adım bir faz; makine "şu an kaçıncı adımdayım" bilgisini içinde tutar ve her adımın kendi bitiş şartı vardır (su seviyesi, süre). Trafik ışığı da öyle: yeşil, sarı, kırmızı fazları arasında sırayla döner. Senin yazdığın int faz ve if zinciri, o makinelerin de içindeki mantığın en yalın hali.

Erken bitirene ek görev: kare çiz (opsiyonel)

Robotuna bir kare çizdir. Kare dört kenar ve dört dönüştür: ileri, dön, ileri, dön, ileri, dön, ileri, dön. Bu, tekrar eden bir faz makinesidir.

İki yol dene:

  1. Uzun zincir. Her kenar ve her dönüş için ayrı faz yaz (faz 0'dan 7'ye kadar). Uzun ama anlaşılır. Son fazı "dur" yap.
  2. Sürelerle oyna. Kenar süresini sabit tut, dönüş süresini ayarlayarak tam 90 dereceyi yakala. Robot kareyi kapatıp başladığı noktaya dönebiliyor mu?

Meydan okuma: robot kareyi tamamladığında başladığı yere tam oturuyor mu, yoksa biraz kaymış mı? Sapmayı gözle. Sence bu sapma nereden geliyor ve bir sonraki derste onu nasıl azaltabiliriz? Bir arkadaşınla tartış.