Etiket arşivi: SPI Slave

Basit Bir Kullanıcı Arayüzü

Bir projeyi değerlendirirken (onu karmaşıklık, zorluk, uygulanabilirlik vs. konularda “derecelendirirken” demek daha doğru olacak) kullandığım ölçütlerden biri “gereksinim duyulacak kullanıcı arayüzü” oluyor.
Aslında artık kullanıcı arayüzü için az zahmetle uygulamaya entegre edilebilecek seçenekler var. Ancak bu seçenekler bu işe ayırabileceğimizden fazla I/O gerektirebilir veya işimizin astarı yüzünü geçebilir.
Ben IO sayısının yeterli olmadığı bir board için sonradan bir arayüz modülü tasarlamıştım. Böyle bir şeyi yapmak zorunda olmasam oturup özellikle yapmazdım. Ama bir kere yapınca birden çok yerde kullanılabilecek bir şey olmuş oluyor, pek çok şey gibi.
Ben de RRUI isimli (adının pek önemi yok) board hakkında notlarımı temize çekip yedekleyeyim, başka yerde gerektiğinde hızla kullanabileyim diye düşündüm.

Donanım

Board üzerindeki LED’lerin anotları +5V olarak gösterdiğim uca bağlı. İşlemcinin LED süren portlarını open-drain olarak konfigüre ediyorum. Yakmak istediğimiz LED için bağlı olduğu porta 0 yazmamız gerek (Active-low).
Bargraph LED’leri işlemci portlarını ortaklaşa kullanırlar. Q1 ve Q2 transistörlerinin emetörleri de 5V’ta olduğu için bunların bazlarını süren çıkışlar da open-drain olmalı.

Bu devreyi iki amaçla kullanabilirsiniz:
Bir host controller olarak:
Üzerindeki SPI portunu master olarak konfigüre edip, SPI arayüzü olan bir sensor board’unu veya bir kablosuz haberleşme modülünü çalıştırmak için kullanabilirsiniz. Bu durumda butonlar kumanda için kullanılabilir. Bunun için gerekli ayarları ayrıca paylaşacağım.


Bir kullanıcı arayüzü modülü olarak:
Bu blog yazısı esasen bunu anlatıyor. Board üzerindeki SPI portu slave olarak konfigüre edilmiştir ve LED’ker belli SPI komutlarına göre belli yanma desenleri ile çalışırlar. Ayrıca her SPI erişiminde butonların basma durumu okunur.
Yukarıda şemasını paylaştığım board’un spi slave olarak çalışması için başlatım kodu şöyle:

void Initialize (void)
{
  // WDT'yi kapat:
  SFRPAGE = 0;
  WDTCN = 0xDE; 	 // First key
  WDTCN = 0xAD; 	 // Second key

  // Portlari ayarla:
  P0SKIP  = 0xF0;	 // P0.0 .. P0.3 : Crossbar'da!
  P0MDIN  = 0xFF;		
  P0MDOUT = 0x02;
  P0 = 0xFD;		 // P0.1 hariç hepsi digital giris
  P1SKIP = 0xFF;		
  P1MDIN = 0xFF;
  P1MDOUT = 0x00;	 // tum P1 portlari open drain 
  P1 = 0xFF;		 // hi-z durumdalar..	
  P2SKIP = 0x0F;
  P2MDIN = 0xFF;  	
  P2MDOUT = 0x00;	 // tum P2 portlari open drain
  P2 = 0x7F;	
  SFRPAGE = 0x20;
  P3MDIN = 0xFF;
  P3MDOUT = 0x00;	 // tum P3 pinleri de open drain!	
  P3 = 0x9F;

///// Crossbar:
  SFRPAGE = 0;
  XBR0 = 0x02;		// SPI sinyalleri crossbar'a çiksin!
  XBR2 = 0xC0;		// WEAKPUD ve XBARE = 1 yapildilar..
		
// SYSCLK ayari: 24,5MHz
  CLKSEL = 0;
  CLKSEL = 0;
				
/////////////////////////////////////////////////
/// Timer ayarlari:

CKCON0 = 0x01;	// TMR3, TMR2 <- EXTCLK;  TMR1,TMR0<- prescaler=SYSCLK / 4
TMOD = 0x11;		// TMR1,TMR0 : MODE-1 (16 bit timer) 
// TMR0,TMR1 artimi = 4/24,5M = 0,163us
TCON = 0;	
SPI0CN0 = 0x06;	 //  4 wire slave (NSS:input) modül henüz devreye alinmadi
// Kesmelerin ayari:
IE = 0xC0;	// kesmeleri yetkilendir, SPI kesmesini yetkilendir.
	
}

Board üzerinde iki grup LED var: Her biri 10 noktadan oluşan iki satır bargraph, bir düzey belirtici olarak kullanılabilir.
Yanda da 6 noktalı dairesel bir indikatör var. Buna beacon diyorum. Bir çalışma durumunu, durma durumunu ya da durum bildirimini basitçe bir LED’i yakıp söndürmek yerine bu çemberdeki noktaların animasyonu şeklinde göstermek daha dikkat çekici olabilir ve uzaktan algılama şansını arttırır.
Host uygulama beacon’a ve bargraph’ların her birine ayrı ayrı kumanda edebilmelidir. Yani bizim board açısından söylersek, bu kısımlar birbirinden bağımsız olarak çalıştırılmalıdırlar.

Beacon LED’leri

Beacon LED’leri belli bir hızla verilen canlandırma komutuna göre yanıp sönerler. Yani host uygulamadan beklediğimiz komut hangi canlandırmanın oynatılacağının söylenmesinden ibaret. Bu sürümde 6 farklı animasyon tanımladım:


Oynatma desenlerini flash’ta bir dizi olarak oluşturuyorum ve onları periyodik olarak beacon LED’lerini süren portları güncellemede kullanıyorum. Her animasyon 6 sahneden oluşuyor ve başka bir komut gelmediği sürece sürekli tekrar edip duruyor.

// beacon animasyonları:
unsigned char code beacon_pattern[42] = 
{
	0xDB, 0x6F, 0xB7, 0xDB, 0x6F, 0xB7,        // dönen çizgi
	0xDB, 0x27, 0xDB, 0x27, 0xDB, 0x27,        // unlem
	0x7B, 0x3F, 0x9F, 0xCF, 0xE7, 0xF3,        // dönen solucan
	0x73, 0x8F, 0x73, 0x8F, 0x73, 0x8F,        // yari-küre
	0xFB, 0x7F, 0xBF, 0xDF, 0xEF, 0xF7,        // dönen nokta
	0x03, 0xFF, 0x03, 0x00, 0x03, 0xFF,        // flash   
	0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF         // kapali  
};

Beacon LED’lerinin işletilmesini sağlayan fonksiyon aşağıda. Burada D26 tahmin edebileceğiniz üzere P3’te kalan beacon LED’i oluyor.
pattern_index ünite içinde tanımlı yerel bir değişken. Beacon thread’ini devreye alan fonksiyon parametre olarak buna atanması beklenen değeri alıyor. Bu değişkende beacon_pattern[] dizisinin seçilen animasyon için başlangıç indeksini tutuyorum.

static void Beacon_Program_Run(void)
{
  static unsigned char pix; // pattern sahne adım indeksi 
  unsigned char x;
	
	if (pix > 5) pix = 0;
	x = beacon_pattern[pattern_index + pix];
	
	LAT2 |= 0x7C;
	LAT2 &= x;
	if (x & 0x80) D26=1; else D26=0;	
	// çiki$i güncelleyelim:
	P2 = LAT2;
	++pix;
}

Ana programın beacon thread’ini açıp kapattığı global fonksiyonlar şunlar:

// arayüz fonksiyonlari:  STOP ve START
// kullanici program tarafindan herhangi bir anda çagrilabilirler

// Stop: Beacon animasyonunu durdurur. 
// turnoff > 0 olursa tüm led'ler söner,
// turnoff = 0 olursa son LED görüntüsü korunur.
void Beacon_Stop(unsigned char turnoff)
{
  if (turnoff)
	{
	  Disable_Beacon();
	}
	// artik beacon güncellenmeyecek..
	Beacon_Program = Beacon_Program_Idle;
}


void Beacon_Start(unsigned char animation)
{
   pattern_index = animation;
   Beacon_Program = Beacon_Program_Run;
}

Beacon işlerini yapan thread fonksiyonu şu şekilde tanımlanmış bir fonksiyon pointer’ıdır:

void (*Beacon_Program)();

Bunun ana programda systick timer’ının belli bir önbölücü ile çalıştırılması ile beacon işlerimiz halloluyor. Ana kodda da _Start() ve _Stop() fonksiyonları ile dilediğimiz zaman hareketi durdurup başlatabiliyoruz. Stop fonksiyonu son durumu korumak ya da tüm LED’leri söndürmek şeklinde davranabiliyor.

Bargraph LED’leri

Bargraph LED’leri zaman bölümlü çoğullama ile çalışıyorlar bu yüzden LED dirençlerini seçerken LED’lerin %50 duty cycle ile sürüldüklerini unutmayalım. Bu arada, bargraph’lardan birinde tüm LED’ler sönükse onun satırını boşu boşuna seçip parlaklığı yarı yarıya düşürmemek için gerekli yazılım desteğini sağlıyorum. Satırlardan biri kapalıysa diğeri sürekli seçili kalıyor.
Bargraph’lar için de, beacon’dakine benzer şekilde 3 farklı çalışma şekli kabul ediyorum. Biri, alışılageldik progressbar türü bargraph, diğeri gezen nokta ve sonuncusu da çift gezen nokta. Bunları bir sinyal seviyesi göstermede ya da bir menü/ayar değeri göstermede kullanabilirsiniz. İki bargraph’tan her biri ayrı ayrı ayarlanabilir.

Burada da her seviye için doğru LED desenini gösteren bir sabit dizi kullanıyorum. Ancak eleman sayısı 8’den büyük olduğu için _L ve _H suffix’li iki dizim var:

unsigned char code bargraph_L[33] =
{
  0xFF, 0xFE, 0xFC, 0xF8, 0xF0, 0xE0, 0xC0, 0x80, 0x00, 0x00, 0x00,    // bar
  0xFF, 0xFE, 0xFD, 0xFB, 0xF7, 0xEF, 0xDF, 0xBF, 0x7F, 0xFF, 0xFF,    // dot
  0xFF, 0xFE, 0xFC, 0xF9, 0xF3, 0xE7, 0xCF, 0x9F, 0x3F, 0x7F, 0xFF     // doubledot
};

unsigned char code bargraph_H[33] =
{
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFC,    // bar
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFD,    // dot
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFC     // doubledot
};

_L dizisi doğrudan P1 portunu güncellerken _H dizisinin yalnızca LSB 2 biti anlamlı ve P2’nin 0. ve 1. bitlerini değiştiriyorlar. Diğer bitler 1 olduğu için bir mantıksal VE işlemi maskeleme yapmadan kullanılabiliyor. Doubledot desenleri için bit değerlerini gösterdiğim tabloda ne dediğim daha kolay anlaşılıyor:

Bargraph işlemlerini yürüttüğüm thread’in organizasyonu beacon’ın aynısı. Tek fark, her satır için ayrı bir task fonskiyonu yazmış olmam. Çalışma durumu için (*Bargraph_Program)() pointer’ı _Run1 ve _Run2 olarak iki farklı fonksiyon arasında değişir.

static void Bargraph_Program_Run1(void)
{
  unsigned char x;
  
  LAT1 = bargraph_L[bar_index1 + bar1];
  x = bargraph_H[bar_index1 + bar1];
  LAT2 |= 0x03;	// bargraph'la ilgili bitleri set et..
  LAT2 &= x;      // en dü$ük anlamli 2 bit reset edilecekse edilecek..
  // portlari güncelle:
  Select_Line_1();
  P1 = LAT1;
  P2 = LAT2;   
  
  // diger kanal devredeyse o programa atla:
  if (L2) Bargraph_Program = Bargraph_Program_Run2;
  
}


static void Bargraph_Program_Run2(void)
{
  unsigned char x;
  
  LAT1 = bargraph_L[bar_index2+bar2];
  x = bargraph_H[bar_index2+bar2];
  LAT2 |= 0x03;	// bargraph'la ilgili bitleri set et..
  LAT2 &= x;      // en dü$ük anlamli 2 bit reset edilecekse edilecek..
  // portlari güncelle:
  Select_Line_2();
  P1 = LAT1;
  P2 = LAT2;   
  
  // diger kanal devredeyse o programa atla:
 if (L1) Bargraph_Program = Bargraph_Program_Run1;

}

L1 ve L2 yerel bit değişkenleridir. Sırası gelen satırın etkin olup olmadığını belirtirler.
bar_index1, bar_index2, o satırı devreye alan _Start() fonksiyonunun parametre olarak kullanıcıdan aldığı, hangi tipte gösterge yürütüleceğini belirten, desen indeks değeridir.
bar1 ve bar2 değişkenleri de, bargraph’ın geçerli değerini gösterirler ve ana program tarafından gerektiğinde güncellenirler.
Söylemeye gerek yok, _Start ve _Stop fonksiyonları her bar için ayrı ayrı:

/////////////////////////////////////////////////////////////////////////////////////////
/// kullanici programin bargraph'lari çali$tirmak ve durdurmak için kullanacagi fonksiyonlar

void Bargraph_Start1(unsigned char bartype)
{
  L1 = 1;
  bar_index1 = bartype;
  Bargraph_Program = Bargraph_Program_Run1;
}


void Bargraph_Start2(unsigned char bartype)
{
  L2 = 1;
  bar_index2 = bartype;
  Bargraph_Program = Bargraph_Program_Run2;
}


void Bargraph_Stop1( void )
{
  L1 = 0;
  // diger kanal da kapaliysa bargraph'lari tamamen kapatabiliriz:
  if (L2==0) 
  {
    Disable_Lines();
    Bargraph_Program = Bargraph_Program_Idle;
  }

}


void Bargraph_Stop2( void )
{
  L2 = 0;
  if (L1 == 0)
  {
    Disable_Lines();
    Bargraph_Program = Bargraph_Program_Idle;
  }
}

Bargraph’lar beacon animasyon adımlarından daha hızlı güncellenmeliler. Aksi halde LED’lerdeki kırpışmalar görünür olabilir. Diğer taraftan, çok hızlı güncelleme de parlaklığı azaltacak bir şey. Ben ana programda 10,7ms periyotla (93Hz) ile satır anahtarlaması yapıyorum.

SPI Slave Portu

Bu program beacon, bargraph1 ve bargraph2 durumlarını SPI portundan aldığı komutlara göre ayarlar. Bunun için açılışta SPI modülü slave olarak ve mod0 faz ayarıyla konfigüre edilir. Yani, SCK’nın boşta durumu 0’dır ve MISO/MOSI veri durumu değişimi düşen kenarda yapılır.
Datasheet BB3’te 4 byte TX ve 4 byte RX FIFO’su olduğunu belirtse de biz daha önemli bir işimiz olmadığı için düşük gecikmeli bir kesme rutininde, byte geldikçe veri alması yapacağız.
Daha fazla uzatmadan, sözü interrupt handler’a bırakıyorum:

void ISR_SPI (void) interrupt 6 
{
	static unsigned char xdata *p = 0; 
	unsigned char spidata;
    
	spidata = SPI0DAT;      // rx buffer'i oku... 
	if ( SPI0CN0_RXOVRN )
	{
	   Disable_SPI();
	   SPI_ERR = 1;		// programa hata oldu bilgisi yolla!
	}
        else   // buffer overrun yoksa rx olmu$tur:
	{
	   SPI0CN0 &= 0x0F;  // flag'leri sifirla!	  
	   SPI0DAT = *(p+5);
	   *p = spidata;     // gelen veriyi siradaki yerel konuma yaz.
	   ++p;
	   if (p > 3)
	   {
	      p = 1;		     // pointer'i basa sar!
	      SPI_RX = 1;     // ana programa da durumu bildir
	      SPI0DAT = dev_type;
	   }   
	} 

}

SPI’ı slave olarak kullanırken karşı karşıya olduğumuz bir mantıksızlık var: Konuşmayı başlatan biz olmadığımız için, ve ilk byte gelinceye kadar bir konuşma olduğundan bile haberdar olmayacağımız için SPI kesmesi aldığımızda ilk byte’ı değil ikinciyi yollamakla karşı karşıyayızdır. Çünkü SPI full duplex, senkron bir haberleşme. Veri almaya başladığımız anda veri de yollamaya başlamış oluyoruz.
Bu sorunu aşmak için, SPI boşa çıktığı anda TX buffer’ına “değişmeyen” bir veri yazıyorum. Burada bu aygıt tipi sabiti.

Bir konuşmanın 4 byte olması uygun. Master şu an için kullanmadığımız bir byte yollayıp haberleşmeyi başlatsın, ardından beacon, 1. satır ve 2. satır için çalıştırma komutlarını yollasın. Biz de slave tarafı olarak, aygıt tipi, buton durumları, 1. satırın bizdeki durumu ve 2. satırın bizdeki durumu olarak MISO portunu yükleyelim.

RAM’de sıralı yerleşmiş değişkenler erişim sırasında kullandığım TX ve RX buffer’ı oluyor:

unsigned char xdata resd1        _at_ 0;		
unsigned char xdata cmd_beacon	 _at_ 1;		
unsigned char xdata cmd_line1	 _at_ 2;
unsigned char xdata cmd_line2	 _at_ 3;
unsigned char xdata resd2        _at_ 4;
unsigned char xdata input_state  _at_ 5;
unsigned char xdata bar1         _at_ 6;
unsigned char xdata bar2         _at_ 7;
unsigned char xdata dev_type     _at_ 8;

Değişkenlerin mutlak olarak bildirilmiş adresleri kesme kodundaki pointer değerlerini anlatıyor sanırım. İlk 4 byte RX konumları. Her veri alma kesmesinde o anda geçerli pointer adresinin 5 fazlasını sonraki haberleşmede yollamak için tx buffer’a yüklüyorum. Örneğin, p=1 iken, cmd_beacon’ı alıyorum ve bar1 içeriğini yüklüyorum. Unutmayın, yüklediğimiz veri, bir sonraki kesmede gitmesi tamamlanacak veri. Peki bu durumda ilk byte alınırken ne oluyor? Onun esprisi de dev_type değişkeninin 4 adresinde değil 8 adresinde olması işte. Son byte alındığında aslında biz TX buffer’a ilk byte’ı yazmış oluyoruz. (3+5)
En başta, yani SPI başlatılırken TX buffer’a dev_type’ı manual olarak yazmalıyız elbette.

SPI Komutları

Beacon Komutları

Bargraph Komutları

Komut byte’larının MSB 3 biti komut türünü belirtir. Beacon için “ÇALIŞ” komutu LSB 3 bit ile hangi canlandırma türünün yürütüleceğini belirtir:

  • 000 : Dönen Çizgi
  • 001 : Ünlem
  • 010 : Dönen Solucan
  • 011 : Yarı-Küre
  • 100 : Dönen Nokta
  • 101 : Hepsini Aç-Kapa
  • 110, 111 : Geçersiz

Bargraph için, belli bir anda L bitleri ile belirtilmiş düzey bilgisi (0..10 arası değer alabilir) gösterge türü ile birlikte, yanacak LED sayısını ya da konumunu tanımlar.

Kullanım Örneği

Aşağıdaki program her bir bargraph’ın farklı modlarda çalışmasını kumanda ediyor. Burada, SPI_RX=1 yapmakla sanki cihaza dışarıdan komut yollamış gibi yapıyoruz.

// başlangıçta:
 devicestatus = 0;
 level1 = 0;
 level2 = 0;
 cmd_beacon = 0;	// komut yok (beacon kapalı)
 cmd_line1 = 0x40;	// bargraph'i "bar" mode'da ba$lat..
 cmd_line2 = 0x60;      // bargraph'i "dot" mode'da ba$lat..
 SPI_RX = 1;

/// ANA PROGRAM:
/* input_state: buton basılı olma durumları
* level1, level2 : göstermek istedigimiz duzey bilgileri
* SPI_RX : "normal" çalışmada SPI modülünün set edecegi veri geldi bayrağı 
* BTN1 ve BTN2 ile üstteki, BTN3 ve BTN4 ile alttaki bargraph değiştirilir.
*/ 
 switch ( devicestatus )
  {
   case 0:
       // bargraph-1'i arttir:
       if (input_state & 0x01) 
       {
	  if ( level1 < 10 ) 
	  {
	    ++level1;
	    cmd_line1 = 0xA0 | level1;
	    SPI_RX = 1;
	  }
	  devicestatus = 1;
	}
        // bargraph-1'i azalt:
	else if (input_state & 0x02)
	{
	   if (level1)
	   {
	      --level1;
	      cmd_line1 = 0xA0 | level1;
	      SPI_RX = 1;
	   }
	   devicestatus = 1;
	 }
         // bargraph-2'yi arttir:		
	 else if (input_state & 0x04) 
         {
	    if ( level2 < 10 ) 
	    {
	       ++level2;
	       cmd_line2 = 0xA0 | level2;
	       SPI_RX = 1;
	    }
	    devicestatus = 1;
	  }
          // bargraph-2'yi azalt:	
	  else if (input_state & 0x08)
	  {
	     if (level2)
	     {
	       --level2;
	       cmd_line2 = 0xA0 | level2;
	       SPI_RX = 1;
	      }
	      devicestatus = 1;
	  }			 
	 break;
	  
	  
	  case 1:
	    if ( input_state == 0 )  devicestatus = 0;
	  break;
	  
  }

Yukarıdaki programın hex kodu için tıklayın.
Bu programı board’a yüklediğinizde ana programda yukarıdaki kod çalışmaya başlayacak ve bir host işlemciden spi komutu beklemeksizin butonlar ile bargraph’ı kumanda edebileceksiniz.