Raspberry Pi Pico ile PIO Kullanımı (Micropython)
|Merhaba,
Bu konuda Raspberry Pi Pico ile Micropython kullanarak durum makinesi kullanımına değineceğiz. RP2040 denetleyicisi bir çok özelliği ve düşük maliyetli ile günümüzde oldukça dikkat çeken bir MCU haline geldi. Bu güzel özelliklerinden bir tanesi de içerdiği durum makineleridir. Durum makinesi, mikrodenetleyicinin merkez işlemci birimini kullanmadan pin giriş çıkışlarını kontrol etmenizi sağlayan bir donanımdır. Bu donanım aynı zamanda UART, I2C gibi protokoller dışında bir protokol ile haberleşmeniz gerektiğinde size yardımcı olabilecek bir donanımdır. Ayrıca PWM üretme, yada Neopixel LED kontrolü gibi bir çok işleminizi de bu durum makinelerini kullanarak merkez işlem birinden bağımsız bir şekilde gerçekleştirebilirsiniz. Durum makineleri merkez işlemciden bağımsız çalışma özelliği ile zaten 2 çekirdekli olan RP2040 mikrodenetleyicisini uygulamanız açısından daha çok çekirdekliymiş gibi bir çok paralel işlemi aynı anda yapmanızı sağlamaktadır.
Rp2040 mikrodenetleyicisi içerisinde iki adet PIO modülü bulunur. Her PIO modülü içerisinde 4 adet durum makinesi (State Machine) ve bunların giriş ile çıkışlarını kontrol edebilen 8 adet FIFO içerir. Bu FIFO’lar normalde 32 bit uzunlukta veri depolayabilirler. Ancak bu FIFO’lar ile DMA kullanılarak daha uzun verilerin durum makinesi yardımı ile gönderimi sağlanabilir. FIFO’dan durum makinesine geçişte ISR (Input Status Register), durum makinesinden FIFO’ya geçişte OSR (Output Status Register) kullanılır. Ayrıca her durum makinesi komutları çalıştırmak için paylaşılan bir hafızaya sahiptir. Bu paylaşılan hafıza aynı anda en fazla 32 komut tutabilir. Bu sebeple bir durum makinesi içerisinde 32 komuttan daha fazla komut tanımlamak mümkün değildir. PIO ile kullanılabilecek 9 adet komut mevcuttur. Bu komutlar JMP, WAIT, IN, OUT, PUSH, PULL, MOV, IRQ, ve SET komutlarıdır. Her komut hassas bir şekilde 1 cycle sürede çalıştırılır. Her komuta ek olarak 31 cycle’a kadar cycle eklenerek daha uzun sürmesi sağlanabilir. Yani durum makinesi içerisinde tanımlanan bir komut en fazla 32 cycle süreye kadar uzatılabilir. İşlemlerin 1 cycle süresinden daha uzun sürede gerçekleşmesi istenilen durumlarda bu oldukça kullanışlı bir özelliktir. Her durum makinesi tüm komutları çalıştırabilir ve tüm GPIO pinlerine erişibilir. PIO ve durum makineleri hakkında daha detaylı bilgi için RP2040’ın datasheet’inde bulunan 3. kısmı inceleyebilirsiniz.
LED Yakıp Söndürme Uygulaması
Bu konuda PIO kullanarak öncelikle basit bir LED yakıp söndürme uygulaması gerçekleştireceğiz. Daha sonra bir WS2812 şerit LED’in PIO komutları kullanarak kontrol etmeyi bu konu içerisinde anlatmaya çalışacağım. Öncelikle LED yakma uygulamasından başlayalım. Pico’nun 15 numaralı pinine bir LED bağlı olduğunu düşünelim ve bunu saniyede 10 kez yakmaya çalışalım. Öncelikle ana yazılımımızı oluşturalım.
sm = rp2.StateMachine(0, blink, freq=2000, set_base=Pin(15)) # create state machine
sm.active(1) # activate state machine
time.sleep(5) # wait 5 seconds
sm.active(0) # deactivate state machine
Yukarıda verilen kod bloğu oldukça anlaşılması oldukça basit. İlk olarak durum makinesini oluşturuyoruz. Durum makinesi constructor’u oluşturulurken bazı parametrelere ihtiyaç duyar. Bunların ne olduğuna ve durum makinelerinin detaylı kullanımına Micropython dökümantasyon sayfasından ulaşabilirsiniz. Ben bu konuda sadece kullandıklarımızı açıklayacağım. Kullanılan ilk parametre durum makinesinin numarasıdır. Yukarıda da belirttiğimiz gibi 8 adet durum makinesi mevcuttur. O sebeple ilk parametreyi 0 ile 7 arasında kullanılmayan bir durum makinesinin numarası seçilebilir. İkinci parametre durum makinesi içerisinde kullanmak istediğimiz komutları yazacağımız fonksiyonun ismidir. Bu fonksiyonun içeriğine birazdan bakacağız. “freq” parametresi adından da anlaşılacağı üzere durum makinesinin çalışması için gerekli saat frekansını belirlediğimiz yerdir. Aslında bu parametre 1 cycle süreye sahip bir komutun çalışması için gereken süre ile aynı anlama gelir. Yani oluşturacağımız yazılımda yapmak istediğimiz süre hesabını bu frekans referans alınarak gerçekleştirilmelidir. Şimdi komutlarımızı çalıştıracağımız fonksiyonu oluşturalım.
Burada öncelikle LED’i yakıp söndürmek istediğimiz frekansın hesabını gerçekleştirelim. Durum makinesinin tanımlanması sırasında bir komutun çalıştırılması için geçecek süreyi belirlemiştik. Bu süreyi referans olarak kullanarak gerekli hesaplamaları yapacağız. Durum makinesinin frekansı 2Khz olduğu için bir komutun gerçekleşmesi için gereken süre 0.5ms’dir. LED’i saniyede 10 kere yakmak istiyorsak bu süreyi 100ms olacak şekilde uzatmamız gerekir. Diğer mikrodenetleyicilerde de olduğu gibi nop komutu kullanarak durum makinesinde de işlemleri 1 cycle bekletebiliriz. Yukarıda da belirttiğimiz gibi kullanılacak bir komutun süresi 32 cycle’ a kadar genişletilebilmektedir. Yani LED yakma komutundan sonra uygun sayıda NOP komutu kullanarak istediğimiz gecikme süresini sağlayabiliriz. Ancak gecikme süresi NOP komutları kullanılırken bir durum makinesi içerisinde en fazla 32 komut kullanılabileceği de unutulmamalıdır. Bu durumda 2Khz saat frekansı olması durumunda 6 adet 32 cycle uzunluğunda komut kullanarak istediğimiz gecikme süresine yakın bir gecikme elde edebiliriz. Sonuç olarak ortaya çıkan yazılım aşağıdaki gibi olmaktadır.
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def blink():
wrap_target()
set(pins, 1) [31]
nop() [31]
nop() [31]
nop() [31]
nop() [31]
nop() [31]
set(pins, 0) [31]
nop() [31]
nop() [31]
nop() [31]
nop() [31]
nop() [31]
wrap()
Durum makinesi fonksiyonu oluşturulurken rp2 sınıfı içerisinde bir dekoratör kullanmamız gereklidir. Bu dekoratör bazı parametreler alarak fonksiyonu durum makinesi ile kullanıma uygun hale getirir. Bu dekoratörün alabildiği parametrelere ve bu parametrelerin ne işe yaradığına detaylı bir şekilde bağlantıdan ulaşabilirsiniz. Bu konu içerisinde sadece kullandığımız parametrelerden bahsedeceğim. Bu fonksiyonda da “set_init” parametresini kullandık. Bu parametre durum makinesini tanımlama sırasında “set_base” olarak belirttiğimiz pinin başlangıç durumunu ifade eder. “rp2.PIO.OUT_LOW” ifadesi bşalangıç durumunda bu pinin LOW konumunda olmasını sağlar. Ayrca fonksiyon içerisindeki “wrap_target” ve “warp” komutları durum makinesi içerisindeki yazılımın başlangıç ve sonunu ifade eder. Böylece yazılımın belirtilen sınırlar içerisindeki bir döngüde dönmesi sağlanabilir.
WS2812 Uygulaması
Pico ile durum makinesi kullanarak ilk LED yakma uygulamamızı gerçekleştirdik. Şimdi WS2812 LED kullanımı uygulamamızı Pico içerisindeki durum makinesi kullanarak gerçekleştireceğiz. Bu uygulamayı tercih etmemin sebebi WS2812 LED’lerin standart protokollerin dışında kendine özgü bir haberleşme protokolünün olması. Durum makinesi içerisinde kullanacağımız komutların zamanlamalarını WS2812’nin çalışma protokolüne uygun bir şekilde ayarlamamız gerekli. Daha sonra ayarlamak istediğimiz renklere uygun veriyi durum makinesi buffer’ına göndererek LED’in istediğimiz renklerde yanmasını sağlayacağız. Öncelike WS2812’nin çalışma prensibini inceleyerek başlayalım.
WS2812’ler kaskat bağlantıyı desteleyen bir iletişim protokolüne sahiptir. Arka arkaya gönderilen her komut LED üzerinden bir sonraki LED’e aktarılır. Ancak komutlar arasında 50 mikrosaniyeden daha uzun süre boşluk bırakılırsa bu reset komutu olarak algılanır ve yeni gönderilen komut ilk LED’den tekrar işlenmeye başlar. Her WS2812 üzerinde 3 adet LED bulunur ve her LED 8 bitlik renk kodu ile kodlanır. Yani her bir WS2812 için en az 24 bitlik komut gönderilmesi gerekir. Aşağıdaki şekilde WS2812’nin çalışması için gönderilmesi gereken komut süreleri verilmiştir.
Verilen şekilde de görüldüğü üzere WS2812, hassas bir zamanlamaya sahip PWM işareti ile kontrol edilmektedir. Toplamda her bir komutun periyodu 1.25 mikrosaniye olmalıdır. Veri gönderim süresi içerisinde HIGH süresi 0.35 mikrosaniye olması durumunda bu komut WS2812’de 0 olarak yorumlanmaktadır. Veri süresi içerisinde HIGH süresi 0.7 mikrosaniye olması durumunda veri 1 olarak algılanmaktadır. Bu süreler en fazla 150 nano saniye toleransa sahip olmalıdır. Aksi durumda gönderilen veriler WS2812 tarafından yanlış yorumlanabilir.
WS2812’nin çalışma mantığını anladığımıza göre PIO ile kullanacağımız durum makinesinin çalışma frekansını hesaplayalım. Çalışma frekansını hesaplamak için veriyi nasıl göndermek istediğinizi planlamış olmalısınız. Benim oluşturduğum yazılıma göre WS2812 periyodunun 6’da biri kadar komut kullanmam gerekmekte. Bu sebeple durum makinesi frekansını 5Mhz olarak kullanacağım. WS2812 ile haberleşme için belirlediğim zamanlama ve kullanacağım komutlar aşağıda verilmiştir. Oluşturacağımız yazılımın ilk satırı başlangıç ile ifade edilen yerde başlayacaktır. Zamanlama datasheet’de verilen ile tam üst üste örtüşmese de aradaki fark datasheet’de ifade edilen tolerans değerinden daha küçük olduğundan sorun meydana gelmeyecektir.
Durum makinesi içerisinde kullanılan yazılım aşağıdaki gibidir. Bu yazılım yukarıda verilen zamanlamaya göre oluşturulmuştur. Yazılım oluşturulurken WS2812’nin zamanlamasında sabit olan yerler referans alınmıştır. Veri olarak WS2812’ye 1 yada 0 gönderilecek olsa ilk başta 0.4 mikrosaniyelik HIGH süresi olmalı. Bu sebeple zaman aralığında 2 döngülük lojik 1 süresi “set” komutu ile verilmiştir. Bir sonraki 0.4 mikrosaniyelik zaman aralığında gönderilmek istenilen verinin 1 ya da 0 olmasına göre lojik seviye değişiklik gösterecektir. Bu sebeple mov komutu kullanılarak buffer’dan alınan bit değeri bu kısımda pine atanmıştır. Son 0.4 mikrosaniyelik kısmın ilk yarısında pinin lojik değeri 0 olarak değiştirilmiştir. Çünkü veri periyodunun son kısmı gönderilmek istenilen veriden bağımsız olarak lojik 0 seviyesindedir. Veri süresinin geri kalan son 0.2 mikrosaniyelik kısmında da buffer’dan bir sonraki gönderilmek istenilen bit çekilecektir. Böylece bir döngü içerisinde sürekli olarak veri gönderimi sağlanabilecektir.
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW, out_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT, autopull=True, pull_thresh=24)
def ws2812():
wrap_target()
out(x, 1)
set(pins, 1) [1]
mov(pins, x) [1]
set(pins, 0)
wrap()
WS2812 kullanırken durum makinesinin içinde ve dekoratör parametrelerinde farklı komutlar kullandık. Yazılım diğer kısmına geçmeden onların ne olduklarına bakalım.
- set_init: Durum makinesi tanımlanırken set komutu ile kullanılacak pinlerin ilk değerini belirlemeyi sağlar.
- out_init: Durum makinesi tanımlanırken out komutu ile kullanılacak pinlerin ilk değerini belirlemeyi sağlar.
- out_shiftdir: Durum makinesi buffer’ından çekilen verinin ne tarafa kaydırılacağını ifade eder. Kaydırılan veri durum makinesi içerisine alınırken, verinin diğer tarafına 0 eklenir
- autopull: Durum makinesi içerisinde pull komutunu kullanman buffer’dan veri çekilmesine izin verir
- pull_thresh: Durum makinesi buffer’ından autopıll ile en fazla kaç bit çekileceğini ifade eder.
- mov: Konu başında da belirttiğimiz bir durum makinesi komutudur. İlk parametresi hedef, ikinci parametresi kaynaktır. Kaynaktaki veriyi hedefe yazdırır. Hedef pin ya da bir değişken olabilir.
Şimdi durum makinesinin tanımlamasını yapalım. Bu kısım önceki uygulamamızdan çok farklı değil. Sadece frekansı ihtiyacaımız olan değer ile değiştirmemiz yeterlidir.
sm = rp2.StateMachine(0, ws2812, freq=5000000, set_base=Pin(15), out_base=Pin(15))
WS2812 üzerinde ayarlamak istediğimiz rengi 24 bitlik değer olarak gönderecek şekilde durum makinesini ayarladık. Ancak belirlemek istediğimiz rengi kolay kodlanabilir olması için 3 elemanlı R, G, B renk kodlarını ifade bir dizi ile ifade edip onu 24 bitlik değere çeviren bir fonksiyon oluşturalım. Bu fonksiyon basitçe şu şekilde yazılabilir.
def setColor(color):
r = int(color[0]*BRIGHTNESS)
g = int(color[1]*BRIGHTNESS)
b = int(color[2]*BRIGHTNESS)
return (g<<16) + (r<<8) + b
Son olarak da bu rengi nasıl durum makinesi buffer’ına yükleyeceğimize bakalım. Bunun için önlikle rengi tanımlayıp, “setColor” fonksiyonunu kullanarak 24 bitlik sayıya çevirdikten sonra durum makinesinin “put” fonksiyonunu kullanarak ayarlamak istediğimiz rengi durum makinesine yükleyebiliriz. Örneğim 6’lı WS2812 şerit ledimiz var ise aşağıdaki yazılımı kullanarak tüm LED’lerini kırmızıya çevirebiliriz.
for i in range(6):
ar[i] = setColor((255, 0, 0))
sm.put(ar, 8)
“put” fonksiyonun ikinci parametresi gönderilen verinin kaç bit kaydırılarak iletileceğini ifade eder. Her renk 8 bitlik olduğundan gönderilecek 24 biti 8 bitlik gruplar halinde buffer’a yüklemeliyiz. Yukarıda anlatımları gerçekleştirilen uygulamaların son haline aşağıdaki Github bağlantılarından ulaşabilirsiniz.
Pico ile Micropython ve PIO kullanarak led yakıp söndürme uygulaması
Pico ile Micropython ve PIO kullanarak WS2812 kontrol etme uygulaması
İyi çalışmalar dilerim…