Tüm yazılar Yazılım

Python’la Donanım: Raspberry Pi ve Arduino Notları

Python öğrendikten bir yıl sonra Raspberry Pi ve Arduino ile bir deprem erken uyarı prototipi yaptım. Ekranda beklediğim düz çizgi yerine titreyen bir sayı bulutu gördüm. GPIO, seri hat, örnekleme, filtreleme ve güç konusunda o projede öğrendiklerim.

DT
Demir Taşdemir Mobil Uygulama & Web Geliştirici
— dk okuma

2019'da Raspberry Pi'yi ilk kez elime aldığımda bir yıllık Python bilgim vardı ve ekranda çalışan her şeyin masanın üstünde de çalışacağını sanıyordum. İvmeölçeri bağlayıp ilk döngüyü yazana kadar. Beklediğim şey hareketsiz bir sensörden gelen düz bir çizgiydi; gördüğüm şey sürekli titreyen bir sayı bulutu oldu. Bu yazı, yarıda bıraktığım o TÜBİTAK deprem erken uyarı projesinden ve sonrasında donanımla uğraşırken tuttuğum notlardan.

Ekrandaki dünya ile masadaki dünya

Yazılımda bir değişkeni okursanız yazdığınız şeyi geri alırsınız. Donanımda bir değeri okumazsınız, ölçersiniz. Ölçmenin ise her zaman bir hatası vardır. Bunu kitapta okumak ile print(sensor_oku()) satırını çalıştırıp ekranda 512, 509, 514, 511 diye akan sayıları görmek çok farklı iki şey.

O ilk gün yaptığım şey utanç verecek kadar basitti: değer belli bir sayının üstüne çıkarsa “deprem var” diyordum. Masaya yumruğumu vurunca çalışıyordu, dolayısıyla bittiğini sanıyordum. Sistemi bir gece boyunca açık bıraktığımda onlarca kez kendi kendine tetiklendi. Asıl proje o gece başladı.

GPIO: en basit arayüz, en çok yanılttığı yer

GPIO pinleri dijitaldir: ya yüksek ya alçak. Buradaki tuzak, hiçbir yere bağlı olmayan bir pinin “alçak” okumaması. Boşta bırakılmış pin havadaki gürültüyü toplar ve rastgele okur. Çözüm yazılımda değil devrede: pini bilinen bir seviyeye çeken pull-up ya da pull-down direnci. Raspberry Pi'de bunlar yonga içinde var, tek satırla açılıyor — ama ne işe yaradıklarını anlamadan açtığınızda ters mantıkla karşılaşıp saatlerinizi kaybediyorsunuz: pull-up'lı bir butonda basılı hâl 0'dır, 1 değil.

İkinci tuzak butonlarda. Mekanik bir kontak kapanırken milisaniyeler içinde birkaç kez açılıp kapanır. Yazılım tarafında tek bir basış on kesme (interrupt) olarak görünür. Buna “debounce” deniyor ve çözümü basit: son geçerli tetikten sonra kısa bir süre yeni tetiği yok saymak.

Gerilim farkını atlamayın

Raspberry Pi'nin GPIO pinleri 3,3 V ile çalışır, çoğu Arduino kartı 5 V. Arduino'nun çıkışını doğrudan Pi'nin pinine bağlarsanız pini yakma ihtimaliniz var. Seviye çevirici ya da en azından gerilim bölücü gerekiyor. Bir de ortak toprak: iki kartın GND uçlarını birleştirmezseniz veri hattındaki seviyeler bir anlam ifade etmez.

İki kartı konuşturmak: seri hat

Sonunda mimariyi ikiye böldüm. Arduino sensörü sabit periyotla okuyup hattan yolluyor, Raspberry Pi bu veriyi topluyor, işliyor ve ağa çıkarıyordu. Sebebi şu: Pi'nin üstünde bir Linux var ve Linux gerçek zamanlı bir işletim sistemi değil. Bir dosya yazma işlemi ya da arka planda uyanan bir servis, döngünüzü onlarca milisaniye geciktirebilir. Arduino'da ise sizin döngünüzden başka bir şey yok. Zamanlamayı basit karta, karar mekanizmasını güçlü karta verdim.

Seri hattı ilk kurduğumda veriyi virgülle ayrılmış satırlar hâlinde yolluyordum ve doğrudan split(",") yapıp int()'e veriyordum. İlk çökme beş dakika sonra geldi: port açıldığı anda hattın ortasından giriyorsunuz ve elinize yarım bir satır geçiyor. İkincisi, uzun kabloda bozulan tek bir bit sayıyı sessizce değiştiriyor. Bugün gelen her satıra şüpheyle bakıyorum:

def satir_coz(ham):
    # Beklenen biçim: "S,zaman_ms,x,y,z,saglama"
    parca = ham.strip().split(",")
    if len(parca) != 6 or parca[0] != "S":
        return None
    try:
        zaman = int(parca[1])
        x, y, z = (int(p) for p in parca[2:5])
        saglama = int(parca[5])
    except ValueError:
        return None
    if (zaman + x + y + z) % 256 != saglama:
        return None
    return zaman, x, y, z

Sekiz bitlik bu toplama sağlaması ciddi bir hata düzeltme yöntemi değil, ama bozuk satırı veri setine sokmuyor. Bir de şunu öğrendim: Python tarafı okumakta yavaş kalırsa işletim sisteminin tampon belleği doluyor ve veri ya kayboluyor ya da gecikmeli geliyor. Okuma döngüsünün içinde ağa istek atmak, tam olarak bu yüzden kötü bir fikir.

Örnekleme: en çok hata yaptığım yer

“100 Hz örnekleme yapacağım” deyip döngünün sonuna time.sleep(0.01) yazmak, o dönem yaptığım en büyük hataydı. Çünkü bu satır “her 10 ms'de bir” demek değil, “işini bitirdikten sonra 10 ms daha bekle” demek. İşlemenin süresi periyoda ekleniyor ve saatler içinde ciddi bir kayma birikiyor. Doğrusu, uyunacak süreyi değil, uyanılacak anı hesaplamak:

import time

PERIYOT = 0.01                 # 100 Hz
hedef = time.monotonic()
kacan_toplam = 0

while calisiyor:
    oku_ve_isle()
    hedef += PERIYOT
    kalan = hedef - time.monotonic()
    if kalan > 0:
        time.sleep(kalan)
    else:
        # Geri kaldık: kaçan periyotları say, kuyruğu büyütme
        kacan = int(-kalan // PERIYOT) + 1
        hedef += kacan * PERIYOT
        kacan_toplam += kacan

time.monotonic() tercihi de bilinçli: sistem saati NTP ile geriye çekilebilir, monotonik saat çekilmez. Kaçan periyotları saymak ise ayrı bir kazanç oldu; o sayaç yükseldiğinde sorunun sensörde değil benim döngümde olduğunu biliyordum.

Örnekleme hızını seçerken de kural belli: ilgilendiğiniz en yüksek frekansın en az iki katı (Nyquist). Altına inerseniz yüksek frekanslı bileşen kaybolmaz, düşük frekanslı sahte bir sinyal olarak geri gelir. Sismik sinyalde ilgi bandı kabaca birkaç Hz ile yirmi Hz arası olduğu için 100 Hz rahat bir seçimdi.

Gürültü nereden geliyor

“Sensör bozuk” demeden önce gürültünün kaynaklarını ayırmayı öğrendim:

  • Sayısallaştırma: Arduino Uno'nun analog-dijital çeviricisi 10 bit. 5 V referansta bir adım yaklaşık 4,9 mV eder. Bundan daha ince bir değişimi göremezsiniz; son basamağın titremesi normaldir.
  • Besleme: Aynı hattan bir motor veya röle çalıştığında ölçüm sıçrıyordu. Anahtarlama gürültüsü besleme üzerinden ölçüme biniyor.
  • Kablo: Uzun ve ekransız kablo anten gibi davranıyor. Sensörü kartın yanına almak, çoğu yazılımsal filtreden daha etkili oldu.
  • Mekanik: Kapının çarpması gerçek bir ivme. Bu gürültü değil, istemediğim bir sinyal — ve ayırt etmesi en zor olanı bu.
  • Kayma: Sıcaklık değiştikçe sıfır noktası kayıyor. Sabah kalibre ettiğim eşik akşam yanlış oluyordu.

Filtreleme ve gecikmenin bedeli

İki filtreyi arka arkaya kullandım. Medyan filtresi tek tük sıçramaları temizliyor, üstel hareketli ortalama (EMA) yüksek frekanslı titremeyi yumuşatıyor:

from collections import deque

class Filtre:
    def __init__(self, pencere=5, alfa=0.2):
        self.tampon = deque(maxlen=pencere)
        self.deger = None
        self.alfa = alfa

    def ekle(self, ham):
        self.tampon.append(ham)
        sirali = sorted(self.tampon)
        medyan = sirali[len(sirali) // 2]
        if self.deger is None:
            self.deger = medyan
        else:
            self.deger = self.alfa * medyan + (1 - self.alfa) * self.deger
        return self.deger

Burada asıl ders filtrenin kendisi değil, bedeli oldu: her filtre gecikme ekler. Pencereyi büyüttükçe çizgi güzelleşiyor ama olay geç görülüyor. Erken uyarı yapmaya çalışan bir sistemde bu doğrudan sistemin varlık sebebini yiyor. Güzel görünen grafik ile işe yarayan grafik aynı şey değilmiş.

Sabit eşiği ise en sonunda bıraktım. Sismolojide klasik yaklaşım, kısa bir pencerenin ortalamasını uzun bir pencerenin ortalamasına oranlamak (STA/LTA). Böylece “şu değeri geçerse” değil, “ortamın kendi gürültüsüne göre aniden yükselirse” diyebiliyorsunuz. Zemin kayarsa eşik onunla birlikte kayıyor. Bunu projeyi bıraktıktan sonra öğrendim ve hâlâ keşke daha erken okusaydım diyorum.

Güç: yazılımcının hiç düşünmediği şey

Sistem birkaç gün üst üste kendini yeniden başlattı, ben de günlerce kodda hata aradım. Sorun adaptördeydi. Yetersiz akımda kart, gerilim düşünce resetleniyor; en kötüsü de tam o anda SD karta yazıyorsa dosya sistemi bozulabiliyor. Raspberry Pi'nin düşük gerilim uyarısını ciddiye almak, o hafta öğrendiğim en pahalı ders oldu.

Elektriği kesilebilen her sistem için

Kaydı tek bir büyük dosyaya değil, saatlik parçalara böldüm ve her yazımdan sonra tamponu diske boşalttım. Bir kesinti artık tüm veriyi değil, en fazla son satırı götürüyor. Aynı mantığı bugün mobil tarafta da kullanıyorum: uygulama her an öldürülebilir, kritik durum diskte olmalı.

Donanımdan öğrenip yazılıma taşıdıklarım

Proje yarıda kaldı; bunu saklamıyorum. Ama oradan çıkardığım şeyler bugün yazdığım mobil uygulamaların içinde duruyor:

  1. Her girdi gürültülüdür. Sensör de öyle, kullanıcı da öyle, ağdan gelen JSON da öyle. Gelen veriyi doğrulamadan içeri almıyorum.
  2. Zaman ölçülür, varsayılmaz. sleep bir garanti değil, bir ricadır.
  3. İşleri ayır. Zamana duyarlı iş ile yavaş iş aynı döngüde durmamalı. Seri okuma ile ağ isteğini ayırmakla, arayüz iş parçacığı ile arka plan işini ayırmak bana aynı şeyi öğretti.
  4. Fiziksel sınır her zaman vardır. Donanımda bu güç ve akım; telefonda pil, bellek ve mobil veri. İkisinde de sınırı önce görmezden gelip sonra çarpıyorsunuz.

Donanımla uğraşmanın bir yazılımcıya en büyük faydası şu bence: kodun soyut olmadığını hatırlatıyor. Ekranda bir if yazıyorsunuz, masanın üstünde bir LED yanıyor ya da yanmıyor. Yanmadığında bahane üretemiyorsunuz. Yeni başlayan birine tavsiyem, Python'u öğrendikten sonra ucuz bir kart alıp bir sensörü okumaya çalışması. Kitapta yazmayan kısımlar orada başlıyor.

  • Python
  • Raspberry Pi
  • Arduino
  • Donanım
  • Gömülü Sistemler
  • Sensör
Paylaş: LinkedIn X WhatsApp
DT

Demir Taşdemir

Mobil Uygulama & Web Geliştirici

2018'den beri yazılım geliştiriyorum. App Store ve Google Play'de 11 uygulama yayınladım; şu an 6 mobil uygulama, 1 e-ticaret platformu ve 1 masaüstü oyun üzerinde çalışıyorum.

Donanım tarafında bir fikriniz mi var?

Sensör verisini toplayan, işleyen ve mobil tarafa taşıyan sistemler kurmayı seviyorum. Aklınızdaki projeyi konuşmak isterseniz yazın.