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.
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.
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.
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:
- 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.
-
Zaman ölçülür, varsayılmaz.
sleepbir garanti değil, bir ricadır. - İş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.
- 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
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.