AVR- I2C (TWI) AHT10 Kullanımı Bölüm 2

 Önceki bölümde TWI biriminde bulunan registerler ile veri yazma ve okuma işlemini yaparak çalışma mantığını anlatmış oldum. Bu bölümde bu işlemleri birer fonksiyon halinde derli toplu bir hale getireceğim. TWI kesmesiyle yazma-okuma yaparak AHT10 ile sıcaklık ve nem bilgisini alacağım.

Fonksiyonlar

Başlatma, start ve stop için burada tekrar söyleyecek bir şey yok. Kesme kullanacağım için sadece TWIE bitini "1" yapacak kodu ekliyorum. Bu fonksiyonları aşağıda görebilirsiniz. Veri okuma ve yazma kısmında işler biraz farklı olduğundan bu fonksiyonlara geçiyorum.

Veri Yazma

Bunun için birkaç farklı fonksiyon tanımladım. Adres ve komut bilgisi sonrası göndereceğim veriyi bir tane veya birden fazla olacak şekilde göndermemi sağlayan fonksiyonlar oluşturdum. Fonksiyonların ilk parametresi slave aygıtın adresidir. Yazma olduğu için komut belli bunda bir parametre oluşturmaya gerek yok. Bir byte veri göndereceğim fonksiyonda gerekli değil ama birden fazla olacaksa ne kadar göndereceğimi de parametre olarak girmem gerekir. Dizi oluşturup gönderir ve boyutu için NULL (0x00) karakterine kadar gönder diyebilirim ama sensörler NOP (0x00) boş komut isteyebilirler. Bu durumda NOP gönderme şansım olmaz. Önceden tanımlı sensör komut dizisi göndermek için de bir fonksiyon oluşturdum. Bir diğer parametre tekrarlayan başlangıç (repeated start) için gereklidir. Stop koşulu oluşturmadan yazma veya okumaya devam etmek için kullanabiliriz.


Burada ilk veri 0x61 gittikten sonra stop koşulu gerçekleşmiş sonraki yazma komutu ve adres başlangıcı için start oluşmuş ve sonraki veri yazılmış.


Burada ilk veri 0x61 gittikten sonra stop koşulu gerçekleşmemiş sonraki yazma komutu ve adres başlangıcı için start oluşmuş ve sonraki veri yazılmış. Bu durumu gerçekleştirmek için bir değişken kullandım. İki tip fonksiyon için en önemli parametre yazacağımız veridir. Bir tane olduğunda sorun yok ama birden fazla olduğunda bu verileri bir bellekte tutmamız ve yazma işini oradan yürütmemiz gerekir. Belleğin neresinde olduğumuzu da bilememiz ve bunun için de bir işaretçi oluşturmamız gerekir.

Daha önce UART kullanımı ile ring buffer konusunu yazmıştım burada tekrar etmeyeceğim. Fonksiyona parametre olarak gelen verileri belleğe birer birer kaydederken verinin kayıt yerini tutan işaretçiyi de birer birer artırıyorum. Bu işlemin ne kadar olacağı ya NULL karaktere ya da dizinin boyutunu bildirdiğim ve parametre ile gösterdiğim boyutla sınırlı olacaktır. Bu ana fonksiyonlar bir önceki konuda alt başlıklarla ifade etmeye çalıştığım start, adres ve data gibi alt fonksiyonlardan oluşacaktır. İlk olarak adres ve komutu (R/W) parametrelere bağlı olarak bir değişkene kaydediyorum. Sonraki adım olarak veriyi belleğe alıyorum. En son olarak start koşulu oluşturarak TWI birimi iletişimi başlatıyor.

Alt Fonksiyonlar

void i2c_start(){//start koşulu için gerekli bitler 1 yapıldı
	TWCR=(1<<TWEN)|(1<<TWSTA)|(1<<TWINT)|(1<<TWIE);
	while(!(TWCR & (1<<TWINT)));// twint beklendi
}
void i2c_adr(uint8_t adr,i2c_cmd_t cmd){	
	Sladr_RW=((adr<<1)|cmd);//adres bir bit sola kaydırıldı ve komutla değişkene yazıldı
}
void i2c_data(uint8_t data){
	i2c_tx_bas=(i2c_tx_bas+1)&I2C_Tx_Mask;//veri işaretçisi artırıldı
	i2c_tx_ring[i2c_tx_bas]=data;//işaretçinin gösterdiği belleğe veri yazıldı
}
void i2c_end( i2c_rep_t repst){
	i2c_rep=repst;//repeated start kontrol
	i2c_start();//start koşulu
}
void i2c_stop(){
	TWCR=(1<<TWEN)|(1<<TWSTO)|(1<<TWINT)|(1<<TWIE);// stop koşulu
}

 

Sadece bu fonksiyonlar ile veri göndermek mümkün. Sırasıyla adres, data (veri miktarı kadar) ve end fonksiyonu çağrılarak işlem yapılır. Burada görünmeyen asıl işlemler TWI kesme rutini içindedir. Ana fonksiyonlara göz atıp kesme içinde neler olduğunu anlatmaya çalışacağım. Bir sensörün veya farklı bir entegrenin komut setinde çeşitli veriler olacaktır. Bunları söylediğim sırayla tek tek yazmak yerine bir fonksiyon halinde birleştirmek daha doğru olur. Bu amaçla üç tane fonksiyon oluşturdum. Bunlardan biri tek bir veri için, diğer ikisi birden fazla veri göndermek içindir. Karakter dizisi göndermek için tanımladığım fonksiyona belki hiç gerek olmayacak.

Ana Fonksiyonlar

void i2c_send_data(uint8_t adr, uint8_t data,  i2c_rep_t repst){	
	i2c_adr(adr, I2C_WRITE);
	i2c_data(data);	
	i2c_end(repst);
}
void i2c_send(uint8_t adr, uint8_t* str, uint8_t len,  i2c_rep_t repst){
	i2c_adr(adr, I2C_WRITE);
	for (uint8_t i=0;i<len;i++){//dizi boyutu kadar
		i2c_data( str[i]);// belleğe yazar
	}
	i2c_end(repst);
}
void i2c_send_str(uint8_t adr, const char* str,  i2c_rep_t repst){
	i2c_adr(adr, I2C_WRITE);
	while (*str){//0x00 bulana kadar
		i2c_data(*str++);// belleğe yazar		
	}	
	i2c_end(repst);	
}

Veri Okuma

Okuma kısmında bir  fonksiyon yeterli oluyor. Hangi adresten ne kadar okuyacağımızı parametre olarak aktardığımız fonksiyon ilk olarak adres ve komutu (R/W) değişkene yazıyor. Veri miktarını tutan bir başka değişkene kayıt yapıp start koşuluna gidiyor. Burada tüm iş kesme rutini içinde yapılıyor. Gelen veri belleğe kaydediliyor. Bu kayıt işlemi içinde bellekteki yeri tutan işaretçi artırılıyor. Bu sayede verinin alındığını öğreniyoruz. Bellekten okuma yapmak için de farklı fonksiyonlar oluşturdum.

TWI okuma

void i2c_read(uint8_t adr, uint8_t len){	
	i2c_adr(adr, I2C_READ);
	i2c_rx_len=(i2c_rx_len+len)&I2C_Rx_Mask;//okunacak veri sınırı
	i2c_start();	
}

Bellekten Okuma

uint8_t i2c_gelen(){//bellek işaretçileri eşit değilse yeni veri gelmiştir
	if (i2c_rx_son==i2c_rx_bas)	return 0;
	return 1;	
}
uint8_t i2c_oku(){// bellek işaretçileri eşit olana kadar bellekten veri alır.
	i2c_rx_son=(i2c_rx_son+1)&I2C_Rx_Mask;
	return i2c_rx_ring[i2c_rx_son];
}

Bu fonksiyonlardan sonra kesme rutini içinde neler oluyor onlara değinelim.

TWI Kesme(Write)

Ana program içinde farklı işlemler yapmak için yoklama yerine kesmeleri tercih ediyorum. Bu sayede MCU daha verimli şekilde kullanılmış oluyor. Burada kullanmamam gerektiği halde while döngüleri kullandım. TWI kesme vektörü son sıralarda olduğu için biraz rahat davrandım. Başka bir işlemi engelleme olasılığı daha az olacaktır. Ayrıca bu döngülerde bekleme süresi çok düşük. Eğer arka arkaya veri okuma yazma yapmayacaksanız aralarda farklı işlemler ve hatta bekleme süreleri olacaksa bu döngülere kesme kullandığımız için gerek yoktur. Öncelikle alttaki lejantı paylaşayım.


Datasheetteki şemalar için hazırlanmış bir lejant, şemalar oldukça anlaşılır. Bunların dışında adım adım neler yapılması gerektiğini gösteren farklı tablolar da mevcut. Ama bunun yerine bu şemaları paylaşmak daha mantıklı geldi.


Bu şemada hangi işlem sonrası hangi durum kodunun oluştuğu görülmektedir. Bu kodlara göre kesme rutini içinde yapacağımız işlemlere karar vereceğiz. TWI birimi her işlem sonunda bir durum kodu oluşturduğu gibi kesme bayrağını da "1" yapıyor. Bunu elle temizlememiz gerektiğini önceki konuda söylemiştim. Her seferinde kesme oluyor biz siliyoruz. Bu şekilde yukarıda görülen adımlar arsında sürekli kesme oluşuyor. Kesme rutini içine girdiğimizde hangi adımda olduğumuzu durum kodu ile belirlemek için Switch-case kullanılmış ben de önceki konularda benzer şeyler yapmıştım. Burada Switch ifadesi olarak TWSR registerini 3-7 bitlerine bakacağız.

#define I2C_STATUS_MASK					0xF8//tws7-tws3
#define I2C_STATUS					(TWSR&I2C_STATUS_MASK)

I2C_STATUS kesmeye girdiğimizde hangi seçeneğe gideceğimizi yukarıdaki şemaya göre şekillendirecektir.

Start

İlk adım olan Start ile 0x08 değerini alır. Bu bize start koşulunun gerçekleştiğini söyler. Bu durumda adres ve komutu TWDR ye yazmamız gerekir. Ardından TWINT "1" yaparak işlemi tamamlarız. Yukarıda bahsettiğim tekrarlayan başlangıç durum kodu 0x10 dur. Bu gerçekleştiğinde farklı bir durum kodu olmasına rağmen yapılması gereken işlemler aynıdır. Start ve Rep_Start için aynı kodu ayrı ayrı yazmak yerine iki seçeneği alt alta yazıp 0x08 içine break; yazmıyorum. Böylece kesme içine girdiğimiz zaman durum kodu 0x08 (START) veya 0x10 (REP_START) olsa da tek bir işlem yapılıyor.

case (I2C_START):
case (I2C_REP_START): 
	TWDR=Sladr_RW;//adres ve komut yazıldı
	TWCR=(1<<TWEN)|(1<<TWINT)|(1<<TWIE);//OR ile twint "1" yapmak da yeterlidir.			
		while(!(TWCR & (1<<TWINT)));// iletişim aralıkları sık değilse gereksiz.
	break;

 

TWINT  "1" yapıldıktan sonra kesme while döngüsü olmazsa kesme içinden çıkıp ana döngüye geri döner. Eğer döngüyle beklenirse sonraki adıma kadar kesmede kalır ve sonraki adım için oluşan kesme gerçekleşir. Fakat iletişim koparsa sonsuz döngüye girer burada en hızlı haliyle denemek için böyle yaptım.

Adres

Slave aygıt adresi algılayıp ACK ile dinlemeye başladığını belirtmiş olur. Bu gerçekleşmezse NACK koşulu oluşur. ACK için ( (MTR_ADR_ACK) 0x18, NACK için (MTR_ADR_NACK) 0x20 kodu oluşturulur. NACK durumunda hattı meşgul etmemek için stop koşuluna geçilebilir veya aynı iletişimi tekrar deneyebiliriz. Bu tekrarı bir değişkenle kontrol etmesek sonsuz bir döngüye girebiliriz. ACK durumundaysa veriyi hatta göndermemiz gerekir. Veri tek bir byte ile sınırlıysa burada yapılacak işlemler vardır ama birden çok olduğunda veri yazma sonrası farklı durum kodu oluşur ve burada yapılacak işlemin aynısı orada da yapılması gerekir. Yine kod kalabalığı olmaması için adres ACK ve data ACK alt alta yazıp 0x18 içine break; yazmıyorum. Bunun yerine case (I2C_MTR_ADR_ACK | I2C_MTR_DATA_ACK): başka bir durum koduyla çakışmamış olsaydı yazabilirdik. Fakat bu çakışma olduğu için bu yöntemi yapamıyorum.

Data

Adres sonrası ACK ile data sonrası ACK işlemleri aynıdır. Burada öncelikli olarak bellekte göndereceğimiz verinin sonuna gelip gelmediğimizi kontrol ediyoruz. Sona gelmemişsek  TWDR ye gidecek olan veriyi bellekteki yeriyle eşitleyerek yazıyoruz. Ardından yine TWINT "1" ve döngüyle kontrol ediyoruz. Haliyle veri hatta yazılıyor ve yine kesme oluşuyor. Burada datanın alınması veya alınmamasına bağlı olarak ACK (MTR_DATA_ACK) 0x28 veya NACK (MTR_DATA_NACK) 0x30 koşulu oluşuyor. NACK durumunda adres NACK ile aynı şeyleri yapıyoruz. Data ACK durumunda yine aynı yere gelip tekrar bellekteki veriyi kontrol ediyoruz. Sona geldiğimizde yani gidecek veri kalmadığında iki seçenek belirliyoruz. Eğer bir Rep_Start ayarlıysa start koşulu oluşturuyoruz. tekrar bir başlangıç ayarlı değilse stop ile işlemi sonlandırıyoruz.

		case (I2C_MTR_ADR_ACK):				
		case (I2C_MTR_DATA_ACK):
			if (i2c_tx_son!=i2c_tx_bas){//bellek sonu kontrol ediliyor
				i2c_tx_son=(i2c_tx_son+1)&I2C_Tx_Mask;
				TWDR=i2c_tx_ring[i2c_tx_son];//veri yazılıyor
				TWCR |=(1<<TWINT);				
				while(!(TWCR & (1<<TWINT))){
					timeout++;
					if (10>=timeout){
						timeout=0;
						break;;
					}
				}
			}
			else if (i2c_rep==1){//repeated start
				i2c_rep=0;
				TWCR|=(1<<TWSTA);				
			}
			else{				
				i2c_stop();
			}
			break;	
		case I2C_MTR_ADR_NACK:	
			//tekrar dene, dur v.b
			break;				
		case I2C_MTR_DATA_NACK:	
			//tekrar dene, dur v.b
			break;

Master mod veri yazma kesmelerle bu şekildedir. Arbitration çoklu master olan bir hat için geçerli olduğundan bu konuya girmiyorum. Hattın kontrolünü başka bir master aygıt aldığında yapılması gerekenler için bu kodlara göre işlem yapılır.

TWI Kesme(Read)

Yazma kısmında olduğu gibi öncelikle datashette yer alan şemayı paylaşıyorum. Burada yine her adımda oluşturulan durum kodlarını ve neler yapılması gerektiği görünüyor.


Veri okumak için adresi TWDR ye yazıp kaç adet veri okumak istediğimizi belirleyip start koşulu ile iletişimi başlatıyorduk. Bu start veya rep_start bölümüyle ortaktır. Durum kodları da aynıdır.

Adres

Adres ve komutu (Sladr_RW) start-rep_start  seçeneğinde yazıyorduk. Bu komut okuma olduğu için sonrasında adres ACK -NACK durum kodu farklı olacaktır. Yazmada bu adımdan sonra 0x18 veya 0x20 durum kodları oluşturulurken, okumada adres ACK (MRD_ADR_ACK) 0x40 ve adres NACK (MRD_ADR_NACK) 0x48 durum kodu oluşturulur. NACK durumunda hattı meşgul etmemek için stop koşuluna geçilebilir veya aynı iletişim tekrar deneyebiliriz.  ACK durumunda yani slave aygıtın cevap vermesi durumunda veriyi okumaya başlamamız gerekiyor. Bunun için TWINT "1" yapıyor ve kontrol ediyoruz. Veri alındıkça ACK koşulunu oluşturan bu sefer master cihaz olacaktır. Son sıradaki veri alınınca NACK koşulu oluşturarak iletişimi sonlandıracağız. Bu nedenle TWINT ile beraber TWEA da "1" yapılıyor.  Tek bir byte veri alacaksak adres ACK cevabından sonra alınacak veri adetini sorgulamamız gerekir.  Sonraki adımda veriyi almaya başlıyoruz. Tekrar bir sorgulama yapmak gerekeceğinden veri alma koşulunun adres ACK koşulunun üstüne yazarak komut tekrarı yapmıyoruz.

Data

Adres ACK sonrası data ACK (MRD_DATA_ACK) 0x50 durumu oluşur TWDR belleğe kaydedilir. TWINT ve TWEA "1" yapılır. Yukarıda bahsettiğim gibi bir veya birden fazla olma durumunu kontrol için aynı kodları yazmak yerine adres ACK durumuna geçilir. Son veri alınınca NACK koşulu oluşturulur. Bu durumda data NACK (MRD_DATA_NACK) 0x58 durum kodu oluşur ve TWDR den son veri belleğe alınarak iletişim sonlandırılır.

		case I2C_MRD_DATA_ACK:		
			i2c_rx_bas=(i2c_rx_bas+1)&I2C_Rx_Mask;//bellek işaretçisi artıldı
			i2c_rx_ring[i2c_rx_bas]=TWDR;// veri belleğe alındı			
		case I2C_MRD_ADR_ACK:
			if (i2c_rx_bas!=i2c_rx_len){//okunacak veri sınırı kontrolü
				TWCR=(1<<TWEN)|(1<<TWINT)|(1<<TWIE)|(1<<TWEA);//ACK
				
				while(!(TWCR & (1<<TWINT)));
				}else{
				TWCR=(1<<TWEN)|(1<<TWINT)|(1<<TWIE);//NACK
				
				while(!(TWCR & (1<<TWINT)));				
			}			
			break;			
		case I2C_MRD_ADR_NACK: 	
			i2c_stop();
			break;		
		case I2C_MRD_DATA_NACK:
			i2c_rx_bas=(i2c_rx_bas+1)&I2C_Rx_Mask;
			i2c_rx_ring[i2c_rx_bas]=TWDR; 
			i2c_stop();
			break;

 

Bundan sonraki durum kodları slave aygıt ve hata kodları içindir. Aşağıda toplu halde tüm durumlara ve başlık dosyasına ulaşabilirsiniz.

AHT10

Üreticinin sitesinde datasheet yok. Güncellendiği yazılı başka kaynaklardan  çevrilmiş halini buldum buradan ulaşabilirsiniz. Bu çevirilerde eksik bilgiler var. Bu nedenle biraz daha araştırdım ve üreticinin sayfasında yönlendirmediği bir  Çince datasheet buldum. Burada eksik kalan komutlara ulaştım. AHT10 ±0,3 °C hata payı, 0,01 °C çözünürlükte ve -40+85 °C aralığında sıcaklık ölçebiliyor. ±2 %RH hata payı, 0,24%RH çözünürlükte ve 0-100%RH aralığında nem ölçümü yapabiliyor. Sıcaklık ve nem belli bir aralık dışına çıkınca hata payı artıyor. AHT10 en fazla 3,6 V ile çalışabiliyor ama ben modül olarak aldım ve modül 6V a kadar çalışabiliyor. Modül üstünde SDA-SCL pinleri TWI iletişimi için ve VIN-GDN besleme pinleri olarak çıkartılmış. ADR pini lehimli bu nedenle modülün adresi 0x38, lehimli değilse adres 0x39 olur.

İletişim

I2C (TWI) ile bağlantı kurulabilen bu modülün komut sayısı az veya datasheette yazılı değil. Bu kanıya nereden vardığımı merak edenler için. Modülün durum kodlarında ismi geçen komut, çevrim ve normal (CMD,CYC ve NOR) modlara nasıl geçildiği yazılı değil. Bunlar ölçüm anında da geçilen değişen modlar değil. Bu durumu kontrol ettim daima normal mod dönüşü aldım. İngilizce datasheette init kısmı da eksikti bu bölümü Çince datasheetten aldım.


Datasheeten alıntı tablo ile devam edersek üç adet komut bulunmaktadır. İlk elektrik verildikten sonra 40ms bekleyip init komutu ile başlatma tavsiye edilmiş. Modülün kalibrasyonu için State bit 3 kontrol edilmeli denilmiş. Bu kontrol yerine soft reset ile başlıyorum. Bunun için önce adres 0x38 ve yazma komutu sonra reset komutu 0xBA gönderiyoruz. Bundan sonra başlatma komutu göndereceğiz. ilk olarak adres 0x38 ve yazma komutu sonra init komutu 0xE1, ilk parametre 0x08 ve son parametre 0x00 gönderiyoruz. Ölçüm için tetikleme komutu 0xAC adres sonrası gönderiyoruz. İlk parametre 0x33 ve son olarak 0x00 ile tetikleme başlatılıyor. Bundan sonra 75ms beklememiz öneriliyor. Bu beklemeyi yapmadan da okumak mümkün sadece bir önceki çevrimin sonuçlarını almış oluruz.

Okuma yapmak için adres ve okuma komutu gönderip 6 byte veri alıyoruz. ilk byte veri[0] durum kodlarını içeriyor. Durum kodlarından bit 7 meşgul bayrağıdır. Tetik sonrası bekleme yapmadan okuduğunuz zaman "1" bekleme sonrası okuduğunuz zaman "0" olacaktır. Bit 6-5 numaralı mod gösterir ama hep "00" olarak gördüm değişim için bilgi verilmemiş. Bit 3 kalibrasyon durumunu gösterir "0" ise kalibrasyon tamamlanmamış demektir. İnit komutu ile başlatma önerilmiş. Sonraki iki byte veri[1],veri[2]ve 3. byte veri[3]yüksek değerli 4 biti nem için ayrılmış. 3. Byte veri[3] düşük değerli 4 biti ile son iki byte veri[4],veri[5] sıcaklık içindir. Bu şekilde okunan veriyi bir değişkene kaydedip sıcaklık-nem dönüşüm hesabı için kullanacağız.

Sıcaklık

3. byte yarısı ve son iki byte ile aldığımız veriyi 32 bit bir sayı olarak saklıyoruz. Bunun için son byte veri[5] en sağda olacak. Veri[4] 8 bit sola kaydırıp veri[3] ilk 4 biti 16 bit sola kaydırıp sayımızı oluşturuyoruz.

Bu formüle göre işlemi yaparak sıcaklık değerini buluyoruz. St okuduğumuz veri değeridir. Sıcaklık için virgülden sonrası önemli değilse 20 bit sağa kaydırıp işlem yapabiliriz. Önemliyse bölme işlemini yaparak float değer olarak sonucu  sıcaklık değişkenine atıyoruz.

Nem

Nem için de sıcaklık gibi sayımızı oluşturmamız gerekiyor. Bunun için son byte veri[3] en sağda olacak. Veri[2] 8 bit sola kaydırıp veri[1] 16 bit sola kaydırıp sayımızı oluşturuyoruz. Veri[3] ilk 4 biti sıcaklık için ayrılmış bu nedenle oluşan sayıyı 4 bit sağa kaydırmamız gerekiyor.

Yine formüle göre işlemi yaparak nem değişkenine atıyoruz. Modül hakkında söylenecek başka bir şey kalmadı.  Her şeyi toplu olarak aşağıda paylaşıyorum.

Güncelleme Notu:

Bir önceki konuda arka arkaya veri yazma ve okuma konusunda sorun olduğu için while döngüsü kullandığımı yazmıştım. Birkaç yerde birden kullanmak yerine tek bir döngü kullanarak aynı sonucu elde ettim. Bunun için bir meşgul bayrağı tanımladım. Tüm iletişimin bitmesini bekleyip bundan sonra sıradaki iletişimi başlattım.

Bunun dışında tekrar başlatma kısmında start koşulunu kaldırdım. Meşgul bayrağı "0" olunca fonksiyon start koşulunu oluşturacak. Burada kesmeyi de kapattım. Sürekli olarak kesme içine giriyor ve 400kHz de sorun oluyordu.

Son haliyle dosyalar aşağıdaki gibidir.

 

/*
 * aht10_i2c.c
 *
 * Created: 24.01.2022 00:56:31
 * Author : haluk
 */ 

//#include <xc.h>
#include <avr/io.h>
#include <util/twi.h>
#include "uart.h"
#include "i2c_master.h"
#include <util/delay.h>
#include <stdio.h>

uint8_t aht_adr=0x38;//aht10adres
uint8_t aht_res=0xBA;//soft reset
uint8_t aht_init[]={0xE1,0x08,0x00};//init
uint8_t aht_trig[]={0xAC,0x33,0x00};//ölçüm başlatma
uint8_t aht_data[6];
uint32_t aht_temp=0;
uint32_t aht_hum=0;
float sicaklik=0.0;
float nem=0.0;
char str_data[20];

int main(void)
{	
    uart_basla(115200);
    i2c_init();	
	
	i2c_send_data(aht_adr,aht_res, N_REPEAT);//soft reset
	i2c_send(aht_adr,aht_init,3, N_REPEAT);//init komutu
	uint8_t i=0;
    while (1){
		i2c_send(0x38,aht_trig,3,N_REPEAT);//ölçme tetik
		//_delay_ms(75);//75ms bekleme önerilmiş	
		i2c_read(aht_adr,6);// veri okuma		
		while (i2c_gelen()){//okunan veri alındı
			aht_data[i++]= i2c_oku();
		}
		i=0;
		aht_temp=((uint32_t)(aht_data[3]&0x0F)<<16)|((uint16_t)aht_data[4]<<8)|(aht_data[5]);
		aht_hum=(((uint32_t)aht_data[1]<<16)|((uint16_t)aht_data[2]<<8)|aht_data[3])>>4;
		sicaklik=((float)aht_temp/5242.88)- 50;//formülün sadeleştirilmiş hali
		//sicaklik=((aht_temp*25)>>17)- 50;//virgülden sonrası önemsizse
		nem=((float)aht_hum/10485.76);//formülün sadeleştirilmiş hali
		//nem=(aht_hum*25)>>18;//virgülden sonrası önemsizse	
		sprintf(str_data,"%.2f--%.2f\n",sicaklik,nem);//sonuçları ekrana yazmak için
		uart_dizi(str_data);					
		//_delay_ms(500);//ölçüm aralığı		
    }
}

//////////////////////////////////////////////////////////////////////////////////
/*
 * i2c_master.c
 *
 * Created: 3.02.2022 13:55:35
 *  Author: haluk
 */ 

#include "i2c_master.h"

static volatile uint8_t i2c_rx_bas=1,i2c_rx_son=1,i2c_tx_bas=1,i2c_tx_son=1,i2c_rx_len=0;
static volatile uint8_t i2c_rx_ring[I2C_Rx_Boyut];
static volatile uint8_t i2c_tx_ring[I2C_Tx_Boyut];
static volatile uint8_t Sladr_RW=0, i2c_rep=0;
static volatile i2c_state_t i2c_state=I2C_READY;
volatile uint32_t timeout=0;
void i2c_init(){
	cli();
	TWSR|=(0<<TWPS0)|(0<<TWPS1);//prescaler 1
	TWBR=((F_CPU/F_SCL)-16)>>1;//i2c scl frekans
	TWCR=(1<<TWEN)|(1<<TWIE);//i2c kesme açık ack açık i2c açık (1<<TWEA)|	
	sei();
}
void i2c_disable(){
	TWCR=0x00;
}
void i2c_start(){
	i2c_state= I2C_BUSSY;//iletişim başladı meşgul bayrağı 
	TWCR=(1<<TWEN)|(1<<TWSTA)|(1<<TWINT)|(1<<TWIE);
}
void i2c_adr(uint8_t adr,i2c_cmd_t cmd){
	while (i2c_state!=I2C_READY){//hazır olana kadar bekle
		timeout++;		
		if (timeout>I2C_TIMEOUT){
			timeout=0;
			return;
		}
	}	
	Sladr_RW=((adr<<1)|cmd);
}
void i2c_data(uint8_t data){
	i2c_tx_bas=(i2c_tx_bas+1)&I2C_Tx_Mask;
	i2c_tx_ring[i2c_tx_bas]=data;
}
void i2c_end( i2c_rep_t repst){
	i2c_rep=repst;	
	i2c_start();
}
void i2c_stop(){
	TWCR=(1<<TWEN)|(1<<TWSTO)|(1<<TWINT);
	i2c_state= I2C_READY;
}
void i2c_send_data(uint8_t adr, uint8_t data,  i2c_rep_t repst){	
	i2c_adr(adr, I2C_WRITE);
	i2c_data(data);
	i2c_end(repst);
}
void i2c_send(uint8_t adr, uint8_t* str, uint8_t len,  i2c_rep_t repst){
	i2c_adr(adr, I2C_WRITE);
	for (uint8_t i=0;i<len;i++){
		i2c_data( str[i]);
	}
	i2c_end(repst);
}
void i2c_send_str(uint8_t adr, const char* str,  i2c_rep_t repst){
	i2c_adr(adr, I2C_WRITE);
	while (*str){
		i2c_data(*str++);		
	}	
	i2c_end(repst);	
}
void i2c_read(uint8_t adr, uint8_t len){
	i2c_adr(adr, I2C_READ);
	i2c_rx_len=(i2c_rx_len+len)&I2C_Rx_Mask;
	i2c_start();	
}
uint8_t i2c_gelen(){
	if (i2c_rx_son==i2c_rx_bas)	return 0;
	return 1;	
}
uint8_t i2c_oku(){
	i2c_rx_son=(i2c_rx_son+1)&I2C_Rx_Mask;
	return i2c_rx_ring[i2c_rx_son];
}
ISR(TWI_vect){
	switch(I2C_STATUS){
		case (I2C_START):
		case (I2C_REP_START): 		
		TWDR=Sladr_RW;
		TWCR=(1<<TWEN)|(1<<TWINT)|(1<<TWIE);
		break;
		case (I2C_MTR_ADR_ACK):	
		case (I2C_MTR_DATA_ACK):			
			if (i2c_tx_son!=i2c_tx_bas){
				i2c_tx_son=(i2c_tx_son+1)&I2C_Tx_Mask;
				TWDR=i2c_tx_ring[i2c_tx_son];
				TWCR=(1<<TWEN)|(1<<TWINT)|(1<<TWIE);
			}
			else if (i2c_rep==1){
				i2c_rep=0;
				i2c_state= I2C_READY;
				TWCR=(1<<TWEN);									
			}
			else{								
				i2c_stop();
			}
			break;	
		case I2C_MTR_ADR_NACK:
			i2c_stop();	
			//tekrar dene, dur v.b
			break;				
		case I2C_MTR_DATA_NACK:	
			i2c_stop();
			//tekrar dene, dur v.b
			break;
		case I2C_ARB_LOST: 			
			break;
		case I2C_MRD_DATA_ACK:
			i2c_rx_bas=(i2c_rx_bas+1)&I2C_Rx_Mask;
			i2c_rx_ring[i2c_rx_bas]=TWDR;			
		case I2C_MRD_ADR_ACK:
			if (i2c_rx_bas!=i2c_rx_len){
				TWCR=(1<<TWEN)|(1<<TWINT)|(1<<TWIE)|(1<<TWEA);
				}else{
				TWCR=(1<<TWEN)|(1<<TWINT)|(1<<TWIE);
			}			
			break;			
		case I2C_MRD_ADR_NACK: 	
			i2c_stop();
			break;		
		case I2C_MRD_DATA_NACK:
			i2c_rx_bas=(i2c_rx_bas+1)&I2C_Rx_Mask;
			i2c_rx_ring[i2c_rx_bas]=TWDR; 
			i2c_stop();
			break;
		case I2C_STR_ADR_ACK:
		break;					
		case I2C_STR_ADR_ACK_M_ARB_LOST:		
		break;	
		case I2C_STR_DATA_ACK:				
		break;
		case I2C_STR_DATA_NACK:				
		break;
		case I2C_STR_DATA_ACK_LAST_BYTE:	
		break;
		case I2C_SRD_ADR_ACK:	
		break;				
		case I2C_SRD_ADR_ACK_M_ARB_LOST:	
		break;	
		case I2C_SRD_GEN_ACK:	
		break;				
		case I2C_SRD_GEN_ACK_M_ARB_LOST:	
		break;	
		case I2C_SRD_ADR_DATA_ACK:
		break;			
		case I2C_SRD_ADR_DATA_NACK:		
		break;	
		case I2C_SRD_GEN_DATA_ACK:	
		break;		
		case I2C_SRD_GEN_DATA_NACK:
		break;			
		case I2C_SRD_STOP:		
		break;				
		case I2C_NO_INFO: 
		break;
		case I2C_BUS_ERR:
		break;
	}
}
/////////////////////////////////////////////////////////////////////////
/*
 * i2c_master.h
 *
 * Created: 3.02.2022 13:55:35
 *  Author: haluk
 */ 

#ifndef I2C_MASTER_H
#define I2C_MASTER_H


#define F_CPU 16000000UL
#define F_SCL 100000 
//#include <xc.h>
#include <avr/io.h>
#include <avr/interrupt.h>

#define I2C_Rx_Boyut					16
#define I2C_Tx_Boyut					16
#define I2C_Rx_Mask						(I2C_Rx_Boyut-1)
#define I2C_Tx_Mask						(I2C_Tx_Boyut-1)
#define I2C_TIMEOUT						1000
//I2C status
#define I2C_STATUS_MASK					0xF8//tws7-tws3
#define I2C_STATUS						(TWSR&I2C_STATUS_MASK)
#define I2C_START						0x08// gönderim hazır
#define I2C_REP_START					0x10// tekrar gönderim //sonrası adres ister
//I2C master status
#define I2C_MTR_ADR_ACK					0x18// adres yazıldı ack geldi
#define I2C_MTR_ADR_NACK				0x20// adres yazıldı nack geldi
#define I2C_MTR_DATA_ACK				0x28// veri gitti ack geldi
#define I2C_MTR_DATA_NACK				0x30// veri gitti nack geldi
#define I2C_MRD_ADR_ACK					0x40//adres yazıldı ack geldi
#define I2C_MRD_ADR_NACK				0x48// adres yazıldı nack geldi
#define I2C_MRD_DATA_ACK				0x50// veri alındı ack gönderildi
#define I2C_MRD_DATA_NACK				0x58//veri alındı nac gönderildi
//I2C slave status
#define I2C_STR_ADR_ACK					0xA8
#define I2C_STR_ADR_ACK_M_ARB_LOST		0xB0
#define I2C_STR_DATA_ACK				0xB8
#define I2C_STR_DATA_NACK				0xC0
#define I2C_STR_DATA_ACK_LAST_BYTE		0xC8
#define I2C_SRD_ADR_ACK					0x60
#define I2C_SRD_ADR_ACK_M_ARB_LOST		0x68
#define I2C_SRD_GEN_ACK					0x70
#define I2C_SRD_GEN_ACK_M_ARB_LOST		0x78
#define I2C_SRD_ADR_DATA_ACK			0x80
#define I2C_SRD_ADR_DATA_NACK			0x88
#define I2C_SRD_GEN_DATA_ACK			0x90
#define I2C_SRD_GEN_DATA_NACK			0x98
#define I2C_SRD_STOP					0xA0
//I2C error status
#define I2C_ARB_LOST					0x38//hat yönetim kaybı
#define I2C_NO_INFO						0xF8//belirsiz durum
#define I2C_BUS_ERR						0x00//hat hatası
//////////////////////////////////////////////////////////////////////////
typedef enum{
	DISABLE			=0x00,
	ENABLE			=0x05,
	CL_FLAG			=0x85,
	NACK			=0x85,
	STOP			=0x95,
	START			=0xA5,
	STO_STR			=0xB5,
	ACK				=0xC5	
}i2c_twcr_t;
typedef enum {
	I2C_WRITE		=0,
	I2C_READ		=1,
}i2c_cmd_t;
typedef  enum {
	_REPEAT			=1,
	N_REPEAT		=0,
}i2c_rep_t;
typedef  enum {
	I2C_READY		=0,
	I2C_BUSSY		=1,
}i2c_state_t;
void i2c_init();
void i2c_disable();
void i2c_start();
void i2c_stop();
void i2c_adr(uint8_t adr,i2c_cmd_t cmd);
void i2c_data(uint8_t data);
void i2c_end( i2c_rep_t repst);
void i2c_send_data(uint8_t adr, uint8_t data,  i2c_rep_t repst);
void i2c_send(uint8_t adr, uint8_t* str,uint8_t len,  i2c_rep_t repst);
void i2c_send_str(uint8_t adr, const char* str,  i2c_rep_t repst);
void i2c_read(uint8_t adr, uint8_t len);
uint8_t i2c_gelen();
uint8_t i2c_oku();

#endif

 

 

AVR- I2C (TWI) AHT10 Kullanımı Bölüm 1

 

 

Arduino ile ilgilendiğim zaman DS3231 kullanmıştım. Bunu hazır kütüphaneyle yapmıştım. Ondan sonra hiç kullanmadım haliyle öğrenme gereği duymadım. Sıcaklık sensörü araştırırken ucuz ve 20 bit çözünürlükle fiyatına göre iyi diyebileceğim AHT10 sensörü aldım. I2C ile haberleştiğinden ve artık hazır kütüphaneler kullanmak yerine öğrenme yöntemini seçtiğimden oturup incelemek zorunda kaldım. AVR de TWI olarak isimlendirilen birimin registerleri ve I2C hakkında internette yeterince bilgi var.  Ben bu bilgilerin çok gerekli olmadıkça tekrarını yapmayacağım.  İsmi ve tanımları yapılan registerleri tek tek anlatmayacağım. SCL SDA tanımı yapmayacağım bunlar için lütfen linkten veya başa bir kaynaktan bilgi sahibi olmanız gerekiyor. TWI  biriminin nasıl kullanıldığını, haberleştiğini ve son olarak TWI ile AHT10 sensörün kullanımını yazacağım. Açıkçası çok fazla benzer konu olduğundan yazıp yazmama konusunda tereddüt ettim ama kesme kullanarak yapılan örnek azlığından ve türkçe kaynak olması açısından yazmaya karar verdim. Umarım hatasız bir anlatımı başarabilirim. Başlamadan önce SDA ve SCL hatlarına uygun (4,7K) pull up dirençler muhakkak bağlı olmalıdır.  

TWI (I2C) Write

Başlatma

Datasheet rehberliğinde ilerleyeceğim ama AVR315 aplikasyon notlarından da faydalandığımı ve kendi kütüphanemi oluştururken bu notun kaynak kodlarından faydalandığımı belirteyim. Öncelikle birimin çalışması için bazı ayarlamalar yapmamız gerekiyor. Bu ayarlardan ilki TWI biriminin çalışma frekansını belirlemek olacaktır. MCU saat frekansına bağlı olarak verilen formülle SCL frekansını belirliyoruz.


 


Formülden TWBR registerine yazmamız gereken değeri buluyoruz. Genellikle SCL 100kHz veya hızlandırılmış olarak 400kHz frekansı kullanılmaktadır. Prescaler 1 olarak seçip bilinmezleri azaltıyoruz. Böylece formülden TWBR yi çektiğimizde TWBR=((F_CPU/F_SCL)-16)>>1; şekline dönüşüyor. (F_CPU 16000000Hz ve F_SCL 100000Hz)  Yine TWI birimini çalıştırmak için TWEN "1" yapılmalı ayrıca TWI kesmesi için TWIE de "1" yapılmalıdır. Bütün bunlar için oluşturduğum init fonksiyonu aşağıda görebilirsiniz ama ben adım adım gideceğim ve fonksiyonlara geçmeden tek tek ilerleyeceğim. Main içine bu ayarlar için yazacağımız kodlar bu şekilde olacak. Burada kesmeyi henüz devreye almayacağım.

	TWSR|=(0<<TWPS0)|(0<<TWPS1);//prescaler 1
	TWBR=((F_CPU/F_SCL)-16)>>1;//i2c scl frekans	
	TWCR=(1<<TWEN);

Bu kodlar ile TWI birimini başlatmış oluyoruz. I2C protokolünde işleyiş için bu şema gerekli:


Start

Görüldüğü gibi haberleşme için öncelikle START gereklidir. Start denilen SCL hattı "1" durumundayken SDA hattının "0" olmasıdır. TWI biriminin bunu yapması için TWCR TWI kontrol registerinde yer  alan TWSTA "1" (TWCR|=(1<<TWSTA);) yapılmalıdır.

	TWCR|=(1<<TWSTA);// twsta 1
	TWCR|= (1<<TWINT);
	while(!(TWCR & (1<<TWINT)));& (1<<TWINT)));

NOT: Ben burada veya "OR" kullanarak işlem yaptım. Adım adım anlatmak için bu yöntemi seçiyorum. Normalde bitlerden "1" ve "0" olanları aynı anda işlemek adına "=" kullanılmıştır.  Kodda değişiklik yaparken buna dikkat edilmesi gerekiyor.

Bu durumda SDA (D0) "0" olduktan yaklaşık 5μs sonra SCL (D1) "0" oluyor ve Start gerçekleşiyor. TWI birimi start  sonrası TWINT kesme bayrağını "1" yapıyor ve TWI durum registeri TWSR de yer alan TWS7-TWS3 arası 5 bit ile bir durum kodu oluşturur. Start için bu 0x08 dir. Bu durum kodlarına erişmek için TWSR ilk üç biti görmezden gelmemiz gerekir. TWI birimi her adım için bir durum kodu oluşturur bunlara datasheet veya <util/twi.h> kütüphanesinden ulaşabilirsiniz. Durum kodları için aşağıdaki makroları tanımlarız.

#define I2C_STATUS_MASK	0xF8//tws7-tws3
#define I2C_STATUS	(TWSR&I2C_STATUS_MASK)

TWI birimi bana biraz karışık geldi bunun en belirgin nedeni alışık olduğumun dışında çalışan kesmesidir. Start veya daha sonra değineceğim her adım sonrası TWINT "1" yapılır. Normalde kesme rutini içinde  donanım otomatik olarak bu kesme bayrağını "0" yapar. TWI biriminde bunu elle "0" yapmak için TWINT e "1" yazmak gerekir. Bu yapılmazsa bir sonraki işlem başlatılmaz. Ayrıca "1" yapılsa bile burada while ile döngü şart. Bu döngü konusuna daha sonra tekrar değineceğim.

Adres

Start sonrası TWI biriminde gönderilen ve alınan veriler için kullanılan TWI data registeri TWDR ye adres ve komut bilgisini yazmamız gerekir. Slave (köle) aygıtın belli bir adresi olur ve 7 bitten oluşur. TWDR ye bu adres bir bit sola kaydırarak yazılır. TWDR ilk biti slave aygıta ne amaçla veri gönderdiğimizi gösteren (R/W) okuma veya yazma bildiren bittir.  Kısaca Sladr-RW olarak tanımlayacağım değişken üstünden kayıt işlemini yapıyorum. Sladr-RW ilk biti"0" olursa yazma "1" olursa okuma komutu olarak adresi kaydeder. 0x01 numaralı slave aygıta yazma görevi için Sladr-RW =0000'0010=0x02 olmalıdır. Okuma içinse Sladr-RW =0000'0011=0x03 olmalıdır.  TWDR=Sladr-RW; veya TWDR=0x02; şeklinde registere veriyi yazmamız gerekir. Bu adımdan sonra SCL saat sinyali başlar bunun için yine TWINT "1" TWCR|= (1<<TWINT); ve TWSTA "0" TWCR&=~(1<<TWSTA);  yapılmalıdır.

	TWDR=0x02;
	TWCR&=~(1<<TWSTA); //twsta 0
	TWCR|= (1<<TWINT);	
	while(!(TWCR & (1<<TWINT)));

Artık start sonrası SCL hattı sinyaline bağlı olarak SDA "1" veya "0" oldu böylece slave aygıta veriyi gönderdik. SDA hattı son kısımda tekrar "1" oldu. Bu bize slave aygıtın bir cevap vermediği veya öyle ayarlandığını gösterir. SCL nin 8 saat darbesi veriyi gönderir. 9. darbede eğer slave iletişim başarılı olmuşsa ve cevap vermesi için ayarlanmışsa SDA hattını "0" yapacaktır.  Buradan hazır bir kodu kullanarak çalıştım. Aldığım bir sensörün kontrolünü genelde hazır kodlarla test edip sonra datasheet çalışıp kullanıyorum. TWI içinde çalışan bir slave ihtiyacım oldu. Şimdi adresi 0x01 olan bir slave aygıta veri gönderelim.

Artık 9. darbede SDA "0" durumdadır, bu da slavin adres verisini aldığını ve cevap verdiğini gösterir. Bu cevaplar ACK veya NACK şeklinde olur. Bu durumlardan her ikisi için de TWI birimi birer durum kodu oluşturur. (adres ACK: 0x18, NACK:0x20)

Bu durum koduna göre sonraki adımımızı planlarız. Slave ACK aldığımızda okuma veya yazma işlemine devam ederiz. NACK durumunda ya terkar deneme ya da sonraki slave seçme gibi işlemler yapabiliriz. Yukarıdaki şemada 4. adıma geldik. Adres ve komut bildirdik ACK aldık. Sonraki adım veri yazma kısmına geçelim.

Data

Hazır kodlarda slave aygıta "a" ve "b" gönderdiğimizde   D13 (PB5) bağlı olan LED yanıp sönüyor. Bunun için göndereceğimiz ilk veri "a" olacaktır. Küçük "a" için Decimal "97" veya Hex "0x61" şeklinde TWDR yazıyoruz. Adres ve komut gönderimi bittiği için bu registere yazabiliriz. Tabi bunun için yukarıdaki gibi while ile TWINT kontrolü yapmamız şart. Bunu yapmazsak TWDR de yazma çakışması olur. TWINT "0" olduğunda TWCR de bulunan TWWC biti "1" olur. Bu durumda TWDR veri yazamayız. Bu nedenle döngü ile TWINT kontrolü şart. Kesme kullanırken döngü şart değil ama bunun da bir şartı var. Benim oluşturduğum bir diziyi gönderdiğim fonksiyonu arka arkaya arada bekleme veya başka işlem olmadan çağırmamak gerekiyor.

Bunun nedeni her adımda (start, adres, data vb.) kesme rutinine girip çıkmasıdır. Start sonrası kesme olur. Kesme içine girer ilgili işlem yapılır. TWINT "1" yapınca kesmeden çıkar. İşte bu çıkış ile sonraki fonksiyona veya satıra geçer. İlk fonksiyonda belleğe "abc" yazmış ikinci fonksiyonda "def" yazmış ve farklı bir slave aygıta göndermek istediğimde sadece ilk aygıta "abcdef" göndermiş oluyorum. Bunun nedeni ring buffer kullanmam ve ilgili işaretçileri sıfırlamamamdır. Sıfırladığımda ring olmasının anlamı kalmaz ve kesme içinde kullanmayı istemesem de doğru olmasa da burada kullandım. Bu sayede arka arkaya çok hızlı bir şekilde iletişim kurabildim. Böyle bir zorunluluk yoksa kesme ile TWI kullanımında while döngülerine ihtiyaç yoktur.

	TWDR=0x61;
	TWCR|= (1<<TWINT);
	while(!(TWCR & (1<<TWINT)));

Bu şekilde kodumuzu ekledikten sonra veriyi gönderiyoruz ve slave ACK yanıtını geliyor. Bu işlem sonunda durum kodu oluşturulur. (Data ACK:0x28, NACK:0x30)

SDA ve SCL hatları normalde "1" durumdadır. İletişim başında  "0" oldu adres ve veri gitti ama halen "0" durumdadır. Hattın bu şekilde kalmaması için Stop gereklidir. Hemen ardından bir iletişim daha başlatılacaksa Stop olmadan Start gelir buna tekrarlayan başlatma (Rep_Start) diyebiliriz. Bu durumdaki start için 0x10 kodu oluşturulur. Bazı aygıtlar yazma ve okuma arasında Stop beklerken bazıları ihtiyaç duymayabilir.

Stop

Stop durumunda SCL hattı "1" olduktan sonra SDA "1" olur. TWI birimi hattın kontrolünü bırakır. Başka bir ana (master) aygıt varsa iletişime geçebilir. Stop için TWCR de bulunan TWSTO "1" yapılmalıdır. Donanım daha sonra kendisi "0" yapar. TWINT  temizlendikten sonra stop koşulu yürütülür. Stop sonrası TWINT "1" yapılmaz.

	TWCR|= (1<<TWSTO);

SCL ve SDA hattı "1" oldu ve stop koşulu gerçekleşti. Buraya kadar olan kısmı tekrar edip "b" göndererek test edebilirim. Main fonksiyonu ve sonsuz döngü aşağıdaki gibi olacaktır. Ayrıca TWI durum (status) kodlarını görebileceğiniz şekilde hazırlanmış hali en altta paylaşıyorum. Yorum satırlarını açıp kapatarak işlem yapabilirsiniz. Veri yazmayı bu şekilde yaptıktan sonra sıra okumaya geldi. Benzer adımları daha kısa olarak anlatmak durumundayım.

int main(void){      
  ///başlatma
  TWSR|=(0<<TWPS0)|(0<<TWPS1);//prescaler 1
  TWBR=((F_CPU/F_SCL)-16)>>1;//i2c scl frekans  
  TWCR=(1<<TWEN);
  
    while (1){
    //start
    TWCR|=(1<<TWSTA);// twsta 1
    TWCR|= (1<<TWINT);
    while(!(TWCR & (1<<TWINT)));//sonraki adım için gerekli

    //adres
    TWDR=0x02;//0x01 adres ve write komutu
    TWCR&=~(1<<TWSTA); //twsta 0
    TWCR|= (1<<TWINT);
    while(!(TWCR & (1<<TWINT)));//burada kontrol edilmezse TWWC "1" olur.
    
    //data
    TWDR=0x61;// a
    TWCR|= (1<<TWINT);
    while(!(TWCR & (1<<TWINT)));//sonraki adım için gerekli

    //stop
    TWCR|= (1<<TWSTO);
    _delay_ms(100);

    //start
    TWCR|=(1<<TWSTA);// twsta 1
    TWCR|= (1<<TWINT);
    while(!(TWCR & (1<<TWINT)));//sonraki adım için gerekli

    //adres
    TWDR=0x02;//0x01 adres ve write komutu
    TWCR&=~(1<<TWSTA); //twsta 0
    TWCR|= (1<<TWINT);
    while(!(TWCR & (1<<TWINT)));//burada kontrol edilmezse TWWC "1" olur.
    
    //data
    TWDR=0x62;// b
    TWCR|= (1<<TWINT);
    while(!(TWCR & (1<<TWINT)));//sonraki adım için gerekli

    //stop
    TWCR|= (1<<TWSTO);
    _delay_ms(100);
    }
}


TWI (I2C) Read

Master aygıt okuma veya yazma sırasında aynı şekilde başlatılır bir farklılık yoktur. Slave için adres registeri TWAR a  seçilen adresin yazılması gerekir ama bu TWI slave konusuna girmeyeceğim. Slave tarafta veri istendiğinde "Merhaba" gönderecek şekilde ayarlanmıştır. Aşağıdaki şemada işleyiş görülüyor. Bu şemanın bir kısmını aldım tüm işleyiş ve durum kodları için datasheet e bakabilirsiniz. Yazma kısmında adres ve veri gönderimi sonrası ACK slave tarafından oluşturuluyordu. Okumada adres sonrası slave ACK cevabı verir devamında veriyi gönderir. Bundan sonra her veri gönderiminde eğer farklı ayarlanmamışsa master ACK oluşturur. Son alınan veri sonrası NACK ile iletişim sonlanır.



Gri renkli kısımlar master, beyaz olanlar slave aygıttan gelir. Şemada görünen HEX kodlar durum kodlarıdır. (S: start, SLA-R: adres ve okuma komutu, A: ACK, DATA: veri, P: stop ve üstü çizgili A ise NACK)

Start

Slave aygıttan veri okumak için TWI birimini başlatıp yine Start koşulunu gerçekleştirmek gerekir. Yukarıda her biti adım adım gösterdiğim için burada tek seferde işlem yapılacak şekilde gösterip geçeceğim.

	TWCR=(1<<TWEN)|(1<<TWSTA)|(1<<TWINT);
	while(!(TWCR & (1<<TWINT)));

TWCR de yalnızca "1" olmasını istediğim bitler "1" diğerleri "0" yapıldı. Start için asıl koşul olan TWSTA "1" yapılarak ve TWINT "1" olması beklenerek işlem yapıldı. Yukarıdaki aynı görüntüyü tekrar paylaşmıyorum. Bu işlem sonucu yine bir durum kodu üretilir.(Start:0x08, Rep start: 0x10)

Adres

Slave adresi 0x01 olarak ayarlıydı. Okuma görevi için ilk bit "1" olacak. Adresi bir bit sola kaydırıp ilk biti "1" yaptığımız zaman Sladr-RW=0x03 olacaktır. TWDR=Sladr-RW; veya TWDR=0x03; olarak adresi ve komutu yazıyoruz. Sonrasında yine ilgili TWCR bitleri düzenleyip TWINT "1" olmasını bekliyoruz.

	TWDR=0x03;
	TWCR = (1<<TWINT) | (1<<TWEN);
	while(!(TWCR & (1<<TWINT)));

Bu işlem sonucu bu şeklide olur. gördüğünüz gibi 8. darbe de Read komutu için SDA "1" oldu. Bu işlem sonunda durum kodu oluşturulur. Master okuma komutu için adres ACK:0x40, NACK:0x48 dir.


Data

Adres ve okuma komutu sonrası ACK ile SDA hattına SCL darbelerine göre slave tarafından veri yazılır. TWI birimi bu veriyi TWDR registerine kaydeder. Bunu yapmak için adres sonrası TWINT "1" ve data=TWDR yapmamız gerekir. Buna ek olarak veri alınca ACK göndermek istersek TWEA da "1" yapılır aksi halde veriyi alsak da ACK yerine NACK koşulu oluşur. Bu işlem sonucu durum kodu oluşturulur. (Data ACK:0x50, NACK:0x58)

TWCR = (1<<TWINT)|(1<<TWEN)|(1<<TWEA);
//twea eklenmezse nack 
while(!(TWCR & (1<<TWINT)));		
data[0]=TWDR;

Veri alındı NACK oluşturuldu.

Veri alındı ACK oluşturldu.

"Merhaba" için ilk harf 'M' "0x4D" alındı bunu sonrasında kullanmak için oluşturduğum char data[7]; isimli dizinin ilk karakteri olarak kaydettim. Kalan altı harfi de bu şekilde almaya devam edeceğim. Bunun için yukarıdaki kod bu şekilde uzayacak.

	TWCR = (1<<TWINT)|(1<<TWEN)|(1<<TWEA);
	while(!(TWCR & (1<<TWINT)));		
	data[0]=TWDR;//M
	TWCR = (1<<TWINT) | (1<<TWEN)| (1<<TWEA);
	while(!(TWCR & (1<<TWINT)));	
	data[1]=TWDR;//e	 
	TWCR = (1<<TWINT) | (1<<TWEN)| (1<<TWEA);
	while(!(TWCR & (1<<TWINT)));
	data[2]=TWDR;//r
	TWCR = (1<<TWINT) | (1<<TWEN)| (1<<TWEA);
	while(!(TWCR & (1<<TWINT)));
	data[3]=TWDR;//h
	TWCR = (1<<TWINT) | (1<<TWEN)| (1<<TWEA);
	while(!(TWCR & (1<<TWINT)));
	data[4]=TWDR;//a
	TWCR = (1<<TWINT) | (1<<TWEN)| (1<<TWEA);
	while(!(TWCR & (1<<TWINT)));	
	data[5]=TWDR;//b
	TWCR = (1<<TWINT)|(1<<TWEN);//twea 0yapıldı.
	while(!(TWCR & (1<<TWINT)));		
	data[6]=TWDR;//a

Her adımdan sonra TWINT "1" yapıldı beraberinde TWEN ve TWEA da "0" olmaması için koda eklendi. Son karakter 'a' alınmadan  önceki adımda TWEA "0" olması için yazılmadı. Bu sayede son karakteri alınca NACK koşulu oluştu. Bu kullandığınız yere bağlı olarak değişebilir. Bu çalışma için tüm karakterleri NACK ile alabilirdik. Bu ACk-NACK durumu iletişimin devamı için gereken kararları vermemizi sağlayacaktır. Aşağıda tüm karakterlerin alınmış hali vardır. ASCII seçeneğiyle karakterler üstte görünüyor.


Stop

Okuma işleminin de son adımı olarak stop koşulu gerekiyor. Yapılan işlem farklı değildir. Tüm okuma işlemleri birlikte bu şekilde olur.

int main(void)
{    
  //başlatma 
  TWSR|=(0<<TWPS0)|(0<<TWPS1);//prescaler 1
  TWBR=((F_CPU/F_SCL)-16)>>1;//i2c scl frekans  
  TWCR=(1<<TWEN);  
    while (1){    
    //slave okuma "Merhaba"
    //start
    TWCR=(1<<TWEN)|(1<<TWSTA)|(1<<TWINT);
    while(!(TWCR & (1<<TWINT)));
    //sprintf(str_data,"twsta %x\n",(TWSR&0xf8));//TWI status
    //uart_dizi(str_data);
    //adres
    TWDR=0x03;
    TWCR = (1<<TWINT) | (1<<TWEN);
    while(!(TWCR & (1<<TWINT)));
    //sprintf(str_data,"twsta %x\n",(TWSR&0xf8));//TWI status
    //uart_dizi(str_data);
    //data
    TWCR = (1<<TWINT)|(1<<TWEN)|(1<<TWEA);
    while(!(TWCR & (1<<TWINT)));
    data[0]=TWDR;
    //sprintf(str_data,"twsta %x\n",(TWSR&0xf8));//TWI status
    //uart_dizi(str_data);
    TWCR = (1<<TWINT) | (1<<TWEN)| (1<<TWEA);
    while(!(TWCR & (1<<TWINT)));
    data[1]=TWDR;
    TWCR = (1<<TWINT) | (1<<TWEN)| (1<<TWEA);
    while(!(TWCR & (1<<TWINT)));
    data[2]=TWDR;
    TWCR = (1<<TWINT) | (1<<TWEN)| (1<<TWEA);
    while(!(TWCR & (1<<TWINT)));
    data[3]=TWDR;
    TWCR = (1<<TWINT) | (1<<TWEN)| (1<<TWEA);
    while(!(TWCR & (1<<TWINT)));
    data[4]=TWDR;
    TWCR = (1<<TWINT) | (1<<TWEN)| (1<<TWEA);
    while(!(TWCR & (1<<TWINT)));
    data[5]=TWDR;
    TWCR = (1<<TWINT) | (1<<TWEN);//twea 0 yapıldı son karakter nack
    while(!(TWCR & (1<<TWINT)));
    data[6]=TWDR;
    //sprintf(str_data,"twsta %x\n",(TWSR&0xf8));//TWI status
    //uart_dizi(str_data);
    //stop
    TWCR=(1<<TWEN)|(1<<TWSTO)|(1<<TWINT)|(1<<TWIE);
    uart_dizi(data);
    uart_gonder('\n');
    _delay_ms(500);
    }
}


Buraya kadar olan bölümde TWI birimini veri yazma ve okuma için nasıl kullanacağımızı öğrenmiş olduk. Bundan sonraki bölümde bu işlemleri kesme kullanarak nasıl yapacağımıza ve AHT10 kullanımına değineceğim.

/*
 * aht10_i2c.c
 *
 * Created: 24.01.2022 00:56:31
 * Author : haluk
 */ 
#define F_CPU 16000000UL
#define F_SCL 100000L
//#include <xc.h>
#include <avr/io.h>
#include <util/twi.h>
#include "uart.h"
#include "i2c_master.h"
#include <util/delay.h>
#include <stdio.h>

char str_data[20];
char data[7];

int main(void){  
    uart_basla(115200);     
  ///fonksiyon dışı başlatma a ve b gönderimi için 
  TWSR|=(0<<TWPS0)|(0<<TWPS1);//prescaler 1
  TWBR=((F_CPU/F_SCL)-16)>>1;//i2c scl frekans  
  TWCR=(1<<TWEN);  
    while (1){
        
    /// a ve b gönderimi
    //start    
    TWCR=(1<<TWEN)|(1<<TWSTA)|(1<<TWINT);
    while(!(TWCR & (1<<TWINT)));//sonraki adım için gerekli
    //sprintf(str_data,"twsta %x\n",(TWSR&0xf8));//TWI status
    //uart_dizi(str_data);//bu satırları açarsanız status kodlarını görebilirsiniz.
    //adres
    TWDR=0x02;//0x01 adres ve write komutu    
    TWCR=(1<<TWINT)|(1<<TWEN);
    while(!(TWCR & (1<<TWINT)));//burada kontrol edilmezse TWWC "1" olur.
    //sprintf(str_data,"twsta %x\n",(TWSR&0xf8));//TWI status
    //uart_dizi(str_data);
    //data
    TWDR=0x61;    
    TWCR=(1<<TWINT)|(1<<TWEN);
    while(!(TWCR & (1<<TWINT)));//sonraki adım için gerekli
    //sprintf(str_data,"twsta %x\n",(TWSR&0xf8));//TWI status
    //uart_dizi(str_data);
    //stop    
    TWCR=(1<<TWEN)|(1<<TWSTO)|(1<<TWINT);
    _delay_ms(500);
    //start    
    TWCR=(1<<TWEN)|(1<<TWSTA)|(1<<TWINT);
    while(!(TWCR & (1<<TWINT)));//sonraki adım için gerekli
    //sprintf(str_data,"twsta %x\n",(TWSR&0xf8));//TWI status
    //uart_dizi(str_data);
    //adres
    TWDR=0x02;//0x01 adres ve write komutu    
    TWCR=(1<<TWINT)|(1<<TWEN);
    while(!(TWCR & (1<<TWINT)));//burada kontrol edilmezse TWWC "1" olur.
    //sprintf(str_data,"twsta %x\n",(TWSR&0xf8));//TWI status
    //uart_dizi(str_data);
    //data
    TWDR=0x62;    
    TWCR=(1<<TWINT)|(1<<TWEN);
    while(!(TWCR & (1<<TWINT)));//sonraki adım için gerekli
    //sprintf(str_data,"twsta %x\n",(TWSR&0xf8));//TWI status
    //uart_dizi(str_data);
    //stop    
    TWCR=(1<<TWEN)|(1<<TWSTO)|(1<<TWINT);
    _delay_ms(500);
    
    //slave okuma "Merhaba"
    //start
    TWCR=(1<<TWEN)|(1<<TWSTA)|(1<<TWINT);
    while(!(TWCR & (1<<TWINT)));
    //sprintf(str_data,"twsta %x\n",(TWSR&0xf8));//TWI status
    //uart_dizi(str_data);
    //adres
    TWDR=0x03;
    TWCR = (1<<TWINT) | (1<<TWEN);
    while(!(TWCR & (1<<TWINT)));
    //sprintf(str_data,"twsta %x\n",(TWSR&0xf8));//TWI status
    //uart_dizi(str_data);
    //data
    TWCR = (1<<TWINT)|(1<<TWEN)|(1<<TWEA);
    while(!(TWCR & (1<<TWINT)));
    data[0]=TWDR;
    //sprintf(str_data,"twsta %x\n",(TWSR&0xf8));//TWI status
    //uart_dizi(str_data);
    TWCR = (1<<TWINT) | (1<<TWEN)| (1<<TWEA);
    while(!(TWCR & (1<<TWINT)));
    data[1]=TWDR;
    TWCR = (1<<TWINT) | (1<<TWEN)| (1<<TWEA);
    while(!(TWCR & (1<<TWINT)));
    data[2]=TWDR;
    TWCR = (1<<TWINT) | (1<<TWEN)| (1<<TWEA);
    while(!(TWCR & (1<<TWINT)));
    data[3]=TWDR;
    TWCR = (1<<TWINT) | (1<<TWEN)| (1<<TWEA);
    while(!(TWCR & (1<<TWINT)));
    data[4]=TWDR;
    TWCR = (1<<TWINT) | (1<<TWEN)| (1<<TWEA);
    while(!(TWCR & (1<<TWINT)));
    data[5]=TWDR;
    TWCR = (1<<TWINT) | (1<<TWEN);//twea 0 yapıldı son karakter nack
    while(!(TWCR & (1<<TWINT)));
    data[6]=TWDR;
    //sprintf(str_data,"twsta %x\n",(TWSR&0xf8));//TWI status
    //uart_dizi(str_data);
    //stop
    TWCR=(1<<TWEN)|(1<<TWSTO)|(1<<TWINT);
    uart_dizi(data);
    uart_gonder('\n');
    _delay_ms(500);
    }
}


/*
 * uart.c
 *
 * Created: 9.01.2021 15:26:37
 *  Author: haluk
 */ 
#include "uart.h"

static volatile uint8_t rx_bas=0,rx_son=0,tx_bas=0,tx_son=0;
static volatile uint8_t rx_ring[UART_Rx_Boyut];
static volatile uint8_t tx_ring[UART_Tx_Boyut];
static volatile uint8_t uaflag=0;

ISR (USART_RX_vect){
  uint8_t gelen=UDR0;
  rx_bas=(rx_bas+1) & UART_Rx_Mask;
  rx_ring[rx_bas]=gelen;
}
ISR (USART_UDRE_vect){  //uart veri gönderim kesmesi
  tx_son=(tx_son+1)&UART_Tx_Mask;
  UDR0=tx_ring[tx_son];
  if (tx_son==tx_bas)
  UART_Bos_Off;
}
ISR(USART_TX_vect){
  
}
void uart_basla(uint32_t baud){
  cli();
  uint16_t baudRate=0;
  baudRate=(F_CPU/baud/16)-1;
  if (baud>=115200){//115200 ve üstünde U2X 1 yapılıyor.
    baudRate=(F_CPU/baud/8)-1;
    UCSR0A|=(1<<U2X0);
  }
  UBRR0H=(baudRate>>8);
  UBRR0L=baudRate;
  UCSR0B|=(1<<RXEN0)|(1<<TXEN0)|(1<<RXCIE0)|(1<<TXCIE0);
  UCSR0C|=(1<<UCSZ01)|(1<<UCSZ00);
  sei();
}
uint8_t uart_oku(){
  rx_son=(rx_son+1) & UART_Rx_Mask;
  return rx_ring[rx_son];
}
void uart_gonder(uint8_t uData){
  tx_bas=(tx_bas+1)&UART_Tx_Mask;
  tx_ring[tx_bas]=uData;
  UART_Bos_On;
}
void uart_dizi(const char *str){
  while(*str){
    tx_bas=(tx_bas+1)&UART_Tx_Mask;
    tx_ring[tx_bas]=*str++;
  }
  UART_Bos_On;
}
uint8_t uart_gelen(){
  if (rx_son==rx_bas)  return 0;
  return 1;
}
void uart_dizi_al(char *stri){
  unsigned char gdata=0;
  uint8_t poz=0;
  do
  {  gdata=uart_oku();
    stri[poz]=gdata;
    poz++;
  } while (!(rx_bas==rx_son));
  stri[poz]='\0';
}


/*
 * uart.h
 *
 * Created: 9.01.2021 15:26:29
 *  Author: haluk
 */ 
#ifndef UART_H_
#define UART_H_


#define F_CPU 16000000UL

//#include <xc.h>
#include <avr/io.h>
#include <avr/interrupt.h>

/////////////////////////////////
#define UART_Rx_Boyut 128
#define UART_Tx_Boyut 128
#define UART_Rx_Mask (UART_Rx_Boyut-1)
#define UART_Tx_Mask (UART_Tx_Boyut-1)
#define UART_Bos_On UCSR0B|=(1<<UDRIE0)// uart UDR register boş kesmesi
#define UART_Bos_Off UCSR0B&=~(1<<UDRIE0)// uart UDR register boş kesmesi
////////////////////////////////////////////////////
void uart_basla(uint32_t baud);
uint8_t uart_oku();
void uart_gonder(uint8_t uData);
void uart_dizi(const char *str);
uint8_t uart_gelen();
void uart_dizi_al( char *stri);

#endif

 

 

AVR- AC Dİmmer

 

Burada anlattığım tehlikeli bir işlem olduğundan dikkatli olmanız gerekir 220 V şakası yok.

Alternatif Akımda PWM?

Bu konu hakkında bir eğitim almadım sadece okuyup öğrendiklerimi kendime notlar olarak yazıyorum. Hatalarım yanlış öğrendiklerim olabilir. Düzeltmeme yardımcı olabilirsiniz. Daha önceki bir yazımda motor hızını kontrol etmek için PWM konusuna değinmiştim. Doğru akımda belirli aralıklarla açma kapama yaparak motor veya bir led diyotun ışığını ayarlayabiliyoruz. Alternatif akımda (AC) hem yön hem genlik değişken olduğundan bu şekilde açma kapama yaparsak muhtemelen kömürlerinde ark patlamaları olan bir motor veya aşırı titreyen bir ampulle karşılaşırdık. AC de bu yön değişimi yani şebekenin frekansı Türkiye'de 50Hz dir. Saniyede 50 defa değişmektedir.


Bu değişime rağmen DC gibi aç kapa yaptığımızda bu şekilde bir sonuç alırdık.

Şekilde görüldüğü gibi eğrinin altında kalan taralı alan değişken bu nedenle çıkış değer de değişken olacaktır. Bu haliyle çıkış voltajını düşürememiş oluruz. AC de aç kapa şeklinde sonuca ulaşmak için grafikte taralı alanın aynı olması gerekir. Bunu yapabilmenin yolu sinüs dalgasının sıfır geçişini yakalayıp açma işlemini buna göre yapmaktır.


Bu şekilde yaptığımızda her iki tarafta da eğrinin altında taralı alanlar eşit olur. Böylece çıkış değeri sabit ve ayarlanabilir olur. AC de transistörler gibi bu şekilde anahtarlama yapabilen Triyak denilen parçalar vardır. Triyak gate ucuna verilen akım ile iletime geçer fakat gate ucu sıfır olsa bile üstünde akım olduğu sürece iletken olmaya devam eder. Ancak sıfır geçişinde iletimi durdurur. Bu açma kapama işini triyak ile yaptığımızda grafik değişir.

Şekilde görüldüğü gibi biz sinyali "0" yapsak da AC taralı alan sıfır geçişine kadar devam eder. Bu durumda çıkıştaki voltajı ayarlamak için sıfır geçişini bilmek kadar bir sonraki sıfır geçişine kalan süreyi de bilmek önemli oluyor. Şebeke frekansı 50hz olduğuna göre 1 periyot 0,02 sn o da 20ms olacaktır. Bir periyotta iki sıfır geçiş olduğundan geçişler arası 10ms olur. Çıkış voltajını yarı yarıya (%50 duty) azaltmak istediğimiz zaman yapmamız gereken sıfır geçişi bulmak. Bir sonraki sıfır geçişi 10ms sonra olacağına göre 5ms beklemek. 5ms sonra triyakı iletime geçirmek olacaktır.


Triyak iletime geçince sıfır geçişi beklediğine göre Gate bacağına gönderdiğimiz sinyali uzun tutmamıza gerek yok. Aslında ilk iş olarak gate sinyalini "0" yapmak daha doğru olur. Teorik olarak ne yapmak gerekir kısmı buraya kadar. Artık bunları nasıl yapacağız kısmına geçiyorum.

PC814-Sıfır Geçiş Tespiti (Zero Cross Detection)

Bu işlem için çeşitli yöntemler var. Alternatif akımı diyotlar ile doğrultup transistörle veya izolasyon için optokuplörle yapabilirsiniz. Ben bunlara gerek kalmadan yapmak için PC814 kullandım. İçinde birbirine ters paralel bağlanmış iki LED diyot ve foto transistör bulunuyor. Bu sayede hem izolasyon hem de daha az komponentle çözüm getiriyor. PC814 datasheet anlatmayacağım.


Dikkat etmeyi gerektiren ilk şey AC tarafındaki dirençlerin değeri olacak. En az 47K 2W iki direnç (Rd) takmak gerekir. Daha aşağısı sıfır geçişi yakalamak için daha uygun olsa da ısınma ve PC814 ün zarar görmesi olasılığından riskli görüyorum. Dayanabileceği akım çok daha yüksek ama direnç yerine bir rezistans takmak  gibi bir durum olur. Direnç ne kadar düşük olursa LED o kadar geç söner ve sıfır geçiş noktasına o kadar yakın bir tespit yapılır. Ayrıca çıkışta kullanacağımız pull up (Rl) direnç ne kadar büyük olursa gecikme süresi (Tf) o kadar artar. Tf artışı da sıfır geçişe yaklaştırır. Tabi bunu söylerken alttaki grafikte görülen Input sinyalinin "0" olduğu zamanın gerçek sıfır geçişinin önünde olduğunu düşünerek söylüyorum. Düşük Rd direnciyle gerçek sıfır geçişe çok yaklaştığımız  zaman Rl direnci daha düşük olması doğru olacaktır. Benim kullandığım dirençlerle bir miktar erken sıfır tespiti olacaktır ve bunu yazılımla gidereceğiz.

Datasheet bağlantı şeması verilmiş benim uyguladığım şema bu şeklide. Bu bağlantı sonrası PD2 çıkışı grafiği aşağıdaki gibi olur. Bahsettiğim erken tespit ve gecikme de bu şekilde olacaktır.


Sıfır Geçiş Tespit Hatası

Giriş sıfıra yaklaşınca LED sönecek bu sayede foto transitör iletimi kesecektir. Pull up direnç ile PD2 çıkışı "1" olarak çıkış verecektir. Giriş negatif tarafta yükselirken diğer LED yanacak ve foto transistör iletime geçerek PD2 çıkışını "0" yapacaktır. PD2 her "1" oluşu sıfır geçişi olduğunu gösterecektir. Burada oluşan tespit hatasını yazılımda düzeltmemiz gerekecek. İki geçiş arası 10ms olarak tespitimizi yine yapacağız fakat bir miktar önce olacak bunu 500μs olduğunu düşünelim. Biz %100 duty ile çıkış vermek istiyorsak geçiş anında triyakı iletime geçirmeli gate bacağına "1" sinyalini göndermeliyiz.

Gate sinyalini hemen "0" yapmamız gerektiğini söylemiştim.  50μs sonra gate "0" yaptığımızı düşünelim. Bu durumda gerçekte diğer alternansa geçmeden triyakı iletime geçirmiş oluruz. Gate bacağını da "0" yaptığımızdan 450μs süresince çıkış ile %100 yerine çok daha küçük bir çıkış alırız. Aşağıdaki grafikte durumu "biraz abartarak" göstermeye çalıştım. Bunu engellemek için bir ofset süre eklemesi yapacağız.


Görüldüğü gibi sıfır geçiş tespiti yapıldığı an Gate "1" yapılır ve sonrasında "0" yapılırsa böyle bir durumla karşılaşırız. Hemen "0" yapmazsak çıkışı ayarlama şansımız hiç kalmaz. Yukarıdaki devreyi dikkatli bir şekilde kurup PD2 çıkışını bir Arduino Unonun D5 pinine bağlayarak linkteki frekans sayıcı ile kontrol edebilirsiniz. Her şey doğru yapıldıysa seri ekranda 100Hz olarak sonucu görürsünüz. Şebeke 50Hz ve bir periyotta yukarıda görüldüğü gibi iki sıfır geçişi olduğundan 100Hz olarak görmemiz gerekir.  Bu da sıfır geçişi tespit ettiğimizi ve bir sonraki aşama olan triyak iletim kısmına geçebileceğimizi gösterir.

MOC3020- Triyak kontrol

Triyakı kullanabilmek için gate bacağına çıkış sinyalini vermemiz gerekiyor. Burada mikro denetleyici direk bağlamak sağlıklı olmayacağından bu işi izole bir şekilde yapmak için MOC3020 kullanıyorum. Triyak çıkışlı bir optokuplör olan 3020 kolay bulunan, ucuz ve gerekli değerleri fazlasıyla karşıladığı için seçtim. Bağlantı şeması Datasheette verilmiş ve aynen uyguladım. Bu şema SSR rölelerin içinde kullanılanla aynıdır. Bu devre yerine SSR kullanabilirsiniz ama muhtemelen daha pahalıya gelir. Yük durumuna göre farklı koruma devreleri de eklenmiş ama bilmediğim ve öğrenmeme gerek olmadığını düşündüğüm için araştırmadan uygulamak mantıklı geldi. Bu tipik şemada da koruma için gerekli parçalar mevcut ve yük olarak bir motor bağlanabilir. C9 ve C10 400v R2 ve R4 1W yeterlidir.


Datasheete göre dikkat etmemiz gereken LED için 15mA üst sınır. Bu akım değeri düştükçe bir miktar gecikme olmaktadır. Mikrosaniye  biriminden gecikme olduğundan çok önemli değil. Bu nedenle Rin direncini 470Ω olarak seçtim. En fazla 2μs gecikme olacaktır. Bu durumda Gate (PD4) sinyalini 2μs den uzun bir süre vermem yeterli olacaktır. Bir yanlış anlaşılma olmasın diye uyarı yapayım. Yukarıda Gate sinyalini "1" olarak verdim. Triyak sürerken MOC3020 kullandığım ve mikro denetleyiciyle kontrol edeceğim için ilgili pini (PD4) "0" yaparak triyakın (BT139) gate bacağını "1" yapmış oluyorum.

Dış Kesme Ayarları

Önceki konularda ve ayrıca bir dış kesme konusu olarak değindiğim için detaya girmiyorum. PC814 PD2 ye bağlı bunun için PD2 yi giriş yapıp yükselen kenarda kesme olacak şekilde ayarlıyorum. INT0 yani PD2 kesmesini açarak bu kısmı ayarlıyorum. Sıfır geçişi ile PD2 "1" olduğunda dış kesme oluşur. Bu kesme rutini içine bir sonraki adım olan beklemeyi gerçekleştirmek için zamanlayıcı başlatıyorum. Çıkışı ayarlamak için gereken bekleme süresi için OCR1A ya da değeri giriyorum.

Zamanlayıcı Ayarları (Timer1)

Sıfır geçiş sonrası çıkışı ayarlamak için gerekli olan Timer birimini ayarlayalım. Sıfır geçiş sonrası ne kadar çok bekleyip triyakı sürersem çıkış voltajını o kadar düşürürüm. 50Hz şebeke frekansında bir periyot 20ms bir alternans 10ms sürüyor demiştim. Buna göre bekleme süresinin teorik olarak 0 ile 10ms arasında olması gerekir. Daha önce farklı konularda farklı timer birimleri hakkında detaylı bilgi verdiğim için burada sadece olması gereken değerleri söyleyeceğim.  Ben timer1 kullandım farklı bir birimde kullanılabilir. 16MHz saat sinyali için 64 prescaler seçtim böylece 16.000.000/64 sonucunda 1 sn de TCNT toplam değeri 250000 olacaktır.

1ms de TCNT 250 ve 10ms de 2500 olacaktır. En fazla 10ms bekleyeceğimizi düşünürsek bu değer TCNT1 de taşmaya neden olmaz ve küsurlu sayılar çıkarmaz. 0 ile 10ms arası bekleme için TCNT değerleri 0 ile 2500 olacak. OCR1A ile istediğimiz bir değere gelince TCNT kendini sıfırladığı için CTC modu seçeceğim. İstediğim sürenin sonunda bir kesme oluşması için OCR1A kesmesini de açacağım. Kesme oluştuğunda bekleme sürem dolmuştur ve triyakı sürmem gerekir. Bunun için oluşan kesme rutini içinde ilk iş MOC3020 nin bağlı olduğu pini "0"  ve hemen tekrar "1" yapıyorum. Kesme rutini içinden çıkmadan tekrar zaman kesmesi olmaması için timer1 birimini durduruyorum. Bu önemli aksi halde kesme oluşur ve alakasız bir anda triyak iletime geçer.

ADC Ayarları

Çıkışı ayarlamak için gereken bekleme süresini potansiyometre ile ayarlamak istedim. Bunun için ADC kullanmam gerekiyor. Yine daha önce detaylı değindiğim bir konu olduğundan tekrar etmeyeceğim. Öncelikle referans olarak AVCC seçtim. ADC aktif edip prescaler 128 olarak ayarladım. ADC kesmesini açıp ADC başlangıç için otomatik tetiklemeyi seçtim. Otomatik tetikleyici kaynağı olarak da dış kesmeleri seçtim. Böylece her sıfır geçiş olduğunda ADC çevrimi başlayacak çevrim sonunda ADC kesmesi oluşacak. Bu kesme içinde, bulunan 0-1023 arasındaki değeri 0ms ile 10ms arsında bekleme yapmak için gerekli olan TCNT değerleri olan 0 ile 2500'e oranlayarak çevireceğim.

Koda bakınca göreceğiniz gibi kullandığım Map fonksiyonu bu oranlama işlemini yapıyor. Orada 0 ile 2500 yerine 530 ile 2600 e oranlıyor. Buradaki 530 (132,5μs) bende ki donanımın yapmış olduğu erken tespitin ofsetidir. Neden 2600 de 2630 değil derseniz başta önemli olan 30 TCNT darbesi 7,5μs gibi küçük bir değer olduğundan sonda önemsiz oluyor. Ayrıca diğer alternansa taşma riskini de yok ediyor.

Tüm bu ayarları aşağıda kodlarda görebilirsiniz. Umarım faydalı bir yazı olur.


 * main.c
 *
 * Created: 4/4/2021 11:00:14 AM
 *  Author: haluk
 */ 
#define F_CPU 16000000UL
#include <xc.h>
//#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include <stdio.h>
//#include "uart.h"
#define MOC_PORT        PORTD
#define MOC_DDR            DDRD
#define MOC_PIN            PORTD4
#define TRI_DIR            MOC_DDR|=(1<<MOC_PIN)
#define TRI_ON            MOC_PORT&=~(1<<MOC_PIN)
#define TRI_OFF            MOC_PORT|=(1<<MOC_PIN)
#define  oranH 0.9
#define  oranL 0.1
volatile uint16_t adc=0;
volatile float sonuc=0;
volatile uint16_t dimmer=2500;
//char sadc[20];
uint32_t mapint( uint32_t x,uint32_t in_min, uint32_t in_max, uint32_t out_min, uint32_t out_max);
void timer1Con();
void irqZeroCross();
void adc_basla();
uint16_t adcOku(uint8_t kanal);
ISR(INT0_vect){
    TCCR1B|=(1<<WGM12)|(1<<CS10)|(1<<CS11);
    OCR1A=dimmer;
}
ISR (TIMER1_COMPA_vect){
    TRI_ON;
    _delay_us(10);
    TRI_OFF;
    TCCR1B=0x00;
}
ISR (ADC_vect){
    adc=ADCW;
    sonuc=(float)((sonuc)*oranH)+(float)(adc*oranL);    
    dimmer=mapint((uint16_t)sonuc,0,1023,530,2600);
}

int main(void){
    DDRB|=(1<<5);
    TRI_DIR;
    irqZeroCross();
    timer1Con();
    //uart_basla(115200);
    adc_basla();
    while(1)
    {
        /*sprintf(sadc,"%d--%d--%.2f\n",dimmer,adc,sonuc);        
        uart_dizi(sadc);        
        _delay_ms(100);*/               
    }
}
void irqZeroCross(){
    DDRD&=~(1<<PORTD2);//pd2 giriş yapıldı
    //PORTD|=(1<<PORTD2);//pd2 dahili pull-up
    EICRA|=(1<<ISC01)|(1<<ISC00);//pd2 yükselen kenar
    EIMSK|=(1<<INT0);//pd2
    sei();// tüm kesmeler açık
}
void timer1Con(){
    TCCR1B|=(1<<WGM12)|(1<<CS10)|(1<<CS11);// ctc mode prescaler 64 1sn 250000 
    TIMSK1|=(1<<OCIE1A);//ocra eşleşme kesmesi açık
    OCR1A=249;//1ms de kesme
    sei();// tüm kesmeler açık
}
uint32_t mapint( uint32_t x,uint32_t in_min, uint32_t in_max, uint32_t out_min, uint32_t out_max){
    return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

void adc_basla(){
    cli();
    ADMUX=(1<<REFS0);
    ADCSRA=(1<<ADEN)|(1<<ADPS0)|(1<<ADPS1)|(1<<ADPS2)|(1<<ADIE)|(1<<ADATE);//oto triger
    ADCSRB |=(1<<ADTS1);//dış kesme adc triger
    ADCSRA |=(1<<ADSC);
    sei();
}