Merhaba. Bu yazıda Raspberry Pi Pico mikrodenetleyicisiyle yaptığım bir kaç deneyden ve elde ettiğim ilginç sonuçlardan söz ettim. Uzunca bir yazı oldu, o yüzden sonuçları ortalarda ele aldım. Kodları nasıl çalıştırdığımı ek okuma olarak sona bıraktım.
Raspberry Pi Pico Nedir?
Raspberry Pi Pico (kısaca Pico), Raspberry Pi Vakfı'nın (Raspberry Pi Foundation) Arduino'ya her ne kadar doğrudan cevabı olmasa da, çok güçlü bir alternatif mikrodenetleyici. Arduino, giriş seviyesi hobi kullanıcıları hedef alıyor. Pico bence biraz daha teknik bilgi gerektiriyor. Ben bu yazıda Pico üzerinde farklı programlama yaklaşımlarını gösterip bunların hızını karşılaştırdım. Elimde önceki yazıdan hazır kod var: SPI ve MAX7219 ile LED sürme, bu yazıda da bu konudan devam ettim. Ama daha önce bunu yalnız Arduino üzerinde uygulamıştım, şimdiyse yalnız Pico üzerinde.
Arduino 8-bitlik ATmega328P (16 MHz) mikrodenetleyicisine sahip. Pico'daysa 32-bitlik çift çekirdekli ARM Cortex mikrodenetleyici var ve dökümante edilen maksimum çalışma hızı 133 MHz. İşlemci, varsayılan 125 Mhz ile çalışmaya başlıyor. Saat hızı sonradan programla değiştirilebiliyor. Örn. pille beslenen cihazlarda pili idareli kullanmak için saat hızı yavaşlatılabiliyor. Sonradan dökümante edildi mi bilmiyorum ama sahada Pico'nun 200 MHz ile çalıştırıldığını okudum. Ben kendi kodumu paralelleştirmeye uğraşmadığımdan çift çekirdeği dikkate almıyorum. Pico'da RAM ve flash bellek miktarı Arduino'ya göre daha fazla ama benim uygulamam CPU'yu yoğun olarak kullandığından bunları da burada karşılaştırmayacağım.
Pico'ya yeni başlayanlar için "Get Started with MicroPython on Raspberry Pi Pico" adlı açık ekitap var. Son baktığımda bu sayfadaki bağlantı düzgün çalışmıyordu ama kitabın adını Google'da aratınca .pdf sürümü çıkıyor.
Pico için kod geliştirirken kullanılabilecek çeşitli alternatifler var. İlki Arduino IDE ve bunun güzel yanı Arduino'da çalışan C kodu Pico'da da aynı şekilde çalışıyor. pinMode() veya digitalWrite() gibi komutlar tamamen uyumlu. Eğer çok Arduino-native bir kod değilse Pico'da da çalışacaktır.
İkinci alternatif Micropython. Pico'yu (micro)python'la da programlamak mümkün. Örneğin aşağıdaki satır onüçüncü GPIO pinini çıkış olarak ayarlayıp set ediyor:
Micropython'da SPI protokolü için kullanılacak pinlerle bir SPI nesnesi oluşturup bunun write() metodunu çağırmak yeterli. Micropython basit, ama biraz yavaş. Pico için yazılmış 660 KB boyutunda bir Micropython yorumlayıcısı var. Pico'yu boot select modunda çalıştırıp bu dosyayı kopyalayınca (boot select modunda Pico bilgisayarda bir USB flash bellek gibi görünüyor) seri arabirimi (RS232) USB üzerinden emüle ederek bilgisayarla haberleşiyor. Bu yolla python çalıştırmak mümkün.
Üçüncü alternatif C/C++. Pico'nun geniş bir C desteği ve kütüphaneleri var. Platforma özgü çok fazla fonksiyon olması (get_absolute_time(), cyw43_arch_gpio_put(), stdio_init_all() vb.) en başta benim için kafa karıştırıcı oldu. Ancak her konu için Raspberry'nin resmi github hesabında sayısız örnek var. C için en iyi kaynak Getting started with Raspberry Pi Pico-series adlı kitap. Pico SDK'nın kullanım kılavuzu da oldukça detaylı ama bu okunacak bir kitap değil, 745 sayfa. SDK'yı ve örnekleri derlerken aşağıdaki videodan yararlandım.
Elbette Assembly de bir seçenek ama ben ne Arduino'da ne de Pico'da assembly yazmadım. C kodları içinden assembly çağırmak da bir olasılık.
Nasıl Test Ettim?
Bütün testler önceki yazıda kütüphane kullanmadan yazdığım sayaç kodunu temel alıyor. Elbette Micropython için algoritmayı python'a çevirdim. Sayaç kodu Arduino IDE'den çalıştırılınca hem Arduino'da hem de Pico'da sorunsuz ve aynı şekilde çalışıyor. İlginç şekilde bu kodun kütüphane kullanan sürümü Pico'da çalışmadı. IDE, farklı platformlar için header dosyalarını muhtemelen farklı dizinlerde arıyor. Kodun kütüphanesiz sürümü sorunsuz çalıştığından bunu önemsemedim.
Aslında kafamdaki plan a dizisinin (satır 67) ilk elemanında gerçekleşen her taşmada bir pini set/reset yapıp frekansmetreyle taşmaların periyodunu bulmaktı. Bu plan frekansmetremin düzgün çalışmaması nedeniyle en başta bozuldu. Pini osiloskopa bağlayıp test ettim, bu sefer de örnekleme hızından dolayı bazı atımları kaçırdı. Set ile reset arasına ufak bir gecikme eklediğimde sorunsuz ölçüm yapabildim. Yukarıdaki görselde her bir atımın çıkan ve inen kenarı arasında 10 ms gecikme görülüyor (ölçek sağ üstte). Burada atımların frekansı 2.182 Hz. Bu periyoda çevrildiğinde 458 ms ediyor. Bunun 10ms'si gecikme olduğu için her 448 ms.'de sayaç taşıyor.
Önceki yazıda onuncu LED'in yaklaşık bir saniye yanık kaldığını gözlemlemiş ve hesabı buna istinaden yapmıştım. Sekizinci LED'in periyodu 448 ms. ise dokuzuncu LED'in periyodu 892 ms. olur, dolayısıyla onuncu LED 892 ms boyunca yanık kalır. Gözleme dayalı kaba bir tahmin için fena değil.
Ancak süreye dayalı bir ölçüm yaparken araya gecikme eklemek mantıklı olmadığı için, paralelde a dizisinin ikinci elemanındaki iki taşma arasında geçen süreyi seri porttan yazdırdım. Bu süre ortalama 114.486 saniye. Birinci dizi elemanı 892 milisaniye aralıklarla 256 kere taşarsa (overflow) 114.176 saniye yapıyor. Hesapla deney arasında 310 ms'lik bir fark var. Bu da Arduino'nun osilatörünün yeterince hassas olmadığı için ölçümleri yaptığım günler arasında odadaki sıcaklık farkından kaynaklanıyor olabilir, veya seri port iletişiminin getirdiği yük (overhead) olabilir, ki bu daha olası. Arduino Serial.println()'yi asenkron olarak yapamıyor, üstelik seçtiğim baud hızı da yavaş. Diğer yandan ‰2.7 kabul edilebilir bir hata. Kısacası süreleri seri porttan okumaya karar verdim.
Seri portun başlatılması için sekizinci satırdan #ifdef'i kaldırdım. Yeni kod github'dan ulaşılabilir. Süre ölçümü için millis() fonksiyonunu kullandım (satır 69). Eski koda ek olarak sekseninci satırda bir if bloğu var. Burada StartTime1'in değeri güncel tick count'tan çıkarılıp aradan geçen zaman seri porta yazılıyor.
Bu arada Arduino IDE'de ilginç bir durum var. Nedense LED Matrix'e yazılan sayının paritesine bağlı olarak döngü süresi değişiyor. Üstelik değişim Arduino'da pozitif, Pico'da negatif yönlü. Aşağıdaki grafik Arduino'nun sürelerini gösteriyor.
Örneğin, ortadaki pik 127'de ortaya çıkıyor, 128'de birden süre aşağıya düşüyor. O pikle sıfır noktasının tam ortasında daha küçük bir sıçrama 63 ile 64 arasında, ve benzeri bir sıçrama 191 ile 192 arasında var. En büyük sıçramaysa 255 ile 256 arasında, sağda. Diğer küçük sıçramaların 2n deseninde olduğu görülebilir. Nedeni konusunda bir fikrim yok. Pico'nun grafiğine bakarsak:
Bu grafik sanki öncekinin simetriği gibi, sayıdaki birlerin adedi arttıkça süre azalıyor. İlk grafiktekine benzer sıçramalar ters yönde 64, 128, 192 ve 256 civarında görülebilir. Neden Pico'da tersi davranış gösterdiği konusunda yine bir fikrim yok.
Python kodunu, C'den birebir çevirdiğimi yazmıştım. spiwrite() MAX7219'a iki byte'lık komutlar gönderiyor. SPI için kullanacağım pinleri initialize ettikten sonra, bu pinlerle SPI metodununu çağırarak bir nesne oluşturdum (satır 19). Ardından Arduino'dakiyle aynı şekilde MAX7219'u initialize ettim. Sonsuz döngüye girmeden önce süreyi StartTime1'e yazdım ve Z[1]'deki her taşmada bilgisayara print() ile süreyi ilettim.
C kodunu büyük ölçüde pico-examples/spi/max7219_32x8_spi/max7219_32x8_spi.c kodundan sadeleştirerek aldım. Örnek kod, SPI pinleri için "hardware/spi.h"daki varsayılan tanımları (PICO_DEFAULT_SPI_SCK_PIN, PICO_DEFAULT_SPI_CSN_PIN vb.) kullanıyor. Ben kendi tanımladığım pinleri kullandım (satır 12, 13, 14). MAX7219 komut tanımlamalarını, cs_select() ve write_register() fonksiyonlarını orjinal koddan aldım. Bu kodda StartTime1 (satır 48) değişkenine süre get_absolute_time() ile main()'in başında yazılıyor. Bunun aslında sonsuz döngüden hemen önce olması gerekirdi ancak sonuçlarda ciddi bir fark yaratmıyor. Burada dikkat edilmesi gereken bir konu, Pico'da denetleyicinin içinde iki tane SPI çevre donanımı (peripheral) var. Bu da demek oluyor ki (bit-banging ve Programmable IO gibi ileri konulara girmeden) aynı anda iki SPI bus bağlanabilir. Bu SPI donanımları birden fazla GPIO pinine bağlı. Demek istediğim Pico'nun pinout şemasından daha iyi görülebilir:
SPI0 devresini yalnız SPI0 etiketli pinlerle kullanabilirim. Örn. GP0, GP1, GP2 ve GP3 (Pin 1, 2, 4, 5). Eğer bir SPI cihazı SPI1'e bağlayacaksam, bunu da benim kodumda olduğu gibi GP10, GP11, GP12 ve GP13 (Pin 14, 15, 16, 17) üzerinden bağlayabilirim. Ve SPI'ı initialize ederken hangi donanımı kullandığımı bildirmem gerekir. Orjinal kodda spi_default, spi0. Ben spi1'i kullandığımdan elli dördüncü satırda spi_init()'i spi1 ile çağırmam gerekti. Sırf SPI için bile bunca detay bulunması, C'nin donanıma yakın olmasının bir sonucu ama bu durum hız olarak kullanıcıya geri dönüyor.
MAX7219 initialization kısmı kodun 64 ve 70. satırları arasında. Ondan sonraki blok zaten Arduino'da da çalışan kod bloğu. Tek fark, sayının değeri SPI'a write_register() fonksiyonu üzerinden yazılıyor.
Test Sonuçları
Kodları nasıl çalıştırdığıma, yazının asıl konusu olmadığı için, sonuçlardan sonra ayrıca değineceğim.
İlk olarak Arduino'dan elde ettiğim sonuçları, verim ve saat hızına temel olması için ele alacağım. Arduino'da gerçekleşen 282 taşma ortalama 114.486 sn sürmüş. Yani 216'ya kadar saymanın süresi ve diğer bir ifadeyle on yedinci LED'in periyodu. 64 tane LED olduğuna göre daha 64 - 17 = 47 bit var. Bunların tamamının yanması için gereken süre 114.486 sn * 247 = 1.611 * 1016 saniye = 5.106 * 108 yıl (511 milyon). Önceki yazıda göz kararı hesapladığım 5.709 * 108 yıldı.
Bu, satranç tahtasındaki buğday taneleri problemine benziyor. Hikayeye göre satrancın mucidi, icadını Hint hükümdarına sunar. Hükümdar bunu çok beğenir ve klasik olarak "dile benden ne dilersen" der. Mucit, tahtanın birinci karesine bir buğday tanesi ve takip eden karelere öncekinin iki katı olmak üzere tahtaya koyulacak miktarda buğday ister. Hükümdar başta basit bir istek gibi algılasa da, mucidin istediği buğday tam olarak
Bu kadar buğday tanesi günümüz koşullarındaki yıllık buğday üretiminin 1600 katına denk. Bu arada hikayenin çeşitli varyasyonları var, ben lise matematik hocamdan duyduğumu aklımda kaldığı kadarıyla aktardım. Ana fikir üstel sayıların akıl almaz şekilde büyüyebileceği. İlk LED'in periyodu 1.75 ms (aslında mikroişlemci için uzun bir süre) kısa gibi gelse de bunun 264 katı inanılmaz büyük.
Konuya geri döneyim. Aynı kodun Arduino IDE içinden Pico'da çalışması ortalama 14.658 saniye sürüyor. Pico, Arduino'dan 7.8125 kat hızlı (125 MHz / 16 MHz). 14.658 sn * 7.8125 = 114.516 sn ediyor. Saat hızına göre normalleştirildiğinde Arduino ile aralarında sadece 30 ms fark var. Demek ki verim olarak ikisi de neredeyse aynı. Tüm LED'lerin yanması için 14.658 sn * 247 = 2.063 * 1015 sn = 6.537 * 107 (65.3 milyon) yıl gerekiyor.
Aynı algoritma Micropython'la hepsinden yavaş çalışıyor. Bir çevrim ortalama 128.641 saniye. Arduino IDE'deki koddan 8.78 kat daha yavaş. Bu, Micropython'ın mikrodenetleyici gibi düşük seviye bir platformda çok verimsiz olduğunu gösteriyor. Ben bu sonuçları SPI hızını 1 MHz olarak ayarladıktan sonra aldım. İlk denemede yanlışlıkla hızı 400 KHz ayarladığımda aynı döngü ortalama 138.291 saniye sürmüştü, ve ertesi gün 142.800 saniye. Baud'un genel hıza etkisi var ama yavaşlık kesinlikle program kaynaklı. Aksi halde baud'u 2.5 katına çıkarmak %7'den fazla etkili olurdu. Bu sonuçların ne zaman 264'e varacağını hesaplama zahmetine girmeye gerek yok.
Benim açımdan en heyecan verici olan C'deki sürelerdi. C'de 65 536'ya kadar 1.656 saniyede sayabiliyor. Yani, Arduino IDE'den bile 8.85 kat hızlı çalışan bir kod üretiliyor. Bu arada çıktı için UART veyea USB seri port kullanmak arasında süre farkı yok. 1.656 sn * 247 = 2.331 * 1014 sn = 7.385 * 106 (7.385 milyon) yıl. Bu da Otostopçunun Galaksi Rehberi'ne göre yaşamın, evrenin ve her şeyin cevabı olarak 42'yi hesaplamak için süper bilgisayarın ihtiyaç duyduğu süreden sadece biraz az.
Ek: Raspberry Pico'da Kod Çalıştırmak
Yazıda bahsettiğim üç farklı yaklaşımın (Arduino IDE, Micropython, C) ortak noktası Pico'nun Bootsel butonu. Bu butona basılı tutarken Pico'yu USB'ye bağlayınca Bootloader modunda açılıyor ve bilgisayarda 128 MB'lik bir USB flash bellek gibi görünüyor. Bu diske Raspberry Pico için derlenmiş .uf2 uzantılı bir dosya kopyalayınca, Pico otomatik olarak bunu tanıyıp çalıştırıyor. UF2, Pico'ya özgü çalıştırılabilir dosya formatı. Çapraz derleyici çıktı olarak .uf2 dosyası üretiyor. Bu dosya bizim yazdığımız C kodu veya Micropython yorumlayıcısı olabilir.
Arduino IDE'de çalışmak için, Bootsel'e basılı tutarken Pico USB'ye bağlanmalı. Sonra IDE'de yukarıdaki drop-down menüden "Select other board and port..."u seçip, çıkan pencerede Boards altında Pico diye aratınca çıkanlardan "Deprecated" olmayan board'u seçmek gerekiyor. Pico bağlandığında /dev/ttyACM0 aygıtını oluşturuyor, port olarak bu seçilmeli. Bunu yaptıktan sonra IDE, Pico çapraz derleyicisini ve kütüphanelerini indireyim mi diye soruyor. Buna evet diyip kurulumu tamamladıktan sonra, Pico Blink'le test edilebilir (File -> Examples -> 01. Basics -> Blink). Bu çalışıyorsa Pico diğer sketch'leri çalıştırmaya hazır demektir.
Micropython ile çalışmak için ilkin Micropython yorumlayıcısına ihtiyaç var. Bu daha önce belirttiğim gibi .uf2 uzantılı bir dosya. Dosyanın bağlantısı Raspberry'nin resmi sayfasında var. Pico bootloader modundayken çıkan .htm dosyanın bu yorumlayıcının bulunduğu sayfaya yönlendirdiği ve .txt dosyada da model bilgisinin yer aldığı yazıyor ama bende düzgün çalışmadı. Doğru .uf2 dosyasını Pico'ya kopyalayınca disk ortadan kayboluyor, yanlış .uf2 dosyasını kopyalayınca hiç birşey olmuyor.
İkinci olarak bir python IDE gerekli. Genellikle, Linux ve Windows'ta Thonny IDE öneriliyor. Bu yazıyı hazırlarken paket yöneticisinin resmi reposunda 4.1.6 sürümü vardı ancak Thonny'i Linux'ta çalıştırmak çok basit değil. Ben önce paket yöneticisiyle kurdum, fakat aşağıdaki hatayı aldım:
Bu bir izin (permission) kaynaklı değil, çünkü daha sonra github'dan .tar.gz dosyasını indirip açtım ve çalıştırdığımda hiç bir hata almadım ve daha da garibi paket yöneticisiyle kurduğum thonny'i tekrar çalıştırdığımda aynı hatayı almama rağmen sorunsuz çalıştı. Bu arada .tar.gz'den çıkan dosyayla paket yöneticisinden gelen dosya binary olarak farklı. Muhtemelen derleme parametreleriyle ilgi bir durum veya paket yöneticisi bazı config dosyalarını düzgün oluşturamıyor.
Eğer Pico, Micropython yorumlayıcısını çalıştırmaya başlamışsa, thonny'nin sağ alt köşesinden doğru board'u ve portu seçince ekrana gelen python prompt'u seri port üzerinden Pico'da çalışan python'dan geliyor. Yani buraya bir komut yazarak doğrudan Pico üzerinde çalıştırılabilir. Komutun çıktısı yine USB üzerinden thonny'e gelecektir. Her ne kadar bir ekran bağlı olmasa da Pico'daki stdout seri port olduğu için, print komutu seri porta yazıyor ve bu thonny'le görülebiliyor. Yukarıda "doğru portu board'u seçince" yazdım, çünkü thonny aynı zamanda bilgisayardaki python yorumlayıcısını da kullanabiliyor. Eğer bu seçiliyse aynı çıktı yerel python'dan gelecektir. Üst tarafta python betiği yazılıp çalıştırılabiliyor ve kaydedilebiliyor. Bir dosya açarken veya kaydederken, thonny kullanıcıya bilgisayarı mı yoksa Pico'yu mu kullanacağını soruyor. Yani board'a dosya da kaydedilebiliyor. Üstelik eğer dosya main.py olarak adlandırılırsa, board'a elektrik verilir verilmez bu dosya Micropython tarafından otomatik çalıştırılıyor. Yazılan betik IDE'deki yeşil play tuşu ile seçilen platformda çalıştırılıp kırmızı stop tuşu ile durdurulabiliyor. thonny herhangi bir örnek içermediğinden basit bir blink kodunu aşağıya ekledim:
import time
led = Pin(25, Pin.OUT)
while True:
led.on()
time.sleep(0.5)
led.off()
time.sleep(0.5)
Bu kadar thonny anlattıktan sonra şunu da ekleyeyim ki, aslında Pico'da python kodu çalıştırmak için thonny'e gerek yok. İletişimin RS232 protokolüyle gerçekleştiğini yazmıştım. Dolayısıyla minicom'da (veya Windows'ta hala var mı bilmiyorum ama Hyperterminal veya putty'de) sudo minicom -D /dev/ttyACM0 -b 115200 komutuyla bir oturum açıp, Pico üzerindeki python yorumlayıcısına ulaşmak mümkün. Yanılmıyorsam kodları kaydetmek ve kayıtlı dosyaları çalıştırmak için de klavye kısayolları mevcut ancak tüm bunları akılda tutmamak için thonny ile çalışmayı tercih ediyorum. thonny için oldukça detaylı bir videoyu buraya bırakayım:
Son olarak C/C++ kodlarını çalıştırmak aslında hepsinden daha kolay. Çapraz derleyicinin çıktısı olan .uf2 dosyayı Pico'ya atınca çalışıyor. Zor kısım SDK'nın ön koşullarını yükleyip derlemek. Ben bunun için
- arm-none-eabi-gcc-cs.x86_64
- arm-none-eabi-gcc-cs-c++.x86_64
- arm-none-eabi-newlib.noarch
- arm-image-installer.noarch
paketlerini yükledim. Ancak ben yıllardır kendi makinamda türlü çeşit paket derlediğim için zaten bir sürü devel paketi yüklü. Dolayısıyla yukarıdaki dört paket minimal gereklilik. Örn. cmake de gerekli ama bu benim bilgisayarımda zaten vardı.
Pico SDK'yı ve Pico Examples'ı github'dan klonladım. PICO_SDK_PATH çevre değişkenine SDK'yı indirdiğim yolu girip pico-examples dizininde cmake -S . && make -j4 dedim. Küçük küçük bir sürü örnek olduğu için paralel make derlemeyi çok hızlandırıyor. Bu arada bir build/ dizini oluşturup derlenen dosyaların oraya yazılması tavsiye ediliyor. Ben biraz üstünkörü derledim. Derleme bittikten sonra her örneğe karşılık bir .uf2 dosyası üretiliyor. Bunu denemek için blink/blink.uf2 Pico'ya kopyalanabilir.








