Etiket arşivi: EFM8BB3

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.

Pulse Encoder Knob Interface

Keyes-KY040 encoder’dan gelen harekete göre değer girişi yapmak için bir arayüz hazırlamam gerekti, bununla ilgili düşüncelerimi burada paylaşmak istiyorum.

KY-040 modülünde 5 pinli bir header var. Bu pinler sırasıyla şöyle isimlendirilmişlerdir:

GND, + Besleme
SW Pushbutton NO ucu
DT Faz-B
CLK Faz-A

Modülün iç yapısını yukarıdaki gibi gösterebiliriz:

BQ’ları açık kollektör NPN çıkışlı endüktif proximity switch’ler olarak görelim. Rod Push Switch, knoba basınca kısa devre olan bir push-button. Bendeki modülde R3 olarak gözüken direnç takılı değil. Ama bu şekliyle bunu ham kontak olarak kullanamayız çünkü diğer uç (-) beslemeye gidiyor, yani her durumda bunu bir pull-down button olarak kullanacağız demektir.

İnternette yapılan bir aramada, beslemenin +5V’a bağlandığını gösteren şemalar geliyor ama sanırım bu, arduino modüllerinin bir kısmının 5V ile çalışması yüzünden böyle belirtilmiş. Ben modülü +3V ile besliyorum çünkü benim devrede, +3V’a bağlı harici pull-up dirençleri de var.
DT ve CLK uçları endüktif algılayıcıların pull-up yapılmış çıkışlarına bağlı ve aralarında dönme yönüne bağlı faz farkı olan encoder çarkı izleme sinyalleri.

Bu arada, modülün üstünde pull-up dirençleri olduğu için encoder sinyallerini işlemciye doğrudan bağlamak yeterli gibi gözükse de kenarlarda debounce olmasına tahammülü olmayacak kadar hızlı bir kod çalıştıracaksanız, pull-up kondansatörleri kullanmanız iyi olur. Ben işlemciye girmeden önce 10nF kondansatör ile pull-up yapıyorum.

ENC-A ve ENC-B olarak isimlendirdiğim encoder pulse sinyallerini P06 ve P07 portlarına giriyorum. P0’ın kullanılması şart değil, işaret fazlarını değerlendirmekte kullanacağım port-match fonksiyonu EFM8’in tüm pinlerinde var. Ancak, söylemeye gerek yok ama aynı portun pinlerini kullanmak register ayarlarını tek yazmada yapmamız açısından hızlandırıcı olabilir elbette.

Firmware

Eğer üzerinde çalıştığımız işlemcinin üstünde QEI (Quadrature Encoder Interface) modülü yoksa encoder fazlarını takip etmek için bir yol olarak port match fonksiyonunu kullanabiliriz.
Ben bu encoder’la ilk denemeleri yaparken doğrudan port değerini okuyan bir döngü ile sistemi çalıştırdım. Zaten örnek program diye bulacağınız şeylerin hepsinde de öyle yapılıyor.
Ancak pratik kullanımlarda çoğu zaman deneme programında olduğu gibi, port okumasına âmâde bomboş bir ana çevrim olmayacaktır. Bu durumda, özellikle yüksek encoder hızlarını doğru okumak için port-mismatch kesmesini kullanmak yerinde olur.

EFM8’de her bir portun MAT register’ıyla ayarlanan bir port match değeri var. Bunu portun “olmasını beklediğimiz” değeri, “boşta” değeri olarak düşünelim. Herhangi bir port bitinin durumu bu boşta değerden farklı olduğu an bir kesme tetiklenir. Elbette bu mekanizmayı sadece ilgilendiğimiz port bitleri için devreye almamız gerekir. Bunu da her portun MASK değerini ayarlayarak yaparız.
Port mismatch kesmesi EIE1<1> (_EMAT) biti ile devreye alınır ve 8 numaralı kesme vektörüne atlama yaptırır.
Kesmenin tetiklenme koşulunu şöyle yazabiliriz:
Py.x & PYMASK.x != PYMAT.X & PyMASK.x
Bu kesmenin bir flag’i yok. Kesmeye atlandığı an durum sıfırlanıyor. Microchip’teki gibi mismatch tetikleyen portu okuma zorunluluğu da yok.
Benim uygulamada, P0^6 ve P0^7 için mismatch kesmelerini devreye almak, sürekli olarak çağrılan bir kodla portların durumunu yoklama külfetinden (overhead) bizi kurtarıyor:

#define Enable_Port_Mismatch() P0MAT = 0xFF; P0MASK = 0xC0; EIE1 |= 0x02


Encoder’da bir hareket olduğunda kesme tetiklenecek. Ancak iki şeyi daha ayırt etmemiz gerekiyor: Hareketin yönü ve bitişi.
Bunun için kesme kodu içinde basit bir durum makinesi çalıştırmak gerek. Bu durum makinesinin çalışmasını şöyle özetleyebilirim:

0ENC_IDLEHangi girişin kesmeye neden olduğunu bul (önde olan faz). Bu, hareket yönünü belirlememizi sağlar. Ardından önde olan fazın kesme üretmesini iptal et. [Diyagramda 1 durumu]
1ENC_CW_LEADINGBu aşamada gelen kesme kesinlikle B girişinden geliyordur. Artık uygulamaya saat yönünde bir hareket olduğunu söyleyebiliriz. B fazının mismatch durumunu 1 yaparsak hareket fazının bitişinde kesme almış olacağız. [Diyagramda 2 durumu]
2ENC_CCW_LEADINGBu aşamada gelen kesme kesinlikle A girişinden geliyordur. Uygulamaya saat yönünün tersinde hareket olduğunu söyleyebiliriz. A fazının mismatch durumunu 1 yaparsak hareket fazının bitişinde kesme almış olacağız. [Diyagramda 2 durumu]
3ENC_PULS_TRAILINGGeride kalan fazın 0->1 gerçişi yapmasını bekle. Bu, hareketin bitişi demektir, yani durum makinesi başa dönebilir. [3] durumu. Her iki yön için de bu adım ortak.
4ENC_WAKEUPBu, sistemi uykudan uyandırmak için kullanılan adım. Bir hareket algılaması yapılmayacağı için durum makinesi çalışmaz.
void ISR_Port_Mismatch(void)  interrupt 8
{
 switch ( encoder_state )
  {
    case ENC_IDLE:
    // Encoder bo$ta iken bir mismatch kesmesi gelmi$se
    // önde olan faz =0 olmu$ demektir. Bu, cw/ccw ayrimini yapmamizi saglar
	  if ( ENC_A == 0 )
	  {
             P0MASK = 0x80;	// P0.6 (ENC_A port match fonksiyonu kapatildi)
	     encoder_state = ENC_CW_LEADING;
	  }
	  else
	  {
	     P0MASK = 0x40;	// P0.7 (ENC_B port match fonksiyonu kapatildi)
	     encoder_state = ENC_CCW_LEADING;
	  }
     break;
	
	
     case ENC_CW_LEADING:
     // bu a$amada gelen interrupt enc-b: 1->0 geçi$i sebebiyle tetiklenmi$tir..
     // $imdi, enc-b: 0->1 geçi$ini bekleyebiliriz:
	P0MAT = 0x40;
	ENC_PULS_CW = 1;
	/// todo: ENC_A = 1 durumu hata olarak algilanabilir!
	encoder_state = ENC_PULS_TRAILING;			
    break;
		
		
    case ENC_CCW_LEADING:
    // bu a$amada gelen interrupt enc-a: 1->0 geçi$i sebebiyle tetiklenmi$tir..
    // $imdi, enc-a: 0->1 geçi$ini bekleyebiliriz:		
	P0MAT = 0x80;
	ENC_PULS_CCW = 1;
    /// todo: ENC_B = 1 durumu hata olarak algilanabilir.
	encoder_state = ENC_PULS_TRAILING;
    break;
		
		
    case ENC_PULS_TRAILING: 
    // bu interrupt gelmi$se gerideki fazin: 0->1 geçi$i olmu$ demektir.
    // encoder fazi tamamlanmi$tir.
    // fiziksel olarak tamamlanmi$ olsa bile, bir fazin bitmesini 
    //bekleyen i$lem flag'inin sifirlanmasi ile mumkun kilalim:
	P0MAT = 0xC0;
	P0MASK = 0xC0;
	encoder_state = ENC_IDLE;
   break;		
		
   // CPU encoder hareketi ile IDLE durumdan çikmi$ demektir.
   // encoder okuma state mach. çali$mayacak.		
   case ENC_WAKEUP:
    // wake-up durumundan çikiliyor.
      EIE2 = 0;
   break;

   }
}

Bakınca, ana döngü içinde port poll eden “örnek” uygulamadan bile daha basit gözüküyor. Artık, pulse üreten encoder’ınızı,
ENC_PULS_CW ve ENC_PULS_CCW
adında, Port mask ve(/veya) _EMAT bitleri ile kolayca devreye alınıp çıkarılabilen mantıksal bit girişleri olarak düşünebilirsiniz.
Bu bitlerin sıfırlanması uygulama programının sorumluluğundadır. Siz ENC_PULS_XX bitini işlemeden yeni bir pulse algılanırsa bu hareket “atlanacaktır”. Ben uygulamamda bunu özellikle istiyorum çünkü ekrandaki bir değeri encoder hareketine göre değiştiriyorum. Öte yandan hız/konum algılama gibi bir iş yapacaksanız encoder kesmelerini ana programın işlemesi öncesi buffer’lamanın bir yolunu düşünmelisiniz. En basiti, hareket ile artan/azalan bir tamsayı sayaç kullanmaktır, değil mi?

EFM8BB3 Sıcaklık Sensörü

Sıcaklık ölçümü ile ilgili hikayelerimizde sıra hiçbir sensör kullanmadan sıcaklık ölçmeye kadar geldi.

İşlemcilerin üzerinde bir sıcaklık sensörü olması alışıldık bir durumdur. Silabs EFM8 serisinde de analog modülde, giriş multiplexer’ını ayarlayarak ADC bağlantısını yapabildiğimiz bir sıcaklık sensörü var.

Bu sensörü ortam sıcaklığı ölçmede kullanmayı düşünüyorsanız bunun çoğu durumda pek iyi bir fikir olmayacağını baştan söylemem gerek. Daha önce anlattığım kendi kendini ısıtma etkisi, tahmin edeceğiniz gibi işlemci üstündeki sensör için ziyadesiyle geçerli olacaktır. Elbette koskoca işlemciyi yalnızca sıcaklık ölçmek için kullanmayacağınızı varsayıyorum.

İşlemci üstünde sıcaklık ölçmenin bize donanımsal bir maliyeti yok. Sıcaklık ölçmek için ADC’nin herhangi bir dış bağlantısına ihtiyaç duymuyoruz. Yazılımsal olarak da büyük bir maliyet yok. Birkaç satırlık kod ve birkaç ms içinde işimiz halloluyor. Herkes adına ve her durumu açıklamak için konuşamam ancak ben daha önce onboard sensörü iki sebeple kullandım:

Birincisi board’umun sağlıklı çalışacağı bir sıcaklıkta olup olmadığını kontrol etmek için. Zamanında tasarladığım bir DC sürücü oldukça küçük bir board üstünde çıkış katı işlemci regülatör vs. bir aradaydı. İşlemcinin sıcaklığının 60 küsur dereceyi geçmesi durumunda önce çıkış gücünü sınırlamak sonra da aleti tamamen kapatmak gibi bir iş için onboard sensörü kullanmıştım.

İkinci kullanımda da aslında başka bir şeyi ölçmek için bir donanım kurmuşuzdur ama ölçeceğimiz şey sıcaklığa bağlıdır. Bu durumda sıcaklığa göre düzeltme yapmak için eğer uygunsa board sıcaklığını referans alabiliriz.

Vereceğim örnek kod ADC’yi hali hazırda kullanıyor olup olmamamızdan bağımsızdır çünkü ADC’nin güç bağlantılarının belli bir durumda olmasını gerektirir:

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

Onboard sıcaklık sensörünün çalışması için TEMPE kontrol bitini 1 yapmak lazım. Fakat öncesinde, ADC referansını 1,65V dahili referansa, eksi bağlantıyı da çip GND’ına bağlıyorum.
ADC giriş multiplexer’ına 0x14 (AMUX_TSENS) yazınca girişi de bağlamış oluyoruz.
Sıcaklık sensörünün açılma süresine bakarsanız 1,8us gibi bir süre görüyorsunuz ancak buna referansın açılmasını ve SAR girişinin settle olmasını da eklemek gerek.
Ben çeşitli donanımsal sebelerle ADC’nin kendi power-up delay zamanlamasını kullanmıyorum. O yüzden sıcaklık sensörünü açtıktan sonra, alışkanlıktan, 1ms bekliyor ve ADC dönüştürme işlemini sonra başlatıyorum.

ADC sequencer’ı, çözünürlüğü, örnekleme hızı vs. mevcut ayarlarımızda olabilir. Onları burada yeniden yazmadım. Sonuçta onboard sıcaklık sensörünün parametreleri gerilim cinsinden tanımlanmışlardır:

Sensörün V / *C kazancının offset hatasına oranına bakarsanız, çipten çipe 6 *C’lik bir 0 *C noktası farkı olduğunu göreceksiniz ki bunun kullanacağınız 100 çipten 68’i için garanti edildiğini de göz önüne almalısınız. Uzun lafın kısası, eğer mutlak değer doğruluğu sizin için önemliyse her bir board için 0*C offset’ini ölçüp flash’ta saklamanız gerekir. (Bununla kim uğraşır bilemiyorum)

Bir termostat uygulaması için sıcaklık okuması yapıyorsak *C skalası çalışma esnasında bizim için gerekli değil. Ancak yaptığımız ölçümün *C karşılığını bilmek istersek, yaptığımız ADC ölçümünden, 757mV ‘a karşılık gelen ADC okuma sayısını çıkarıp sonucu da 2,85 mV’a karşılık gelen ADC sayısına bölerek bir *C sonucuna “yaklaşabiliriz.

Sıfır noktası konusundaki belirsizliğe karşılık mV / *C doğruluğu oldukça belirlidir. Bunun sizin için anlamı şu: Bir noktada, bilinen bir değere ölçekleme yapmanız doğruluk açısından yeterli olacaktır.

Onboard sensörü kullanmanın en güzel yanı, self heating’in ne kadar dramatik bir etki olduğunu gözlerinizle görmenizi sağlayacak olması. Bunu olumsuz bir şey olarak düşünmüyorum. İşlemcinin kendi sıcaklığını görmesi sistemin toplam güvenliği için her zaman çok iyi bir şeydir. Bu arada, buzlu suyla 0 noktasını ayarlarsanız bu sensörün oldukça iyi bir doğruluğu olduğunu keşfedeceksiniz. Teşekkürler Silabs..