Kategori arşivi: Referans

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.

EFM8 DAC

EFM8 ‘in üstündeki DAC biriminden bahsetmeye başlamadan önce, digital-to-analog converter denen şeyler hakkında uzun süre devam eden bir kanaatimi not etmek isterim: DAC bana hep lüks bir şey gibi gelmiştir. ADC bir şekilde en başından beri erişilebilirliği olan bir birimdi. Ama DAC, ister bir mcu’da isterse de bir plc’de olsun, sanki hep maliyeti arttıran bir şey gibi geldi bana.

15 sene kadar önce bir iplik boyama makinesinde, boya desenleri oluşturmak için boya püskürten kafaların dönme hızlarını akan ipliğin uzunluğuna bağlı olarak değiştirilebilir yapmam istenmişti. Bulduğum çözüm küçük bir işlemciyle encoder darbeleri saymak ve 8 bitlik bir DAC’a çıkış vererek asenkron motor sürücülerinin hızlarını değiştirmekti. O zaman National’ın paralel bir DAC’ını kullanmıştım, çıkışa bir de opamp koyunca sonuç muhteşem olmuştu. DAC deyince aklıma hep bu iş gelir.

Yıllar sonra da ilginç bir proje için dalga şekli üretme işiyle uğraşmak zorunda kaldım. Artık işlemcilerin üzerinde oldukça hızlı DAC’lar var. Ayrıca çok yüksek çözünürlüklü PWM’lerin olması zaten pek çok durumda DAC ihtiyacını tamamen ortadan kaldırıyor. Ama ben yine de üzerinde DAC birimi olan mcu’lara ayrı bir gözle bakıyorum.

EFM8’de 4 kanal DAC var. Bunlar iki çift olarak kullanılıyor gibi gösterilse de tamamen bağımsız çıkışlar olarak kullanabiliyorsunuz. Bir çifti, birbirinin aynı ya da eşlenik çıkış üretmek istersek beraber kullanabiliriz. DAC çözünürlüğü 12 bit.

Anlatacağım örnek uygulamada DAC çıkışını ses üretmede kullanıyorum. Bunun için 2 DAC kanalını toplam şeklinde kullanmak için bir devre hazırladım. Bu şekilde basit dalgalardan daha çok harmonik üretmek mümkün.

DAC sinyal yolu (bazı şekiller sevgili kızım tarafından çizilmişlerdir)

Bir stereo kuvvetlendirici kullanıyor olsam da mono ses üretiyorum. Kuvvetlendiricinin yüksek giriş direncinin karşısında iki çıkışı dirençlerle birbirine bağlamak bir toplama kuvvetlendiricisi elde etmemizi sağlıyor. Benim kullandığım amplifikatörün bir bias off girişi de var. Bununla yükleme/geçiş vb. anlarda pop/click türü gürültüler olmasın diye çıkışı kapatabiliyorum.

Referans gerilimini, referans gerilim bölücüsünü (REFGN) ve çıkış sürücüsü kazancını (DRVGN) ayarlayarak çeşitli çıkış aralıkları elde edebiliriz. Ben bunlardan 4 tanesini kullanıcı konfigürasyonu ile seçilebilir yaptım:

Bir dalga üretmek iki eksende hareket etmemizi gerektirir: Y ekseni tahmin etmesi kolay olacağı üzere
V = Vref * REFGN * DRVGN * (DAC0H:DAC0L) / 4095
çarpanlarıyla belirlenir.
X ekseni ise zaman. Dalga şeklimizin çözünürlüğüne bağlı olarak, istediğimiz frekansı bir tam periyotta üretecek bir hızla DAC güncellemesi yapmamızı gerektirir.

Daha açık bir deyişle, bir dalga şeklini 12 örnekle tanımlamışsak DAC güncelleme frekansımız istenen dalga frekansının 12 katı kadar olmalı. Örneğin;

Yandaki dalga şeklini tanımlayan “normalize” sinüs dizisi;
32,48,60,64,60,48,32,16,4,0,4,16

Bu diziyi, DAC’a 208us periyotla yüklersek yaklaşık 2,5ms periyotlu bir sinüs çıkışı alırız. Bu da 400Hz kadar eder. Ses çıkışını test etmek için bunu deneyebiliriz.
Burada ilginç olan iki nokta var: Birincisi sample genlikleri unipolar. Çünkü DAC çıkışımız 0V ile seçtiğimiz pozitif Vref gerilimi arasında bir gerilim üretebilir. O yüzden, genliği simetrik bir işaret üretmek istiyorsak Vref/2 kadarlık bir offset eklememiz gerekir.
İkinci nokta, DAC sayısal değerimizle ilgili. Ben bir dalga şekli tanımlarken onu 0..64 gibi bir aralıkta normalize ederek üretiyorum. Eğer donanımsal değil, yazılımsal genlik ayarı yapmak isterseniz bu 0..64 aralığını 0dB kabul edip, sample değerlerini istediğiniz bir sabitle çarpıp, anlık bir çıkış genliği üretirsiniz.

Eğer bu bir ses işareti ise ters-logaritmik bir adım bölümlemesi volume ayarı için uygun olacaktır. 4095 verilebilecek en büyük çıkış değeridir. DAC çıkışını yüklüyorsanız ve 3V gibi bir range ayarı yaptıysanız çıkışın max. değerden önce doyuma ulaşması tehlikesi vardır.

Aşağıda, genel amaçlı kullanıma uygun bir dalga tablosu tanımı paylaşıyorum. Burada her bir dalga tanımı 24 byte’tan oluşuyor (her sample little endian yerleşmiş 12 word). Bunu denemek isterseniz dikkatinizi çekmek istediğim bir şey var: İlk 4 dalga haricindeki dalga tanımları kendisini 3 kez tekrar ediyor. Yani bunların frekansını hesaplarken x12 değil x4 hızla yükleme yapmalısınız.
code quailfier ile dizinin flash bellekte yerleşmesini sağlıyorum ancak mutlak adres veremiyorum. _at_ directive kullandığınızda değişkenleri başlatamazsınız.
Bu yüzden de, bu işleri denerken en başta yaptığım gibi, çalma kesmesi içinde doğrudan bir code pointer’ı çalıştıramıyorum. Bunun yerine waveTable dizi indeksini volatile bir değişken olarak kullanıp compiler’ın verimliliğine güveniyoruz.

unsigned char code waveTable[288] = 
{ 
	// wav-1 : sine +18dB
	0x00,0x01,0x80,0x01,0xE0,0x01,0xF8,0x01,
	0xE0,0x01,0x80,0x01,0x00,0x01,0x80,0x00,
	0x20,0x00,0x00,0x00,0x20,0x00,0x80,0x00,
       // wav-2 : sine +28dB
	0x00,0x03,0x80,0x04,0xA0,0x05,0xE8,0x05,
        0xA0,0x05,0x80,0x04,0x00,0x03,0x80,0x01,
        0x60,0x00,0x00,0x00,0x60,0x00,0x80,0x01,
	// wav-3 : sine +32dB
	0x40,0x05,0xE0,0x07,0xD8,0x09,0x56,0x0A,
        0xD8,0x09,0xE0,0x07,0x40,0x05,0xA0,0x02,
        0xA8,0x00,0x00,0x00,0xA8,0x00,0xA0,0x02,
	// wav-4: sine +36dB
	0x80,0x07,0x0B,0x40,0x10,0x0E,0xC4,0x0E,
        0x10,0x0E,0x40,0x0B,0x80,0x07,0xC0,0x03,
        0xF0,0x00,0x00,0x00,0xF0,0x00,0xC0,0x03,
	// wav-5: symm.sawtooth +12dB
	0xC0,0x00,0xFC,0x00,0x00,0x00,0x40,0x00,
        0xC0,0x00,0xFC,0x00,0x00,0x00,0x40,0x00,
	0xC0,0x00,0xFC,0x00,0x00,0x00,0x40,0x00,
	// wav-6 symm.sawtooth +24dB
	0x00,0x03,0xF0,0x03,0x00,0x00,0x00,0x01,
        0x00,0x03,0xF0,0x03,0x00,0x00,0x00,0x01,
	0x00,0x03,0xF0,0x03,0x00,0x00,0x00,0x01,
        // wav-7: symm.sawtooth +32dB
	0xE0,0x07,0x56,0x0A,0x00,0x00,0xA0,0x02,
        0xE0,0x07,0x56,0x0A,0x00,0x00,0xA0,0x02,
	0xE0,0x07,0x56,0x0A,0x00,0x00,0xA0,0x02,
	// wav-8: symm.sawtooth +36dB
	0x00,0x0C,0xC0,0x0F,0x00,0x00,0x00,0x04,
        0x00,0x0C,0xC0,0x0F,0x00,0x00,0x00,0x04,
        0x00,0x0C,0xC0,0x0F,0x00,0x00,0x00,0x04,
	// wav-9: sq.wave: +12dB
	0xFC,0x00,0xFC,0x00,0x00,0x00,0x00,0x00,
	0xFC,0x00,0xFC,0x00,0x00,0x00,0x00,0x00,
	0xFC,0x00,0xFC,0x00,0x00,0x00,0x00,0x00,
	// wav-10: sq.wave: +24dB
	0xF0,0x03,0xF0,0x03,0x00,0x00,0x00,0x00,
        0xF0,0x03,0xF0,0x03,0x00,0x00,0x00,0x00,
        0xF0,0x03,0xF0,0x03,0x00,0x00,0x00,0x00,
	// wav-11: sq.wave: +32dB
	0x56,0x0A,0x56,0x0A,0x00,0x00,0x00,0x00,
        0x56,0x0A,0x56,0x0A,0x00,0x00,0x00,0x00,
	0x56,0x0A,0x56,0x0A,0x00,0x00,0x00,0x00,
	// wav-12: sq.wave: +36dB
	0xC0,0x0F,0xC0,0x0F,0x00,0x00,0x00,0x00,
        0xC0,0x0F,0xC0,0x0F,0x00,0x00,0x00,0x00,
	0xC0,0x0F,0xC0,0x0F,0x00,0x00,0x00,0x00
};

DAC güncelleme “frekans” ayarı konusuna geri dönelim: Ben bu iş için Timer-2 kesmesini kullanıyorum. TMR2’ye vereceğimiz bir reload değeri ile DAC güncelleme hızını ayarlarız, böylece dalga üretecimizin X ekseni hassas biçimde ayarlanmış olur.

CKCON0 |= 0x30;   // TMR2 clock source -> SYSCLK
TMR2CN0 = 0;      // 
// çalıştırmak:
TMR2CN0_TR2 = 1;  // bu register bit addressable
// çalma frekansını değiştirmek:
TMR2RLL = rld_L;
TMR2RLH = rld_H; 

TDAC (DAC Erişim Periyodu) = istenen işaret periyodu / N
N : Bir periyodun örnekleme sayısı (yukarıdaki örnekte bu 12)
T2 : (TMR2 tick period) = 1/49M
Reload Değeri = 65536 – (TDAC / T2)

Bir çalma ayarında istenen frekans değerini değil, TMR2 reload değerini saklamayı tercih ediyorum.

TMR2 kesmesinde yapılacak iş “sıradaki” sample’ı DAC’a yazmak. Oynatılacak (veya çalınacak diyelim) dalga örneklerini flash bellekte saklıyorum. Bu noktada iki farklı yaklaşım geliştirdim:
Birinci yöntemde, çalma ayarları yüklenirken (mesela bir sequencer’ın belli bir adımının zamanı geldiğinde) flash’tan, sequencer’ın belirttiği dalga setini okuyorum ve yukarıda anlattığım şekilde düzey ölçeklemesi yapıp (normalize sample’ların volume ayar değeriyle çarpılması) ram’de bir çalma tablosu oluşturuyorum (hatta iki tablo oluşturup biri çalarken diğerini yükleme ve geçişleri hızlı yapma gibi bir şey denedim ama 49MHz çalışmada bu çok kritik bir hızlandırma değil).
Çalınacak sesin genliği ya da dalga şekli değişinceye dek çalma tablosu artık değişmiyor.
İkinci yaklaşım daha sade: Çalınacak sample’ları flash’ta normalize halleriyle değil, ölçeklenmiş halleriyle saklıyorum ve çalma sırasında doğrudan flash’tan okuma yapıyorum. Okunacak dalga tablosu indeksini sequencer yüklemesinde bir rom pointer’ında belirliyorum ve sonra kesme kodunda doğrudan pointer’ı çalıştırıyorum. Bu yaklaşımda TMR2 kesme kodu şöyle:

// Play Kesmesi:
void ISR_TMR2 (void) interrupt 5
{ 
   TMR2CN0_TF2H = 0;
   // playBuffer ba$a sarmasi gerekiyor mu?:
   if ( smpIndex > 11 ) 
   {
     smpIndex = 0;
     playIndex = wavIndex;	// çalma indeksini de ba$a sar.
     // durdurma istegi verilmi$ mi?
     if (STOP_REQ) 
     {
       TMR2CN0_TR2 = 0;  // artik player kesmesi tetiklenmeyecek..
       STOP_REQ = 0;
       return;
     }
  }
    
  ///// DAC Update:
  SFRPAGE = 0x30;     
  DAC0L = waveTable[playIndex];  // yukarida anlattigim mevzu!!
  ++playIndex;
  DAC0H = waveTable[playIndex];
  SFRPAGE = 0;
  ++playIndex;
  ++smpIndex;
  
}

playIndex waveTable’ın sıradaki elemanına erişmek için kullandığımız dizi indeksi. Bunu, çalma adımını yüklediğimizde (ne çalacağımızı bize söyleyen veri) öğreniyoruz ve indeksi başlatıyoruz. Bunun yerine, muhtemelen daha efektif bir kesme kodu için
unsigned char code * volatile data playPtr;
şeklinde bir işaretçi tanımı yapıp bunu çalınacak rom sample’ları boyunca da koşturabilirdik.

Burada, özellikle dikkat edilecek bir nokta var: Kesme kodu içinde SFRPAGE değiştiriliyor. Interrupt handler yedeklemeyi hallediyor mu halletmiyor mu hiç endişe etmemek için, bu kesmeyi başka bir kesmenin kesmesi ihtimalini ortadan kaldıralım. (Böyle cümleleri şık hale getirmeye çalışmak işi sulandırmak olur, sonuçta anlatım bozukluğu yok).
Daha açık konuşursam, bu işlemci bir de UART kesmesi çalıştırıyor (native priority’si daha yüksek). Çipi başlatırken UART kesmesini TMR2 kesmesinden daha düşük öncelikli olacak şekilde ayarlıyorum. Çünkü gelen bir byte biraz bekleyebilir (baudrate düşük).
DAC yazması yaparken başka bir koda atlamak SFRPAGE anahtarlaması dışında da sorunlar yaratacaktır. Çünkü DAC güncelleme hızımız SYSCLK hızında. Bu arada yeri gelmişken onu da not edeyim: DAC güncelleme hızını SYSCLK yapmamızla TMR2 kesmesi içinde DAC yazması yapmamız arasında bir bağlantı yok. DAC’ın kendi güncellemesi başka bir şey. Sanırım yazılımdan daha bağımsız DAC otomasyonları yapmak istersek kullanışlı olur. Yine de, ben merak edip DAC update source ile benim DAC yazma kesmemi aynı timer ile çalıştırdım. Sonuç değişmedi. Sonuçta SYSCLK çok hızlı olsa da biz çıkışı 200kHz hızıyla güncelleyebiliriz (bunu da denedim). Bu, tıpkı ADC’de olduğu gibi, “elektriksel” bir kısıt. Kişisel görüşüm, 200k gayet yüksek bir hız!

Son olarak, bir sequencer (kullanıcı tarafından belirlenmiş dalga şekillerini verilen ayarlara göre bir diziden okumak ve art arda üretmek) yapacaksak, çalmayı anlık olarak durdurmak ve devam ettirmek için kullanabileceğimiz makroları da vereyim:

#define Play_Sequencer()  SFRPAGE = 0x30; DAC0CF0=0x80; SFRPAGE=0; \
			  TMR2L = seqData.pt_rld_L; \
                          TMR2H = seqData.pt_rld_H; \								  
                          TMR2CN0_TR2=1; IE_ET2=1; \ 					  
                          PLAYING=1; MUTE=0; LED1=0
								  
#define Stop_Sequencer()  MUTE=1; \
	  	          TMR2CN0_TR2 = 0; IE_ET2=0; \					  
                          SFRPAGE=0x30; \
			  DAC0L=0; DAC0H=0; \
			  DAC0CF0 = 0; \
			  SFRPAGE=0; PLAYING=0; LED1=1 

Sequencer’ın verilen adımındaki ayarlara bakarak DAC’ı konfigüre etmek de şöyle olabilir:

	// cfg.<pwr> bitlerine bak:
	 switch ( seqData.cfg & 0x03 )
	 {
		case 0:		// 2V çiki$
		  REF0CN = 0x80;	
		  DACGCF2 = 0x11;		  
		  DAC0CF1 = 0x00;
		break;
			
		case 1:	  // 1,2V çiki$
		  REF0CN = 0x40;
		  DACGCF2 = 0x10;
		  DAC0CF1 = 0x00;
		break;
			
		case 2:    // 1,6V çiki$
		  REF0CN = 0x80;
		  DACGCF2 = 0x12;			  
		  DAC0CF1 = 0x00;	  
		break;

		case 3:	 // 2,4V çiki$
		  REF0CN = 0x80;
		  DACGCF2 = 0x10;		  
		  DAC0CF1 = 0x00;
		break;
		
	}

	/// wav pointer'ini ba$latalim:
	wav_address = 0x3C00 + 24 * seqData.wav;	
	playPtr = (unsigned char xdata *) wav_address;
	smpIndex = 0;

        /// çalma frekansini güncelle:
        TMR2RLL = seqData.pt_rld_L;
        TMR2RLH = seqData.pt_rld_H;

Yukarıdaki örnek kodda 12 sample’lık bir wav dizisi kullanılıyor ve flash bellekteki 0x3C00 sayfası bu işe ayrılmış. Kullanıcının seçtiği wav indeksi seqData.wav parametresinden okunuyor.
smpIndex, sınır denetimi yaparken 16 bit sayılarla kendimizi yormayalım diye kullanılan yardımcı bir değişkendir. Kesme koduna bakabilirsiniz.

Sonuçta, gördüğünüz gibi, DAC kullanarak bir dalga şekli üretmek, eğer çıkışta analog bir işaret kullanma ihtiyacınız varsa PWM ile aynı sonucu elde etmeye çalışmaktan daha basit bir iştir. Yukarıda bir audio çıkışı (benim aklıma waveform generator deyince ilk bu geliyor) üzerinden örnek verdim. Ancak daha önce, programlanabilir bir sabit akım kaynağı işi için de bu DAC çıkışını kullanmıştım. Çalıştığımız alan, LED parlaklığını PWM ile ayarlamamızın mümkün olmayacağı bir uygulamaydı. Bu durumda EFM8’in bir çipte 4 adet bağımsız kullanılabilen DAC çıkışlarını bir sabit akım kaynağına referans girişi yaparak basit ama doğruluğu yüksek bir çözüm üretebildik. (Bunu da burada paylaşırım bir ara)

ADC Auto-Scan

Bir analog-dijital dönüştürücünün çalışma hızı ölçüm devresine ve ADC’nin yapısına bağlıdır. Öte yandan, çok sayıda analog kanalı örneklemek gibi bir ihtiyacımız varsa “hız”a dair kaygımız genellikle, ADC’nin kendi ölçüm hızı değil bizim kanallar arasındaki geçiş hızımızla ilgili olur. Çünkü biz bir işlemcinin üstündeki ADC’yi kullanıyoruz ve kanallar arasında geçmek ve ölçüm sonuçlarını bir yere yazmak gibi işler dönüştürme işleminden daha çok zaman alıyor.

Eğer çok sayıda analog girişi okumak gerekiyorsa ve dahası, bu kanalların aynı anda alınmış örnekleri (mesela bir besleme hattının akım ve gerilim ölçümü gibi) anlamlıysa veya kesin bir bir zaman aralığında alınmış belli sayıda örnek toplamak istiyorsak bu işleri donanımsal olarak halletmek işimizi çok kolaylaştıracaktır.

EFM8’de bu tür durumlarda kullanabileceğimiz bir tarama fonksiyonu var. Bu fonksiyon ADC modülünün ayarlarından bağımsız olarak istediğimiz zaman kullanabileceğimiz bir örnekleme otomasyonu sağlıyor.

  • ADC’yi istediğiniz referans ve örnekleme/dönüştürme hızı ayarlarıyla ayarlıyorsunuz.
  • Sonra tarama işleminin başlangıç kanalını seçiyorsunuz. Taramada en çok 4 kanal ardışık olarak örneklenebiliyor.
  • Sonra kaç kez örnekleme yapılacağını belirliyorsunuz. Burada, komutla başlatılan tek bir örneklemenin sayısı belirleniyor. Bir örnekleme kendi içinde istenen sayıda oversampling içerebilir.
  • Dönüştürme işlem sonucunun xram’de yazılacağı konumu belirliyorsunuz.
  • İşlemi başlatıyorsunuz.

Tarama ayarını bir kez yaptıktan sonra işlemleri otomatik başlatabilirsiniz. Tarama içindeki iki dönüştürme işlemi arasına sabit bir süre koyabilirsiniz veya tarama içindeki dönüştürmeleri birbirine olabildiğince yakın yaptırabilirsiniz.
Bir tarama işlemi yapılırken sonraki işlem için istenen ayarları hemen berlirleyip ADC’yi durmaksızın arka arkaya farklı ayarlarla çalıştırabilirsiniz. Bu, mesela double-buffering gereken okuma durumlarında işin buffer güncelleme kısmını çok basitleştirir.

Aşağıdaki kod örneği, AN6..AN9 kanallarının birer kez ölçülmesi ile elde edilen sonuçları xram’in 16..23 konumuna big endian olarak yazdırır:

SFRPAGE = 0x30;
ADC0MX = 6;		  // AN6 seç (ba$langiç kanali)
ADC0ASAH = 0x00;
ADC0ASAL = 0x10;	  // sonuçlari xram=16 adresinden itibaren yaz
ADC0ASCT = 3;		  // tarama 4 sample sürer (her kanal 1 kez)
ADC0ASCF = 0xC3;          // 4 kanal örneklenecek..	
ADC0ASCF = 0x43;          // _ASEN = 0
SFRPAGE = 0;
ADC0CN0_ADINT = 0;
ADC0CN0_ADBUSY = 1;

Yukarıdaki kodda en ilginç kısım, ADC0ASCF_ASEN bitini 1 yapıp işlemi başlattıktan sonra hemen sıfırlamamız.
ASEN=1 yapıldığında donanım tarama ile ilgili tüm ayarları dahili belleğe kopyalar. Yani, ASEN=1 yaptıktan sonra tüm ayarları bir sonraki tarama için değiştirebiliriz.

Yukarıdaki ayarlarla, ADINT=1 olduğunda işlemler bitmiş ve sonuçlar xram’e yazılmış olacak. Eğer durmaksızın sonraki 4 kanala geçmek isteseydik _ASEN=0 yapar yapmaz ADC0MX’i ve ADC0ASAL’i değiştirmemiz yeterliydi.

Ben bu örnekte öyle yapmadım ama ADC0ASCF_STEN bitini 0 yapıp ADC’yi bir timer ile tetiklenecek şekilde ayarlasaydık (ADC tetikleme kaynağı ayarının tarama özelliğiyle ilgisi yok) zaman aralıklı tarama çalışması elde etmiş olurduk. Belki ileride bununla ilgili bir örnek de paylaşırım.

ADC işleminin sonucunda üretilen veriyi eşzamanlı olmayan başka bir süreç kullanıyorsa bu işi otomatize etmek ve kullanıcı sürecin sadece ram’deki datayla uğraşmasını kodlamak performansı oldukça arttıracak bir iyileştirme. Donanımı kullanmasını bilmek ile donanımdan bağımsız kod yazmakta konfor aramak arasındaki farka oldukça dramatik bir örnek olarak göz önüne alınabilir.

nRF9e5 Test Programı

Kablosuz bağlantı kullanan bir şeyler üstünde çalışıyorsanız ve daha da kötüsü donanım da size aitse işe basit haberleşme denemeleri yapmakla başlamak mantıklı.
Burada, nRF9e5 sub-GHz SoC ‘u test etmek için basit bir uygulama paylaşıyorum.

Gerekenler:
* Test edeceğiniz nRF9e5’li board (device under test),
* Bunun üstündeki EEPROM’u programlamak için bir programlayıcı,
* PCT, PC’ye bağlanan bir radyo terminali
* PCT’yi izlemek için bir PC programı veya bir seri terminal programı,
* Vereceğim firmware’ler

Bu programda, radyo cihazları rol değiştirmiyor. PCT daima dinlemede, test edeceğimiz 9e5 cihazı da sadece gönderme yapıyor.
Program 8MHz kristal kullanıyor.
Program 9e5’in P02 portunu (Pin:2) her göndermede kısa bir süreliğine açıp kapatıyor. Buraya LED bağlayabilirseniz TX anlarını gözlemleyebilirsiniz.
Her 786,4ms’de bir veri gönderiliyor. Bunun zamanlamasını Timer1 yapıyor.
Veri gönderilen kanal 866MHz bandında 218. kanal.
Hedef adresi : E2.E2.E2.E2
Alıcının 8 byte payload beklemesi, bu ayarlarda olması ve 16 bit CRC mode’un açık olması yeterli. PCT bu ayarlarla yüklü biçimde dinleme modunda çalışmaya başlayacak ve bir radyo paketi aldığında bunu 62500bps hızında host PC’ye aktaracak.
Veri paketinin yapısı :

byte indexveri içeriği
00x47
1tx_counter[H]
2tx_counter[L]
30x11
40x19
50x35
60x62
70x01

Aşağıda, bootloader yaması yapılmış nRF9e5 firmware hex dosyasını bulabilirsiniz. Bu program yüklenir yüklenmez çalışmaya başlar:

PC Terminal Radyosu için de aşağıdaki firmware’i kullanabilirsiniz. Bu program başlamak için bir komut istemeden, PC’ye bağlar bağlamaz çalışmaya başlayacaktır:

Benim PCT’lerden kullanmıyorsanız, EFM8SB1 kullandığınız sürece, yukarıdaki firmware’i kendi donanımlarınızda da çalıştırabilirsiniz. LED_U (P0.3) ve LED_R (P1.5) çıkışlarının kullanımına dikkat edin!

Alınan verileri gösteren bir PCT PC arayüzünü de burada paylaşacağım.

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?

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.

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..

SendMessage() ile Uygulamalar Arası Veri Paylaşımı

Bazen, iki uygulama arasında veri alış verişi yapmam gerekiyor. Örneğin zamanında bir donanımla haberleşen bir program yazmışız ama sonra aynı donanımdan alınan veriyle başka bir şey yapan uygulamaya ihtiyacımız oluyor. Veya bir cihazdan gelen verileri kaydeden bir uygulamada radikal bir değişiklik isteniyor ve söz gelimi önceden binary dosyaya yaptığımız kaydı artık bir veritabanına yapmamız gerekiyor gibi.
Burada da belki örneğini vereceğim bir başka örnek durumda da, bazen bir programı denemek için normalde gerçek dünyadan gelecek olan verileri bilgisayarda simüle eden bir şeyler yazıyorum.

Uygulama hali hazırda dışarıdan aldığı verileri TCP ya da UDP üzerinden alıyorsa yerel sunucu/istemci üzerinden yine TCP/UDP haberleşmesi yapmak en basit yöntem. Ama daha genel bir uygulamalar arası veri iletişimi çözümü olarak Windows’un mesajlaşma mekanizmasını kullanmayı tercih ediyorum.

Windows’ta süreçlerin birbirleriyle asenkron olarak haberleşmesi için bir mesajlaşma yapısı var. Çeşitli olayları yanıtlayan nesneler yazarken bunları zaten kullanıyoruz. Ben özellikle TThread sınıfından türettiğim bazı veri işleme nesnelerinde mesajları veri giriş çıkışı için kullanıyorum. Bu mesajlaşma mekanizması aynı zamada uygulamalar arasında da veri iletişimi için kullanılabilir. Bunun için ben WM_COPYDATA mesajını gönderiyor/alıyorum:

WM_COPYDATA mesajının msg alanında TCopyDataStruct diye bir veri türü var. Mesaj içeriğinin aktarımını temelde bu record ayarlıyor.
Gönderici kısımda bu veri türünden bir değişkeni doldurup SENDMESSAGE( ) ile yayınlıyorum.

copyData: TCopyDataStruct;
// dataexchange ünitesindeki copyData değişkenini dolduruyorum:
dataexchange.copyData.dwData:= $0325;   // packet type identifier
dataexchange.copyData.cbData:= event_exchange.Size;
dataexchange.copyData.lpData:= event_exchange.Memory;

Windows.pas içinde tanımlı TCopyDataStruct diye bir tip var. Bu tipten bir değişken tanımlamam gerekiyor: copyData
Bunun dwData alanında mesajla iletilen veri tipini belirtiyorum.
cbData alanında da verilen pointer’ın işaretlediği verinin boyunu belirtiyorum.
lpData alanında da payload işaretçisini paylaşıyorum.
Bu arada, uygulama tarafındaki event_exchange nesnesi de bir TMemoryStream. Yollamak istediğim veriyi buraya yüklüyorum. Genel bir tanımlama olması için bir stream üzerinde çalışmayı uygun buldum. Yerel bir değişken tipi de tanımlayabilirdik.

SendMessage( ) ‘ı kullanmadan önce, hedeflediğimiz alıcı pencerenin o anda canlı olup olmadığına bakabiliriz. Bunun için bir başka windows api’si var:
FINDWINDOW( )
Bu fonksiyonun parametrelerinden söz etmem gerek: Harici bir uygulamanın penceresine erişmek için (buna Delphi’de form diyoruz) bize iki parametre gerekiyor. Biri hedef formun sınıf türü, diğeri de pencere (form) adı. FindWindow() api’si bu iki parametreyi istiyor ve bize dönüş olarak handle sayısını döndürüyor. Alıcı uygulama hangi platformda hazırlanmışsa, o platformun win api tanımını bilip bu iki değeri PChar olarak verebiliyor olmamız gerekir.

SendMessage( ) fonksiyonunun parametrelerinden biri FindWindow ile bulunan receiver_handle. sender_handle büyük ihtimalle gönderici ana formunun handle’ı olur. Daha sonra veri göndermek tek bir fonksiyon çağrısına kalır:

res := SENDMESSAGE(receiverHandle, WM_COPYDATA, Integer(Handle), Integer(@copyData));

Alıcı tarafta yapılması gereken, gönderici tarafta “alıcı form” olarak işaretlenen formun üstünde tanımlanmış bir mesaj yakalama procedure’ü tanımlamaktır:

 procedure WMCopyData(var Msg : TWMCopyData) ; message WM_COPYDATA;

WM_COPYDATA mesajını yakalaması için yazdığımız fonksiyona bir değişken parametre aktarmalıyız ki bu, Msg diye tanımladığım bir TWMCopyData tipinde bir kayıttır. Bu kayıt tipinin .copyDataStruct alanı göndericinin oluşturduğu copyData değişkeninin ta kendisidir!

  // gönderici handle numarası:
  sender:= Msg.From;
  // mesajın data içeriğinin dwData alanını mesaj tipini belirtmede kullanıyorum:
  packet_type:= Msg.CopyDataStruct.dwData;
  packet_size:= Msg.CopyDataStruct.cbData;
  if packet_type = MSG_EVENT_BUFFER then
  begin
   // paket işaretçisini cardinal tipli bir pointer'a eşliyorum:
    pc:= Msg.CopyDataStruct.lpData;  
    inc(pc);
    // ardından "asıl" payload geliyor (burada packet_size'a bakmak gerekebilir)
    pEvent:= PEventRec(pc);
    Process_Event_Data( pEvent , packet_size );
  end;

Yukarıdaki koddan anlayabileceğiniz gibi, gönderici uygulamanın bize gönderdiği payload’u pEvent diye bir işaretçiyle alıyor ve onu işleyen procedure’e veriyorum. En başta cardinal tipli bir sayaç verisi var, onu burada alıyorum. Payload birden çok Event datası içerebilir, o yüzden packet_size değerini de procedure’e veriyorum.

Bir arayüz simülasyonunda bu çalışmanın hayata geçmiş halini şuradan görebilirsiniz. Event receiver ve Event Simulator birbirinden bağımsız iki uygulama. Simulator ile geliştirmekte olduğum kullanıcı arayüzü ve kontrol birimini istediğim türde veriler ile besleyebiliyorum.

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.

MODBUS

RS485 donanımı ucuz, uygulaması kolay bir haberleşme arayüzü olarak öncelikli tercihlerimizden biri olmayı sürdürmektedir. Endüstriyel otomasyon ve veri toplama gibi işlerde eskilerden beri kullanılagelen Modbus namlı bir protokol vardır. Bu protokol çok basit, çok minimalist bir kullanım ile RS485 üzerinden multi-drop, yani ikiden fazla cihazın müşterek kullandığı bir haberleşme ortamı kurmaya iyi bir örnektir.

Yakın zamanda benden Modbus üzerinden kumanda edilen bir takım giriş/çıkış modülleri tasarlamam istendi. Bu vesileyle de, uzun yıllardır pek çok farklı işte kullanmakta olduğum Modbus protokol yürütmesi hakkında birkaç not yazayım diye düşündüm.

Burada, Modbus protokolünün embedded tarafta, kaynakları sınırlı işlemciler üzerinde yürütülmesine dair yaptığım bazı şeyleri paylaşacağım. PC tarafında da kendi yazdığım modbus erişim programlarına örnekler vereceğim.

MODBUS RTU

Modbus protokolünün RS485 hattı üzerinde çalışan RTU yürütmesini kullanıyorum. Bu yürütmede paket byte’ları binary olarak anlamlanıyor. 

Protokol byte formatı olarak 8-E-1 kullanılır diyor. Ancak ben 8-N-1 kullanıyorum. (Hâlâ, donanımsal parite hesabı yapmayan işlemciler kullanan insanlar var çünkü) Aslında protokol, eğer parite kullanamıyorsan bari stop bitini 2 tane yap da diyor ama ben bunu da yapmadım.

Modbus’un standart protokol tanımlamalarına “olabildiğince” sadık kalmaya çalıştım. Aslında her ne kadar yalınlığına saygı duysam da, RS485 multi-drop pek çok uygulamada modbus en etkin yöntem olmuyor. Eğer sizin tasarladığınız cihazların dışında cihazlarla da haberleşecek bir ağ öngörüyorsanız o zaman işlerin bir standardı olmalı dediğiniz için modbus’ı ciddiye alırsınız. Belki bir gün RS485 üzerinde daha efektif veri haberleşmesi yapan kendi yöntemlerimi de burada paylaşmaya başlarım.

MODBUS ZAMANLAMALARI

RS485 asenkron bir seri haberleşme ortamıdır. Bu da demektir ki bu ortamda birbiriyle haberleşecek cihazlar yollanan verilerin içeriği kadar zamanlaması üzerinde de anlaşmış olmalıdırlar.

Modbus spesifikasyonu, iki byte arasında en fazla 1,5 byte süresi kadarlık bir boşluğa izin verildiğini belirtiyor. Bundan çıkaracağımız iki sonuç vardır:

  1. Paket byte’larını bekleme yapmadan art arda yollamamız gerekir.
  2. Bir veri alıyorken, 1,5 byte süresi kadar alma yapmazsak paket sonuna geldik diyebiliriz.

Bir alma işlemi esnasında paket boyunu saymak zorunda kalmadan paket almanın sonuna geldiğimize karar verebilmek işleri kolaylaştıran bir şeydir. Öyle olmasaydı, modbus paket boyları sabit uzunlukta olmadıkları için, bir veri gelmeye başladığında doğru anda paket boyunun ne olacağını hesaplamış olmamız gerekirdi. Ki bunun için de gelen verinin kaçıncı byte’ında olduğuna göre çalışan bir sorgumuz olması gerekirdi. Bir uart rx kesmesinin içine karmaşık sorgular yazmak akıllıca bir iş değildir, bana güvenin.

Yine modbus spesifikasyonu, iki paket arasında en az 3,5 karakter süresi kadar bir boşluk olması gerektiğini söyler. Bundan da bize iki hisse çıkıyor:

  1. Bir paket aldığımızda, cevap vermeden önce, master’ın son byte’ı yollamasından beri 3,5 karakter süresi geçtiğinden emin olmamız gerek.
  2. Bir modbus hattına ilk bağlandığımızda (fiziksel bağlanmayı kastetmiyorum) konuşmaları dinlemeye başlamadan önce en az 3,5 karakter süresi kadar bir sessizlik olmasını beklememiz gerek (paketlerle “senkronize olmak”).
modbus frame sync zamanlama tanımları

Aslında, üzerinde biraz düşünürseniz yukarıdaki iki maddenin birbirini gerektirmediklerini anlarsınız. Bir slave, master’ın sorgusuna 3,5 karakter süresini beklemeksizin yanıt verirse diğer slave’lerin bakış açısından tek bir paket gelmeye devam ediyor gibi olacaktır ve slave’ler sıralarını kollamaya başlamayacaklardır. Bu bir sorun yaratmaz çünkü o esnada konuşan slave zaten master’a cevap veriyordur.

Haberleşme protokolü dediğiniz şeyler bu şekilde paralel dallanan önermelerle doludur. Protokol yürütme denen şeyde aranan sonuç tamamen mantıksal tutarlılık ya da en sadelik değil, bir şeyi açık şekilde kabul etmek esasına dayanır. O yüzden, bu aşamada fazlasına kafa yormayıp TÜM paketlerin en az 3,5 byte süresi aralıkla hatta yüklenmesini gözeteceğim.

Yukarıda, sözünü ettiğim 3 zaman tanımının haberleşme çerçevesindeki yerlerini gösterdim. Slave taraf işlemcimiz DataProcessor( ) fonksiyonunu çalıştırırken bu süreleri gözetmek zorundadır.

Modbus protokolü süreleri karakter müddeti cinsinden tanımlamış. Yani, bu byte süresi dediğim ölçü birimi bir byte’taki bit sayısı bölü geçerli baudrate kadar olacak demektir. Ancak yine modbus protokolü, yüksek baud rate’ler için sürelerin çok küçük olmasının önüne geçmek için 19200bps üstü hızlar için bu sürenin sabit olabileceğini söylüyor.

Ben en başta bu süreleri tamamen baudrate’e bağlı yapmıştım. Ancak sonra, protokol dediğin şeyde önemli olanın mantıksal bütünlük değil, yaptığını açık şekilde ifade etmek esasına dayandığını hatırlayıp (yukarıda bunu anlattım) tüm zamanlamaları 9600bps’ye göre hesapladım ve sabit yaptım.

Bir byte’ı 10 bit kabul ederek zamanlama parametrelerini şu şekilde alıyorum:

t35 = 4,17ms

t20 = 2,6ms

t15 = 1,56ms

modbus data processor stack state diagram
modbus data processor stack state diagram

UART RX Kesmesi:

Haberleşme protokolü üç yazılım modülünden oluşur:

Veri alma kesmesi

Veri işlem

Veri yollama

Yukarıda çizdiğim durum makinesinin ilerlemesi iki dış etki ile olur: Sisteme veri gelmesi ya da bir zaman aşımı olması. Bunun haricindeki zamanlarda Data Processor dediğim durum makinesi ya hiçbir şey yapmaz ya da yeni girdiği durum için işletmesi gereken kodu çalıştırıp durağan bir adıma gider. Thread şeklinde algoritma gerçeklemenin benim uyguladığım yöntemi bu.

Bu uygulamada UART kesmesi dediğimizde yalnızca RX kesmesini düşünmemiz yeterli. TX kesme ile yönetilmek zorunda değil. Öyle olması gereken durumlar elbette vardır ama bunu ayrıca anlatırım. RX kesmesi öyle bir şey olmalı ki, içindeki kod DataProcessor( ) ‘ün o anki durumuna bağlı olmamalı. Böylece kesme kodumuzu (işin donanımın parmak soktuğu kısmı) olabildiğince yalın, asıl fonksiyonlarımızla az teması olan bir hale getirmemiz mümkün olur. Bu, embedded programlamada başarıyı çok etkileyen bir şeydir.

rx kesme fonksiyonu

Hep bahsettiğim gibi, bir kesme fonksiyonu olabildiğince basit olmalıdır, ama daha basit değil.. 🙂

Byte hatası yoksa gelen byte Xbuffer‘ın Xptr indeksli elemanına yazılır ve Xptr ilerler.

Son olarak da zaman aşımı denetimi için kullandığım timer’ı sıfırlıyorum. Byte geldiği müddetçe zaman aşımı olmamalıdır.

Kesme kodu bundan ibaret olacak. Elbette UART modülünün ürettiği donanım hatalarını da handle edip bir flag ile ana programa bildiriyorum. Ama bu işin ilginç kısımlarından biri değil.

Data Processor Durumları:

DataProcessor( ) bir thread fonksiyonu olarak kullanılır. Yani, ana sistem döngüsünde her geçişte koşulsuz olarak çağrılan bir fonksiyondur ve koşulsuz olarak da sistem kaynağı kullanır ( bir donanım timer’ı ve uart kesmesi). DataProcessor’ün timer overflow ya da uart rx kesmelerine vereceği tepkinin hızı ana sistem çevriminin periyoduna bağlı olacaktır.

DataProcessor( ) her çağrıldığında belli bir durumda olur. Durum geçişleri yalnızca fonksiyonun kendisi tarafından yapılır, yani hiçbir dış kod DataProcessor( ) state machine’in durumunu belirleyen commstat değişkenine erişemez.

DISABLED:
UART kapalı ve makine devre dışında. Yani modbus fonksiyonumuz kapalı.
Bu durumdan çıkış için kullanıcı kodun comm_cmd = CC_INIT yapması gerek.
Bu komut algılanınca Restart_RS485( ) fonksiyonu ile UART RX etkinleştirilir.

LISTENING:
UART veri alma etkin, yani makine Modbus hattını dinliyor ancak alınan byte’larla bir işlem yapılmıyor.
Bu durumdan çıkış için T35 süresi boyunca hiç RX olmaması gerekir. T35 süresi boyunca veri alınmaması Modbus hattının boşta durumda olduğu anlamına gelir. Makine artık paketleri dinleyip adresleri kontrol edeceği senrkonize duruma geçebilir.
Eğer bir byte hatası olursa makine SLEEP durumuna atlar.

SYNCD:
3,5 byte süresi boyunca hat sessiz kaldı. Demektir ki şu anda Modbus hattında iletilen bir paket yok. Artık makine senkronize durumda.
Bundan sonra gelen her byte’a bir paketin ilk byte’ı bu deyip adres byte’ı muamelesi yapacağız. Burada iki olasılık var: Paket bizi adresliyordur (ADDRESS_HIT) ya da adreslemiyordur (ADDRESS_MISS).
ADDRESS_MISS için SLEEP durumuna geçilir.
ADDRESS_HIT için de paketin devamını almak için PACKET_RX’e geçilir. Eğer bir byte hatası olursa makine SLEEP durumuna atlar.

PACKET_RX:
Veri alınıyor.
Bu durumda iken makine Xptr indeksini izler. Modbus fonksiyonu için ayrılmış buffer boyunun aşılması BUFFER OVERRUN hatası demek olur ve bu durumda paket alımı iptal edilip SLEEP adımına atlanır.
Bu durumdan çıkış paket sonu ile olacak. Paket sonunun algılanması T15 süresi boyunca bir byte alınmaması ile olur.
Eğer bir byte hatası olursa makine SLEEP durumuna atlar.

PARSE:
Paket sonu algılandığında, veri alma kapatılır.
Xptr sayacı alınan byte sayısını göstermektedir.
Geçerli bir Modbus master paketinde olması gereken genel koşulların sağlaması yapılır. Paket geçerli gözüküyor ise paket türüne bakılır ve ilgili komut işlemine atlanır
Paket hatalı gözüküyorsa SLEEP durumuna atlanır.

SLEEP:
Bir hata ya da ADDRESS_MISS durumunda ortalama bir paket boyu kadar bir süre boyunca makine kapalı durumda bekler. Bu süre baudrate’e göre belirlenir.
Bu sürenin geçmesi sonrasında makine Restart_RS485( ) seri haberleşme yeniden başlar. Makine LISTENING durumuna gider.

Bundan sonraki makine durumları alınan Modbus komutu ile ilgili işlemler ve verilecek yanıtlar ile ilgili kodlardır. Bunları ayrı ayrı anlatmak yerine desteklediğim Modbus komutlarından söz etmeyi uygun buluyorum:

DESTEKLENEN MODBUS KOMUTLARI

Modbus Fonksiyon Kodları
MODBUS fonksiyon kodları

Bu fonksiyon kodlarının kendi başlarına ne anlama geldiklerinin çok fazla konuşulmaya değer bir yanı yok. Önemli olan, üzerinde çalıştığımız cihazda bu kodlar ile neleri okuyup yazacağımızdır. İşin özeti şu: Elinizde bir cihaz vardır. Bu cihazın modbus üzerinden görünür bir bellek haritasını oluşturmuşsunuzdur. Yukarıdaki komutlar artık bu harita üzerinde bir anlam taşır. Bu konuda da olabildiğince özgürsünüz. Holding Register denen şeyle dijital giriş durumu da yollayabilirsiniz. Sonuçta bunu güzelce açıklamayı becerirseniz söz gelimi PLC ile cihazınıza okuma yapan adam holding register olarak okuduğu Word’leri bit olarak kullanabilir. Yani, Modbus’ın tanımındaki veri tiplerinin aslında pek bir bağlayıcılığı yoktur.

Şimdi benden istenen Modbus I/O modüllerinin bellek haritalarına bakalım. Okuyucuya genel bir bakış açısı kazandırmak için şöyle bir not ekleyeyim: Benim için modbus haberleşmeli bir uzak I/O modülü geliştirmek demek 3/4 oranında aşağıdaki tabloları hazırlamak demekti. Bundan sonrası zaten yokuş aşağıdır.

Komutlardan da anlayacağınız gibi Modbus, 4 bellek tipi üzerinde veri erişimi komutlarına sahiptir:

DISCRETE OUTPUTS (Y)
00001 adresinden başlayan dijital çıkış bitleridir.
Bu alan, 0x01 komutu ile okunur ve 0x05, 0x0F komutları ile de buraya yazılır.
Komut adresleri haberleşmede 0 -> 00001 olacak şekilde kullanılır.
DISCRETE INPUTS (X)
10001 adresinden başlayan dijital çıkış bitleridir.
Bu alan 0x02 komutu ile okunur.
Komut adresleri haberleşmede 0 -> 10001 olacak şekilde kullanılır.
ANALOG INPUTS (R)
30001 adresinden başlayan 16 bitlik sadece okunur register alanıdır.
Bu alan 0x04 komutu ile okunur.
HOLDING OUTPUTS (H)
40001 adresinden başlayan 16 bitlik okunabilen ve yazılabilen register alanıdır.
0x03 komutu ile okunur, 0x06 komutu ile yazılır.

X/Y Giriş Çıkışları:

Yaptığım cihazlarda değişik sayıda dijital girişler ve röle çıkışları var. Yani, bu ürünlerin işlevselliği büyük oranda dijital girişleri okumak ve röle çıkışlarını kumanda etmekten ibaret. O yüzden cihazların belirleyici özelliği aşağıda verdiğim X ve Y adres haritalarıdır.

Cihazların gerçek giriş çıkışlarını X ve Y haritalarında 1 adresinden başlayarak konumlandırdım:

MODISEL Cihazları Y Modbus Bellek Haritası

Görseli tam boyutlu görmek için TIKLAYIN.

MODISEL cihazları X Modbus Haritası

Görseli tam boyutlu görmek için TIKLAYIN

Input Register’ları:

Bu cihazlarda tam sayı olarak ifade edilebilecek bir giriş ya da çıkış bulunmuyor. Ancak, dijital giriş çıkışları birer bitmap olarak bu bellek haritasında okunabilir yapmak iyi bir fikir. Bu şekilde girişleri okumak için ayrı çıkışları okumak için ayrı bir komut kullanmak zorunda kalınmadan tek seferde her şey okunabilir.

Modisel Cihazları MODBUS Analog Input Register Map

Görselin tam boyutunu görmek için TIKLAYIN

Holding Register’lar:

Ben bu bellek alanını, yazılabilir olduğu için input register alanından ayrı tanımlıyorum. Buradaki “holding” sıfatına saygı olarak, cihazların silinmez belleğine yazılan parametreleri bu haritada tanımladım. Yine de bu haritanın bir kısmını son kullanıcı için sadece okunur yaptım (kullanıcının cihaz kimlik bilgilerini değiştirebilmesini istemem).

Input register alanında X ve Y vektörlerini bitmap olarak göstermeye benzer şekilde burada da Y vektörünü binary olarak yazma özelliği düşünülebilirdi ama zaten 0x0F komutu tam olarak bu işi yaptığı için böyle bir şeye gerek yok.

Modisel cihazları MODBUS Holding Register Map

Tablonun tam açıklamalarını görmek için TIKLAYIN

MODBUS Komutları:

MODBUS komutlarının benim uygulamalarımdaki yürütmesi belli kısıtlamalara tabi olabilir. Sonuçta protokolün kendisi çok genel olduğu için biz bazı yönleri bizdeki özelliklere ve kısıtlamalara göre basitleştirebiliriz.

Örneğin, discrete okuma komutlarında hedef adresinin 8’in katları şeklinde olmasını şart koşuyoruz. Benzer şekilde, 8’in katı olmayan vektör boylarını da kabul etmiyoruz. Bu işlevsel bir kısıtlama değil. Çünkü bu şart, genel modbus tanımlamasına bir istisna oluşturmuyor. Yalnızca onun bir alt kümesini geçerli kabul ediyor.

Ayrıca, 0x04: Read Input Register komutunda komutun serbestçe hazırlanmasına izin vermiyoruz. Bu komutu kullanacak kişi bir seferde cihazın 4 word’lük R bellek alanının tamamını okumak zorunda.

modbus fonksiyon kodu 01 Read Discrete Output
modbus fonksiyon kodu 01 Read Discrete Output
modbus fonksiyon kodu 02 Read Discrete Inputs
modbus 02 RDI komutu
modbus fonksiyon kodu 03 read holding registers
modbus fonksiyon kodu 03 read holding registers
modbus fonksiyon kodu 04 Read Input Registers
modbus fonksiyon kodu 04 Read Input Registers
modbus fonksiyon kodu 05 write discrete output
modbus fonksiyon kodu 05 Write Discrete Output
modbus fonksiyon kodu 06 Write Single Holding Register
modbus fonksiyon kodu 06 Write Single Holding Register
modbus fonksiyon kodu 0F Write Multiple Outputs
modbus fonksiyon kodu 0F Write Multiple Outputs

Son olarak, yukarıda anlattıklarımın kodda nasıl gözüktüğünü paylaşmak istiyorum.

Ana programda koşulsuz olarak çağrılan bir thread fonksiyonum var:

    RS485Thread[rs485_status] ();

Bu çağrı aslında bir fonksiyon pointer çağrısı. Dizinin elemanları şunlar:

static void (* RS485Thread[]) () = 
{   
    RS485_Disabled, 
    RS485_Listening, 
    RS485_Syncd,
    RS485_Receiving, 
    RS485_Parse,
    RS485_Handler_01, 
    RS485_Handler_02,
    RS485_Handler_04,
    RS485_Handler_05,
    RS485_Handler_0F,
    RS485_Response
};

Daha önce anlattığım dinleme, senkronize olma, veri alma ve alınan veriyi çözümleme işlemlerinin her biri bir fonksiyon adımı olarak işletiliyor. RS485 işlerini yürüttüğüm ana thread bu adımlar arasında dönüp duruyor.

/// RS485 durumları:
typedef enum
{
    DISABLED = 0,       // Transceiver devre dışı
    LISTENING,          // Senkronize olma süresi bekleme
    SYNCD,              // rs485 hattına senkronize olunup paket başı bekliyor olma
    RECEIVING,          // Cihaz adreslendi, veri alınıyor
    PARSE,              // veri paketi tamamlandı, içeriğe bakılıyor
    HANDLER_01,         // READ DISCRETE OUTPUTS komut handler'ı
    HANDLER_02,         // READ DISCRETE INPUTS komut handler'ı
    HANDLER_04,         // READ INPUT REGS komut handler'ı
    HANDLER_05,         // WRITE DISCRETE OUTPUT komut handler'ı
    HANDLER_0F,         // WRITE MULT.DISC. OUTPUT komut handler'ı
    RESPONSE            // yanıt paketini yollama adımı:
} RS485_STATUS;

static RS485_STATUS rs485_status;      // RS485 thread control 
unsigned char uart_tick;        // uart zamanlama sayacı

uart_tick, sistem tarafından işletilen birkaç ms periyotlu bir serbest timer. Thread’in zamanlaması için buna ihtiyacımız var. Ayrıca, bize veri geldiğinde tetiklenen bir de kesme lazım:

void interrupt HI_ISR(void) 
 {
     // UART RX kesmesi :
     if (PIR1bits.RC1IF == 1)     
     {   
         Xbuffer[Xptr] = RCREG1;
         ++Xptr;
         uart_tick = 0;
         PIR1bits.RC1IF = 0; 
     }      
 }

İronik bir şekilde bu yazıyı artık pek kullanmadığım PIC18 işlemcisi üstünde çalışan bir kod ile örneklendirdim. Bu işlemcide artık bana bile tuhaf gelen bir şekilde, kesmeler için müşterek bir vektör var. O yüzden kesme handler’ı içinde bir kez daha receive interrupt flag ‘i sorguluyoruz. Ayrıca burada uart hatası kesmesi diye bir şey yok ki bunu handle etmemek bana pek doğru gelmiyor. Neyse, bunların modbus’ın kendisi açısından pek bir önemi yok. Biz şimdi sözü state fonksiyonlarına bırakıp bu yazıyı noktalayalım:

////////////////////////////////////////////////////////////////////////////////
//// RS485 - Modbus State Functions:
////////////////////////////////////////////////////////////////////////////////

// seri port kapalı: bir timer olayı ile yeniden başlatılacaktır.
static void RS485_Disabled(void)
{
    // Bu durumda iken, tick gelmesini bekleriz: 
    if ( uart_tick > 1)                    // uart timer'ından kesme gelene kadar burada dur...
    {
        uart_tick = 0;
        // UART'ı yine aç:
        Xptr = 0;
        RS485_DIR = 0;          // alici modu
        RCSTA1bits.SPEN = 1; 
        RCSTA1bits.CREN = 1;
        PIE1bits.RC1IE = 1; 
        rs485_status = LISTENING;   // veri bekleme modunda bekleyeceğiz
    }    
}

// alıcı boşta: (yani master'dan komut bekleme adımı)
static void RS485_Listening(void)
{
    /// sync beklerken buffer overrrun olabilir:
    if ( Xptr > 19 )  Xptr = 0;
    
    /// senkronize duruma gelmek için T35 timeout'u bekiyoruz:
    if ( uart_tick > 1 )
    {
        uart_tick = 0;
        Xptr = 0;
        rs485_status = SYNCD;
    }
}

// alıcı dinleme moduna geçti: gelen byte paket başı olacak.
static void RS485_Syncd(void)
{
    if ( Xptr )  // başlık alındı mı?
    {
      // addr. hit =>
        if (( Xbuffer[0] == my_rs485_address)||( Xbuffer[0] == 255 ))
        {
            LED2 = 1;
            uart_tick = 0;
            rs485_status = RECEIVING;
        }
        else // addr. miss =>
        {
            Disable_RX();
            rs485_status = DISABLED;             
        }
    }      
}

// veri alınıyor: bizi adresleyen bir paket geliyor.
static void RS485_Receiving(void)
{
    // üzerinde çalışılabilecek max. rx paket boyu aşıldıysa yanıt verilmeyecek:
    if ( Xptr > 29 )
    {
        Disable_RX();
        rs485_status = DISABLED; 
    } 
    else if ( uart_tick > 1 )
    {
       Disable_RX();
       rs485_status = PARSE;
    } 
}


static void RS485_Parse(void)
{
    unsigned char c;
    
    ////////////////////////////////////////////////////////////////////////////
    // gelen paketin doğruluğunun kontrolü
    // komut byte'ı doğru mu:
    if ( ( Xbuffer[1] > 0x10 )||( Xbuffer[1] == 0 ) ) 
    {
        rs485_status = DISABLED;
    }
    // paket boyu beklenen en düşük değerin de altındaysa:
    else if ( Xptr < 8 )  
    {
        rs485_status = DISABLED;
    }
    // bu aşamada gelen verinin CRC'sini sorguluyoruz:
    else
    {
        myCRC.i = 0xFFFF;
        c = Xptr-2;         // crc hesabına girecek son byte indeksi
        // gelen paketin crc'sini hesapla: (0..n-2 byte'lar arası)
        for (Xptr=0; Xptr<c; Xptr++)
        {
            DoCRC(Xbuffer[Xptr]);
        }
        // TODO: crc doğru mu bakmak!
        // gelen crc hesapladığımızla aynı mı:
        /*
        if ( ( myCRC.b[0] == Xbuffer[c] ) && ( myCRC.b[1] == Xbuffer[c+1] ) )
        {
           if ( Xbuffer[1] == 0x01 )  rs485_status = HANDLER_01;
        }   
        else rs485_status = DISABLED;
         */
        if ( Xbuffer[1] == 0x01 ) rs485_status = HANDLER_01;
        else if ( Xbuffer[1] == 0x02 ) rs485_status = HANDLER_02;
        else if ( Xbuffer[1] == 0x04 ) rs485_status = HANDLER_04;
        else if ( Xbuffer[1] == 0x05 ) rs485_status = HANDLER_05;
        else if ( Xbuffer[1] == 0x0F ) rs485_status = HANDLER_0F;
        else rs485_status = DISABLED;
        
    }
       
}

/// Read Discrete Outputs
static void RS485_Handler_01(void)
{
   // genel çağrı adresi için bu komut yanıtlanmaz:
    if ( Xbuffer[0] == 255 )
    {
        uart_tick = 1;      
        rs485_status = DISABLED;
    }
    // vector start 0 olmalı:
    else if ( Xbuffer[2] & Xbuffer[3] )
    {
        uart_tick = 1;
        rs485_status = DISABLED;
    }
    // vector size = 32 olmalı 
    else if ( Xbuffer[4] != 32 )
    {
        uart_tick = 1;
        rs485_status = DISABLED;
    }
    // her şey doğru ise:
    else
    {
       // çıkışların geçerli durumunu oku:
       Read_Y();  
       // yanıt paketini hazırla:
       Xbuffer[0] = my_rs485_address;
       Xbuffer[1] = 1;
       Xbuffer[2] = 4;
       Xbuffer[3] = dY[0].b[0];
       Xbuffer[4] = dY[0].b[1];
       Xbuffer[5] = dY[1].b[0];
       Xbuffer[6] = dY[1].b[1];
       // yanıt paket boyu:
       response_size = 7;
       rs485_status = RESPONSE;
    }
}


/// Read Discrete Outputs
static void RS485_Handler_02(void)
{

   // genel çağrı adresi için bu komut yanıtlanmaz:
    if ( Xbuffer[0] == 255 )
    {
        uart_tick = 1;      
        rs485_status = DISABLED;
    }
    // vector start 0 olmalı:
    else if ( Xbuffer[2] & Xbuffer[3] )
    {
        uart_tick = 1;
        rs485_status = DISABLED;
    }
    // vector size = 48 olmalı 
    else if ( Xbuffer[4] != 48 )
    {
        uart_tick = 1;
        rs485_status = DISABLED;
    }
    // her şey doğru ise:
    else
    {
       // girişlerin geçerli durumunu oku:
       Read_X();  
       // yanıt paketini hazırla:
       Xbuffer[0] = my_rs485_address;
       Xbuffer[1] = 2;
       Xbuffer[2] = 6;
       Xbuffer[3] = dX[0].b[0];
       Xbuffer[4] = dX[0].b[1];
       Xbuffer[5] = dX[1].b[0];
       Xbuffer[6] = dX[1].b[1];
       Xbuffer[7] = dX[2].b[0];
       Xbuffer[8] = dX[2].b[1];
       // yanıt paket boyu:
       response_size = 9;
       rs485_status = RESPONSE;
    }
}


static void RS485_Handler_04(void)
{
    unsigned char a, b, i;
   // genel çağrı adresi için bu komut yanıtlanmaz:
    if ( Xbuffer[0] == 255 )
    {
        uart_tick = 1;      
        rs485_status = DISABLED;
    } 
    else if ( (Xbuffer[2] > 11) || (Xbuffer[4] > 12) )
    {
        uart_tick = 1;
        rs485_status = DISABLED;
    }
    else if ( (Xbuffer[2] + Xbuffer[4]) > 12 )
    {
        uart_tick = 1;
        rs485_status = DISABLED;        
    }
    else 
    {
        // dR alanında haritalanmış dX ve dY vektörlerini güncelle:
        Read_X();
        Read_Y();
        dR[2].i = dX[0].i;
        dR[3].i = dX[1].i;
        dR[4].i = dY[0].i;
        // yanıt paketini hazırla:
        a = Xbuffer[2];  // okuma vektörü başlangıç adresi
        b = Xbuffer[4];  // okuma vektörü eleman boyu
        // header:
        Xbuffer[0] = my_rs485_address;
        Xbuffer[1] = 4;
        Xbuffer[2] = 2*b;
        
        // dR okuma vektörünü kopyala:
        for ( i=0; i<(a+b); i++ )
        {
            Xbuffer[2*i+3] = dR[a+i].b[0];
            Xbuffer[2*i+4] = dR[a+i].b[1];
        }
       
       response_size = 2*b + 3;
       rs485_status = RESPONSE; 
       
    }
    
}


static void RS485_Handler_05(void)
{
    unsigned char bix;
    unsigned int mask;
    
   // operator word'ünün Hi byte'ı kesinlikle 0 olmalıdır:
   if ( Xbuffer[4] )
   {
        uart_tick = 1;      
        rs485_status = DISABLED;       
   }
   else if ( Xbuffer[2] > 31 ) 
   {
        uart_tick = 1;      
        rs485_status = DISABLED;        
   } 
   else
   {
       // adreslenen bit dY[1] üzerinde ise:
       if ( Xbuffer[2] > 15 )
       {
           bix = Xbuffer[2] - 16;
           mask = (unsigned int) ( 1 << bix );
           if (Xbuffer[5] == 0)     dY[1].i &= ~(mask);
           if (Xbuffer[5] == 0xFF)  dY[1].i |= mask;
           if (Xbuffer[5] == 0x11)  dY[1].i ^= mask;    
       }
       // adreslenen bit dY[0] üzerinde ise:
       else
       {
           bix = Xbuffer[2];
           mask = (unsigned int) ( 1 << bix );
           if (Xbuffer[5] == 0)     dY[0].i &= ~(mask);
           if (Xbuffer[5] == 0xFF)  dY[0].i |= mask;
           if (Xbuffer[5] == 0x11)  dY[0].i ^= mask;              
       }
       
       UpdateOutputs();
     // komuta verilecek yanıt, komutun aynısının tekrar edilmesidir:
     response_size = 6;
     rs485_status = RESPONSE;       
   }
    
     
}


/*
 * 
 *
 */
static void RS485_Handler_0F(void)
{
  // bit address = 0, bit count = 32 olmalı.. H byte'lar önemli değil..
   if ((Xbuffer[2]) || (Xbuffer[4] != 32))
   {
     uart_tick = 1;
     rs485_status = DISABLED;
   } 
   else if ( Xbuffer[6] != 4 )
   {
     uart_tick = 1;
     rs485_status = DISABLED;
   }
   else
   {
     // gelen güncellemeyi dY vektörüne al:
     dY[0].b[0] = Xbuffer[7]; 
     dY[0].b[1] = Xbuffer[8];
     dY[1].b[0] = Xbuffer[9];
     dY[1].b[1] = Xbuffer[10];
     // ve output vektör güncellemesini çıkışlara yansıt:
     UpdateOutputs();
     // yanıt paketi, gelen paketin ilk 6 byte'ı + CRC:
     response_size = 6;
     rs485_status = RESPONSE;       
   }
}


/*
 * Bu fonksiyon Xbuffer[ ] dizisindeki response_size adet payload'u seri porttan yollar
 * Ardına da yollama esnasında hesaplanan 2 byte CRC'yi ekler.
 */
static void RS485_Response(void)
{
    unsigned char i;
    // not: Response fonksiyonuna girilmeden önce uart kesmeleri kapatılmış olmalı!
    RS485_DIR = 1;              // transceiver'ı gönderme moduna sok..            
    RCSTA1bits.CREN = 0;        // uart alıcısını kapat..
    TXSTA1bits.TXEN = 1;        // uart göndermesini devreye al..
    myCRC.i = 0xFFFF;           // CRC hesaplama değişkenini başlat
                                // gönderilen her byte için crc hesabı yapılmalı  
    for (i=0; i<response_size; i++)
    {
        TXREG1 = Xbuffer[i];
        DoCRC(Xbuffer[i]);
        while (TXSTA1bits.TRMT == 0) ;
    }
    // payload gittikten sonra CRC'yi yolla:
    TXREG1 = myCRC.b[0];
    while ( TXSTA1bits.TRMT == 0 );
    TXREG1 = myCRC.b[1];
    while ( TXSTA1bits.TRMT == 0 );
    
    TXSTA1bits.TXEN = 0;
    uart_tick = 1;
    rs485_status = DISABLED;   
    
    RS485_DIR = 0;
}