Etiket arşivi: PIC24

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

I/O Config. on PIC24

PIC24 işlemcilerinde her bir port için aşağıdaki register’lar tanımlanmış:

PORTx, LATx, TRISx, ODCx, ANSELx, CNPUx, CNPDx, CNENx

Bunlardan, PORT, LAT, TRIS zaten tüm PIC’lerde olan register’lar.. Bunlar hakkında bir şey dememiz gerekmiyor.
UART pinlerinden TX olacak olanın TRIS’ini 0 yapıyorum. (Tam hatırlamıyorum ama ya PIC18 ya da uğraştığım başka bir mcu’da uart pinlerini TX de olsa RX de olsa giriş olarak konfigüre ediyordun, bu öyle değil!)

Bir pin analog giriş olarak kullanılacaksa ilgili TRIS biti 1 olmalı, ANSEL biti de 1 olmalı (bu zaten reset sonrası varsayılan durumdur).
ANSELx.y = 0 yaptığımız zaman x portunun y. bitini digital I/O ya da uart gibi bir peripheral olarak kullanabilir oluruz.
Bu arada, ben kullanmadığım işlemci portlarını donanımı tasarlarken herhangi bir nete bağlamıyorum (en iyi ihtimal bir header konnektöre çıkar bırakırım) ve bu portları ÇIKIŞ yapıp LAT değerine de 0 yazarım.

Peripheral Remapping
Pin sayısı az, çevresel fonksiyonları çok işlemcilerde pinleri fonksiyonlara paylaştırmak için kesinlikle bir çeşit pin-peripheral multiplexing’e ihtiyaç var. Silabs işlemcilerdeki priority crossbar’a benzer iş yapan bir özellik PIC24’lerde de var ve Peripheral Pin Select fonksiyonu olarak adlandırılıyor.

Bu yapı iki kısımdan oluşuyor: Input Mapping, Output Mapping.
Eğer şu PIC24EP işlemcisini bir-iki projede daha kullanacak olursam port konfigürasyonu için kendim bir program hazırlayacağım. (Harmony Configurator diye bir şey zaten var ama bildiğim kadarıyla 24EP desteklemiyor, hem ben kendi kodlama tarzıma ve kullanımıma uygun bir şey yapacağım) Bunu burada paylaşırım. Çip konfigürasyonunu hızlandırmamıza yardımcı olur.

Input Mapping
Bu, peripheral temelli bir kontroldür. Yani her peripheral’in kendisine ait bir register alanı (7 bit) vardır. O alana, peripheral’i hangi pine route edeceğimizi söyleyen değeri yazarız. Bu durumda her bir giriş pini için de bir adres değeri tanımlanmıştır. Bu, datasheet’te tablo olarak verilmiştir.
Böyle olmasının mantığı açık: Bir giriş birden çok kaynağa bağlanabilir. Sonuçta hedefin (peripheral) bir tane olması yeterli.
Bazı remappable pinler sadece giriş olabilirken bazıları hem çıkış hem de giriş olabilirler. İsimlendirmeden bunu ayırt edersiniz. RPIx ya da RPy gibi..

Örnek olarak, ben UART1 RX fonksiyonunu RF0/RPI96 pinine atayacaksam gider tablodan RPI96 adres değerine bakarım: 0x60 gördüm.
Sonra da bu değeri U1RX’in adres tanım register’ına yazarım: RPINR18 = 0x0060

Özetle, giriş mapping’de kullanacağım pinin adres değerini bulurum, bunu kullanacağım fonksiyonun register’ına yazarım…

Output Mapping
Bu, giriş mantığının tam tersi ile çalışır. Yani her bir pinin kendisine ait bir register alanı vardır. O alana, pini hangi fonksiyona route edeceğimizi söyleyen değeri yazarız. Her bir fonksiyonun bir remap değeri vardır. Bu, tablo olarak verilmiştir. Bunun mantığı bence daha da açık: Bir çıkış ancak tek bir fonksiyona atanabilir. Hedefn (pin) bir tane olması gereklidir.

Benim UART1 ‘in TX pinini RF1/RP97’ye atayacağım. Tabloya bakıyorum: U1TX için peripheral değeri = 1
Tamam.. Sonra gidiyorum, RP97’yi ayarlayan register’ı buluyorum: RPOR7
RPOR7bits.RP97R = 1;

Konfigürasyon Kilidi
Bazı register’lara erişimin ancak özel bir kilit açma işlemi sonrası münkün olduğunu bilirsiniz (mesela on-chip EEPROM). Benzer şey bu peripheral mapping’de de var. Yukarıdaki register’lara erişebilmeniz için önce
OSCCONbits.IOLOCK = 0;
yapmak zorundayız. Öte yandan IOLOCK bitine yazabilmek için önce özel bir kilit açma yazması yapmak gerek.

XC16’da bunu yapan iki builtin fonksiyonu var. OSCCONL ve OSCCONH için built-in yazma:

__builtin_write_OSCCONL (OSCCON | 0x40) ; // IOLOCK, OSCCON’daki 6. bit.

İşler bitince kilidi yine devreye almak için:

__builtin_write_OSCCONL (OSCCON & 0xBF);

Bu, IOLOCK bitinin PIC24’lerin hepsinde olmadığını okudum. Emin olmak için kullandığınız çipin datasheet’indeki OSCCON register açıklamasına bakın..

Initialize( )

İşlemcinin başlatılmasına sistem saatini ayarlayarak başlıyorum. 12MHz kristal ile 120MHz osilatör çalıştıracağım (bu 60MHz sistem saati demek).
Mevcut ayarlarla bende sorun olmuyor ama PLL’i konfigüre ederken çarpım/bölüm işlemlerinin hiçbir ara adımında belirtilen sınırların dışına çıkmamanız gerekiyor. Bu, özellikle düşük harici kristal değerleri için olanaksız olabilir. Böyle durumlarda en mantıklı olan işlemciyi RC osilatörle başlatıp sonra kristale geçmek ve PLL’i açmak.

PLL bloğunun kristal girişini sistem saati çıkışına dönüştürmesi aşağıdaki sıralamayla oluyor:

XTAL >>>> /N1 (PLLPRE+2) >>>> xM (PLLDIV+2) >>>> /N2 (2*PLLPOST+1) >>>> Fosc


// PLL konfigürasyonu:
PLLFBD = 38;              // (2) : M=40  : 6*40  = 240
CLKDIVbits.PLLPRE = 0;    // (1) : N1=2  : 12/2  = 6
CLKDIVbits.PLLPOST = 0;   // (3) : N2=2  : 240/2 = 120 MHz

// Bu PLL ayari bize 60MHz komut cycle'i veriyor..
// Clock Switch: Primary Oscillator with PLL (NOSC=0b011)
__builtin_write_OSCCONH(0x03);
__builtin_write_OSCCONL(OSCCON | 0x01);
// Clock switch bekle:
while (OSCCONbits.COSC!= 0b011);
// PLL'in kilitlenmesini bekle:
while (OSCCONbits.LOCK!= 1);