Kategori arşivi: Referans

EFM8BB3 Sıcaklık Sensörü

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:

ADC0CF2 = 0x70;	 // Vref= internal ref. (1,65V) gnd= GND
ADC0MX = AMUX_TSENS;
ADC0CN0_TEMPE = 1;  // onboard temp. sensor enabled
Delay(96);  // 1ms bekle
ADC0CN0_ADBUSY = 1;

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..

SendMessage() ile Uygulamalar Arası Veri Paylaşımı

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.

copyData: TCopyDataStruct;
// dataexchange ünitesindeki copyData değişkenini dolduruyorum:
dataexchange.copyData.dwData:= $0325;   // packet type identifier
dataexchange.copyData.cbData:= event_exchange.Size;
dataexchange.copyData.lpData:= event_exchange.Memory;

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:

 procedure WMCopyData(var Msg : TWMCopyData) ; message WM_COPYDATA;

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 ile Sıcaklık Probu

LMT01 sıcaklık probu / temperature probe
LMT01 Temperature Probe

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.

TMOD = 0x25;        // TMR1: mode 2, TMR0: 16 bit counter, TMR0 T0 pin counter mode
TH0 = 0;
TL0 = 0;
TCON_TF0 = 0;
TCON_TR0 = 1;   

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.

MODBUS

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:

  1. Paket byte’larını bekleme yapmadan art arda yollamamız gerekir.
  2. 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:

  1. 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.
  2. 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”).
modbus frame sync zamanlama tanımları

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
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.

rx kesme fonksiyonu

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ı
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:

MODISEL Cihazları Y Modbus Bellek Haritası

Görseli tam boyutlu görmek için TIKLAYIN.

MODISEL cihazları X Modbus Haritası

Görseli tam boyutlu görmek için TIKLAYIN

Input Register’ları:

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.

Modisel Cihazları MODBUS Analog Input Register Map

Görselin tam boyutunu görmek için TIKLAYIN

Holding Register’lar:

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.

Modisel cihazları MODBUS Holding Register Map

Tablonun tam açıklamalarını görmek için TIKLAYIN

MODBUS Komutları:

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 Output
modbus fonksiyon kodu 01 Read Discrete Output
modbus fonksiyon kodu 02 Read Discrete Inputs
modbus 02 RDI komutu
modbus fonksiyon kodu 03 read holding registers
modbus fonksiyon kodu 03 read holding registers
modbus fonksiyon kodu 04 Read Input Registers
modbus fonksiyon kodu 04 Read Input Registers
modbus fonksiyon kodu 05 write discrete output
modbus fonksiyon kodu 05 Write Discrete Output
modbus fonksiyon kodu 06 Write Single Holding Register
modbus fonksiyon kodu 06 Write Single Holding Register
modbus fonksiyon kodu 0F Write Multiple Outputs
modbus 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:

static void (* RS485Thread[]) () = 
{   
    RS485_Disabled, 
    RS485_Listening, 
    RS485_Syncd,
    RS485_Receiving, 
    RS485_Parse,
    RS485_Handler_01, 
    RS485_Handler_02,
    RS485_Handler_04,
    RS485_Handler_05,
    RS485_Handler_0F,
    RS485_Response
};

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.

/// RS485 durumları:
typedef enum
{
    DISABLED = 0,       // Transceiver devre dışı
    LISTENING,          // Senkronize olma süresi bekleme
    SYNCD,              // rs485 hattına senkronize olunup paket başı bekliyor olma
    RECEIVING,          // Cihaz adreslendi, veri alınıyor
    PARSE,              // veri paketi tamamlandı, içeriğe bakılıyor
    HANDLER_01,         // READ DISCRETE OUTPUTS komut handler'ı
    HANDLER_02,         // READ DISCRETE INPUTS komut handler'ı
    HANDLER_04,         // READ INPUT REGS komut handler'ı
    HANDLER_05,         // WRITE DISCRETE OUTPUT komut handler'ı
    HANDLER_0F,         // WRITE MULT.DISC. OUTPUT komut handler'ı
    RESPONSE            // yanıt paketini yollama adımı:
} RS485_STATUS;

static RS485_STATUS rs485_status;      // RS485 thread control 
unsigned char uart_tick;        // uart zamanlama sayacı

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:

void interrupt HI_ISR(void) 
 {
     // UART RX kesmesi :
     if (PIR1bits.RC1IF == 1)     
     {   
         Xbuffer[Xptr] = RCREG1;
         ++Xptr;
         uart_tick = 0;
         PIR1bits.RC1IF = 0; 
     }      
 }

İ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 SPI Arayüzü

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.

Adı spi2_busy

extern unsigned int spi2_busy; 
#define SPI_OWNER_SYSTEM    1       // ana spi işlemi (setup, debug vb.)
#define SPI_OWNER_W5500     2       // spi kaynağını server ana task'leri kullanıyor (boot vs.)
#define SPI_OWNER_W5500_SRW 3       // spi kaynağını soket okuma / yazma task'leri kullanıyor
#define SPI_OWNER_W5500_IR  4       // interrupt flag'lerinin okunması task'i
#define SPI_OWNER_W5500_CI  5       // spi kaynağını soketin client info'sunu okuyan task kullanıyor
#define SPI_OWNER_EEPROM_1  6       // eeprom 1 erişimi
#define SPI_OWNER_EEPROM_2  7       // eeprom 2 erişimi

busy flag’i şu şekilde kullanıyorum:

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.

W5500 spi framing

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ı..

// COMMON REGISTER BLOCK için control phase:
	#define     COM_CFG_W       0x04        
	#define     COM_CFG_R       0x00	
// SOCKET REGISTER BLOCK için control phase:	
	const unsigned char SOCK_BLOCKSEL_REG_WR[8] = 
	{ 0x0C, 0x2C, 0x4C, 0x6C, 0x8C, 0xAC, 0xCC, 0xEC };
	const unsigned char SOCK_BLOCKSEL_REG_RD[8] = 
	{ 0x08, 0x28, 0x48, 0x68, 0x88, 0xA8, 0xC8, 0xE8 };	
// Soketlerin TX ve RX veri buffer'ları için control phase:	
	const unsigned char SOCK_BLOCKSEL_TXB_WR[8] = 
	{ 0x14, 0x34, 0x54, 0x74, 0x94, 0xB4, 0xD4, 0xF4 };
	const unsigned char SOCK_BLOCKSEL_TXB_RD[8] = 
	{ 0x10, 0x30, 0x50, 0x70, 0x90, 0xB0, 0xD0, 0xF0 };
	const unsigned char SOCK_BLOCKSEL_RXB_WR[8] = 
	{ 0x1C, 0x3C, 0x5C, 0x7C, 0x9C, 0xBC, 0xDC, 0xFC };
	const unsigned char SOCK_BLOCKSEL_RXB_RD[8] = 
	{ 0x18, 0x38, 0x58, 0x78, 0x98, 0xB8, 0xD8, 0xF8 };

EC_SPI_Thread( )

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:

static struct 
{
    WORD            offset_address; 
    unsigned char   control_phase; 
    unsigned char   bytecount; 
    unsigned char   *ptr;
} spi_handle ;

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ştaDevam 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)
SPI2BUFTX ve RX fifo son elemana erişim İşlemler buna yazma ile başlar. RX FIFO buradan okunur.

EC_SPI_Thread( ) kaynak kodu

Avokado Yetiştirmek

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: