Kategori arşivi: Embedded

EFM8 ile I2C Master

Çalışma Frekansı Ayarı

Master mode’da, SCL ‘yi master ürettiği için çalışma frekansını biz belirliyoruz. EFM8SB1’de I2C modülü iki farklı zamanlama kaynağına ihtiyaç duyar: Biri zaman aşımı denetimi için diğeri clock frekansını belirlemek için (baud-rate generation gibi düşünün).
Bizim için ikincisi önemli. Bunun için, diğer modüllerde olduğundan farklı olarak birden çok seçeneğimiz var. TMR0 pek çok uygulamada genel amaçlı sistem saati ya da harici sayaç, TMR1 uart baud rate generator veya harici sayaç olarak kullanılabildiği için ben TMR2 kullanıyorum. Ayrıca, I2C çalışmasında gerekli taşma hızları çok yüksek olduğu için, 16 bitlik bir sayaca da ihtiyacımız yok. Değerli kaynakları boşa harcamamak için split mode çalışabilen bir timer kullanmak daha mantıklı.
Seçtiğimiz saat kaynağının kesmesini başka işlerde kullanmaya devam edebiliriz. I2C modülü arka planda taşma bayrağına bakarak I2C işlemlerini zamanlayabilir.
Kullanmak istediğimiz SCL frekansının 3 katı hızıyla tick üreten bir sayaç ayarlamamız gerek:

Split mode yaptığın bir sayacın alt yarısı ile üst yarısını kullanmak arasında küçük bir fark var: Sayacın açma/kapama kontrolü yalnızca üst yarıya kumanda eder. Alt yarı sürekli çalışır. Eğer üst yarıyı başka bir iş için kullanacaksak bu I2C için daha uygun bir paylaşım olur.

Program Tasarımı

Aşağıdaki zaman diyagramına bakın. Bu, bir sensörden veri okumak için yapılması gerekenleri gösteriyor:

I2C, ne yazık ki kendi başına iş görme yeteneği düşük bir haberleşme modülüdür. Yukarıdaki gibi, belli bir register’dan 2 byte veri okumasından ibaret bir işin SPI ile ne kadar kolay olacağını ve durumlar tanımlamak gerekmeden tek satırda bitebileceğini düşünün.
I2C’de işe yarar bir konuşma (transaction) yapmak için birden çok durum çalıştırmak gerekir. EFM8SB1’de bunları donanıma yaptırmak için kullandığımız kumanda bitleri tek bir register’da yer alıyor.

// I2C kumanda bitleri:
sbit SMB_SI = SMB0CN0^0;
sbit SMB_ACK = SMB0CN0^1;
sbit SMB_ARBLOST = SMB0CN0^2;
sbit SMB_STO = SMB0CN0^4;
sbit SMB_STA = SMB0CN0^5;

I2C konuşması fazla işlemci meşguliyeti yaratan bir iş olduğu için bu tür çevresel birimlere sürücü yazarken yaklaşımımız ana uygulamanın çok-görevlilik durumuna göre olmalı. Eğer I2C erişimi önceden bilinen zamanlarda yapılmayacaksa ve başka işlerle çakışması olasılığı varsa çok-görevli bir kodlama daha uygun olur. Çok görevlilik, I2C işlerini tüm durumları kapsayan bir kesme fonksiyonu içinde çalıştırmak şeklinde olabilir. Veya ayrı bir thread içinde bu işlerin kotarıldığı task’ler şeklinde bir durum makinesi yazılabilir. Birincisi için Silabs’ın örnek i2c kodlarına bakabilirsiniz. İkincisi benim genellikle tercih ettiğim yöntemdir. Daha büyük işlemcilerde genellikle i2c thread’i içinde erişimleri yazarım.

Benim bu yazıda paylaşacağım yaklaşım yukarıdakilerden biraz daha farklı. Programın asıl işinin zaten bu i2c modülüne erişim olduğu durumlarda bu yöntem daha efektif olacaktır. Burada esas olan, işlemcinin i2c erişimi yaparken başka işinin olmayacak olması. Ek olarak, i2c okuması uygulamanın zamanlamasını kesin olarak bildiği anlarda başlar ve biter. Örneğin, zamanının çoğunu uyku modunda geçiren ve uyanıp, i2c üzerinden bir sensörle haberleşen bir uygulama için bu yöntem biçilmiş kaftandır.

Genel Amaçlı Fonksiyonlar

Aşağıda paylaştığım i2c fonksiyonlarını doğru sıra ile çağırarak, herhangi bir slave aygıt için ihtiyaç duyulan sürücü fonksiyonları gerçekleştirilebilir. Bu fonksiyonlar blocking olarak çalışırlar. Yani, düz bir sıra ile verilen i2c okuma/yazma işlemini yaparlar.

// START durumu üretir ve ardından verilen slave adresini yazdırır (read ya da write)
bool I2C_Start(unsigned char slave_address)
{
  SMB0CN0 = 0x20;   // START durumu başlat!
  while (SMB_SI == 0)  ;

  if (SMB_ARBLOST) return(false);
  SMB0DAT = slave_address;
  SMB0CN0 = 0;
  return(true);
  // fonksiyon, işlemin bitmesini beklemeden hemen döner..
}

// ilk erişimde, yazma adreslemesi sonrasında erişilecek
// çip adresini (/yollanacak komutu) gönderir:
bool I2C_Register_Set(unsigned char reg_addr)
{
   while (SMB_SI == 0) ;  // yazma adreslemesinin bitmesini bekle

   if (SMB_ACK)   // yazma adreslemesine ACK aldıksa..
   {
     SMB0DAT = reg_addr;
     SMB_SI = 0;
     return(true);
   }
   else
   {
     SMB0CN0 = 0x10;   // STOP durumu üret
     return(false);
   }

}


// Start'tan farkı, devam etmek için önceki işlemin sonuçlanmasını bekler
// ve slave adres yüklemesi sonrası da slave'den ACK bekler
bool I2C_ReStart(unsigned char slave_address)
{
  while (SMB_SI == 0) ;  // önceki yazma işleminin bitmesini bekle

  if (SMB_ACK == 0)
  {
    SMB0CN0 = 0x10;  // STOP durumu üret
    return(false);
  }

  // önceki yazmaya ACK almışsak devam edelim:
  SMB_STA = 1;
  SMB_SI = 0;     // Repeated-Start üret
  while (SMB_SI == 0) ;

  if (SMB_ARBLOST)
  {
     SMB0CN0 = 0x10;   // STOP durumu üret
     return(false);
  }

  // repeated start sonrası, verilen slave adresini yolla:
  SMB0DAT = slave_address;
  SMB0CN0 = 0;
  while (SMB_SI == 0) ;

  if (SMB_ACK)  // re-start adreslemesine ACK aldık mı?
  {
      // slave'e adres yazması sonrası modülü hold durumda bırakıp dön:
     return(true);
  }
  else
  {
    SMB0CN0 = 0x10;
    return(false);
  }
}

// bu fonksiyon i2c denetimini bir önceki adımdan hold durumda devralmıştır
// (ilk okuma için read address yazması sonrası,
// sonraki okumalar için master ACK üretimi sonrası)
// ack = 1 olursa daha veri okunacak demektir.
// slave'e ACK üretilir ve i2c makinesi hold durumda iken fonksiyon döner.
// ack = 0 olursa stop durumu üretilir ve durum makinesi de boşa çıkar.
unsigned char I2C_Byte_Read(bool ack)
{
  unsigned char x;

  SMB_SI = 0;   // state machine'i sal..
  while( SMB_SI == 0)

  if (ack) SMB_ACK = 1; else SMB_ACK = 0;

  x = SMB0DAT;  // gelen veriyi al.

  if (!ack)
  {
    SMB0CN0 = 0x10;
    while (SMB_SI == 0) ;
    SMB_STO = 0;
  }

  return(x);
}


// kendisinden önceki yazma (pointer set / data yazma) devam ederken çağrılır
// geri döndüğünde de yükleme yapılan yazma sürüyordur.
bool I2C_Byte_Write(unsigned char b)
{
  while (SMB_SI == 0) ;  // önceki yazma işleminin bitmesini bekle

  if (SMB_ACK == 0)
  {
    SMB0CN0 = 0x10;  // STOP durumu üret
    return(false);
  }

  SMB0DAT = x;
  SMB_SI = 0;
  return(true);
}

void I2C_Stop(void)
{

  while (SMB_SI == 0) ;  // önceki işlemin bitmesini bekle
  SMB0CN0 = 0x10;
  SMB_SI = 0;
  while(SMB_SI == 0) ;
  SMB0CN0 = 0;
}

DPS310

Infineon firmasının duyarlılığı yüksek ve düşük güç tüketimli basınç sensörü DPS310’u birkaç sene önce bir reklamda görmüştüm. Orada bu malzemenin giyilebilir teknolojiler için ideal olduğu vurgulanıyordu.
Ardından bir süre önce, bugünlerin popüler konusu iç ortamda konum bulma (indoor navigation) ile ilgili bir proje için çalışırken aklıma bu malzeme geldi ve bunun demo board‘undan sipariş verdim.
Board çiklet kutusu gibi bir kutuda geldi. İçinden de Infineon’un standart hale getirdiği bir form faktöründe üretilmiş (ki sonra bunun host tarafı için bir board da sipariş ettim), prototip çalışması yapması kolay bir kart çıktı. Aslında bu board’un herhangi bir özelliği yok. Tek işlevi montajı kolay olmayabilecek olan sensörü üzerinde taşımak.

İvme ölçerlerde gördüğümüz hem SPI hem I2C ile kullanılabilen senkron seri port bunda da var. Ben mümkün oldukça SPI seçen biri olduğum için bu sensörde de SPI bağlantısını kullanıyorum.
Bu sensörü çalıştırmak için elimdeki PIC32MM USB Curiosity dev. board’u kullandım.
Geliştirme kartının üstündeki USB ile PC bağlantısı kurup I2C üzerinden de bir LCD ekran bağlayıp sensör ölçümlerini izlemeyi planladım.

Söylemeye gerek yok, hassas basınç ölçümü düşey konumun belirlenmesi için kullanılabilir (benim bu sensörü ilgi alanıma almamın ilk nedeni aslında böyle bir kullanım değildi, bundan ileride söz edeceğim).
Bu sensörlerde aradığım meziyet mutlak doğruluktan çok (atmosfer basıncının o anki tam değerini bize söyleyecek bir barometre istemiyoruz) ölçüm çözünürlüğünün yüksek olması ve bağıl duyarlılık. Bu sayede 2-3 santimetrelik yükseklik değişimlerini yanılgısız ayırt edebiliyoruz.

Sensörün düşük güç tüketimli olması ve çalışma modu için farklı seçeneklerimiz olması da elbette pille çalışacak bir cihazda olmazsa olmazlarımızdan ikisi.

Bu sensörde beğenmediğim tek şey düzeltme katsayılarını kullanarak ölçüm değerini kullanıcının hesaplamak zorunda olması. (Düzeltme işine dökümantasyonlarda kaibrasyon demişler, düzeltme/kompanzasyon sanki daha uygun bir tabir). 8 bitlik bir işlemci ile bunu arabirimlemeyi düşünüyorsanız makineyi epey yoracaksınız demektir. Diğer taraftan deneme yanılmalarla, düzeltme uygulamadan da anlamlı sayısal sonuçlar elde edebilirsiniz (kompanzasyon işi sıcaklığa göre de yapıldığı için bunu tavsiye etmem).

Arayüz

Bu çipe SPI üzerinden erişiyorum. Çipte 3 telli SPI modu da var, bu modu etkinleştirdiğimizde SDO çıkışı INT çıkışına dönüşüyor. Ben bunu kullanmıyorum. 4-wire SPI çalışması için mode ’10’ olarak ayarladım. Yani mikrodenetleyicinin SPI ayarlarında;
CKP = 1  // clock polarity = 1 (Boşta iken 1)
CKE = 0  // MOSI 1->0 geçişinde güncellenir.
SMP =1 // MISO 0->1 geçişinde güncellenir.
Prototip kurulumumda sensor board’unu yaklaşık 15cm’lik jumper kablolarıyla geliştirme kartına bağlamıştım. Bu şekilde 1MHz SPI hızında sorunsuz haberleşme yapabiliyorum. Bu iş için yapılmış bir board’da bu hız çok daha fazla olabilir. Datasheet 10MHz SPI hızında çalışılabilir diyor.

SCLK’ın boşta durumunun 1 olması I2C ile uyumlu olmanın bir zorunluluğu. Çipin açılıştan hemen sonraki varsayılan modu I2C. Ancak CS pinini bir kere 0’a çekip SPI ‘ı etkinleştirince, arayüz sonraki yeniden başlatmaya dek SPI modunda kalır. Ayrıca, 3/4 wire seçimi için de config register’ında bir ayar biti var. Oraya hiçbir şey yazmazsanız 4wire kullanmış oluyorsunuz.

Her konuşmanın ilk byte’ı adrestir. Her adresin ilk biti de (MSB) bu erişimin yazma mı yoksa okuma mı olduğunu belirtir. Yazma yalnızca belirtilen adrese 1 byte olarak yapılabilirken okuma ardışık olarak çok byte devam ettirilebilir. Çipten okuma yapmak bize şu durumlarda lazım:
** Katsayılar 18 byte;
** Ölçüm sonuçları 3’er byte;
** Sensör durumunu öğrenmek için MEAS_CFG’i okumak (1 byte)

Sensörün Başlatılması

Çipin, enerjilenmesi sonrası 40ms kadar bir başlatma süresi var. Başlatma sonrasında alet otomatik olarak ölçüme başlamıyor. Zaten ilk işimiz (eğer daha önceden işlemci üstünde saklamadıysanız) sensörün düzeltme katsayılarını okumak olmalı.

Ben, başlatma kodunda öncelikle MEAS_CFG register’ını okuyup COEF_RDY ve SENSOR_RDY flag’lerinin set edilmiş olup olmadıklarına bakıyorum. Eğer bu bitlerin her ikisi de 1 değilse Init fonksiyonumuz işleme devam etmeyip FALSE dönmeli. Çünkü ana uygulama henüz ölçüm yapamayacağını bilmeli.
Eğer çip çalışmaya hazırsa, ilk işimiz PROD_ID register’ını okumak. Bu register’da DPS310’un ürün kimliği ve revizyon numarası yazılı. Beklenen değer 0x10 olmalıdır.

Her şey normal gözüküyorsa ikinci işimiz kalibrasyon katsayıları denen diziyi okumak olacak. Burada da can sıkıcı bir durum var. Parametreleri burst mode olarak 18 byte’lık bir dizi içine okuyorum. Ancak buradaki bazı katsayılar 24 bit, bazıları ise 12 bitlik işaretli (2’nin tümleyeni) tamsayılardır. Bunları açabileceğimiz uygun değişken boyları 32 bit ve 16 bitlik işaretli sayılar olacağı için bir dönüştürme işlemi yapmamız gerekiyor. Buna örnek datasheet’te verilmiş. Kısaca anlatmak gerekirse, açılacak katsayı kaç byte’lıksa en yüksek anlamlı bitine bakıyorsun, o bit eğer 1 ise sayı – işaretli olduğu için senin değişkenini sayının kendisinden o sayının bit genişliğinin alabileceği değerden çıkarıyorsun. Yani, 12 bitlik 2’s complement bir sayıdan 16 bitlik negatif bir sayı elde etmek için;
signed short c = (okunan sayı) – 4096;
Hedef değişken 15 bit (1 bit de işaret) olduğu için rollover olmadan sayının negatifini buluyoruz. c0 parametresinin üretilmesini aşağıda görebilirsiniz:

    // c0:
    w = (unsigned short) c[0] << 4;
    x = c[1] >> 4;
    dps310_info.c0 = w | x;
    // 2's comp. signed dönüşümü yap:   
    if (w & 0x0800)  dps310_info.c0 = dps310_info.c0  - 4096;

Datasheet Sayfa 37’de buradaki değişkenlerin, 18 byte’lık bir stream içindeki ardışık konumları verilmiş. Ne yazık ki padding olmadığı için dizinin bazı elemanları iki farklı parametrenin nibble’larını taşıyor olacaklar. Burada anlatmanın artık geyiğe gireceği bit kaydırma işlemleri ile zaman harcamak zorundasınız.

Sensörün Ölçmesi

Tıpkı nem ölçümünde olduğu gibi, yalnızca basınç ölçmek diye bir şey yok. Kompanzasyon için, sıcaklığı da hassas biçimde, tam bu çipin üstünde ölçmeniz gerekiyor. O yüzden, ölçüm çevrimlerimizin bir kısmında sıcaklığı da ölçmemiz gerekli (sıcaklığın daha yavaş değiştiğinden eminsek her çevrimde ölçmemiz gerekmez ama bu konuda benim sözüme değil kendi uygulamanızın durumuna bakın).

Bu sensör, süreki ya da talep edildiğinde ölçüm yapacak şekilde çalıştırılabilir. Üzerinde bir FIFO var ve son 32 ölçüm sonucu saklanabiliyor (sıcaklık ve basınç için ortak kullanılan bir buffer). Ben deneme uygulamamda bu özelliği tercih etmedim.
Diğer ölçüm modunda biz sensöre basınç ya da sıcaklık ölçümü yapmasını söylüyoruz, çip ölçümü yapıyor ve duruyor. Sonuç da PRS_Bn ya da TMP_Bn register’larında güncelleniyor.
INT pinini kullanmadığımız için işlemin tamamlandığını MEAS_CFG register’ındaki TMP_RDY ve PRS_RDY flag’lerini okuyarak anlıyoruz. İlgili flag 1 oldu ise TMP_B2 ya da PRS_B2 register’larından itibaren 3’er byte okuyup sonucu alıyoruz (okuma işlemi flag’leri otomatik olarak sıfırlar).
Okuma ile aldığımız değerler 24 bitlik işaretli sayılardır üstelik bunlar big endian’dılar. Ham okumaları, düzeltme işlemi için yapacağımız aritmetik işlemlerde kullanabilmek için 32 bitlik işaretli tamsayılara çevirmemiz gerekir. Bu işlem de yukarıda, kalibrasyon katsayılarını açarken kullandığımız yöntemle dönüşüm gerektirir.

Okumanın ne genlikte sonuç döndüreceği seçtiğimiz oversample ayarına bağlı. Örneğin ben, aşağıdaki ayarları tercih ettim:

Oversampling x ölçüm periyodu değeri 1’i geçmemeli. 32 oversampling seçmekle 0,3 Pascal’lık bir ölçüm duyarlılığına erişiyoruz (3cm). Ancak bu, ölçüm hızımızı 16 Hz ile sınırlıyor. Buna ek olarak bir de bit shift ayarı hikayesi var. x32 oversampling, 24 bitlik sonuç register’ını taşırdığı için sensöre sonucu sağa kaydırıp toplama yapmasını söylememize yarayan bir bit var. O da, CFG_REG register’ının P_SHIFT ve T_SHIFT bitlerini set ederek oluyor.

Peki ölçüm aralığımız oversampling’e göre değişebiliyorsa ama katsayılarımız hep aynıysa sonuçları nasıl normalize edeceğiz? İşte burada Infineon bizi floating point aritmetiğine mecbur bırakan hareketini yapıyor. Kalibrasyon katsayıları, ölçüm sonucunun normalize edilmiş değerine göre verilmiş. Bu normalizasyon için okuma sonucunu kullandığınız oversample değerine göre bir sayıya bölmeniz gerek. Bu da datasheet’in 15. sayfasında verilmiş:

Bu bölme işleminden sonra, ölçüm değerlerini c katsayılarıyla çarpıp toplayıp fiziksel büyüklükleri elde edeceksiniz.

Almanların Dünya Savaşı’nı neden kaybettiklerine bakıyor gibiyiz, değil mi!

alpha-S

decolit alpha-s rgb led lamba modüllerinin çıplak kart halleri

Üç renkli LED elemanlar kullanarak rengarenk aydınlatmalar yapmak son zamanlarda bana da çekici gelmeye başlamıştı. Aslında bunun için hazır pek çok seçenek var ancak ben küçük bir board yapıp, üzerine kendi seçtiğim RGB LED’leri yerleştirip kendi tanımladığım kumanda şemasını yürütebilecek bir şeyler yapmak istedim. Sonrasında bu küçük kartları istediğim yerlere yerleştirip, onları değişken renklerle yakmak niyetindeyim. Noktasal olarak büyük bir ışık yoğunluğunu hedeflemiyor olsam da bir board üstüne 5 tane PLCC kılıf LED koymaktan kaçınmadım.

Kartın boyutları 58mm x 13.5mm oldu.


Deneme amacıyla ürettiğim board’ların üzerine Cree marka CLP6C-FKB model LED’ler taktım. Sonuç gayet güzel.

LED’leri doğrudan kartların besleme hattı üzerinden besliyorum ve her renk için bir pull-down transistör ile sürüyorum. Şemada 100R olarak gözüken seri dirençler besleme gerilimine göre seçilmesi gereken denge dirençleri.

Bu projeyi bir hafta sonu çalışması olarak uğraşmaya değer bulmama neden olan şey bu kartları seri haberleşme ile kumanda edilebilir ve kaskat bağlanabilir yapmam. Bu şekilde iş basit bir renk ayarından çok daha fazlası olabilir.

Aslında arka arkaya bağlanabilir renkli LED deyince akla gelen Neopixel diye bir LED çipi de var. Bunun Çin malı kopyaları ucuza bulunabiliyor. Ama bunlar adreslenebilir modüller değiller. Ben uzun clock katarları üretmek zorunda kalmak yerine doğrudan UART kullanan bir şey yapmak istedim. Bu şekilde tek bir komutla tüm lambalara aynı anda aynı şeyi yaptırabilmek mümkün. Veya istediğiniz belli bir adrese doğrudan komut verebilirsiniz. Eğer hızlı animasyonlar yapmak söz konusu ise bu daisy-chanining’e göre çok büyük bir fark yaratabilir.

Her modülün sol tarafında uart’ının RX ‘i sağ tarafında ise TX çıkışı bulunuyor.

Modülleri arka arkaya (kaskat) bağladığımızda soldan sağa birbirlerini sürmüş olacaklar.Bir modül sol tarafından bir paket aldığında bu paket onu adreslemiyorsa veya bir genel çağrı adresi taşıyorsa o komutu sağ taraftaki TX pininden aktaracaktır. 0-3V gerilim düzeyinde çalışıyor olsak bile bu şekilde alavere yaparak uzun bir lamba zincirini bir uçtan kumanda edebilmeyi umuyorum. Bu arada, 24mA kaynak akımı verebilen bir logic buffer kullanmakla sonraki modül ile aramızdaki kablonun kapasitesinden kaynaklanacak yavaşlamaları azaltmış olacağız. Şemadaki paralel kondansatöre takılmayın. Orada bir komponent yeri olsun istedim sadece.

Benim “adreslenebilir” bir lamba modülünden beklediğim kumanda işlevleri her kanal için ayrı ayrı olmak üzere;
* Açma ve kapama denetimi,
* Çıkış gücünü ayarlama,
* Rampalı açma/kapama,
* Çıkış gücünü rampalı değiştirme,
* Rampa sürelerini değiştirebilme
* Çıkış gücünü çıkışa yansıtmaksızın değiştirebilme (komut verildiğinde uygulanmak üzere)
* Tüm modüllere eşzamanlı komut verebilme

Bu fonksiyonlar basit seri komutlarla kumanda edilebildiğinde ana kontrolcünün karmaşık efektleri yürütmesi kolayca mümkündür.

Firmware Yapısı

Ana program, dört ayrı thread’den oluşur:
+ Sürekli olarak çalışan ve her aşaması belirli bir zaman aşımı denetimi ile korunan bir seri haberleşme thread’i..
+ Zaman çoğullamalı olarak çalışan ve her biri bir renk sürücüsüne çıkış veren üç özdeş paralel renk kontrol thread’i.

Her renk kontrol thread’i 4 farklı duruma dair task’ler çalıştırır. Her task’in o renk için bir “durum” olduğunu düşünün. Bunlar;
OFF
ON
RAMPUP
RAMPDOWN
task’leridir. OFF ve ON task’leri kalıcı durumlardır. Bir komut alınmadığı sürece denetim bu task’i çalıştırmayı sürdürür. RAMP task’leri ise belli bir süre boyunca çalışan geçiş durumlarıdır. Set değeri değiştirildiğinde (kademeli geçiş komutu verilmişse) veya kademeli açma kapama komutları verildiğinde yeni set değerine ulaşılıncaya dek bu adımlarda kalınır.

Çıkış kontrol task’lerinin zaman bölümlemesi 10,7ms ‘dir. Yani her 10,7ms’de bir sıradaki renk kanalının geçerli durumuna dair task çalışır. Ve bu çevrim art arda sürekli devam eder. İki rezerve zaman slotunun da eklenmesiyle, bir tam çevrim 5×10,7 = 53,5ms ‘de tamamlanır. Bu demektir ki, çıkış güncellemeleri 53,5ms çözünürlükle yapılmaktadır. Durağan adımlarda bu önemli olmamakla beraber kademeli geçiş adımlarında bu süre değişim hızını tanımlar.

PWM

İşlemcinin üzerindeki 3 PWM kanalını da kullanıyoruz. PWM çözünürlüğü 8 bittir. PWM frekansı da 7,98 kHz’dir.
Bu projenin donanımını tasarlarken EFM8BB1 işlemcisini kullanmıştım. Firmware’e de bu işlemci üzerinde çalışarak başladım. Sonra iş deneme aşamasına gelince prototip montajları için bana yardımcı olan arkadaşımın elimdeki board’lara BB1 yerine yanlışlıkla SB1 takmış olduğunu fark ettim.
Sleepy-Bee ile Busy-Bee “hemen hemen” pin uyumludur. Ancak firmware’de oldukça yaşamsal farklar vardır. Ne yazık ki buna PCA modülü de dahil, ki bu board’da PWM’i bununla üretiyorum. O yüzden, elde olan asıl olandır mantığıyla benim firmware’i SB1 üzerinde çalışacak şekilde değiştirdim. Ancak asıl üretimim BB1 ile olacak. Çünkü bunda daha çok özellik var.
Bu işlemcilerde PWM çıkışı aktif duty cycle’da 0, pasif durumda da 1 oluyor. Ben BB1’de PCA0POL register’ı ile bu evirme işlemini donanımsal olarak halledebiliyordum. Ancak SB1’de POL register’ı yok. Bu durumda çıkış gücü 0 yazarsak en büyük 255 yazarsak en küçük olmuş olur. Ayrıca, kanalı kapatmak istediğimizde önce çıkışa 255 yazmalı sonra da _ECOM bitini sıfırlamalıyız.
8 bit PWM’de set değerini PCA0CPHx register’ına yazmak gerek. Daha yüksek çözünürlüklü PWM ayarlarında kullanılabilen auto-reload register’larının kullanım seçimi bu modda kullanılmıyor.
Çalışma sırasında PCA modülü ve sayacı sürekli devrede. Bir kanalın kapalı olması gerektiğinde
PCA0CPMx = 0x02; yazarak o çıkışın toggle olmasını engelliyorum. (PCA0CPHx = 255)
PCA0CPMx = 0x42; yazdığımda da PWM çalışmaya devam ediyor.

BB1 kullanan versiyonda işler biraz daha değişik. Burada o kanalın POL bitini = 0 yapıp _ECOM=0 yapmam çıkışı kapatmaya yetiyor. Duty cycle’ı değiştirmeme gerek yok.
Ayrıca, BB1 ‘de PWM’i center-aligned kullanıyorum.

Kontrolcü, komutlar tamamlanmadan yeni komutu işleme almaz. Ancak rampa komutları icra edilirken seri haberleşmeden yeni bir komut alınırsa komut kuyruğa eklenir ve geçerli komut tamamlanınca işleme alınır. Ani açma / kapama /set değeri değiştirme komutlarında bunun olma olasılığı yoktur. Rampa işlemlerinde komutun tamamlanması rampa ayarına bağlı olarak uzun sürebileceği için bu olasılık vardır.

Üç renk kanalı birbirinden tamamen bağımsız olarak çalışabilir ve kumanda edilebilirler. Ancak tamamen eşzamanlı olarak da kumanda edilebilirler.

Bir zincirde 128 adet modül arka arkaya bağlı olabilir. Her modülün adresi sabittir. Bir ağda aynı adresli iki modül olması çalışmayı bozmaz, sadece bu modüller ayrı ayrı ayarlanamamış olurlar. 0x81 adresi tüm modüllerin ortak çağrı adresidir.

Seri portun haberleşme hızı 38400bps’dir. Yazdığım ilk versiyonda modül aldığı paketin adresi kendi adresi değil ise paketi tamamlanmasını beklemeden iletmeye başlıyordu. Ancak sonra, kesme kodunu kısa tutma takıntım yüzünden bu özellikten vazgeçtim. Bu durumda, her modül için paket aktarım gecikmesi yaklaşık 1,6ms’dir.

Haberleşme Yapısı

Paket boyu tüm komutlar için sabittir ve 6 byte’tır. Her paket sync byte’ı (0xAA) ve hedef node adresi ile başlar (0x01..0x81). 0x81 genel çağrı adresidir.
Ardından gelen cmd byte’ı paketi tanımlar. Geçerli komutlar ve kullandıkları parametrelerin anlamı şöyledir:

node: 1..128 arası, hedef adresi. 129 tüm modülleri adresler.
cmd_X : kanal komutları. anlamları şunlar:
0: Bu kanala ait komut yok.
1: forced-on : Kanal kapalıysa hemen açılmasını sağlar. Eğer açıksa out_X’te o an yazılı olan set değerini çıkışa yansıtır.
2: turn_off : Kanalı hemen kapatır.
3: ramp-up: Çıkış gücünü kademeli olarak out_X ‘te yazılmış değere yükseltir. Kanal kapalı idiyse açılır ve set değerine kademeli yükselir.
4: ramp-down: Çıkış gücünü kademeli olarak out_X’te yazılmış değere azaltır.
5: rampdown-off: Çıkış gücünü sıfır değerine kadar kademeli azaltır ve sonra kanalı kapatır.
NOT: Ramp-up ve ramp-down komutları, o anki çıkış oranı, out_X set değerinden sırasıyla yüksekse ve düşükse tek seferde yeni set değerine gelir.

Bu projede önemli bileşenin lambaların kontrolcüsü olduğunu düşünüyorum. Firmware’i tamamlayıp denemelere başladığımda işin bu yönünün geliştirilmeye oldukça açık olduğunu gördüm. Yukarıdaki komutların çalışmasını hızlı biçimde deneyebilmek için basit bir PC programı hazırladım. Aşağıda onun görüntüsünü görüyorsunuz. Üzerine tıklayarak programı indirebilir ve deneyebilirsiniz:

Vertical Counter’lar Kullanarak Digital Giriş Filtreleme

Klasik Yönteme Bir Örnek

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:

void (* ButtonRead_Task[] ) () = 
{
    Button_Idle,
    Button_Read_A,
    Button_Read_AM,
    Button_Read_B,
    Button_Read_BM
};

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.

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.

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

UDP Load Data

Bu soket görevi, belirtilen konumdan itibaren, belirtilen miktarda veriyi W5500 üstündeki ilgili soketin (socket-0) TX buffer’ına yazar.


 

W5500’deki bir soketin TX buffer’ına yazma işlemleri şöyle olmalı:

  1. Buffer’da yeterince yer olup olmadığını Sn_TX_FSR okuması yapıp öğrenmek.
  2. Sn_TX_RD okuması yapıp, yazma işlemine başlayacağımız konumu öğrenmek.
  3. Veriyi buradan itibaren TX buffer’a aktarmak.
  4. Sn_TX_WR içeriğini yazdığımız byte sayısı kadar artırmak.

W5500_TX_bufferSEND komutu verildiğinde W5500, Sn_TX_RD‘den Sn_TX_WR‘ye doğru TX buffer’ındaki verileri sırayla gönderir. İşlem tamamlandığında Sn_TX_RD = Sn_TX_WR olacaktır. O yüzden, yeni bir veri yüklemesi yaptıktan sonra, Sn_TX_WR içeriğini Sn_TX_RD’den gidecek veri miktarı kadar öteye alıyorum.

Bu işaretçilerin tümü 16 bitlik işaretsiz sayılardır. W5500 0xFFFF -> 0x0000 başa sarmasını önemsemiyor. Bu durumda biz de önemsemiyoruz. Paketimiz yüklenirken bu başa sarma olsa bile bu gönderme işlemini etkilemeyecek. Ki bu başa sarma da zaten eninde sonunda olacak..

Ben ayrıca, 1. adımda söz ettiğim Free Size Register’ını da okumuyorum. Çünkü benim uygulamalarda hem yollanan veri boyları küçük, hem de uygulamalarım veri yollama işlemini senkronize olarak kullanıyorlar.

Burada not etmek istediğim son şey, veri YÜKLEME işlemi ile GÖNDER komutunun farklı şeyler olduğuna dikkat etmeniz. Birden çok kez veri yüklemesi yapıp, hepsini tek seferde yollamak istemeniz çok muhtemel.. İşlemci üstünde integral bir çıkış paketi oluşturma zorunluluğunu ortadan kaldırmak için böyle yaptım. Yani, gidecek verinin tam gideceği paket yapısı ile bir yerde üretilmiş olmasına gerek yok. İstediğiniz datayı bir yerden, sonrakini bir başka yerden W5500’e yükleyip, asıl paketi TX Buffer üzerinde oluşturabilirsiniz. Toplamda 16kB TX buffer’ımız var ne de olsa!

W5500 Interrupt Logic

W5500’den durum sinyali almanın bir yolu çipin /EC_INT pinini okumaktır. Benim uygulama periyodik SPI okuması yerine bu pinin durumunu izleyerek soketlere dair durum güncellemelerinden haberdar olur.
Her kesme kaynağının bir de mask biti var. Mask biti 1 yapılmadıkça o kaynak bir sonraki kademeye kesme iletmez.
W5500’den PC’ye bağlantı veren /EC_INT girişinin etkin olmasını sağlayan iki kaynak var:
* Çip kesmeleri : IR register’ındaki bir bitin aktif olması ile oluşur.
Benim uygulamada çip kesmelerini kullanmıyorum.
* Soket kesmeleri: SIR register’ındaki bir bitin aktif olması ile oluşur. 8 soket kesmesi register’ına (Sn_IR) ait kesme kaynaklarından birinin aktif olması SIR üzerindeki o sokete ait bitin aktif olmasını sağlar.

w5500_interrupt_logic

Benim uygulama, bir soket açıldığında o sokete ait TIMEOUT, RECEIVE ve DISCON kesmelerini otomatik olarak yetkilendirir. Ayrıca, açılan soketin Sn_IMR maske bitini de etkinleştirir. Kullanıcının bu kesmelerin devreye alınması üzerinde bir denetimi yoktur.
Yani, EC_INT pininin aktif olması bize açık soketlerden birinde zaman aşımı, veri alma ya da bağlantının sonlandırıldığı bilgilerini verir.

W5500’ün kesme pininin izlenmesi işini W5500_SPI_Thread( ) halleder. Bu thread aynı zamanda, kesme algılar algılamaz SIR register içeriğini de okur.

W5500_SPI_Thread( ) kesme durumu algıladığında işlemesi gereken bir komut yoksa derhal SIR okuma işlemine atlar.
Bu işlem esnasında; hEC.status = 200 olur.
İşlem tamamlandığında; hEC.interrupt_flag = 255 yapılarak üst thread uyarılır.
hEC.socket_interrupt içeriğine de SIR değeri yüklenmiş olunur.

W5500 Embedded Ethernet

WizNet markasını ilk kez bir PLC’nin içini açtığımda görmüştüm. O zamandan aklımda kalmış. Bir süre sonra embedded ethernet gerektiren bir iş masama geldiğinde aklıma bu marka geldi ve küçük bir araştırmayla W5500 çipini kullanmaya karar verdim. Şimdiye dek üç projede bu çipi kullandım ve deneyimlerimi burada paylaşacağım.

W5500’den önce üzerinde MAC modülü olan işlemcilerle ethernet uygulaması da yapmıştım. Kişisel görüşüm, W5500 gibi bir harici tümleşik çözümle çalışmak daha esnek ve konforlu.

W5500 bir donanıma asgari düzeyde kodlama yükü ile ethernet bağlanabilirliği özelliği kazandırmak için çok uygun bir çip. Bunun avantajı, PHY çipi kullanmak zorunda olmamanız, işlemcinizde MAC olmuş olsa bile TCP ya da UDP protokolünün handler’larıyla CPU’nuzu meşgul etmekten kurtulmanızdır. Ayrıca, kişisel gözlemim, W5500 daha önce kullandığım PHY çiplerine göre daha az ısınan bir şey.

W5500 ürün sayfasında bu malzeme hakkında açıklamaları görebilirsiniz.

Donanım

W5500 Schematic
W5500 schematic
RJ45 Ethernet Jack
RJ45 Ethernet Jack

W5500’ün işlemci bağlantıları;
* 4 pin SPI : SCK, MISO, MOSI, CS
* Giriş: INT, Çıkış: RST

SPI Erişimi

W5500’e erişim SPI üzerinden oluyor. Elemanlar yukarıda linkini verdiğim ürün sayfasında 80MHz SPI clock destekliyoruz diyorlar ancak bir yerlerde 33MHz ile garanti çalışıyor gibi bir şeyler de yazdığını hatırlıyorum. Benim SPI hattı EEPROM çipiyle paylaşıldığı için ben yüksek hıza fazla odaklanmadım, 10MHz SPI clock ile yetindim. Genel olarak şöyle bir şey var: Daha yüksek SPI hızı, SPI buffer erişim fonksiyonlarınızın “blocking” çalışmasına olanak tanır ve işlemcinin toplam performansını iyileştirebilir. Ben bu uygulamada non-blocking spi erişimi kullanıyorum ve spi modülünü başka görevlerle müşterek kullanmaya da açık çalışıyorum. O yüzden yüksek spi hızını fazla dert etmiyorum. Sözü daha fazla uzatmadan W5500’e nasıl erişiliyor, onunla ilgili notlarıma link vereyim:

W5500 Erişimi İle İlgili Notlarım

Controller Configuration

Şu veri tipi, cihaz sıfırlandıktan sonra common register block’taki temel W5500 ayarlarının yapılması için gerekli bilgileri saklar:

typedef struct
{
    unsigned char       gateway_IP[4];      
    unsigned char       subnet_mask[4];
    unsigned char       MAC[6];
    unsigned char       device_IP[4];
    WORD                retry_period;   // Little-endian olarak yeniden deneme periyot süresi
} ECCONFIG;

w5500.c içindeki cEC değişkeni reset sonrası, çipin kullanacağı konfigürasyon değerlerini içeriyor olmalıdır:

ECCONFIG    cEC;       // ec configuration: (reset sonrası yüklenir)

NOT: retry_time hariç tüm değişkenler BIG ENDIAN olarak saklanıyor olmalı!