AVR- PID Sıcaklık Kontrolü


Çok ilgimi çeken bir konu olmuştur. Ne yazık ki uygulamaya dönük pek kaynak bulamadım. Genelde yabancı dilde kaynaklar veya benim gibi amatörün anlamakta zorlandığı yazılar var. Bu konuda en basit anlatım ne yazık ki artık yayında olmayan, bu nedenle de bağlantı veremediğim bir yerdeydi. En zor olan kısım başta PID algoritmasını kurmak ve bunu yazılıma aktarmak oldu. Önceki bir yazımda (P) oransal kontrolü denemiştim. Hatta hazır kodlarla PID denemesi de yaptım ama o zaman beceremedim. Bilmediğim bir konuda ezbere bir şey denemekle doğru sonuca ulaşmayı beklemek büyük bir hataydı. Şu anda da biliyorum diyemem. En azından pratik olarak çözüm getirebilecek PID kontrolü kullanabilecek kadar anladım diyebilirim. PID konusunda sorun olan diğer kısım da katsayıların bulunması oldu. Bunun gibi bazı kaynaklar mevcut. Ben bu şeklide bir sonuca varamadım. Bu konuya daha sonra değineceğim.

PID

Birçok kaynağı okumaya kavramaya çalıştım ve anladığımı basitçe aktarmaya çalışayım. PID bir sistemde belirlenen hedef değere ulaşmaya çalışan çeşitli yöntemlerinden birisidir. PID oransal, integral ve türevin İngilizce kısaltmasıdır. Hedef bir değer ve anlık olarak mevcut durum değeri vardır. Hedefle bu anlık değer arasındaki fark oluşan hatadır. Bu hata değeri kullanarak çeşitli hesaplamalarla çıkış gücünün belirlenmesi ve bunun bir döngü halinde yapılması PID kontroldür. Sürekli olarak anlık kontrol ve buna göre çıkış gücü belirlenmektedir. Çıkışı belirleyen üç çeşit hesaplama yapılmaktadır. Bunlar oransal, integral ve türev hesaplamalarıdır. Günlük yaşantımızda farkında olmadan refleks olarak yaptığımız birçok şey örnek olabilir. Araba ile hızımızı kontrol etmemizden tutunda bir şişeye musluktan su doldurmaya kadar,  beynimizin kullandığı bir sistem. Şişeyi doldururken önce sonuna kadar açıp dolmaya yakın yavaş yavaş musluğu kapatmamız, taşacak sanıp biraz daha kapatıp ama dolmadığı için tekrar açmamız bir örnek olabilir. Öncelikle oransal kısmını ele alalım.

Oransal (P)

Bir ortamı ısıtmak istiyorum. Limit değer olarak 30°C belirledim. Ortam 21°C ve farkı kapatmak için ısıtıcıyı çalıştırıyorum. İstenen sıcaklığa gelince ısıtıcıyı kapatıyorum. Bu şekilde aç-kapa yaptığımızda asla istenen değerde sabit tutamayız. Sıcaklık istenen değere geldiğinde kapatsak da bir süre ısıtıcı sıcaklığı artırmaya devam edecektir. Limit altına düşünce de ısıtıcı çalışmaya başlayıp ısıtana kadar sıcaklık düşmeye devam edecektir. Bu nedenle limite yaklaştıkça ısıtıcının verdiği enerjiyi azaltmak ve düştükçe artırmak daha mantıklı olacaktır.

Bunun için ısıtıcı kaynağa verdiğim çıkışı dimmer ile ayarlıyorum. Dimmer ayarı için sıfır geçişi sonrası bekleme süresini değiştirerek ayarlama yapıyorduk. Bu bekleme süresini de limitle anlık değer arasındaki farka göre belirliyoruz. Örneğin 30 °C ayarlı ortamın sıcaklığı 25°C ise aradaki 5°C fark sistemin hatasıdır. Bu farkın kapanması için ısıtıcı belli bir oranda çalıştırılmalıdır. Sistem çalışmaya devam ediyor ve belli aralıklarla sıcaklığı okuyoruz. Aradaki fark 1°C ye geldiğinde ısıtıcı yine çalışmalı ama bu daha düşük bir voltajda olmalıdır. 5°C de dimmer değeri "0" yani tam güçle çalışırken 1°C olduğunda dimmer değeri 500 olmalı, 0,1°C için 2300 olmalıdır. Burada dimmer değerinin artması kafa karıştırıcı duruyor. Tekrar hatırlatayım bu değerler bekleme süresidir ve arttıkça çıkış voltajı azalır. Bekleme  yani dimmer değeri olarak belirttiğim değerler 0 ile 2500 arasındadır. 2500 değeri 10ms ye karşılık gelir.

Sıcaklık farkı (hata) birer birer değişirken benim dimmer değerini 0 ile 2500 arası değerlere getirmem gerekiyor. Hatanın değişim oranına göre dimmer değerini belirlemek için bir katsayıya ihtiyaç var. Bu katsayı, PID kısaltmasının P yani oransal değerini bulacağımız kp  katsayısıdır. Böylece P=hata x kp şeklinde P değerini buluruz. Sıcaklık farkı azaldıkça dimmer değerimiz artıyor. Bunun için de Dimmer=2500-P şeklinde yazmamız yeterli olur. Fark 1°C olduğunda P= 1 x2000 ile P=2000 ve Dimmer= 2500-2000=500 olur.

Kısaca tekrar edeceğim. İstenen değer ile anlık değer arasındaki fark hatadır. Bu hatanın büyüklüğüne göre çıkışın ayarlanmasına oransal kontrol denir. Burada çıkış dimmer değeri ile kontrol ettiğimiz devrenin voltajıdır. Fark azaldıkça dimmer değeri artar çıkış voltajı da azalır. Fark artarsa dimmer değeri azalır çıkış voltajı artar.

Oransal Uygulama

Oluşturduğum test ortamı basit bir plastik kutu, bir ampul, bir  PC fanı ve sıcaklık sensöründen oluşuyor. Ampulü dimmer ile kontrol ediyorum. Aydınlık seviyesi artıkça yaydığı ısı da artıyor. Kutu içindeki sıcaklığı AHT10 ile okuyorum ve bu şekilde dimmer seviyesini belirliyorum. (AHT10 ve AC dimmer isimli yazılarıma göz atmanız buradaki anlatımı daha anlaşılır kılacaktır.) Ortam sıcaklığı 21°C ve kutuda izolasyon olmadığı için çok fazla kayıp var bu nedenle en fazla 44 °C oldu. Bir saniye aralıklarla yaptığım ölçümlerde sıcaklık artışı başta 0,8 °C iken Δt arttıkça bu sıfıra düştü. Yine en üst seviyeden aşağı doğru 0,25 °C bir kayıp ile başladı. Bu rakamları neden verdiğimi daha sonra açıklayacağım.

Öncelikle dimmer, P, kp ve Fark için değişkenlere ihtiyacımız var.  Bu değişkenleri Float cinsinden belirleyeceğiz. Böylece virgülden sonrası içinde hesap yapabileceğiz ve daha hassas bir çıkış alacağız. Aşağıda tüm fonksiyonları eklemedim en altta toplu olarak paylaşacağım. Sonsuz döngü  içinde sıcaklık değerini okuyup set değerinden çıkartıyoruz. Bu işlem sonucunda bulduğumuz değer  hatadır. Bulunan hatayı Kp ile çarparak P_dim değerini buluyoruz. Bu değer oransal kontrol değeridir. Dimmer beklemesi için gereken süreyi 2500 den çıkartıp buluyoruz.

uint16_t set_deger=0;
float hata=0;
float kp=2000.0;
volatile int32_t dimmer=2500;
volatile float p_dim=0.0;

ISR(INT0_vect){			
		dimmer=2500-p_dim;
		if (dimmer<=0){//dimmer değeri sınırlanıyor
			dimmer=0;
		}
		if (dimmer>=2500){
			dimmer=2500;
		}
		TCCR1B|=(1<<WGM12)|(1<<CS10)|(1<<CS11);//timer çalıştı
		OCR1A=dimmer;//timer kesmesi için gereken süre		
}
int main(void){
	aht10_init();	
	while(1){
		
		hata=set_deger-aht10_read();//sıcaklık okundu set değer farkı hata
		p_dim=kp*hata;				
		
	}
}

Bu şekilde kontrol ettiğimizde alacağımız sonuçlar aşağıdaki gibi olacaktır. Kp katsayıları sırasıyla 1500,3000 ve 6000 olarak ayarlıdır.





Grafiklerde görüldüğü gibi Kp büyüdükçe hedefe yaklaşıyor ama tam hedef değeri yakalayamıyor. Bu grafikler için bilerek böyle büyük değerler seçtim ve mümkün olduğunca eşit şartlarda başlattım. PID katsayıları hesaplama yöntemlerinden biri olan ve uygulayabileceğim bir yöntem gibi gelen  Ziegler-Nichols yönteminde ben yanlış anlamadıysam Kp değeri ile bir salınıma girmesi gerekiyor. Ben ne yazık ki salınımı bu düzenekte göremedim. Belki ısıtıcı olarak kullandığım ampul, izolasyonu olmayan kutu belki de bunu tespit etmek için gerekli ekipmana sahip değilim. Kp için 50000 gibi bir değer yazınca çok küçük bir miktar salınım yakalamış olsam da bu salınımın periyodunu bulmam çok zor. Yaklaşık bir değer bulup bununla Ki ve Kd hesaplaması yaptığımda çıkan değerlerin uygun değerler olmadığını söyleyebilirim.

Oransal kısımda hedef ile anlık değer arasındaki hataya göre çıkış değerimizi ayarlıyoruz, hata azaldıkça çıkış azalıyor. Bu nedenle hedef değer altında kalıyor istenen değere ulaşamıyoruz. Biz istenen değere ulaşamıyoruz ama hata asla sıfırlanmıyor. Küçük bir değer olsa da devam ediyor. Üstteki grafik üstünde konuşursak. Hedef değer 40 °C seçili ilk başta 36 °C olan sıcaklık artmış 39,5-39,6 °C arasında bir değerde kalmıştır. Bu durumda hata 0,5-0,4 arasında olmuştur. Buna göre bir hesap yapalım. P=Kp x hata =1500x0,5=750 olur. P 750 ise dimmer bekleme değeri 2500-750=1750 olur. Bu değer ancak bizim ısı kaybımızı karşılamıştır. Daha düşük bir dimmer değeri olmalı ki çıkış gücü artsın ve ortama verilen ısı artsın, böylece hedef değere ulaşabilelim. Sistemin sıcaklığı arttıkça P azalacak dimmer artacaktır yani sadece oransal çıkış değeriyle hedefe ulaşamayız.

İntegral (I)

Bu deney kutusunda ve gerçek bir sistemde ısı kaybı olacaktır. Dış ortamla iç hacim arasında sıcaklık farkı arttıkça bu kayıp artacaktır. Hedef altında kaldığımızda oluşan hata her ölçüm anında devam eder. Başta söylediğim gibi PID kontrolde sürekli sabit aralıkta ölçüm ve buna göre çıkış hesaplaması yapılır. Bu ölçümlerde belirlenen geçmiş hataların toplamı yani "I" dimmer değerinin düşmesini sağlayacak olan çıkışı verir. Sistemin kaybını bulur ve buna göre çıkışı düzenler. (Aslında integral hata-zaman grafiğinde oluşan eğrinin altında kalan alanın hesabını verir. Bunlara girmeye gerek olmadığını düşünüyorum. Amacım basit bir şekilde anlatmak.)

Yaptığım hesapta bulunan hata=0,5 tir ve bu küçük bir değerdir. Yukarıdaki grafikte x ekseni zamanı gösterir ve birimi saniyedir ama sistemin okuma aralığı 100ms dir. Sıcaklığın hedefe en yakın olduğu zaman, ilk açılıştan yaklaşık 60sn sonrasıdır. İlk açılışla birlikte oluşan hata=4 ve bu her ölçümde azalarak 0,5 e kadar gelmiştir. Kabaca bir ortalama alırsak 60. saniyede hata toplamı=(ortalama hata)2,25 x 600=1350 olur. Küçük miktardaki hatalar toplandığında bulduğumuz hata toplamı oldukça büyük bir rakam oldu. Bu haliye P değeri gibi dimmer bekleme değeri için toplama koyduğumuzda küçük bir değer olan 0,5 in etkisini görürüz. Bunun için dimmer=2500-(P+I) şeklinde değişiklik yapacağız ve dimmer= 2500(750+1350)=400 buluruz. Artık dimmer değeri düştü çıkış gücümüz arttı.

Bu sayede hedef değeri yakalamış olur hatta geçeriz. Geçtiğimizde hata eksi değer almaya başlar ve hata toplamı da azalmaya başlar. Hata toplamı azaldıkça dimmer değeri artar, çıkış azalır. Hatalar ilk açılıştan itibaren toplanıyor, istenen değer çok daha yukarıda bir değer olsun. Haliyle 60sn yerine çok daha uzun bir sürede hedefe yaklaşırız ve hatalar toplamı çok büyük rakamlara ulaşır. Hedef değeri geçip azalmaya başlaması da uzun zaman alır. Bu sistemin kararsız olmasına neden olur. Sadece integral hesabıyla kontrol edilen sistemde 1350 lere çıkan hata toplamının tekrar düşmesi için yine bir 60 sn gerekir.



Böyle bir grafik elde ederiz ki bu istemediğimiz bir şeydir. Bunu önlemek için öncelikle hata toplamını sınırlamamız gerekir. Sınır koysak bile yine de yüksek değerlere çıkacak olan hata toplamını hesabımıza oransalda olduğu gibi bir katsayıyla dahil etmeliyiz. ( I=Ki x Thata ) Hata toplamı sınırını 1500 yaptığımızda ve bu değere ulaştığımızda (I=1500) integralin çıkışa etkisi fazla olacaktır. Katsayıyı 0,8 gibi bir değer belirlersek hata üst sınıra ulaştığında Thata x Ki =1500x0,8=1200 gibi daha uygun bir değer buluruz. (Dimmer 2500 yani 10ms olduğundan bulunan I değeri çok büyük olursa sistemin toparlanması zorlaşır. )

Kısaca tekrar edersek, integral ile bulduğumuz çıkış kazancı, geçmiş hataların toplamıdır. Sistemin kaybını bulur ve hedef değerde sabit kalmasını sağlar.

İntegral Uygulama

Test kutusundaki ısı kaynağının yetersizliği ve ısı kaybı fazlalığından oransal hesapla bulduğumuz çıkış değeriyle hedef değere ulaşamadık. İntegral ile hedef değere ulaşacağız. Bunun için her okuma anında bulduğumuz hatayı bir önceki hatayla toplayarak toplam hata değerine ulaşacağız. Toplam hatayı Ki katsayısı ile çarparak i_dim değerini bulacağız. P_dim ile birlikte dimmer değerini bularak çıkışı ayarlayacağız. Toplam hatayı sınır değerler içinde tutarak çok büyük değerlere ulaşmasını engelleyeceğiz.

uint16_t set_deger=0;
float hata=0.0,  t_hata=0.0;
float kp=1500.0, ki=1.0;
volatile int32_t dimmer=2500;
volatile float p_dim=0.0, i_dim = 0.0;

ISR(INT0_vect){			
		dimmer=2500-(p_dim+i_dim);
		if (dimmer<=0){//dimmer değeri sınırlanıyor
			dimmer=0;
		}
		if (dimmer>=2500){
			dimmer=2500;
		}
		TCCR1B|=(1<<WGM12)|(1<<CS10)|(1<<CS11);//timer çalıştı
		OCR1A=dimmer;//timer kesmesi için gereken süre		
}
int main(void){
	aht10_init();	
	while(1){		
		hata=set_deger-aht10_read();//sıcaklık okundu set değer farkı hata
		p_dim=kp*hata;
		i_dim=(t_hata*ki);
		t_hata+=hata;
		if (t_hata<=-1200){// toplam hata sınırlama
			t_hata=-1200;
		}
		if (t_hata>=1200){// toplam hata sınırlama
			t_hata=1200;
		}		
	}
}

Bu arada Ki değeri çok büyük olursa sıcaklık hedef değerin çok üstüne çıkar hatta sistemde salınıma neden olur. Aşağıda sırasıyla (Kp=3000-Ki=1),(Kp=3000-Ki=1,25) ve (Kp=1500-Ki=1,25) için sonuçlar vardır.




Sistemin istenen değerlere hızlı bir şekilde çıkması ve sabit kalması için PI kontrolü yeterlidir ama ani değişimlere cevap vermekte yetersiz kalabilir. Bunun için türev hesaplamasını dahil etmek gerekir.

Türev(D)

İntegral kontrolde geçmiş hataların toplamıyla çıkışa etki ediyorken türevde gelecek hatalara göre çıkışı kontrol ediyoruz. Bunun için bir önceki okunan değerle anlık okunan değerin farkına bakıyoruz. Yine zaman sabit olduğundan sadece hatayı ele alacağız. Okunan sıcaklık değeri her bir saniyede 0,05 °C  arttığını düşünelim. Bu durumda hata her bir saniyede 0,05 azalacaktır. Bu küçük rakam çok etkili olmayacaktır bu nedenle ve diğer hesaplamalarda (P-I) olduğu gibi çıkışa etkisini değiştirebilmek için bir katsayıyla (Kd) hesaplama yapacağız. Bunun için kullanacağımız formül Pd=(hata-önceki hata) x Kd olacaktır.

Hata miktarı küçük sayı farklarıyla sabit bir değişim içindeyse türevin çıkışa önemli bir etkisi olmayacaktır. Hata ve önceki hata aynı olduğunda türev kontrolün kazancı "0" olacaktır. Durağan bir sistemde hiç etkisi olmazken değişim aniden yükselir veya düşerse bir sonrakinin de bu ani sapmayı devam ettirme olasılığı yüksek olacaktır. Sıcaklık değişimi 0,05 ten  0,1 hatta 1 °C gibi bir fark oluşturduğunda hata ve önceki hata arasındaki fark büyür ve çıkışa etkisi artar. Doğru katsayı seçildiğinde çıkışa etkisi yukarıdaki grafiklerdeki ani değişimleri yumuşatma şeklinde olacaktır. Kp ve Ki de olduğu gibi yüksek değerlerde Kd sistemi salınıma sokacaktır. Doğru Kd değeriyle ani sıcaklık değişimine daha hızlı cevap veren bir kontrol mekanizmasına sahip oluruz. Sabit aralıklarla artan sıcaklık durumunda hata ve önceki hata farkı negatif olacaktır. Bu P ve I ya ters olarak etki edecektir ama ani bir sıcaklık düşüşünde hata-önceki hata farkı pozitif işaret alacaktır. Böylece P ve I ye eklenerek çıkışı yükseltecektir.



Yukarıda dimmer değerini hesaplamakta kullandığım  P,I ve D değerlerinin zamana göre değişimi görünmektedir. Dengeye gelmekte olan düzeneğin kapağını açtığımda yeşil olan türevin hareketi anlatmak istediğimi gösteriyor. Sıfıra yaklaşan P değeri ile birlikte yükseliyor haliyle aynı oranda sıcaklık artışı yaşanıyor. Hata halen var olduğundan P artmaya devam etse de D artmıyor ve tekrar sıfıra yaklaşıyor. Sıcaklık değişimi dengelenip bir miktar aştığında P düşmeye başlarken yine D bu düşüşe destek olarak eksi yönde hareket ediyor. P düşmeye devam ederken D yine sabit salınımlarla sıfıra yaklaşıyor.

Türev Uygulama

Ani değişimlere aynı hızda cevap verebilmek için türev kontrolünü de ekliyorum. Bunun için önceki hatayı tuttuğum bir değişken daha tanımlayıp aşağıdaki şekilde kodları oluşturuyorum. Türev için kullandığımız Kd katsayısı çok büyük olursa ve sistemin okunmasında gürültü varsa sistem kontrolden çıkacaktır.

uint16_t set_deger=0;
float hata=0.0, o_hata=0.0, t_hata=0.0, ohata=0.0;
float kp=1500.0, ki=1.0, kd=1500.0;
volatile int32_t dimmer=2500;
volatile float p_dim=0.0, i_dim = 0.0, d_dim=0.0;

ISR(INT0_vect){			
		dimmer=2500-(p_dim+i_dim+d_dim);
		if (dimmer<=0){//dimmer değeri sınırlanıyor
			dimmer=0;
		}
		if (dimmer>=2500){
			dimmer=2500;
		}
		TCCR1B|=(1<<WGM12)|(1<<CS10)|(1<<CS11);//timer çalıştı
		OCR1A=dimmer;//timer kesmesi için gereken süre		
}
int main(void){
	aht10_init();	
	while(1){		
		hata=set_deger-aht10_read();//sıcaklık okundu set değer farkı hata
		p_dim=kp*hata;
		i_dim=(t_hata*ki);
		d_dim=(o_hata-hata)*kd;
		o_hata=hata;
		t_hata+=hata;
		if (t_hata<=-1200){// toplam hata sınırlama
			t_hata=-1200;
		}
		if (t_hata>=1200){// toplam hata sınırlama
			t_hata=1200;
		}		
	}
}



Grafiklerden ilki Kd=5000 ayarlı, ikinci grafikte Kd=20000 ayarlandı.

Katsayılar

Katsayıları ayarlamak için çeşitli yöntemlerin olduğunu araştırma yaparken görebilirsiniz. Ben ne yazık ki yapamadım. Yukarıda test kutusunun zamana bağlı sıcaklık artış ve düşüşlerini aktarmıştım. Bu değerlere göre bir excel dosyası oluşturdum. Kendi katsayılarımı bulurken en büyük yardımcım bu dosya oldu. Simülasyon demek ne kadar doğru olur bilmiyorum ama öyle de diyebilirim. Belirli aralıklarla ölçüm yapılmış ve bu aralıklarda ısı kaybı ve kazancı olan bir sistemin simülasyonu oldu. Bu dosyadaki  katsayıları, ortam sıcaklığını ve set değeri değiştirerek sonucu görme şansı buldum.

Isı kazancı ve kaybını kutudaki gibi ayarladım. Sadece ani değişiklikler için bir şey yapmadım. Bu nedenle türev kontrolün bu durumlardaki etkisini gözlemleyemeyiz ama hatalı değerlerde sistemi nasıl bozacağını görebiliriz. Bu simülasyonda ve muhtemelen gerçekte de istenen değer değiştikçe oranlarında değişmesi gerekecek. 100ºC ile 250ºC için doğru ayarlar aynı olmayacaktır. Yine tek bir doğru değer olmadığını Kp ve Ki değerlerinin birbirine göre oranlı bir şekilde değişebileceğini göreceksiniz. Sistemin ısı kazancı-kaybı, okuma aralıkları ve okunan değerin gürültüsü de bu oranlarda değişikliğe neden olacaktır. Profesyonel bir iş için deneme yanılma veya böyle bir simülasyonla yapılacak bir iş olmadığını da söyleyebilirim. Amatör bir proje için bu şekilde bulmakta bir sakınca yok. Benim uygulamam için bana göre en uygun katsayılarla oluşan grafik bu şekildedir.



Dosyanın içindeki grafiği paylaşıyorum. Katsayılar bilerek bu şekilde yazıldı. Dosyaya buradan ulaşabilirsiniz.


 

Yukarıda paylaştığım kod parçalarının bire bir aynısını kullanmıyorum. Biraz derli toplu hale getirilmiş halini paylaşıyorum. Kısaca işleyişi açıklayayım, kod üstünde açıklamaları yaptım. Sıfır geçiş tespiti ile Dış kesme oluşuyor. Böylece hem set değeri ayarlamak için ADC0 pini için ölçüm başlıyor. Hem dimmer değeri hesaplanıyor. Ayrıca kesme rutini içinde yer alan bir sayaçla AHT10 için gerekli beklemeleri yapıyorum.  Sayaç 50ms de bir durum değişkenini artırıyor. AHT10 ölçüm başlatma ve okuma 50ms aralıklarla delay olmadan yapılıyor. I2C ile bir veri alındıysa veri alındı bayrağı "1" oluyor buna göre sıcaklık için gerekli hesaplamalar yapılıyor. Sonrasında PID için hesaplamalar yapılıyor. Böylece başa dönüp sonraki kesmede dimmer için gerekli değerler bulunuyor.

Microchip Studio dosyalarına buradan erişebilirsiniz.

/*
* dimmer_aht10_pid.c
*
* Created: 29.01.2022 18:10:53
* Author : haluk
*/
#define F_CPU 16000000UL
#include <avr/interrupt.h>
#include <avr/io.h>
#include "uart.h"
#include "i2c_master.h"
#include <util/delay.h>
#include <stdio.h>

#define MOC_PORT			PORTD
#define MOC_DDR				DDRD
#define MOC_PIN				PORTD4
#define TRIAC_DIR			MOC_DDR|=(1<<MOC_PIN)
#define TRIAC_ON			MOC_PORT&=~(1<<MOC_PIN)
#define TRIAC_OFF			MOC_PORT|=(1<<MOC_PIN)
#define ORAN_H				0.9
#define ORAN_L				0.1
#define MAX_T_HATA			1200

float kp=1500.0, ki=1.0, kd=1500.0;
float hata=0.0, o_hata=0.0, t_hata=0.0;
float sicaklik=0.0;
volatile float p_dim=0.0, i_dim = 0.0, d_dim=0.0;
volatile int32_t dimmer=2600;


volatile uint16_t adc=0;
volatile int16_t set_deger=0;
volatile float sonuc=0.0;

volatile uint32_t zero_sayac=0;
uint32_t  onceki_sayac=0, simdiki_sayac=0 ;
uint8_t durum_sayac=0, onceki_durum=0, simdiki_durum=0;

uint8_t aht_adr=0x38;//aht10adres
char str_data[64];
extern volatile uint8_t veri_geldi;

void PID_hesapla(float pk,float ik,float dk, int16_t set,float temp, int16_t maxhata);
void timer1Con();
void irqZeroCross();
void adc_init();
void aht10_init();
void aht10_trig();
void aht10_read();
float aht10_get_temp();

ISR(INT0_vect){	//10ms de bir kesme oluşur.
	if (PIND&(1<<PIND2)){//yükselen kenar kontrol
		if (PIND&(1<<PIND2)){			
		dimmer=2600-(p_dim+i_dim+d_dim);
		if (dimmer<=300){//sıfır geçiş ofset
			dimmer=300;
		}
		if (dimmer>=2600){//sıfır geçiş ofset
			dimmer=2600;
		}
		TCCR1B|=(1<<WGM12)|(1<<CS10)|(1<<CS11);//timer1 çalıştı
		//OCR1A=2600;//dimmer ofset elle girilerek konrtol edildi
		OCR1A=dimmer;//dimmer değeri kadar bekleme
		zero_sayac++;//her sıfır geçişte sayaç artar 10ms de bir
		}
	}
}
ISR (TIMER1_COMPA_vect){
	TRIAC_ON;
	_delay_us(10);
	TRIAC_OFF;
	TCCR1B=0x00;//timer1 durdu
}
ISR (ADC_vect){//her sıfır geçişte ADC0 ölçülüyor
	adc=ADCW;
	sonuc=(float)((sonuc)*ORAN_H)+(float)(adc*ORAN_L);
	set_deger=((uint16_t)sonuc)>>2;//0-255 arası değer alır	
}


int main(void){
	TRIAC_DIR;
	irqZeroCross();
	timer1Con();
	uart_basla(115200);
	i2c_init();
	aht10_init();
	adc_init();
	//set_deger=40;		
	while(1){		
		simdiki_sayac=zero_sayac;
		if ((uint32_t)(simdiki_sayac-onceki_sayac)>=5){//50ms de bir artar						
			onceki_sayac=zero_sayac;
			durum_sayac=(durum_sayac+1)&1;			
		}			
		switch (durum_sayac){//aht10 i2c iletişimin sağlıklı yürümesi için gereken beklemeler ayarlandı.
			//her seçenek arası 50ms
			case 0:
				if (onceki_durum!=durum_sayac){
					aht10_trig();//aht10 ölçüm başlatıldı			
					onceki_durum=durum_sayac;
				}
				break;
			case 1:
				if (onceki_durum!=durum_sayac){
					aht10_read();//aht10 ölçülen veri okundu						
					onceki_durum=durum_sayac;
				}
				break;
			default:
			uart_gonder('d');
				durum_sayac=0;
				break;
		}
		if (veri_geldi==1){
			sicaklik=aht10_get_temp();//sıcaklık bilgisini alındı
			PID_hesapla(kp,ki,kd,set_deger,sicaklik,MAX_T_HATA);//pid hesaplandı
			//sprintf(str_data,"dimmer:%li pdim:%.2f idim:%.2f ddim:%.2f\n",dimmer, p_dim,i_dim,d_dim);
			sprintf(str_data,"sic:%.2f set:%d\n",sicaklik,set_deger);
			uart_dizi(str_data);
		}	
				
	}
}
void PID_hesapla(float pk,float ik,float dk, int16_t set,float temp, int16_t maxhata){
	hata=set-temp;
	p_dim=(pk*hata);
	i_dim=(t_hata*ik);
	d_dim=(hata-o_hata)*dk;
	o_hata=hata;
	t_hata+=hata;
	if (t_hata<=-maxhata){
		t_hata=-maxhata;
	}
	if (t_hata>=maxhata){
		t_hata=maxhata;
	}
}
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
}
void adc_init(){
	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();
}
void aht10_init(){	
	uint8_t aht_res=0xBA;//soft reset
	uint8_t aht_init[]={0xE1,0x08,0x00};//init
	i2c_send_data(aht_adr,aht_res, N_REPEAT);//soft reset
	_delay_ms(100);	
	i2c_send(aht_adr,aht_init,3, N_REPEAT);//init komutu
	_delay_ms(100);
}
void aht10_trig(){
	uint8_t aht_trig[]={0xAC,0x33,0x00};//ölçüm başlatma
	i2c_send(0x38,aht_trig,3,N_REPEAT);//ölçme tetik
}
void aht10_read(){
	i2c_read(aht_adr,6);// veri okuma
}
float aht10_get_temp(){	
	uint8_t aht_data[6];
	uint8_t poz=0;
	uint32_t aht_temp=0;		
	do {			
			aht_data[poz]=i2c_oku();
			poz++;	
		} while (i2c_gelen());	
	if (poz!=6) return sicaklik;//6 byte veri gelmeli hatalı gelirse bir önceki değer döner		
	aht_temp=((uint32_t)(aht_data[3]&0x0F)<<16)|((uint16_t)aht_data[4]<<8)|(aht_data[5]);	
	return ((float)aht_temp/5242.88)- 50;
}
/////////////////////////////////////////////////////////////////////////////
/*
 * 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;
volatile uint8_t veri_geldi=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){
		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(){
        veri_geldi=0;
	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; 
			veri_geldi=1;
			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

  

 




6 yorum:

  1. Merhaba Haluk Bey,
    Mükemmel paylaşımlarınız için teşekkürler.
    Software uart ve donanımsal uartı bir arada kullanmak için nasıl bir yol izlemeliyiz.
    Sizin daha önceki donanımsal uart çalışmalarınızdaki örnek yapıyı sorunsuz kullanıyorum.
    Ama ikisini bir arada kullanmadım.İşlemcim atmega328p.
    Bu tarz bir örnek çalışmanız varmı ?

    YanıtlaSil
  2. Güzel yorumunuz için teşekkür ederim. Donanım ve yazılım birlikte sorunsuz çalışmaz. Yazılımla çalışan veri gönderirken kesmeleri kapatır. Bu durumda eğer donanım tarafına veya yazılım tarafına (RX) veri gelirse, gelen veriyi kaçırır. Bu özellikle işlemci ve UART düşük hızlarda sorun olacaktır. Atmega328p için software çalışması yapmadım. MCU frekansı yüksek olduğundan bahsettiğim çakışma çok nadir olacaktır. Donanım varken yazılım kullanmak pek doğru seçenek olmayacaktır. Farklı yöntemleri denemenizi öneririm.

    YanıtlaSil
  3. Merhaba ,
    Kıymetli geri dönüşüz için çok teşekkür ederim.
    Aslında software uarta bir sensör bağlayacağım ordan aldığım veriyi işleyip donanımsal uarttan ileteceğim yani master/slave gibi çalışacak.Çakisma sorununu çözmüş olur diye düşünüyorum. Paylaşmış olduğunuz çalışmayı software uart kısmı için kullanmam ne kadar mümkün olur , işlemci farkından dolayı dikkat etmem gereken husus varmı teşekkürler

    YanıtlaSil
    Yanıtlar
    1. Merhaba,
      Bana göre RS485 sizin için daha uygun olur. Bu konuda benzer bir şey yapmıştım ve genel hatlarıyla paylaştım.
      Arduino Software serial kütüphanesi Atmega328p için ve işinizi görür. Ufak tefek değişiklikler yapmanız gerekir. İsterseniz https://github.com/haluks/Attiny45
      burada Attiny45 için hazırladığım kütüphane var. Register değişiklikleri gerekecektir. Dış kesme register ve ilgili bit isimleri farklı zaten derleyici uyarır. Bir de logic analizörle kontrol ederek bekleme kontrolü yapmanız doğru olur. F_CPU ile Baud rate hesaplamak ve tx-rx delay bulmak tam doğru sonucu vermiyor. Yapılan işlem için gereken döngü miktarını belirlerken kullanırsınız. Takıldığınız yer olursa bilgim dahilinde yardımcı olmaya çalışırım.

      Sil
  4. Çok teşekkür ederim, en kısa zamanda paylaştığınız kütüphane üzerinden denemelere başlayacağım.Paylasımlarınız sayesinde avr ile projeler gerçekleştirme fırsatım oldu.Buna bir yenisi daha eklenmiş olacak.

    YanıtlaSil