Kategori arşivi: Referans

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.

Function Pointers in C

Fonksiyon işaretçisi, bir fonksiyonu bildirim referansını kullanarak çağırabilmemizi sağlar. Yani, bir fonksiyonu parametrik olarak çalıştırma imkanımız olur.

void (* func_name) ( ) = function_name;

Bunun bendeki en sık kullanma amacı Fonksiyon Listesi hazırlamak:


 int funcA(void);
 int funcB(void);
 int funcC(void);
 int funcD(void);
 int funcE(void);
 int funcF(void);

 int (* functionList[]) () = 
 {
  funcA, funcB, funcC, funcD, funcE, funcF 
 };

 // bu bildirimin cümle içinde kullanımı şöyle:
 ret_val = functionList[list_index]();

Her ne kadar bir kaynak dosyası içinde kullanılacak bir fonksiyon için deklarasyon yazmak zorunda olmasak da, fonksiyon listesi hazırlarken deklarasyon yazmalıyız. Deklarasyon bize yürütme kısmından önce o fonksiyona dair işaretçiyi kullanma şansı verir. Bu şekilde liste bildirimi yaparken liste elemanlarını sabit olarak kullanabiliriz.

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