Kablosuz bağlantı kullanan bir şeyler üstünde çalışıyorsanız ve daha da kötüsü donanım da size aitse işe basit haberleşme denemeleri yapmakla başlamak mantıklı. Burada, nRF9e5 sub-GHz SoC ‘u test etmek için basit bir uygulama paylaşıyorum.
Gerekenler: * Test edeceğiniz nRF9e5’li board (device under test), * Bunun üstündeki EEPROM’u programlamak için bir programlayıcı, * PCT, PC’ye bağlanan bir radyo terminali * PCT’yi izlemek için bir PC programı veya bir seri terminal programı, * Vereceğim firmware’ler
Bu programda, radyo cihazları rol değiştirmiyor. PCT daima dinlemede, test edeceğimiz 9e5 cihazı da sadece gönderme yapıyor. Program 8MHz kristal kullanıyor. Program 9e5’in P02 portunu (Pin:2) her göndermede kısa bir süreliğine açıp kapatıyor. Buraya LED bağlayabilirseniz TX anlarını gözlemleyebilirsiniz. Her 786,4ms’de bir veri gönderiliyor. Bunun zamanlamasını Timer1 yapıyor. Veri gönderilen kanal 866MHz bandında 218. kanal. Hedef adresi : E2.E2.E2.E2 Alıcının 8 byte payload beklemesi, bu ayarlarda olması ve 16 bit CRC mode’un açık olması yeterli. PCT bu ayarlarla yüklü biçimde dinleme modunda çalışmaya başlayacak ve bir radyo paketi aldığında bunu 62500bps hızında host PC’ye aktaracak. Veri paketinin yapısı :
byte index
veri içeriği
0
0x47
1
tx_counter[H]
2
tx_counter[L]
3
0x11
4
0x19
5
0x35
6
0x62
7
0x01
Aşağıda, bootloader yaması yapılmış nRF9e5 firmware hex dosyasını bulabilirsiniz. Bu program yüklenir yüklenmez çalışmaya başlar:
PC Terminal Radyosu için de aşağıdaki firmware’i kullanabilirsiniz. Bu program başlamak için bir komut istemeden, PC’ye bağlar bağlamaz çalışmaya başlayacaktır:
Benim PCT’lerden kullanmıyorsanız, EFM8SB1 kullandığınız sürece, yukarıdaki firmware’i kendi donanımlarınızda da çalıştırabilirsiniz. LED_U (P0.3) ve LED_R (P1.5) çıkışlarının kullanımına dikkat edin!
Alınan verileri gösteren bir PCT PC arayüzünü de burada paylaşacağım.
Keyes-KY040 encoder’dan gelen harekete göre değer girişi yapmak için bir arayüz hazırlamam gerekti, bununla ilgili düşüncelerimi burada paylaşmak istiyorum.
KY-040 modülünde 5 pinli bir header var. Bu pinler sırasıyla şöyle isimlendirilmişlerdir:
GND, +
Besleme
SW
Pushbutton NO ucu
DT
Faz-B
CLK
Faz-A
Modülün iç yapısını yukarıdaki gibi gösterebiliriz:
BQ’ları açık kollektör NPN çıkışlı endüktif proximity switch’ler olarak görelim. Rod Push Switch, knoba basınca kısa devre olan bir push-button. Bendeki modülde R3 olarak gözüken direnç takılı değil. Ama bu şekliyle bunu ham kontak olarak kullanamayız çünkü diğer uç (-) beslemeye gidiyor, yani her durumda bunu bir pull-down button olarak kullanacağız demektir.
İnternette yapılan bir aramada, beslemenin +5V’a bağlandığını gösteren şemalar geliyor ama sanırım bu, arduino modüllerinin bir kısmının 5V ile çalışması yüzünden böyle belirtilmiş. Ben modülü +3V ile besliyorum çünkü benim devrede, +3V’a bağlı harici pull-up dirençleri de var. DT ve CLK uçları endüktif algılayıcıların pull-up yapılmış çıkışlarına bağlı ve aralarında dönme yönüne bağlı faz farkı olan encoder çarkı izleme sinyalleri.
Bu arada, modülün üstünde pull-up dirençleri olduğu için encoder sinyallerini işlemciye doğrudan bağlamak yeterli gibi gözükse de kenarlarda debounce olmasına tahammülü olmayacak kadar hızlı bir kod çalıştıracaksanız, pull-up kondansatörleri kullanmanız iyi olur. Ben işlemciye girmeden önce 10nF kondansatör ile pull-up yapıyorum.
ENC-A ve ENC-B olarak isimlendirdiğim encoder pulse sinyallerini P06 ve P07 portlarına giriyorum. P0’ın kullanılması şart değil, işaret fazlarını değerlendirmekte kullanacağım port-match fonksiyonu EFM8’in tüm pinlerinde var. Ancak, söylemeye gerek yok ama aynı portun pinlerini kullanmak register ayarlarını tek yazmada yapmamız açısından hızlandırıcı olabilir elbette.
Firmware
Eğer üzerinde çalıştığımız işlemcinin üstünde QEI (Quadrature Encoder Interface) modülü yoksa encoder fazlarını takip etmek için bir yol olarak port match fonksiyonunu kullanabiliriz. Ben bu encoder’la ilk denemeleri yaparken doğrudan port değerini okuyan bir döngü ile sistemi çalıştırdım. Zaten örnek program diye bulacağınız şeylerin hepsinde de öyle yapılıyor. Ancak pratik kullanımlarda çoğu zaman deneme programında olduğu gibi, port okumasına âmâde bomboş bir ana çevrim olmayacaktır. Bu durumda, özellikle yüksek encoder hızlarını doğru okumak için port-mismatch kesmesini kullanmak yerinde olur.
EFM8’de her bir portun MAT register’ıyla ayarlanan bir port match değeri var. Bunu portun “olmasını beklediğimiz” değeri, “boşta” değeri olarak düşünelim. Herhangi bir port bitinin durumu bu boşta değerden farklı olduğu an bir kesme tetiklenir. Elbette bu mekanizmayı sadece ilgilendiğimiz port bitleri için devreye almamız gerekir. Bunu da her portun MASK değerini ayarlayarak yaparız. Port mismatch kesmesi EIE1<1> (_EMAT) biti ile devreye alınır ve 8 numaralı kesme vektörüne atlama yaptırır. Kesmenin tetiklenme koşulunu şöyle yazabiliriz: Py.x & PYMASK.x != PYMAT.X & PyMASK.x Bu kesmenin bir flag’i yok. Kesmeye atlandığı an durum sıfırlanıyor. Microchip’teki gibi mismatch tetikleyen portu okuma zorunluluğu da yok. Benim uygulamada, P0^6 ve P0^7 için mismatch kesmelerini devreye almak, sürekli olarak çağrılan bir kodla portların durumunu yoklama külfetinden (overhead) bizi kurtarıyor:
Encoder’da bir hareket olduğunda kesme tetiklenecek. Ancak iki şeyi daha ayırt etmemiz gerekiyor: Hareketin yönü ve bitişi. Bunun için kesme kodu içinde basit bir durum makinesi çalıştırmak gerek. Bu durum makinesinin çalışmasını şöyle özetleyebilirim:
0
ENC_IDLE
Hangi girişin kesmeye neden olduğunu bul (önde olan faz). Bu, hareket yönünü belirlememizi sağlar. Ardından önde olan fazın kesme üretmesini iptal et. [Diyagramda 1 durumu]
1
ENC_CW_LEADING
Bu aşamada gelen kesme kesinlikle B girişinden geliyordur. Artık uygulamaya saat yönünde bir hareket olduğunu söyleyebiliriz. B fazının mismatch durumunu 1 yaparsak hareket fazının bitişinde kesme almış olacağız. [Diyagramda 2 durumu]
2
ENC_CCW_LEADING
Bu aşamada gelen kesme kesinlikle A girişinden geliyordur. Uygulamaya saat yönünün tersinde hareket olduğunu söyleyebiliriz. A fazının mismatch durumunu 1 yaparsak hareket fazının bitişinde kesme almış olacağız. [Diyagramda 2 durumu]
3
ENC_PULS_TRAILING
Geride kalan fazın 0->1 gerçişi yapmasını bekle. Bu, hareketin bitişi demektir, yani durum makinesi başa dönebilir. [3] durumu. Her iki yön için de bu adım ortak.
4
ENC_WAKEUP
Bu, sistemi uykudan uyandırmak için kullanılan adım. Bir hareket algılaması yapılmayacağı için durum makinesi çalışmaz.
void ISR_Port_Mismatch(void) interrupt 8
{
switch ( encoder_state )
{
case ENC_IDLE:
// Encoder bo$ta iken bir mismatch kesmesi gelmi$se
// önde olan faz =0 olmu$ demektir. Bu, cw/ccw ayrimini yapmamizi saglar
if ( ENC_A == 0 )
{
P0MASK = 0x80; // P0.6 (ENC_A port match fonksiyonu kapatildi)
encoder_state = ENC_CW_LEADING;
}
else
{
P0MASK = 0x40; // P0.7 (ENC_B port match fonksiyonu kapatildi)
encoder_state = ENC_CCW_LEADING;
}
break;
case ENC_CW_LEADING:
// bu a$amada gelen interrupt enc-b: 1->0 geçi$i sebebiyle tetiklenmi$tir..
// $imdi, enc-b: 0->1 geçi$ini bekleyebiliriz:
P0MAT = 0x40;
ENC_PULS_CW = 1;
/// todo: ENC_A = 1 durumu hata olarak algilanabilir!
encoder_state = ENC_PULS_TRAILING;
break;
case ENC_CCW_LEADING:
// bu a$amada gelen interrupt enc-a: 1->0 geçi$i sebebiyle tetiklenmi$tir..
// $imdi, enc-a: 0->1 geçi$ini bekleyebiliriz:
P0MAT = 0x80;
ENC_PULS_CCW = 1;
/// todo: ENC_B = 1 durumu hata olarak algilanabilir.
encoder_state = ENC_PULS_TRAILING;
break;
case ENC_PULS_TRAILING:
// bu interrupt gelmi$se gerideki fazin: 0->1 geçi$i olmu$ demektir.
// encoder fazi tamamlanmi$tir.
// fiziksel olarak tamamlanmi$ olsa bile, bir fazin bitmesini
//bekleyen i$lem flag'inin sifirlanmasi ile mumkun kilalim:
P0MAT = 0xC0;
P0MASK = 0xC0;
encoder_state = ENC_IDLE;
break;
// CPU encoder hareketi ile IDLE durumdan çikmi$ demektir.
// encoder okuma state mach. çali$mayacak.
case ENC_WAKEUP:
// wake-up durumundan çikiliyor.
EIE2 = 0;
break;
}
}
Bakınca, ana döngü içinde port poll eden “örnek” uygulamadan bile daha basit gözüküyor. Artık, pulse üreten encoder’ınızı, ENC_PULS_CW ve ENC_PULS_CCW adında, Port mask ve(/veya) _EMAT bitleri ile kolayca devreye alınıp çıkarılabilen mantıksal bit girişleri olarak düşünebilirsiniz. Bu bitlerin sıfırlanması uygulama programının sorumluluğundadır. Siz ENC_PULS_XX bitini işlemeden yeni bir pulse algılanırsa bu hareket “atlanacaktır”. Ben uygulamamda bunu özellikle istiyorum çünkü ekrandaki bir değeri encoder hareketine göre değiştiriyorum. Öte yandan hız/konum algılama gibi bir iş yapacaksanız encoder kesmelerini ana programın işlemesi öncesi buffer’lamanın bir yolunu düşünmelisiniz. En basiti, hareket ile artan/azalan bir tamsayı sayaç kullanmaktır, değil mi?
Girişlerin kontak durumlarını ya da butonları okumamız gereken durumlarda sık sık giriş düzeylerini filtrelememiz gerekir. Buna iyi bir örnek, push-buttonlara tepki veren programların tuş kontaklarının yarattığı sıçramalar yüzünden yanlış sayıda basmalar algılamasıdır. Bu sorun özellikle butonun anlık durumunun program içinde çeşitli yerlerde doğrudan sorgulandığı küçük uygulamalarda oldukça can sıkıcı olabilir.
Bu yazıda ilk olarak bir projemizde kullandığım basit bir tuş okuma arayüzünü anlatacağım. Sonra da tuş sayısının arttığı durumlar için etkin bir çözüm olarak kullanılabilecek vertical counter uygulamasının kodunu paylaşacağım. İlerleyen zamanda, kendi tasarımım olan bir klavyenin girişlerini nasıl okuduğum bilgisini de bu yazıya ekleyeceğim.
Projemizde üretimi pek de başarılı olmayan, ayrı bir tuş takımı levhası var. Bunun üzerindeki 10 adet buton işlemcimizin üstünde olduğu kontrol board’una giriyor. Programın çalışması sırasında belli anlarda belli butonlar okunmalı. Butonlardan bazıları basılı tutulduklarında düz basma işevinden başka işlevler yerine getiriyorlar.
Buton tarama işlerini farklı buton grupları için benzer işler yapan 4 farklı fonksiyon olarak yazdım. Bu uygulamada iki farklı buton grubu sanki birbirinden bağımsız iki arayüzmüş gibi çalışıyor.
Fonksiyonlardan her biri sorumlu olduğu butonlar için kabaca yukarıda gösterdiğim basit algoritmayı çalıştırıyor. Aşağıda bir buton okuması için çalışan lojiği (ve aynı zamanda kullanılan yerel değişkenleri) görüyorsunuz:
// A1: basma durumu sorgulama
if ( button_mask & 0x0001 )
{
if ( button_pressed.A1 == 0 )
{
if ( pcnt_A1 > BTN_PRESSCNT )
{
// TODO: burada birden çok seferde sıfırlanma eklentisi olabilir.
if ( btn_A1 ) pcnt_A1 = 0;
}
else
{
if ( btn_A1 ) pcnt_A1 = 0;
else
{
++pcnt_A1;
if ( pcnt_A1 > BTN_PRESSCNT ) button_pressed.A1 = 1;
}
}
}
/*
* kullanıcı programının pressed flag'i sıfırlaması sonrası butonu bırakmadan
* işlemin tekrarlanmasını istiyorsak burada
* if ( btn_A1 ) pcnt_A1 = 0; sıfırlamasını ekleyebiliriz.
*/
}
Burada tüm butonların taramasının tek bir fonksiyon içinde değil her grup için iki fonksiyonda yapıyorum. Çünkü benim uygulamada tuş takımındaki butonlardan ikisinin basılı tutma için ayrı işlevler yaptırması gerekiyor (button hold fonksiyonu). Buna konu butonların tarama işlerini ayrı bir fonksiyonda yazdım. Ayrıca, basılı tutma okuma mantığı yukarıdaki mantıktan biraz farklı:
Yukarıdaki algoritmanın yürütülmesi şu şekilde oluyor:
if ( button_mask & 0x0008 )
{
if ( btn_A4 ) // buton basılı değilse
{
if ( pcnt_A4 )
{
if ( pcnt_A4 > BTN_PRESSCNT ) button_pressed.A4 = 1;
pcnt_A4 = 0;
}
}
else if ( button_hold.AM == 0 )
{
++pcnt_AM;
if ( pcnt_AM > BTN_HOLDCNT )
{
button_hold.AM = 1;
pcnt_AM = 0;
}
}
}
Bu task fonksiyonlarını bir fonksiyon pointer dizisi olarak grupladım:
Ana programımda 5ms periyotlu bir sistem timer’ı var. Bu timer’ı taramaları zamanlamada kullanıyorum. Timer tick’i geldiğinde ana programdaki buton okuma indeks değişkenini 1 yapıyorum: button_read_index = 1;
Bunun sıfırdan farklı bir değere ayarlamak ButtonRead_Task[ ] fonksiyon dizisinin ilgili elemanının işaretlediği task’in o taramada çağrılmasını sağlıyor. Bu sıralama son task (4 indeksli olan) çağrılana kadar arka arkaya her çevrimde yapılıyor ve sonra thread kapanıyor (one-shot operation).
if ( button_read_index )
{
ButtonRead_Task[button_read_index]();
++button_read_index;
// one-shot bir task sıralaması olduğu için tüm task'ler taranınca işlemler biter:
if ( button_read_index > 4 ) button_read_index = 0;
}
Burada butonları sistem çevrimi içinde, programın en sonunda, başka her şeyden bağımsız olarak okuduğuma dikkat edin. Programım içinde herhangi bir yerde bana herhangi bir butonun basma durumu gerekirse artık butonun bağlı olduğu portun lojik değerine değil yukarıda gördüğünüz task’in çıktısına bakacağım. Ki bu da, programım dahilindeki tüm modüllerin erişimine açık olan; BUTTON_FLAGS button_pressed; değişkenidir. Buradaki BUTTON_FLAGS tip bildirimi, uygulamamızda butonlara verdiğimiz isimlerin tanımlandığı bir struct veri tipidir. Son bir not da buton okumasının yetkilendirilmesi ya da devre dışına alınması mekanizması hakkında yazayım: extern void Enable_Button( unsigned int button_index ); extern void Disable_Button( unsigned int button_index ); isminde iki fonksiyon tanımladım. Bunlar, macro olarak tanımladığım buton isimlerini parametre olarak alıp, o butonu taramaya açıyor ya da devre dışına alıyorlar. Bir butonu taramaya almak yukarıda lojiğini verdiğim koddaki ilgili button_mask bitini 1 ya da 0 yapmaktan ibaret. Ben bu değişkeni kendi ünitesi içinde static olarak saklıyorum. Ek olarak, o kanal devreye alınırken dahili sayacı ve durum flag’lerini de sıfırlıyorum.
Sıcaklık ölçümü ile ilgili hikayelerimizde sıra hiçbir sensör kullanmadan sıcaklık ölçmeye kadar geldi.
İşlemcilerin üzerinde bir sıcaklık sensörü olması alışıldık bir durumdur. Silabs EFM8 serisinde de analog modülde, giriş multiplexer’ını ayarlayarak ADC bağlantısını yapabildiğimiz bir sıcaklık sensörü var.
Bu sensörü ortam sıcaklığı ölçmede kullanmayı düşünüyorsanız bunun çoğu durumda pek iyi bir fikir olmayacağını baştan söylemem gerek. Daha önce anlattığım kendi kendini ısıtma etkisi, tahmin edeceğiniz gibi işlemci üstündeki sensör için ziyadesiyle geçerli olacaktır. Elbette koskoca işlemciyi yalnızca sıcaklık ölçmek için kullanmayacağınızı varsayıyorum.
İşlemci üstünde sıcaklık ölçmenin bize donanımsal bir maliyeti yok. Sıcaklık ölçmek için ADC’nin herhangi bir dış bağlantısına ihtiyaç duymuyoruz. Yazılımsal olarak da büyük bir maliyet yok. Birkaç satırlık kod ve birkaç ms içinde işimiz halloluyor. Herkes adına ve her durumu açıklamak için konuşamam ancak ben daha önce onboard sensörü iki sebeple kullandım:
Birincisi board’umun sağlıklı çalışacağı bir sıcaklıkta olup olmadığını kontrol etmek için. Zamanında tasarladığım bir DC sürücü oldukça küçük bir board üstünde çıkış katı işlemci regülatör vs. bir aradaydı. İşlemcinin sıcaklığının 60 küsur dereceyi geçmesi durumunda önce çıkış gücünü sınırlamak sonra da aleti tamamen kapatmak gibi bir iş için onboard sensörü kullanmıştım.
İkinci kullanımda da aslında başka bir şeyi ölçmek için bir donanım kurmuşuzdur ama ölçeceğimiz şey sıcaklığa bağlıdır. Bu durumda sıcaklığa göre düzeltme yapmak için eğer uygunsa board sıcaklığını referans alabiliriz.
Vereceğim örnek kod ADC’yi hali hazırda kullanıyor olup olmamamızdan bağımsızdır çünkü ADC’nin güç bağlantılarının belli bir durumda olmasını gerektirir:
Onboard sıcaklık sensörünün çalışması için TEMPE kontrol bitini 1 yapmak lazım. Fakat öncesinde, ADC referansını 1,65V dahili referansa, eksi bağlantıyı da çip GND’ına bağlıyorum. ADC giriş multiplexer’ına 0x14 (AMUX_TSENS) yazınca girişi de bağlamış oluyoruz. Sıcaklık sensörünün açılma süresine bakarsanız 1,8us gibi bir süre görüyorsunuz ancak buna referansın açılmasını ve SAR girişinin settle olmasını da eklemek gerek. Ben çeşitli donanımsal sebelerle ADC’nin kendi power-up delay zamanlamasını kullanmıyorum. O yüzden sıcaklık sensörünü açtıktan sonra, alışkanlıktan, 1ms bekliyor ve ADC dönüştürme işlemini sonra başlatıyorum.
ADC sequencer’ı, çözünürlüğü, örnekleme hızı vs. mevcut ayarlarımızda olabilir. Onları burada yeniden yazmadım. Sonuçta onboard sıcaklık sensörünün parametreleri gerilim cinsinden tanımlanmışlardır:
Sensörün V / *C kazancının offset hatasına oranına bakarsanız, çipten çipe 6 *C’lik bir 0 *C noktası farkı olduğunu göreceksiniz ki bunun kullanacağınız 100 çipten 68’i için garanti edildiğini de göz önüne almalısınız. Uzun lafın kısası, eğer mutlak değer doğruluğu sizin için önemliyse her bir board için 0*C offset’ini ölçüp flash’ta saklamanız gerekir. (Bununla kim uğraşır bilemiyorum)
Bir termostat uygulaması için sıcaklık okuması yapıyorsak *C skalası çalışma esnasında bizim için gerekli değil. Ancak yaptığımız ölçümün *C karşılığını bilmek istersek, yaptığımız ADC ölçümünden, 757mV ‘a karşılık gelen ADC okuma sayısını çıkarıp sonucu da 2,85 mV’a karşılık gelen ADC sayısına bölerek bir *C sonucuna “yaklaşabiliriz.
Sıfır noktası konusundaki belirsizliğe karşılık mV / *C doğruluğu oldukça belirlidir. Bunun sizin için anlamı şu: Bir noktada, bilinen bir değere ölçekleme yapmanız doğruluk açısından yeterli olacaktır.
Onboard sensörü kullanmanın en güzel yanı, self heating’in ne kadar dramatik bir etki olduğunu gözlerinizle görmenizi sağlayacak olması. Bunu olumsuz bir şey olarak düşünmüyorum. İşlemcinin kendi sıcaklığını görmesi sistemin toplam güvenliği için her zaman çok iyi bir şeydir. Bu arada, buzlu suyla 0 noktasını ayarlarsanız bu sensörün oldukça iyi bir doğruluğu olduğunu keşfedeceksiniz. Teşekkürler Silabs..
Bazen, iki uygulama arasında veri alış verişi yapmam gerekiyor. Örneğin zamanında bir donanımla haberleşen bir program yazmışız ama sonra aynı donanımdan alınan veriyle başka bir şey yapan uygulamaya ihtiyacımız oluyor. Veya bir cihazdan gelen verileri kaydeden bir uygulamada radikal bir değişiklik isteniyor ve söz gelimi önceden binary dosyaya yaptığımız kaydı artık bir veritabanına yapmamız gerekiyor gibi. Burada da belki örneğini vereceğim bir başka örnek durumda da, bazen bir programı denemek için normalde gerçek dünyadan gelecek olan verileri bilgisayarda simüle eden bir şeyler yazıyorum.
Uygulama hali hazırda dışarıdan aldığı verileri TCP ya da UDP üzerinden alıyorsa yerel sunucu/istemci üzerinden yine TCP/UDP haberleşmesi yapmak en basit yöntem. Ama daha genel bir uygulamalar arası veri iletişimi çözümü olarak Windows’un mesajlaşma mekanizmasını kullanmayı tercih ediyorum.
Windows’ta süreçlerin birbirleriyle asenkron olarak haberleşmesi için bir mesajlaşma yapısı var. Çeşitli olayları yanıtlayan nesneler yazarken bunları zaten kullanıyoruz. Ben özellikle TThread sınıfından türettiğim bazı veri işleme nesnelerinde mesajları veri giriş çıkışı için kullanıyorum. Bu mesajlaşma mekanizması aynı zamada uygulamalar arasında da veri iletişimi için kullanılabilir. Bunun için ben WM_COPYDATA mesajını gönderiyor/alıyorum:
WM_COPYDATA mesajının msg alanında TCopyDataStruct diye bir veri türü var. Mesaj içeriğinin aktarımını temelde bu record ayarlıyor. Gönderici kısımda bu veri türünden bir değişkeni doldurup SENDMESSAGE( ) ile yayınlıyorum.
Windows.pas içinde tanımlı TCopyDataStruct diye bir tip var. Bu tipten bir değişken tanımlamam gerekiyor: copyData Bunun dwData alanında mesajla iletilen veri tipini belirtiyorum. cbData alanında da verilen pointer’ın işaretlediği verinin boyunu belirtiyorum. lpData alanında da payload işaretçisini paylaşıyorum. Bu arada, uygulama tarafındaki event_exchange nesnesi de bir TMemoryStream. Yollamak istediğim veriyi buraya yüklüyorum. Genel bir tanımlama olması için bir stream üzerinde çalışmayı uygun buldum. Yerel bir değişken tipi de tanımlayabilirdik.
SendMessage( ) ‘ı kullanmadan önce, hedeflediğimiz alıcı pencerenin o anda canlı olup olmadığına bakabiliriz. Bunun için bir başka windows api’si var: FINDWINDOW( ) Bu fonksiyonun parametrelerinden söz etmem gerek: Harici bir uygulamanın penceresine erişmek için (buna Delphi’de form diyoruz) bize iki parametre gerekiyor. Biri hedef formun sınıf türü, diğeri de pencere (form) adı. FindWindow() api’si bu iki parametreyi istiyor ve bize dönüş olarak handle sayısını döndürüyor. Alıcı uygulama hangi platformda hazırlanmışsa, o platformun win api tanımını bilip bu iki değeri PChar olarak verebiliyor olmamız gerekir.
SendMessage( ) fonksiyonunun parametrelerinden biri FindWindow ile bulunan receiver_handle. sender_handle büyük ihtimalle gönderici ana formunun handle’ı olur. Daha sonra veri göndermek tek bir fonksiyon çağrısına kalır:
res := SENDMESSAGE(receiverHandle, WM_COPYDATA, Integer(Handle), Integer(@copyData));
Alıcı tarafta yapılması gereken, gönderici tarafta “alıcı form” olarak işaretlenen formun üstünde tanımlanmış bir mesaj yakalama procedure’ü tanımlamaktır:
WM_COPYDATA mesajını yakalaması için yazdığımız fonksiyona bir değişken parametre aktarmalıyız ki bu, Msg diye tanımladığım bir TWMCopyData tipinde bir kayıttır. Bu kayıt tipinin .copyDataStruct alanı göndericinin oluşturduğu copyData değişkeninin ta kendisidir!
// gönderici handle numarası:
sender:= Msg.From;
// mesajın data içeriğinin dwData alanını mesaj tipini belirtmede kullanıyorum:
packet_type:= Msg.CopyDataStruct.dwData;
packet_size:= Msg.CopyDataStruct.cbData;
if packet_type = MSG_EVENT_BUFFER then
begin
// paket işaretçisini cardinal tipli bir pointer'a eşliyorum:
pc:= Msg.CopyDataStruct.lpData;
inc(pc);
// ardından "asıl" payload geliyor (burada packet_size'a bakmak gerekebilir)
pEvent:= PEventRec(pc);
Process_Event_Data( pEvent , packet_size );
end;
Yukarıdaki koddan anlayabileceğiniz gibi, gönderici uygulamanın bize gönderdiği payload’u pEvent diye bir işaretçiyle alıyor ve onu işleyen procedure’e veriyorum. En başta cardinal tipli bir sayaç verisi var, onu burada alıyorum. Payload birden çok Event datası içerebilir, o yüzden packet_size değerini de procedure’e veriyorum.
Bir arayüz simülasyonunda bu çalışmanın hayata geçmiş halini şuradan görebilirsiniz. Event receiver ve Event Simulator birbirinden bağımsız iki uygulama. Simulator ile geliştirmekte olduğum kullanıcı arayüzü ve kontrol birimini istediğim türde veriler ile besleyebiliyorum.
LMT01 TI’nin yüksek doğruluklu dijital sıcaklık algılayıcı çipidir. Benim bir sıcaklık ölçüm probu yapma işine soyunduğumda bu malzemeyi seçmiş olmamın ana sebebi, bunun doğruluğunun yüksek olması. İkinci seçim sebebim, bunun dijital çıkışının pulse-count olması. Bu sayede probu ilave bir önlem almaksızın uzatabilirim. Bir üçüncü sebep de, bu sensörün fiziksel yapısı ve malzemesi sayesinde ısıl eylemsizliğinin düşük olması, yani tepki süresinin hızlı olması. Kullanım alanına göre, bu çok önem kazanabilir (ileride anlatacağım). Belki bir seçim sebebi sayılmaz ama TO92 kılıfta geliyor olması da mekanik işleri kolaylaştırıyor.
Pulse count interface, sıcaklık ölçüm sonucunu pulse sayısı olarak çıkış vermek demek. Ayrıca, çipin kendisi de bu arayüzden besleniyor. Pulse çıkışı akım değişimi olarak oluşturuluyor. Bir ölçüm + veri yollama periyodu 104ms sürüyor. Kullanmadığımız zaman çipin enerjisini kesebiliyoruz. Enerjiyi vermeye devam ettiğimiz müddetçe çevrim 104ms’de bir tekrarlanıyor (yani örnekleme frekansı 9,6Hz olarak sabit).
LMT01 bir pulse yollamak için 125uA, boşta durum için 34uA akım çıkışı yapar. Bu akımları gerilime dönüştürmenin en basit yolu bir direnç üzerinden çıkışı toprağa bağlamaktır. Direnç uçlarındaki gerilim sensörün çıkış dalga şekli olacaktır. Bu direnci seçerken LMT01’in uçları arasında 2,15V’luk bir potansiyel farkının korunmasına dikkat etmek gerek. POWER (Vp) ucuna 3V vereceğimizi düşünürsek ölçüm direncimizin uçlarında 125uA çıkış akımı için en fazla 850mV bir gerilim düşümüne hakkımız olduğunu görürüz.
Ben bu mevkide oynatmak için E96 serisinden 6k19 değerinde bir direnç seçtim. Bu direncin uçlarında 125uA pulse’ı yollanırken 774mV gerilim oluşacaktır. Ancak, fark ettiğiniz üzere bu voltaj, bir mikroişlemci girişi tarafından doğrudan lojik olarak okunmaya uygun bir seviye değildir, özellikle de bahsettiğimiz seviyelere bir gürültü marjının da eşlik edeceğini hesaba katarsak..
Gerilim değişimi doğrudan seviye okumaya uygun olmadığında MCU ‘nun comparator modülünü kullanmak uygundur. Ancak isminden de anlaşılacağı gibi, comparator modülünün diğer ucuna da bir referans gerilim bağlamamız gerekecek. Ben bu tarafı da programlanabilir yapmak için şöyle bir yol düşündüm:
LMT01’in çıkışına koyduğumuz direncin aynısından bir tane daha kullanıyoruz ve bunun üstünden de akım çıkışlı DAC ile ayarladığımız bir referans akımı geçiriyoruz. Dirençler aynı değerde oldukları için artık seçeceğimiz eşik değerini son derece güvenilir biçimde akım cinsinden belirleyebiliriz. Sıcaklıkla değişim gibi şeyleri de dert etmek zorunda kalmayız.
Referans akımı 34uA ile 125uA arasında bir yerlerde olmalı. Tam orta noktayı (80uA) seçmek mantıklı gibi gözüküyor.
EFM8SB1’deki DAC 1uA ya da 8uA (Hi current mode) çözünürlükle çalıştırılabiliyor.
EFM8SB1 IDAC module
EFM8SB1’deki akım referansı modülünü Hi-Current mode’da çalıştırıyorum. Bu durumda modül 8uA’lik adımlarla akım çıkışı üretiyor. Modülün akım ayarı 6 bit olarak tanımlanıyor. IREF0DAT = 10 (desimal) yazmakla 80uA çıkış elde ediyorum.
Güç tasarrufu sağlamak için, akım kaynağını yalnızca ölçüm yapacağım zaman açıyorum. Ek olarak, akım kaynağının sürdüğü direncin paralelinde bir kondansatör de olduğu için komparatör çıkışını saymaya başlamadan bir süre önce akım kaynağını açmış olmam gerek.
IREF0CN0 = 0x4A; // 8*10=80uA akım referansı
Akım kaynağı modülünü 80uA ile çalıştırdığımda iki direncin uçlarındaki gerilim yukarıdaki gibi gözüküyor. Bu seviyenin şimdilik uygun olduğunu düşünüyorum. Şimdilik yazdım çünkü bu denemede LMT01’i 1m uzunluğunda bir kablonun ucuna taktım. Bir de, sensör çıkışındaki dirence paralel 100pF bir kondansatörüm var.
Referans seviyemizi de ayarladığımıza göre artık comparator module’e bakabiliriz:
EFM8SB1 Comparator module
Bizim uygulamada comparator’ün iki girişini de port pinlerine bağlıyorum. Çıkışın asenkron halini de aynı şekilde port pinlerinden birine alıyorum. Interrupt kullanmıyorum. Ama anlayacağınız gibi, eğer pin sayısını azaltma gibi bir gereksinim olsa idi, bu kesmelerden birini kullanarak, sayma işini interrupt handler’a yaptırabilir ve kullanılan modül sayısını azaltabilirdim.
Comparator async. output sinyalini de crossbar üzerinden portlardan birine çıkış veriyorum. Artık burada, komut cycle’ından bağımsız fazlı şekilde LMT01’in count pulse sinyalini görebilirim.
CMP0MD = 0x80; // fastest response, edge interrupt'lar kullanilmiyor
CMP0MX = 0x44; // P11 = CMXN, P10 = CMXP
CMP0CN0 = 0x81; // bunu yapmak comp. modülünü açar ve 5mV negatif hystersiz verir
Comparator modülünü yalnızca sensörü okumak istediğim zaman açıyorum (enerji tasarrufu). 5mV negatif hysteresis (düşen kenar) eklemek, eşik değerini biraz daha yüksek seçsem bile darbe genişliğinin çok azalmamasını sağlıyor (aslında sayma hızım bu mertebelerin çok üstünde olsa da).
5mV’luk düşen kenar hystersis’inin dalga şeklinin duty cycle’ını neredeyse %50’ye getirdiğini görebilirsiniz. Düşen kenardaki overshoot’u benim comparator çıkış pini (CPO) üzerinden ölçüm almam yüzünden görüyorsunuz. CPO pinini T0 girişine 100 ohm gibi bir direnç üzerinden bağlamanın neden iyi olduğunu da açıklıyor (T0 yüksek giriş empedanslı bir sonlandırma ve pull down direnci de bulunmuyor). Elbette burada tüm bu bağlantılar birkaç mm içinde hallolduğu için hiçbir şeyi dert etmeniz gerekmez. Ben genel konuşuyorum, amacımız bu basit sensörü çalıştırmak değil, büyük resmi görün.
Sıcaklık sensöründen gelen pulse’ları lojik seviyeye çevirdikten sonra, şimdi onları saymamız gerekiyor. Karşılaştırıcının çıkışını bir pine alıp onu da işlemcinin sayıcı olarak ayarlayabildiğim bir girişine bağladığımda artık LMT01 pulse’larını kesme falan koşturmadan sayabilirim:
EFM8SB1 T0 Mode0/1
8051 ‘de T0 ve T1 modülleri harici sayaç ya da gated counter olarak ayarlanabiliyor. Bir pini, crossbar’da T0 counter girişine route edip T0’ın CT0 bitini 1 yapmak girişteki pulse’ları saymak için yeterli.
Geriye tek bir şey kalıyor: MCU’nun pinlerinden birini, LMT01’i beslemek için çıkış yapmak. Bu pini 0 yaptığımda sensör devre dışı kalmış olacak.
Bu donanım düzenlemelerini yaptıktan sonra pulse count interface ile okuma yapmam için gereken firmware işlemleri şunlar:
1) Sensörü enerjilendir. 2) Akım kaynağını aç. 3) 20ms bekle. 4) Karşılaştırıcıyı aç. 5) Sayıcıyı devreye al. 6) Sayıcının, 180ms boyunca gelen pulse’ları saymasını bekle. 7) 180ms sonunda sayıcıyı kapat. Karşılaştırıcıyı kapat. Akım kaynağını kapat. 8) Gelen pulse sayısı iki ardışık ölçüm sonucunun toplamıdır.
Bir sıcaklık sensöründen sürekli arka arkaya ölçüm almak istemeyiz. Çünkü; 1) Bu zaten gereksizdir çünkü sıcaklık denen fiziksel nitelik genellikle çok hızlı değişen bir şey değildir. (Ortam sıcaklığı gibi şeyler ölçtüğümüzü varsaydığımızda) 2) Enerji bütçemiz kısıtlıdır. 2V – 34uA besleme ile çalışan bir sensör kullanıyorsam düşük güç tüketimi gerektiren bir uygulamam var demektir. Tasarım kriterini bu yönde olabildiğince ileride karşılamaya çalışırım. 3) Dijital bir sensörü sürekli çalıştırırsam onun kendi kendini ısıtmasına neden olurum. Doğruluğu bu mertebede olan bir çipte bu belirgin bir hata yaratacaktır.
Çipi sürekli enerjili tutarsak LMT01 yukarıda göreceğiniz hızla çalışıyor. Bu, olabilecek en yüksek ölçüm hızımızdır. Bir hatta akan suyun sıcaklığını ya da kimyasal bir tepkimenin sıcaklığını ölçmeniz gerekiyorsa 120ms’de bir ölçüm yapabileceğinizi bilmeniz gerek.
Bu sensörün ölçümlerini başka bir sensörle kıyaslayan basit bir uygulama hazırladım. Aşağıdaki trend grafiğinde gördüğünüz 32 *C’lik plato, LMT01’i parmaklarımın arasına alıp birkaç saniye tutmam sonucunda oluşan sıcaklık değişimi.
Bu çalışmayı aşağıdaki sensör board’u ile yaptım:
Kablolu ve kablosuz olarak bir host aygıta sıcaklık ve nem ölçümleri gönderen bir uygulamaya dair bilgileri burada paylaşacağım.
Ayrıca LMT01 kullanan hassas el termometresi tasarımına dair notlarımı da burada paylaşacağım.
RS485 donanımı ucuz, uygulaması kolay bir haberleşme arayüzü olarak öncelikli tercihlerimizden biri olmayı sürdürmektedir. Endüstriyel otomasyon ve veri toplama gibi işlerde eskilerden beri kullanılagelen Modbus namlı bir protokol vardır. Bu protokol çok basit, çok minimalist bir kullanım ile RS485 üzerinden multi-drop, yani ikiden fazla cihazın müşterek kullandığı bir haberleşme ortamı kurmaya iyi bir örnektir.
Yakın zamanda benden Modbus üzerinden kumanda edilen bir takım giriş/çıkış modülleri tasarlamam istendi. Bu vesileyle de, uzun yıllardır pek çok farklı işte kullanmakta olduğum Modbus protokol yürütmesi hakkında birkaç not yazayım diye düşündüm.
Burada, Modbus protokolünün embedded tarafta, kaynakları sınırlı işlemciler üzerinde yürütülmesine dair yaptığım bazı şeyleri paylaşacağım. PC tarafında da kendi yazdığım modbus erişim programlarına örnekler vereceğim.
MODBUS RTU
Modbus protokolünün RS485 hattı üzerinde çalışan RTU yürütmesini kullanıyorum. Bu yürütmede paket byte’ları binary olarak anlamlanıyor.
Protokol byte formatı olarak 8-E-1 kullanılır diyor. Ancak ben 8-N-1 kullanıyorum. (Hâlâ, donanımsal parite hesabı yapmayan işlemciler kullanan insanlar var çünkü) Aslında protokol, eğer parite kullanamıyorsan bari stop bitini 2 tane yap da diyor ama ben bunu da yapmadım.
Modbus’un standart protokol tanımlamalarına “olabildiğince” sadık kalmaya çalıştım. Aslında her ne kadar yalınlığına saygı duysam da, RS485 multi-drop pek çok uygulamada modbus en etkin yöntem olmuyor. Eğer sizin tasarladığınız cihazların dışında cihazlarla da haberleşecek bir ağ öngörüyorsanız o zaman işlerin bir standardı olmalı dediğiniz için modbus’ı ciddiye alırsınız. Belki bir gün RS485 üzerinde daha efektif veri haberleşmesi yapan kendi yöntemlerimi de burada paylaşmaya başlarım.
MODBUS ZAMANLAMALARI
RS485 asenkron bir seri haberleşme ortamıdır. Bu da demektir ki bu ortamda birbiriyle haberleşecek cihazlar yollanan verilerin içeriği kadar zamanlaması üzerinde de anlaşmış olmalıdırlar.
Modbus spesifikasyonu, iki byte arasında en fazla 1,5 byte süresi kadarlık bir boşluğa izin verildiğini belirtiyor. Bundan çıkaracağımız iki sonuç vardır:
Paket byte’larını bekleme yapmadan art arda yollamamız gerekir.
Bir veri alıyorken, 1,5 byte süresi kadar alma yapmazsak paket sonuna geldik diyebiliriz.
Bir alma işlemi esnasında paket boyunu saymak zorunda kalmadan paket almanın sonuna geldiğimize karar verebilmek işleri kolaylaştıran bir şeydir. Öyle olmasaydı, modbus paket boyları sabit uzunlukta olmadıkları için, bir veri gelmeye başladığında doğru anda paket boyunun ne olacağını hesaplamış olmamız gerekirdi. Ki bunun için de gelen verinin kaçıncı byte’ında olduğuna göre çalışan bir sorgumuz olması gerekirdi. Bir uart rx kesmesinin içine karmaşık sorgular yazmak akıllıca bir iş değildir, bana güvenin.
Yine modbus spesifikasyonu, iki paket arasında en az 3,5 karakter süresi kadar bir boşluk olması gerektiğini söyler. Bundan da bize iki hisse çıkıyor:
Bir paket aldığımızda, cevap vermeden önce, master’ın son byte’ı yollamasından beri 3,5 karakter süresi geçtiğinden emin olmamız gerek.
Bir modbus hattına ilk bağlandığımızda (fiziksel bağlanmayı kastetmiyorum) konuşmaları dinlemeye başlamadan önce en az 3,5 karakter süresi kadar bir sessizlik olmasını beklememiz gerek (paketlerle “senkronize olmak”).
Aslında, üzerinde biraz düşünürseniz yukarıdaki iki maddenin birbirini gerektirmediklerini anlarsınız. Bir slave, master’ın sorgusuna 3,5 karakter süresini beklemeksizin yanıt verirse diğer slave’lerin bakış açısından tek bir paket gelmeye devam ediyor gibi olacaktır ve slave’ler sıralarını kollamaya başlamayacaklardır. Bu bir sorun yaratmaz çünkü o esnada konuşan slave zaten master’a cevap veriyordur.
Haberleşme protokolü dediğiniz şeyler bu şekilde paralel dallanan önermelerle doludur. Protokol yürütme denen şeyde aranan sonuç tamamen mantıksal tutarlılık ya da en sadelik değil, bir şeyi açık şekilde kabul etmek esasına dayanır. O yüzden, bu aşamada fazlasına kafa yormayıp TÜM paketlerin en az 3,5 byte süresi aralıkla hatta yüklenmesini gözeteceğim.
Yukarıda, sözünü ettiğim 3 zaman tanımının haberleşme çerçevesindeki yerlerini gösterdim. Slave taraf işlemcimiz DataProcessor( ) fonksiyonunu çalıştırırken bu süreleri gözetmek zorundadır.
Modbus protokolü süreleri karakter müddeti cinsinden tanımlamış. Yani, bu byte süresi dediğim ölçü birimi bir byte’taki bit sayısı bölü geçerli baudrate kadar olacak demektir. Ancak yine modbus protokolü, yüksek baud rate’ler için sürelerin çok küçük olmasının önüne geçmek için 19200bps üstü hızlar için bu sürenin sabit olabileceğini söylüyor.
Ben en başta bu süreleri tamamen baudrate’e bağlı yapmıştım. Ancak sonra, protokol dediğin şeyde önemli olanın mantıksal bütünlük değil, yaptığını açık şekilde ifade etmek esasına dayandığını hatırlayıp (yukarıda bunu anlattım) tüm zamanlamaları 9600bps’ye göre hesapladım ve sabit yaptım.
Bir byte’ı 10 bit kabul ederek zamanlama parametrelerini şu şekilde alıyorum:
t35 = 4,17ms
t20 = 2,6ms
t15 = 1,56ms
modbus data processor stack state diagram
UART RX Kesmesi:
Haberleşme protokolü üç yazılım modülünden oluşur:
Veri alma kesmesi
Veri işlem
Veri yollama
Yukarıda çizdiğim durum makinesinin ilerlemesi iki dış etki ile olur: Sisteme veri gelmesi ya da bir zaman aşımı olması. Bunun haricindeki zamanlarda Data Processor dediğim durum makinesi ya hiçbir şey yapmaz ya da yeni girdiği durum için işletmesi gereken kodu çalıştırıp durağan bir adıma gider. Thread şeklinde algoritma gerçeklemenin benim uyguladığım yöntemi bu.
Bu uygulamada UART kesmesi dediğimizde yalnızca RX kesmesini düşünmemiz yeterli. TX kesme ile yönetilmek zorunda değil. Öyle olması gereken durumlar elbette vardır ama bunu ayrıca anlatırım. RX kesmesi öyle bir şey olmalı ki, içindeki kod DataProcessor( ) ‘ün o anki durumuna bağlı olmamalı. Böylece kesme kodumuzu (işin donanımın parmak soktuğu kısmı) olabildiğince yalın, asıl fonksiyonlarımızla az teması olan bir hale getirmemiz mümkün olur. Bu, embedded programlamada başarıyı çok etkileyen bir şeydir.
Hep bahsettiğim gibi, bir kesme fonksiyonu olabildiğince basit olmalıdır, ama daha basit değil.. 🙂
Byte hatası yoksa gelen byte Xbuffer‘ın Xptr indeksli elemanına yazılır ve Xptr ilerler.
Son olarak da zaman aşımı denetimi için kullandığım timer’ı sıfırlıyorum. Byte geldiği müddetçe zaman aşımı olmamalıdır.
Kesme kodu bundan ibaret olacak. Elbette UART modülünün ürettiği donanım hatalarını da handle edip bir flag ile ana programa bildiriyorum. Ama bu işin ilginç kısımlarından biri değil.
Data Processor Durumları:
DataProcessor( ) bir thread fonksiyonu olarak kullanılır. Yani, ana sistem döngüsünde her geçişte koşulsuz olarak çağrılan bir fonksiyondur ve koşulsuz olarak da sistem kaynağı kullanır ( bir donanım timer’ı ve uart kesmesi). DataProcessor’ün timer overflow ya da uart rx kesmelerine vereceği tepkinin hızı ana sistem çevriminin periyoduna bağlı olacaktır.
DataProcessor( ) her çağrıldığında belli bir durumda olur. Durum geçişleri yalnızca fonksiyonun kendisi tarafından yapılır, yani hiçbir dış kod DataProcessor( ) state machine’in durumunu belirleyen commstat değişkenine erişemez.
DISABLED:
UART kapalı ve makine devre dışında. Yani modbus fonksiyonumuz kapalı.
Bu durumdan çıkış için kullanıcı kodun comm_cmd = CC_INIT yapması gerek.
Bu komut algılanınca Restart_RS485( ) fonksiyonu ile UART RX etkinleştirilir.
LISTENING:
UART veri alma etkin, yani makine Modbus hattını dinliyor ancak alınan byte’larla bir işlem yapılmıyor.
Bu durumdan çıkış için T35 süresi boyunca hiç RX olmaması gerekir. T35 süresi boyunca veri alınmaması Modbus hattının boşta durumda olduğu anlamına gelir. Makine artık paketleri dinleyip adresleri kontrol edeceği senrkonize duruma geçebilir.
Eğer bir byte hatası olursa makine SLEEP durumuna atlar.
SYNCD:
3,5 byte süresi boyunca hat sessiz kaldı. Demektir ki şu anda Modbus hattında iletilen bir paket yok. Artık makine senkronize durumda.
Bundan sonra gelen her byte’a bir paketin ilk byte’ı bu deyip adres byte’ı muamelesi yapacağız. Burada iki olasılık var: Paket bizi adresliyordur (ADDRESS_HIT) ya da adreslemiyordur (ADDRESS_MISS).
ADDRESS_MISS için SLEEP durumuna geçilir.
ADDRESS_HIT için de paketin devamını almak için PACKET_RX’e geçilir.
Eğer bir byte hatası olursa makine SLEEP durumuna atlar.
PACKET_RX:
Veri alınıyor.
Bu durumda iken makine Xptr indeksini izler. Modbus fonksiyonu için ayrılmış buffer boyunun aşılması BUFFER OVERRUN hatası demek olur ve bu durumda paket alımı iptal edilip SLEEP adımına atlanır.
Bu durumdan çıkış paket sonu ile olacak. Paket sonunun algılanması T15 süresi boyunca bir byte alınmaması ile olur.
Eğer bir byte hatası olursa makine SLEEP durumuna atlar.
PARSE:
Paket sonu algılandığında, veri alma kapatılır. Xptr sayacı alınan byte sayısını göstermektedir.
Geçerli bir Modbus master paketinde olması gereken genel koşulların sağlaması yapılır. Paket geçerli gözüküyor ise paket türüne bakılır ve ilgili komut işlemine atlanır
Paket hatalı gözüküyorsa SLEEP durumuna atlanır.
SLEEP:
Bir hata ya da ADDRESS_MISS durumunda ortalama bir paket boyu kadar bir süre boyunca makine kapalı durumda bekler. Bu süre baudrate’e göre belirlenir.
Bu sürenin geçmesi sonrasında makine
Restart_RS485( ) seri haberleşme yeniden başlar. Makine LISTENING durumuna gider.
Bundan sonraki makine durumları alınan Modbus komutu ile ilgili işlemler ve verilecek yanıtlar ile ilgili kodlardır. Bunları ayrı ayrı anlatmak yerine desteklediğim Modbus komutlarından söz etmeyi uygun buluyorum:
DESTEKLENEN MODBUS KOMUTLARI
MODBUS fonksiyon kodları
Bu fonksiyon kodlarının kendi başlarına ne anlama geldiklerinin çok fazla konuşulmaya değer bir yanı yok. Önemli olan, üzerinde çalıştığımız cihazda bu kodlar ile neleri okuyup yazacağımızdır. İşin özeti şu: Elinizde bir cihaz vardır. Bu cihazın modbus üzerinden görünür bir bellek haritasını oluşturmuşsunuzdur. Yukarıdaki komutlar artık bu harita üzerinde bir anlam taşır. Bu konuda da olabildiğince özgürsünüz. Holding Register denen şeyle dijital giriş durumu da yollayabilirsiniz. Sonuçta bunu güzelce açıklamayı becerirseniz söz gelimi PLC ile cihazınıza okuma yapan adam holding register olarak okuduğu Word’leri bit olarak kullanabilir. Yani, Modbus’ın tanımındaki veri tiplerinin aslında pek bir bağlayıcılığı yoktur.
Şimdi benden istenen Modbus I/O modüllerinin bellek haritalarına bakalım. Okuyucuya genel bir bakış açısı kazandırmak için şöyle bir not ekleyeyim: Benim için modbus haberleşmeli bir uzak I/O modülü geliştirmek demek 3/4 oranında aşağıdaki tabloları hazırlamak demekti. Bundan sonrası zaten yokuş aşağıdır.
Komutlardan da anlayacağınız gibi Modbus, 4 bellek tipi üzerinde veri erişimi komutlarına sahiptir:
DISCRETE OUTPUTS (Y)
00001 adresinden başlayan dijital çıkış bitleridir.
Bu alan, 0x01 komutu ile okunur ve 0x05, 0x0F komutları ile de buraya yazılır.
Komut adresleri haberleşmede 0 -> 00001 olacak şekilde kullanılır. DISCRETE INPUTS (X)
10001 adresinden başlayan dijital çıkış bitleridir.
Bu alan 0x02 komutu ile okunur.
Komut adresleri haberleşmede 0 -> 10001 olacak şekilde kullanılır. ANALOG INPUTS (R)
30001 adresinden başlayan 16 bitlik sadece okunur register alanıdır.
Bu alan 0x04 komutu ile okunur. HOLDING OUTPUTS (H)
40001 adresinden başlayan 16 bitlik okunabilen ve yazılabilen register alanıdır.
0x03 komutu ile okunur, 0x06 komutu ile yazılır.
X/Y Giriş Çıkışları:
Yaptığım cihazlarda değişik sayıda dijital girişler ve röle çıkışları var. Yani, bu ürünlerin işlevselliği büyük oranda dijital girişleri okumak ve röle çıkışlarını kumanda etmekten ibaret. O yüzden cihazların belirleyici özelliği aşağıda verdiğim X ve Y adres haritalarıdır.
Cihazların gerçek giriş çıkışlarını X ve Y haritalarında 1 adresinden başlayarak konumlandırdım:
Bu cihazlarda tam sayı olarak ifade edilebilecek bir giriş ya da çıkış bulunmuyor. Ancak, dijital giriş çıkışları birer bitmap olarak bu bellek haritasında okunabilir yapmak iyi bir fikir. Bu şekilde girişleri okumak için ayrı çıkışları okumak için ayrı bir komut kullanmak zorunda kalınmadan tek seferde her şey okunabilir.
Ben bu bellek alanını, yazılabilir olduğu için input register alanından ayrı tanımlıyorum. Buradaki “holding” sıfatına saygı olarak, cihazların silinmez belleğine yazılan parametreleri bu haritada tanımladım. Yine de bu haritanın bir kısmını son kullanıcı için sadece okunur yaptım (kullanıcının cihaz kimlik bilgilerini değiştirebilmesini istemem).
Input register alanında X ve Y vektörlerini bitmap olarak göstermeye benzer şekilde burada da Y vektörünü binary olarak yazma özelliği düşünülebilirdi ama zaten 0x0F komutu tam olarak bu işi yaptığı için böyle bir şeye gerek yok.
MODBUS komutlarının benim uygulamalarımdaki yürütmesi belli kısıtlamalara tabi olabilir. Sonuçta protokolün kendisi çok genel olduğu için biz bazı yönleri bizdeki özelliklere ve kısıtlamalara göre basitleştirebiliriz.
Örneğin, discrete okuma komutlarında hedef adresinin 8’in katları şeklinde olmasını şart koşuyoruz. Benzer şekilde, 8’in katı olmayan vektör boylarını da kabul etmiyoruz. Bu işlevsel bir kısıtlama değil. Çünkü bu şart, genel modbus tanımlamasına bir istisna oluşturmuyor. Yalnızca onun bir alt kümesini geçerli kabul ediyor.
Ayrıca, 0x04: Read Input Register komutunda komutun serbestçe hazırlanmasına izin vermiyoruz. Bu komutu kullanacak kişi bir seferde cihazın 4 word’lük R bellek alanının tamamını okumak zorunda.
modbus fonksiyon kodu 01 Read Discrete Outputmodbus 02 RDI komutumodbus fonksiyon kodu 03 read holding registersmodbus fonksiyon kodu 04 Read Input Registersmodbus fonksiyon kodu 05 Write Discrete Outputmodbus fonksiyon kodu 06 Write Single Holding Registermodbus fonksiyon kodu 0F Write Multiple Outputs
Son olarak, yukarıda anlattıklarımın kodda nasıl gözüktüğünü paylaşmak istiyorum.
Ana programda koşulsuz olarak çağrılan bir thread fonksiyonum var:
RS485Thread[rs485_status] ();
Bu çağrı aslında bir fonksiyon pointer çağrısı. Dizinin elemanları şunlar:
Daha önce anlattığım dinleme, senkronize olma, veri alma ve alınan veriyi çözümleme işlemlerinin her biri bir fonksiyon adımı olarak işletiliyor. RS485 işlerini yürüttüğüm ana thread bu adımlar arasında dönüp duruyor.
uart_tick, sistem tarafından işletilen birkaç ms periyotlu bir serbest timer. Thread’in zamanlaması için buna ihtiyacımız var. Ayrıca, bize veri geldiğinde tetiklenen bir de kesme lazım:
İronik bir şekilde bu yazıyı artık pek kullanmadığım PIC18 işlemcisi üstünde çalışan bir kod ile örneklendirdim. Bu işlemcide artık bana bile tuhaf gelen bir şekilde, kesmeler için müşterek bir vektör var. O yüzden kesme handler’ı içinde bir kez daha receive interrupt flag ‘i sorguluyoruz. Ayrıca burada uart hatası kesmesi diye bir şey yok ki bunu handle etmemek bana pek doğru gelmiyor. Neyse, bunların modbus’ın kendisi açısından pek bir önemi yok. Biz şimdi sözü state fonksiyonlarına bırakıp bu yazıyı noktalayalım:
////////////////////////////////////////////////////////////////////////////////
//// RS485 - Modbus State Functions:
////////////////////////////////////////////////////////////////////////////////
// seri port kapalı: bir timer olayı ile yeniden başlatılacaktır.
static void RS485_Disabled(void)
{
// Bu durumda iken, tick gelmesini bekleriz:
if ( uart_tick > 1) // uart timer'ından kesme gelene kadar burada dur...
{
uart_tick = 0;
// UART'ı yine aç:
Xptr = 0;
RS485_DIR = 0; // alici modu
RCSTA1bits.SPEN = 1;
RCSTA1bits.CREN = 1;
PIE1bits.RC1IE = 1;
rs485_status = LISTENING; // veri bekleme modunda bekleyeceğiz
}
}
// alıcı boşta: (yani master'dan komut bekleme adımı)
static void RS485_Listening(void)
{
/// sync beklerken buffer overrrun olabilir:
if ( Xptr > 19 ) Xptr = 0;
/// senkronize duruma gelmek için T35 timeout'u bekiyoruz:
if ( uart_tick > 1 )
{
uart_tick = 0;
Xptr = 0;
rs485_status = SYNCD;
}
}
// alıcı dinleme moduna geçti: gelen byte paket başı olacak.
static void RS485_Syncd(void)
{
if ( Xptr ) // başlık alındı mı?
{
// addr. hit =>
if (( Xbuffer[0] == my_rs485_address)||( Xbuffer[0] == 255 ))
{
LED2 = 1;
uart_tick = 0;
rs485_status = RECEIVING;
}
else // addr. miss =>
{
Disable_RX();
rs485_status = DISABLED;
}
}
}
// veri alınıyor: bizi adresleyen bir paket geliyor.
static void RS485_Receiving(void)
{
// üzerinde çalışılabilecek max. rx paket boyu aşıldıysa yanıt verilmeyecek:
if ( Xptr > 29 )
{
Disable_RX();
rs485_status = DISABLED;
}
else if ( uart_tick > 1 )
{
Disable_RX();
rs485_status = PARSE;
}
}
static void RS485_Parse(void)
{
unsigned char c;
////////////////////////////////////////////////////////////////////////////
// gelen paketin doğruluğunun kontrolü
// komut byte'ı doğru mu:
if ( ( Xbuffer[1] > 0x10 )||( Xbuffer[1] == 0 ) )
{
rs485_status = DISABLED;
}
// paket boyu beklenen en düşük değerin de altındaysa:
else if ( Xptr < 8 )
{
rs485_status = DISABLED;
}
// bu aşamada gelen verinin CRC'sini sorguluyoruz:
else
{
myCRC.i = 0xFFFF;
c = Xptr-2; // crc hesabına girecek son byte indeksi
// gelen paketin crc'sini hesapla: (0..n-2 byte'lar arası)
for (Xptr=0; Xptr<c; Xptr++)
{
DoCRC(Xbuffer[Xptr]);
}
// TODO: crc doğru mu bakmak!
// gelen crc hesapladığımızla aynı mı:
/*
if ( ( myCRC.b[0] == Xbuffer[c] ) && ( myCRC.b[1] == Xbuffer[c+1] ) )
{
if ( Xbuffer[1] == 0x01 ) rs485_status = HANDLER_01;
}
else rs485_status = DISABLED;
*/
if ( Xbuffer[1] == 0x01 ) rs485_status = HANDLER_01;
else if ( Xbuffer[1] == 0x02 ) rs485_status = HANDLER_02;
else if ( Xbuffer[1] == 0x04 ) rs485_status = HANDLER_04;
else if ( Xbuffer[1] == 0x05 ) rs485_status = HANDLER_05;
else if ( Xbuffer[1] == 0x0F ) rs485_status = HANDLER_0F;
else rs485_status = DISABLED;
}
}
/// Read Discrete Outputs
static void RS485_Handler_01(void)
{
// genel çağrı adresi için bu komut yanıtlanmaz:
if ( Xbuffer[0] == 255 )
{
uart_tick = 1;
rs485_status = DISABLED;
}
// vector start 0 olmalı:
else if ( Xbuffer[2] & Xbuffer[3] )
{
uart_tick = 1;
rs485_status = DISABLED;
}
// vector size = 32 olmalı
else if ( Xbuffer[4] != 32 )
{
uart_tick = 1;
rs485_status = DISABLED;
}
// her şey doğru ise:
else
{
// çıkışların geçerli durumunu oku:
Read_Y();
// yanıt paketini hazırla:
Xbuffer[0] = my_rs485_address;
Xbuffer[1] = 1;
Xbuffer[2] = 4;
Xbuffer[3] = dY[0].b[0];
Xbuffer[4] = dY[0].b[1];
Xbuffer[5] = dY[1].b[0];
Xbuffer[6] = dY[1].b[1];
// yanıt paket boyu:
response_size = 7;
rs485_status = RESPONSE;
}
}
/// Read Discrete Outputs
static void RS485_Handler_02(void)
{
// genel çağrı adresi için bu komut yanıtlanmaz:
if ( Xbuffer[0] == 255 )
{
uart_tick = 1;
rs485_status = DISABLED;
}
// vector start 0 olmalı:
else if ( Xbuffer[2] & Xbuffer[3] )
{
uart_tick = 1;
rs485_status = DISABLED;
}
// vector size = 48 olmalı
else if ( Xbuffer[4] != 48 )
{
uart_tick = 1;
rs485_status = DISABLED;
}
// her şey doğru ise:
else
{
// girişlerin geçerli durumunu oku:
Read_X();
// yanıt paketini hazırla:
Xbuffer[0] = my_rs485_address;
Xbuffer[1] = 2;
Xbuffer[2] = 6;
Xbuffer[3] = dX[0].b[0];
Xbuffer[4] = dX[0].b[1];
Xbuffer[5] = dX[1].b[0];
Xbuffer[6] = dX[1].b[1];
Xbuffer[7] = dX[2].b[0];
Xbuffer[8] = dX[2].b[1];
// yanıt paket boyu:
response_size = 9;
rs485_status = RESPONSE;
}
}
static void RS485_Handler_04(void)
{
unsigned char a, b, i;
// genel çağrı adresi için bu komut yanıtlanmaz:
if ( Xbuffer[0] == 255 )
{
uart_tick = 1;
rs485_status = DISABLED;
}
else if ( (Xbuffer[2] > 11) || (Xbuffer[4] > 12) )
{
uart_tick = 1;
rs485_status = DISABLED;
}
else if ( (Xbuffer[2] + Xbuffer[4]) > 12 )
{
uart_tick = 1;
rs485_status = DISABLED;
}
else
{
// dR alanında haritalanmış dX ve dY vektörlerini güncelle:
Read_X();
Read_Y();
dR[2].i = dX[0].i;
dR[3].i = dX[1].i;
dR[4].i = dY[0].i;
// yanıt paketini hazırla:
a = Xbuffer[2]; // okuma vektörü başlangıç adresi
b = Xbuffer[4]; // okuma vektörü eleman boyu
// header:
Xbuffer[0] = my_rs485_address;
Xbuffer[1] = 4;
Xbuffer[2] = 2*b;
// dR okuma vektörünü kopyala:
for ( i=0; i<(a+b); i++ )
{
Xbuffer[2*i+3] = dR[a+i].b[0];
Xbuffer[2*i+4] = dR[a+i].b[1];
}
response_size = 2*b + 3;
rs485_status = RESPONSE;
}
}
static void RS485_Handler_05(void)
{
unsigned char bix;
unsigned int mask;
// operator word'ünün Hi byte'ı kesinlikle 0 olmalıdır:
if ( Xbuffer[4] )
{
uart_tick = 1;
rs485_status = DISABLED;
}
else if ( Xbuffer[2] > 31 )
{
uart_tick = 1;
rs485_status = DISABLED;
}
else
{
// adreslenen bit dY[1] üzerinde ise:
if ( Xbuffer[2] > 15 )
{
bix = Xbuffer[2] - 16;
mask = (unsigned int) ( 1 << bix );
if (Xbuffer[5] == 0) dY[1].i &= ~(mask);
if (Xbuffer[5] == 0xFF) dY[1].i |= mask;
if (Xbuffer[5] == 0x11) dY[1].i ^= mask;
}
// adreslenen bit dY[0] üzerinde ise:
else
{
bix = Xbuffer[2];
mask = (unsigned int) ( 1 << bix );
if (Xbuffer[5] == 0) dY[0].i &= ~(mask);
if (Xbuffer[5] == 0xFF) dY[0].i |= mask;
if (Xbuffer[5] == 0x11) dY[0].i ^= mask;
}
UpdateOutputs();
// komuta verilecek yanıt, komutun aynısının tekrar edilmesidir:
response_size = 6;
rs485_status = RESPONSE;
}
}
/*
*
*
*/
static void RS485_Handler_0F(void)
{
// bit address = 0, bit count = 32 olmalı.. H byte'lar önemli değil..
if ((Xbuffer[2]) || (Xbuffer[4] != 32))
{
uart_tick = 1;
rs485_status = DISABLED;
}
else if ( Xbuffer[6] != 4 )
{
uart_tick = 1;
rs485_status = DISABLED;
}
else
{
// gelen güncellemeyi dY vektörüne al:
dY[0].b[0] = Xbuffer[7];
dY[0].b[1] = Xbuffer[8];
dY[1].b[0] = Xbuffer[9];
dY[1].b[1] = Xbuffer[10];
// ve output vektör güncellemesini çıkışlara yansıt:
UpdateOutputs();
// yanıt paketi, gelen paketin ilk 6 byte'ı + CRC:
response_size = 6;
rs485_status = RESPONSE;
}
}
/*
* Bu fonksiyon Xbuffer[ ] dizisindeki response_size adet payload'u seri porttan yollar
* Ardına da yollama esnasında hesaplanan 2 byte CRC'yi ekler.
*/
static void RS485_Response(void)
{
unsigned char i;
// not: Response fonksiyonuna girilmeden önce uart kesmeleri kapatılmış olmalı!
RS485_DIR = 1; // transceiver'ı gönderme moduna sok..
RCSTA1bits.CREN = 0; // uart alıcısını kapat..
TXSTA1bits.TXEN = 1; // uart göndermesini devreye al..
myCRC.i = 0xFFFF; // CRC hesaplama değişkenini başlat
// gönderilen her byte için crc hesabı yapılmalı
for (i=0; i<response_size; i++)
{
TXREG1 = Xbuffer[i];
DoCRC(Xbuffer[i]);
while (TXSTA1bits.TRMT == 0) ;
}
// payload gittikten sonra CRC'yi yolla:
TXREG1 = myCRC.b[0];
while ( TXSTA1bits.TRMT == 0 );
TXREG1 = myCRC.b[1];
while ( TXSTA1bits.TRMT == 0 );
TXSTA1bits.TXEN = 0;
uart_tick = 1;
rs485_status = DISABLED;
RS485_DIR = 0;
}
W5500 çipine erişmede kullandığım SPI modülü başka bir cihaza erişmek için de kullanılıyor olabilir. Benim örneğimde, PIC24E’nin SPI2 modülünü kullanıyorum ve bu modülü aynı zamanda EEPROM çipine erişmek için de kullanıyorum. Bu durumda SPI donanımı EEPROM’da veri yedeklemesi/yüklemesi yapan kod ve TCP server işlerini yöneten kodla ortaklaşa kullanılacak demektir.
Ayrıca, modülün paylaşımı ile yalnızca farklı harici aygıtlara erişmemizi anlamamak gerekir. W5500 üzerindeki farklı soketlere erişen, birbirinden bağımsız prosesler de farklı istemciler olarak düşünülmelidir. Çünkü bunlar her ne kadar aynı fiziksel hedefe erişiyor olasalar da ( W5500 ) zamanlama olarak birbirlerinden bağımsız olarak (asenkron) SPI erişimine ihtiyaç duyacaklar.
SPI modülünü belli bir anda hangi prosesin kullandığını belirleyen global bir değişkenim var.
Bir spi erişimi yapacağım zaman bu flag’in 0 olmasını bekliyorum. Eğer öyleyse, spi modülü artık meşgul değil demek. Bir işleme atlamadan önce bu bayrağı o işlemin tanımlayıcı sabitiyle yüklüyorum. SPI thread’i modülü boşa çıkarınca flag’i sıfırlıyor.
Yani spi2_busy istemci kod tarafından set edilen ve spi thread tarafından sıfırlanan bir flag.
W5500 SPI slave aygıtıdır. Master, tüm işlemler boyunca SCLK’ı üretmeli ve her işlemde önce erişilecek bellek bölgesini yazmalıdır. Yani, çipe her erişim işlemi bir adresleme yazması ile başlar. Bu yazma 3 byte’tır. (Ve ne yazık ki byte adeti tek sayı olduğu için PIC24’te 16 bit SPI erişimini kullanamıyorum.)
Wiznet çipin belleğini kısımlara ayrılmış. Hangi kısma erişilecekse onunla ilgili bir kontrol byte’ını belirlemek gerekiyor. Bu byte’taki bir bit, aynı zamanda işlemin yazma mı yoksa okuma mı olduğunu da belirliyor. Daha sonra bu kısım içindeki register adresi alışıldık spi erişimlerindeki gibi master tarafından tanımlanıyor.
ADDRESS PHASE: Seçili kısım içerisinde tanımlı 16 bitlik başlangıç adresi. Gösterim big endian. CONTROL PHASE: Erişilecek bellek kısımının adresi, okuma/yazma işlemi belirlemesi (RW) ve spi frame türü seçimi (OPMODE)yapılan byte.. DATA PHASE: Okunacak ya da yazılacak verinin aktarımı..
EC_SPI_Thread( ) fonksiyonu ana programda, her döngüde koşulsuz çalışan bir thread fonksiyonudur. SPI erişiminde kesme kullanmıyorum. Öte yandan bir veri yazma ya da okuma işleminin bitmesini çalışmayı bloklayarak da beklemiyorum. Bu yüzden en doğrusu, bu işlemleri ana program içinde sürekli çağrılan bir state machine olarak yürütmek. Bu yüzden buna thread diyorum.
W5500 erişimine ihtiyaç duyan bir kod SPI üzerinden iş yapmak için;
Öncelikle spi2_busy flag’in 0 olmasını beklemeli.
spi_handle handler değişkenini yapmak istediği işe göre ayarlamalı
spi2_busy flag’ine kendisini tanımlayan sabiti yazmalı (diğer proseslere kaynak bende demek için)
spi2_status = 1 yaparak thread’i etkinleştirmeli..
Daha önceki çalışmalarımdakinden farklı olarak artık SPI thread’ine komut yollamıyorum. Daha açık bir tabirle, artık okuma ve yazma için ayrı başlatmalar yok. Yukarıda da bahsettiğim gibi, data_phase içinde zaten işlemin okuma mı yazma mı olduğu tanımlanmış durumdadır.
Bu hikayede en önemli oyuncunun spi_handle olduğu açık. Onun tanımı şu şekildedir:
spi_handle.bytecount : okunacak ya da yazılacak byte miktarı (data phase boyu) spi_handle.control_phase : erişilecek blok ve yazma/okuma işlemine göre belirlenen sabit değer spi_handle.offset_address : register adresi ( soketin tx ve rx buffer’ları için sabit adres değil pointer kullanılır ) spi_handle.ptr : okuma işleminde alınan verinin yazılacağı, yazma işleminde de kaynak verinin kopyalanacağı bellek adresi (bizim taraf)
// ÖRNEK:
// MAC nr yazma işlemi:
spi_handle.bytecount = 6; // 6 byte yazma yapılacak
spi_handle.control_phase = COM_CFG_W; // common reg. block'a yazma
spi_handle.offset_address.ui = COM_SHAR; // source hw. address reg.
spi_handle.ptr = &mac_nr[0]; // bizim bir yerlerde mac nr değişkeni var tabi
FIFO Kullanımı:
Bu uygulamanın üzerinde çalıştığı PIC24E işlemcisinde SPI modülünde RX ve TX için ayrı ayrı 8’er elemanlı FIFO var. Her W5500 erişimi kesinlikle 3 byte’lık bir yazma ile başlayacağı için ben de işe bu 3 byte’ı peşpeşe tx fifo’ya yükleyerek başlıyorum ve sonraki aşamalara geçmek için önce bu adımın tamamlanmasını bekliyorum.
Daha sonra gidecek ya da okunacak veri miktarına bakıyorum: Aktarılacak byte sayısının 8’den büyük ya da eşit/küçük olma durumuna göre iki farklı algoritma yürütüyorum: Byte sayısı 8’den küçükse tek bir FIFO yüklemesi ile işlemleri halledebilirim demektir. Eğer byte sayısı 8’den büyükse biraz daha farklı bir yükleme döngüsü kullanmam gerek, bu durumda başka bir döngüye atlarım. Bu ayrımı donanımı yüksek performansla kullanmak için yapıyorum. Sonuçta bir taraftan kod işletimini bloklamamam lazım, diğer taraftan ise çok kısa bir sürede bitecek bir için denetimi ana programa geri verip sonra modülü boşta bekletmemem lazım. Umarım ne demek istediğimi anlamışsınızdır.
Eğer 8 byte’tan küçük bir veri yazma ya da okuma işim varsa denetimi bırakmadan, o state içinde sanki tek bir byte yüklemesi yapar gibi art arda tüm veriyi gönderirim (yazma için konuşuyor gibiyim ama sonuçta okuma için de SCLK üretmek zorunda olduğum için işlerin başı aynı).
8 byte’tan büyük bir veri yazma ya da okuma işim varsa önce fifo’da yer kalmayıncaya kadar yükleme yapıyor ve denetimi bırakıyorum. Sonra her çalışmada boş yer var mı diye bakıyorum ve hedef sayıma ulaştığımda buffer’ın tamamen boşalmasını (ya da dolmasını) bekliyorum.
8 byte’tan büyük erişimlerde FIFO’nun aslında bize bir performans katkısı yapmadığını sadece işleri 8 byte ötelediğini görmüşsünüzdür.
SPI erişim thread’inin tam kaynak kodunu aşağıda vereceğim. Burada çok ilginç bir şey yok. Yalnızca, donanımsal flag’lerden bahsetmek istiyorum. Ki başka bir işlemci kullanıyor olduğunuzda fark eden bunların adı olacak.
Genel olarak SPI modülünü kullanırken iki donanımsal bayrak yükleme / okuma için çok önemli:
Shift register’ın boş olduğunu anlamak : PIC24’te SPI2STAT.SRMPT = 1
RX FIFO’nun boşalması durumu: SPI2STAT.SRXMPT = 1
_SRXMPT
=1 ise RX FIFO boş demektir.
RX okumak için devam koşulu = 0 olması
_SRMPT
=1 ise SPI shift register boşta
Devam eden işlem var mı kontrol etmek için kullanıyorum.
_SPITBF
=1 ise TX FIFO’da yer yok demektir.
İşleme devam koşulu =0 olmasıdır (yer var)
SPI2BUF
TX ve RX fifo son elemana erişim
İşlemler buna yazma ile başlar. RX FIFO buradan okunur.
Umay’a faydalı olduğunu düşündüğümüz için eve epey avokado alıyoruz. Adını insan taşağından alan bu meyvenin içinden çıkan ilginç çekirdek, suda bir müddet bekleyince altından sıçan kuyruğuna benzer bir kök çıkarıyor. Bunu, çekirdeğin üst tarafından çıkan bir sürgün takip ediyor. Ve elinizde iri yeşil yapraklı, şirin bir avokado fidesi oluyor.
Bu iş bizim merakımızı çekti. Meyvesi olur mu ümidi bir yana, yeşil iri yapraklarıyla güzel bir fide. Bir çok salon bitkisinden daha iç açıcı bir görüntüsü var.
Üstelik, çekirdekleri yeşertme prosedürü oldukça kolay:
İşe, 1,5 lt’lik pet şişeleri ortasından keserek başlıyoruz.
Sonra çekirdeğin gövdesine orta-üst tarafından 3 tane kürdan saplıyoruz.
Bunu içine su doldurduğumuz kafası kesilmiş pet şişeye üstten bırakıyoruz. Maksat çekirdeğin tamamının suya gömülmemesidir. Aslında sadece çekirdeğin kıçının suya değmesi yeterli sanırım ama ben su seviyesini daima biraz daha fazla tutuyorum çünkü bu arkadaşlar kaloriferin üstündeler ve su seviyesi çabuk düşüyor.
Çekirdek önce çatlıyor, sonra alttan üstten kök ve filiz çıkmaya başlıyor. Çekirdeğin canlanmasının belli bir süresi olduğunu sanmıyorum. Dormansi dedikleri şey bu mudur, bilemiyorum.
Birkaç yaprak çıkıp yapraklar iyice biçimleninceye kadar uyanmış çekirdeği suda bekletmeye devam ediyoruz. Aşağıda hala suda yaşayan, toprağa kavuşacağı zamanı bekleyen bir arkadaşı görüyorsunuz:
Saksıda bir süre, çiçek gibi büyüttüğüm avokadoları sonra açık havaya alıyorum. Burada açık hava koşullarında yaşamlarını sürdürüyorlar.
İki sene kadar saksıda yaşayan avokadolarımın bir kısmını toprağa ektim: