AVR - Attiny 13A-AHT10 ile Sıcaklık ve Nem Ölçümü

 


Attiny13A Attiny ailesinin  küçük ve nispeten ucuz üyelerindendir. Datasheete buradan ulaşabilirsiniz. Attiny 45 de olduğu gibi bunda da tam olarak yerleşik UART,SPI ve TWI (I2C) birimleri yoktur. Attiny 45 de en azından USI ile tamamen olmasa da donanımla çalışan TWI ve SPI bulunmaktaydı. 64 byte SRAM ve 1024 byte Flash hafıza ile oldukça zorlayıcı bir mikro denetleyici olduğunu söylemeliyim. Bu deneyimler optimizasyon konusunda oldukça faydalı oldu. Yazılımla çalışan UART-I2C ile uğraşırken bu haberleşme yöntemlerinde de eksiklerimi gidermeme yardımcı oldu. Daha önce I2C kütüphanesi yazmıştım ama bazı sorunları vardı. Örneğin iletişim koptuğunda sonsuz döngüye girmiyordu ama iletişim tekrar başlamıyordu. Buradaki hatalarımı gördüm ve o yazdığım kütüphaneleri de güncelledim. AHT10 için yazdığım konuda datashetteki formülü aynen kullanmıştım. Burada tek bir float değişken dahi tanımlayacak hafıza olmadığından değişiklik yapmak zorunda kaldım. İlk olarak yazılımla çalışan UART ile başlayayım.

Software UART 



Attiny 45 için de bir software UART kütüphanesi yazdım orada birçok hesaplamayı kodların içinde yaptım ama burada önceden yapıp yazmam gerekti malum hafıza kısıtlı. Ama ben bu hesaplamaları anlatmadan geçmeyeceğim.  Bu hesaplamalardan kastım Baud Rate için gerekli olan hesaplamalardır. Baud rate saniyede giden bit sayısıdır. UART iletişimde çoğunlukla start, 8 bit veri ve stop olmak üzere 10 bit veri gönderilir. Parity veya bir den fazla stop biti eklenmiş hali dışında 8 bitten daha fazla verinin gönderildiği hali de kullanılabilir ama bunlara girmeyeceğim. Sıklıkla kullanılan Baud rate değeri 9600 bps üzerinden ilerleyeceğim. 9600 bps, bir saniyede giden bit sayısıdır. Bu durumda bir bit için gereken süre 1/9600 sn dir. Bu işlem sonucunu µs cinsinden yazmak için 1/9600*1000000 haline getirirsek bir bit için 104,166 µs gerekir.

UART iletişimi öncesi veri hattı yüksek "1" durumundadır. Hattın düşük "0" duruma geçmesi Start koşuludur. Bundan sonra 8 bit giden verinin ilk biti yani "bit0" başta "bit7" sonda olacak şekilde hattı "1" veya "0" yapar. Son olarak hattın "1" olması Stop koşuludur. Bu çerçeve (frame) içinde gerçekleşen "1" ve "0" olma durumlarının her birisi için geçmesi gereken süre 104µs dir. UART ile iletişim için saat sinyali taşıyan bir hat gerekli değildir. iki tarafın doğru haberleşmesi için önceden belirlenen baud rate değerine göre her bir bit için gereken süre doğru olmalıdır. Aksi halde iletişim başarısız olacaktır. 


Burada 'A' harfi gönderilmiştir. A dec 65= hex 0x41=bin 0b0100 0001 dir. Start sonrası bit0 "1" olduğundan hattı "1" yapmıştır. İlk olarak düşük öncelikli bitten başlanarak veri gönderilir. Son bit sonrası hat tekrar "1" yapılarak stop biti yazılmıştır. gördüğünüz gibi bir bit için gereken süre 104µs dir. Bu bazen 2-3µs farklı sürelerle gitse de çerçeve dışına taşmadıkça sorun olmayacaktır. Şekilde her bir veri bitinin ortasında bulunan nokta karşı tarafın veriyi okuduğu yerdir. Bu noktalar sınırı aşarsa iletişim başarısız olur.


Yukarıdaki veriyi alan kısımda baud rate 10600 olarak değiştirdim. Bu durumda bir bit için gereken süre 94,339µs olacaktır. Giden taraf 104µs bekler ve bit değişimi yapar ve frame için 1040µs gerekir. Alan taraf 943µs içinde iletişim bitmesini ve stop bitini beklerken 'A' için bit 7 "0" olduğundan stop gerçekleşmemiş olarak görür dolayısıyla çerçeve hatası oluşur. Bu zamanlamalar özellikle yüksek hızlarda önemlidir.

Aplikasyon notlarında olduğu gibi bir bit için gereken bekleme süresini ayarlamak için timer kullanmayı denedim ama yüksek hızlarda tutarsız çalıştı. Bunu farklı yöntemlerle yapmak gerekti. Bir iki farklı kütüphane inceledim onlardan bazıları çalışmadı bazıları assembly dili ile yazılmış kodlar kullanıldığı ve benim hiç bir bilgim olmadığı için örnek alamadım. Klasik olarak delay.h ve tam istediğim olmasına rağmen builtins.h kütüphaneleri sabit sayılar istediğinden kullanamadım. İşlemciyi istenen çevrim kadar bekleme yaptırmasından ve değişken değer aldığından delay_baisc.h kullanmaya karar verdim. Normalde delay kullanmak doğru değil ama burada donanım ile yapabileceğim bir birim yok mecburen delay kullanacağım.

Veri Gönderme (Tx)

Yazılımla çalışan UART için en kolay kısım gönderim kısmıdır. Gönderime başlamadan kesmelerin kapatılması gerekiyor, bir kesme oluşursa bit sürelerinde kayma oluşabilir. Gönderim için bir pini çıkış yap, sürekli yüksek tut, gönderim zamanı "0" yap bekle giden verinin ilk bitini kontrol et "1" ise "1" "0" ise hattı "0" yap. 8 bit için bu beklemeyi ve kontrolü yap hattı tekrar "1" yap ve bitti. Peki bu beklemeyi nasıl belirleyeceğiz. İşlemcinin çalışmasını kontrol eden ve her işlem için gereken süreyi belirleyen dahili bir saat kaynağı bulunmaktadır. Bu MCU ya harici bir osilatör bağlamak çok mantıklı değil zaten toplam 8 adet pin var. Bu dahili osilatör kaynak olacak şekilde sigorta ayarlarıyla işlemcinin çalışma frekansı ayarlanabiliyor. 
Öncelikle CKSEL1 "1" -CKSEL0 "0"yaparak 9,6MHz frekansını seçiyoruz. CKDIV8 "0" yaparak çıkış frekansını 8 e bölüyoruz. Böylece işlemcinin çalışma frekansı 1.2MHz olarak ayarlanıyor. CKDIV8 yerine CLKPR içindeki bitleriyle de istenen değerlere ayarlama yapılabilir. Örnek olarak 9,6MHz prescaler olarak 4 seçilmiş hali için gerekli kodlar:
CLKPR=(1<<CLKPCE);
CLKPR=(1<<CLKPS1);
 Ben neden bu frekansı seçtiğime daha sonra değineceğim.1.2MHz ile bir çevrim için gereken süre1/1.200.000= 0,833333..µs dir.  9600bps de bir bit için 104µs gerekiyordu. Buna göre 104,166/0,833333..=125 çevrimdir. Burada formülü bulmak için işlemleri birleştirelim. (1/9.600)/(1/1.200.000)=(F_CPU)1.200.000/ (BAUD)9.600=125 sonucunu buluruz. Delay_basic.h de tanımlı "_delay_loop_2()" fonksiyonu her bir değer için işlemciyi 4 çevrim boyunca bekletir. _delay_loop_2(1); dediğimiz zaman geçen süre 4*0,833333=3,333µs olacaktır. Bu durumda bulduğumuz çevrim değerinin 4 te birini fonksiyona parametre olarak girmemiz gerekir. Sonuç olarak bir bit için gereken bekleme süresini (Tx_delay) bulmak için formül Frekans/Baud/4 olur. Bekleme dışında yapılan kontrol ve duruma göre ayarlı pinin "1" veya "0" yapılması da bir zaman alacaktır.

Bu gereken zamanı fazladan eksilecek çevrim değeri olarak formülde işleyeceğim. Bunu deneme yanılma yöntemiyle değil lojik analizörle kontrol ederek yaptım. Derleyicinin ne kadar bir çevrim ile kodu işleyeceğini assembly bilmediğim için kontrol edemedim. Pin değişimi ve kontrol için gereken çevrim sayısını "3" olarak buldum ve böylece tx_delay= (F_CPU/BAUD/4)-3 oldu. Bütün bunların sonucunda gönderme için gereken fonksiyonu bu şekilde tanımladım.

void uart_gonder(uint8_t uData){        
        _delay_loop_2(tx_delay);
        TX_LOW;//start koşulu
        _delay_loop_2(tx_delay);//start için gerekli bekleme
        cli();//kesmeler kapatıldı
        for (uint8_t i=0;i<8;i++){//8 bit için tekrar
                if (uData&0x01){//ilk bit kontrol edildi
                        TX_HIGH;//ilk bit "1" ise "1"
                        }else{
                        TX_LOW;//ilk bit "0" ise "0"
        //burada else gerekli değil ama
        //"0" olması durumunda zamanlama yani gerekli çevrim değişmesin diye eklendi
                }
                _delay_loop_2(tx_delay);//her bir bit için gerekli bekleme
                uData>>=1;//veri bir bit kaydırıldı
        }        
        TX_HIGH;//stop koşulu
        _delay_loop_2(tx_delay);//stop için gereken bekleme
        sei();//kesmeler açıldı
}

Veri Alma (Rx)

Gönderim basit ve çok yüksek hızlarda bile daha hatasız olduğu halde alma kısmı o kadar basit ve hatasız değil. En büyük sorun işlemcinin düşük frekansta çalışıyor olması bu nedenle belli bir hızın üstünde veri alımı yapamayız. Veri alırken işlemcinin başka satırları yürütüyor olması ihtimaline karşı kesme kullanmaktan başka çare yok. Bunun için en iyi yöntem dış kesme kullanımı ama ne yazık ki bir tane dış kesme pini var ve onu da kullanarak MCU nun kolunu bacağını kesmek istemedim. ayrıca farklı pinleri seçme özgürlüğü olması için pin değişim kesmesini kullandım. Bu kesmenin de sorun hem yüksek hem düşük durumda kesme oluşturmasıdır. Bunu da kesme girişinde pinin "0" olmasını kontrol ederek ve  "0" ise yani start koşulu oluşmuşsa döngüyle tüm iletişim sonuna kadar işlem yaparak çözdüm. Kesme içinde döngü kullanmak doğru değil ama burada alternatif yok. Aksi halde iletişim gerçekleşmez. 

Alma sırasında yaşadığım bir diğer zorluk karşı taraftan kaç tane verinin geleceğini bilmeyişimdir. Bunu bilmeden örneğin gelen veriyi gönder gibi bir kodun çalışması sorun oluyor. Stop için gereken süreyi artırdığımda bu sorunu çözüyorum ama bu seferde yüksek hızlarda problem oluşuyor. Ben burada yüksek hızlarda veri alacak şekilde bir ayarlama yapmaya çalıştım.

Gönderme sırasında bir bit için beklediğimiz gibi alma sırasında da aynı süreyi kontrol etmek gerekiyor. Start sonrası bekleyip verinin ilk bitini kontrol edip geçici değişkene yazmamız gerekir. Start sonrası 104µs bekleyip hat "1" ise "1" yazmak doğru olmaz. Verinin ilk bitinin ortasında okuma yapmak doğru olacaktır. Öncelikle kesme ayarlarını yaptım, kesme oluşunca eğer hat "0" ise yani start oluşmuşsa işlem yapılacak. Sonrasında tx_delay kadar bir bekleme yapılarak 8 defa hat kontrol edilecek. Sonrasında stop için bir bekleme yapılarak okunan veri değişkene yazılıp kesmeden çıkacak. Bu kontrol işlemini yapmadan önce doğru zamanlamayı yakalamak için veri okuma kısmında bir pini toggle ile "1" ve "0" yaptım. Aşağıda bunu görebilirsiniz.

ISR(PCINT0_vect){                
        if (RX_PIN_LOW){//start kontrol pin "0" ise
                _delay_loop_2(tx_delay);
                for (uint8_t i=0;i<8;i++){                        
                        _delay_loop_2(tx_delay);
                        PORTB^=(1<<4);//zamanlama icin kulandım        
                }
                _delay_loop_2(tx_delay);
        }
}



Burada da 'A' harfi gönderdim, ilk bit için start sonrası geçen süre 104µs olurken kesme sonrası aynı miktar bekleme yapılmasına rağmen az bir gecikme oluşmuş ve çıkış olarak ayarladığım alttaki ikinci hattın ilk "1" olma zamanı 124,6µs olmuştur. Bu anda hattı kontrol edip veriyi yazmış olsaydım ilk bit için belki şansa doğru okuma yapardım ama kırmızı kutu içine aldığım 6 ve 7 numaralı bitleri doğru okuyamazdım. Ayrıca tx_delay için yaptığım hesaplama burada doğru sonuç vermedi  101µs gibi bir süre gerçekleşti. Bunu tam doğru olması için başka bir değişken daha tanımlayıp yeni bir hesaplama yapmam gerekecek. Bu alma kısmı olduğundan Rx_delay diyeceğim. Yine start sonrası okuma yapma zamanımı yani alttaki çıkışın ilk "1" olduğu zamanı üstte gelen verinin ilk biti içindeki noktayı yakalaması için bir gecikme daha gerekecek. Bunun için de yarım_rx_delay isimli bir bekleme daha hesaplayacağım.

İlk olarak rx_delay=(F_CPU/BAUD/4)-(çevrim sayısı) ile rx delayı bulabilirim. Tx ten tek farkı yapılan işlemlerde geçen çevrim sayısının farklı olmasıdır. Yarım rx için yarım_rx_delay=(F_CPU/BAUD/4)/2-(çevrim sayısı) formülünü kullanabilirim. Rx delayın yarısı diyemem çünkü  çevrim sayısı farkı olacaktır. Ayrıca her ikisi içinde bir sorun var baud rate belli değerin üstüne çıkınca sonuç 0 olacaktır. Bu frekansta 115200 zor olduğundan daha düşük değerlerde rx ve tx delay için sorun yok ama rx yarısı dediğimde yarım rx 0 olacaktır. Bunun için eğer yarım_rx 1 den küçükse 1 yap gibi bir koşul eklemesi yapmak gerekiyor. Bu şekilde eklemeler yapınca aldığım sonuç:


Görüldüğü gibi artık çıkışın "1" ve "0" olduğu anda veriyi okuduğumda sorun olmuyor. Start sonrası ilk biti de tam zamanında yakalamış oldum. Artık pin toggle yaptığım kısma hattın durumunu kontrol eden ve buna göre değişkenin ilgili bitini değiştiren kodları ekleyebilirim. Bu değişkeni kesme çıkışında oluşturduğum ring buffera kaydederek sonrasında istediğim gibi işleyebilirim.

Bu arada yukarıda bahsettiğim sorun için bir Stop_rx_delay ilavesi gerekiyor. Okuma sonrası hemen veri göndermek istendiğinde sorun çıkmaması için gerekli. Gönderim yaparken kesmeler kapatıldığından bu gerekiyor. Onu da hesaplamak için aynı formülü kullandım ama bunda yüksek hızlarda daha sorunsuz olması için çevrim sayısını artırdım. Tx e girmeden ilave beklemeler ekledim böylece bir çeşit çözüm buldum ama en iyisi veriyi alır almaz gönderim yapmamaktır.

ISR(PCINT0_vect){                
        if (RX_PIN_LOW){//start kontrol pin "0" ise        
                _delay_loop_2(yarim_rx_delay);//ilk bit için bekleme
                for (uint8_t i=0;i<8;i++){                        
                        _delay_loop_2(rx_delay);//her bir bit için gereken bekleme
                        veri>>=1;//geçici değişken kaydırıldı
                        //PORTB^=(1<<4);//zamanlama icin kulandım                
                        if (RX_PIN_HIGH){// hat kontrolü yapılıyor
                                veri|=0x80;//"1" ise "1"                                
                        }else{//zamanlama dengesi için eklendi
                                veri&=~0x80;//"0" ise "0" yazılıyor                                
                        }//*/                
                }
                _delay_loop_2(stop_rx_delay);// hemen sonrasında gönderime geçmemek için
                rx_bas=(rx_bas+1) & UART_Rx_Mask;//ring buffer indis arttı
                rx_ring[rx_bas]=veri;//ring buffera yazıldı
        }
}

Uart Başlatma

İlk yazacağımı sona bırakmış gibi oldum ama gerekli işlemleri neden yaptığımı anlatmadan buna girmek doğru olmayacaktı. İlk olarak Rx ve Tx pinleri giriş-çıkış yapılıyor. Rx için gerekli kesme ayarları yapıldıktan sonra gerekli bekleme hesaplamaları yapılıyor.  Burada 4 çevrim halinde gidildiği için tam nokta atışı sonuç çıkmıyor. Sabit değerli bir iletişim için Attiny 13A kullanacağım zaman builtins.h ile tam çevrim sayısıyla işlem yaparım. Bu şimdilik işimi görecek seviyede çalışıyor. Tx 38400 den başlıyor. Rx ne yazık ki o hızda sadece tek karakter alarak çalışıyor. 19200 bps ve altı daha sağlıklı çalışıyor.4800bps gibi düşük bir hızda okuma sonrası hemen gönderim sorun çıkartıyor. Stop delay artarsa yani çevrim sayısı azalırsa sorun çözülür ama yüksek hızlarda sorun olacaktır. 

Burada görüldüğü gibi daha alım devam ederken gönderim için fonksiyona giriyor burada kesme kapandığı için arada kaçan veriler oluyor.
İşlemci frekansı artarsa 57600bps ya kadar çıkabilir ama bu başka bir sorun çıkartıyor. Sadece UART kullanacaksam işlemci frekansını 9,6MHz olarak ayarlayabilirim. Sonuçta çok mükemmel çalışmasını beklemiyorum zaten, sadece gönderim amaçlı kullanacağım ve onu da gayet iyi başarıyor.


void uart_basla(Bd_rate_t _baud){
        cli();
        RX_HIGH;
        RX_IN;//pin giriş yapıldı
        TX_HIGH;
        TX_OUT;//pin çıkış yapıldı
        PCMSK|=(1<<RX_);//rx pini için kesme ayarlandı
        GIMSK|=(1<<PCIE);
        tx_delay = ((F_CPU / _baud) / 4)-3;
        if (tx_delay<=1){
                tx_delay=1;
        }
        yarim_rx_delay=(F_CPU/_baud/8)-7;
                if (yarim_rx_delay<=1){
                        yarim_rx_delay=1;
                }
        rx_delay=(F_CPU/_baud/4)-4;
                if (rx_delay<=1){
                        rx_delay=1;
                }
                stop_rx_delay=(F_CPU/_baud/4)-10;
                if (stop_rx_delay<=1){
                        stop_rx_delay=1;
                }        
        sei();
}


Software I2C

UART konusu kadar detaya girmeyeceğim çünkü önceki konularda bahsettiğim şeylerin tekrarını yapmış olurum. Start-Stop koşulu, ACK-NACK ve verinin örnekleme zamanları hakkında yazmıştım. Burada yazılımla çalışan  I2C de tek karışık olan oluşturduğum transfer fonksiyonudur. Her zaman olduğu gibi aplikasyon notlarını ve başka kütüphaneleri inceledim. ACK kontrolü yapılmadan çözülmüş örnekler gördüm. Benim yazdığımda bu şekilde değil. Donanım tabanlı olan nasıl çalışıyorsa bu da o şekilde çalışıyor. 

İşlemcinin çalışma frekansını 1,2MHz e düşürmemin nedeni I2C çalışma frekansını tutturmak içindir. Aksi halde burada da beklemeler kullanmak durumunda kalacaktım. Frekansı düşürünce sadece çıkış yönlendirme pin kontrol veri kaydırma gibi işlemler için harcanan çevrim süresi 100kHz seviyesini yakalamasam da yaklaşmamı (66kHz) sağladı. Diğer tüm frekanslar ya çok yüksek ya da düşük olacaktır. Frekansı 9,6MHz olarak ayarlasaydım. Attiny 45 USI biriminde olduğu gibi her saat darbesi arasında 4µs lik beklemeler eklemem gerekirdi. Bekleme ilaveleri sınırlı olan hafızayı doldurduğu için kullanmamak en doğrusu oldu. UART için de aynı şansım olsaydı Baud rate değerine bakmaz kullanırdım ama ne yazık ki çok yakın değerleri bulmak zorunlu olduğundan orada bekleme kullanmaksızın işlem yapmak imkansız. UART ile sonuçtan çok memnun olmasam da I2C nin çalışmasından memnunum.

I2C Transfer

Start-Stop ve ACK-NACK nedir daha önce bahsetmiştim. Attiny 45 USI biriminde bu işleri zaten yazılımla yürütüp sadece sayaç ve kesmelerle donanımı kullanmıştım. Burada yine aynı şekilde bu koşulları oluşturdum. Donanım olmadığından verinin gönderilmesini UART gibi döngü içinde yaptım. Bu işlemi yine UART gibi veriyi kontrol edip "1" veya "0" olması durumuna göre hattı "1" veya "0" yaparak gerçekleştirdim. Burada bir fark ilk gidenin 7 numaralı bit olmasıdır. Onun dışında UART ile aynıdır. Adresin ve verinin gönderilmesi aynı olduğundan ikisini ayrı ayrı gönderecek kod yerine adres sonrası veri yazma durumu varsa yapılanı tekrar etmesi için bir Do while döngüsü içine aldım.

Yazma

                                I2CDR=i2c_ring[i2c_son++];                                
                                for( uint8_t i=0;i<8;i++){
                                        if (I2CDR&0x80){
                                                SDA_HIGH;
                                                }else{
                                                SDA_LOW;
                                        }
                                        SCL_HIGH;
                                        while (!(PINB&(1<<SCL)));
                                        I2CDR<<=1;
                                        SCL_LOW;
                                }
                                SDA_IN;
                                SCL_HIGH;
                                I2CDR=I2C_PIN;
                                SCL_LOW;
                                SDA_OUT;
Sırasıyla yapılanı anlatayım. Öncelikle bu paylaştığım satırlara start koşulu sonrası giriliyor. I2CDR isimli bir değişkene gidecek olan veriyi yazdım bu adres veya başka bir veri de olabilir ama adres olduğunu düşünelim. Sonrasında 8 defa aynı işlemi gerçekleştirmek için For döngüsü içine giriyor. Burada gidecek olan verinin 7 numaralı biti sorgulanıyor. Bu bitin "1" veya "0" olması durumuna göre SDA "1" veya "0" yapılıyor. Doğru zamanlama için else ekledim. SCL hattı "1" yapılıyor ve "1" olana kadar döngüde kalıyor. Bu gerçekleşince karşı taraf veriyi okuduğu için I2CDR bir bit kaydırılıyor ve SCL "0" yapılıyor. Bu işlem tüm bitler gidene kadar tekrar ediliyor. Sonrasında 9. bit ACK kontrolü olduğundan SDA giriş yapılıyor. SCL "1" yapılınca karşı tarafın cevabı yani hattın durumu PIN registeriyle I2CDR ye yazılıyor. Seçilen pin hangisi olursa buna göre kaydedilmiş oluyor. SCL hattı"0" yapılıp sonraki işlem için SDA çıkış yapılıyor.

Bundan sonrası ACK-NACK durumuna ve yazma-okuma görevine göre değişiyor. ACK ve yazma için tüm bu işlemlerin gidecek veriler bitene kadar tekrar etmesi gerekiyor. Bunun için tüm bu işlemi bir döngü içine alıyorum.

do{
               //yukarıdaki kodlar 

 }while (!(I2CDR&I2C_NACK)&&(i2c_son<i2c_bas)&&(i2c_task== I2C_WRITE));
        

While içinde ilk olarak ACK-NACK durumu sorgulanıyor. Bunun için I2CDR ile PIN durumu kaydı kontrol ediliyor. I2C_PIN-PINB ve I2C_NACK-(1<<SDA) olarak tanımlıdır. NACK durumunda döngüden çıkar. Sonraki kontrol gidecek verinin sonuna gelinip gelinmediğidir. başka veri kalmamışsa yani baş ve son indisi eşitse döngüden çıkar. Son olarak yazma veya okuma görevi kontrol edilir. İlk olarak adres yazıldı ama sonrasında okuma yapılacaksa döngüden çıkar ve alt satırdaki başka bir döngü içinde gerçekleşen okuma kısmına geçilir. Okuma kısmına geçmeden adres sonrası ACK yerine NACK alınmışsa bu alt satıra girmez veya yazma tamamlanmışsa yine alt satıra girmez ve stop koşulu gerçekleşir. Adres sonrası ACK alınmışsa okuma için alt satırdan devam eder.

Okuma

                        SDA_IN;
                        for( uint8_t i=0;i<8;i++){
                                I2CDR<<=1;
                                SCL_HIGH;
                                if (I2C_SDA_HIGH){
                                        I2CDR|=0x01;
                                        }else{
                                        I2CDR&=~0x01;
                                }
                                SCL_LOW;
                        }
                        i2c_ring[i2c_bas++]=I2CDR;
                        SDA_OUT;
                        if (i2c_bas!=i2c_len){
                                SDA_LOW;//ack gonder
                                }else{
                                SDA_HIGH;//son veride nack gonder
                        }
                        SCL_HIGH;
                        SCL_LOW;

İlk olarak veri okunacağından SDA giriş yapılır. 8 bit veri için For döngüsüne girer ve verinin yazılacağı I2CDR bir bit kaydırılır. Bu kaydırma işlemini önceden yapmış gibi görünmesine aldanmayın aslında boş bir kaydırmadır. Veriyi okuyup kaydırsam çıkışta ilk okunan veriyi de kaydırmış olur ve bir bit kaybederdim. SCL bu sefer önceden "1" yapılıyor ki karşı taraf veriyi hatta yazabilsin ve biz de doğru zamanda hat kontrolü yapalım. SDA hattının durumuna göre I2CDR nin 0 numaralı biti "1" veya "0" yapılıyor. I2C_SDA_HIGH-I2C_PIN&(1<<SDA) olarak tanımlı yani PIN registeri kontrol ediliyor. SCL hattı "0" yapılıp tekrar başa geçiliyor. Tüm bitler okunduktan sonra okunacak başka veri yoksa NACK varsa ACK göndermek için yani hattı "1" veya "0" yapmak için SDA yeniden çıkış yapılıyor. Alınacak veri boyutu ile alınan veri boyutu karşılaştırılıp gerekli işlem yapılıyor. SCL hattı karşı taraf ACK durumunu kontrol etmesi için "1" ve "0" yapılıyor. Eğer verinin sonuna gelinmişse NACK ile stop koşuluna geçilirken sona gelinmemişse aynı işlemin tekrarı için bir Do while döngüsü çalıştırılır.

do{
            //yukarıdaki kodlar
}while (i2c_bas!=i2c_len);

Burada while içinde alınacak veriyle alınan veri indisi karşılaştırılır eşit olduğunda okuma işlemi tamamlanmış demektir. Bir önceki adımda NACK ile işlem sonlanır ve stop koşulu oluşur. Bunun dışında tüm fonksiyonlar aynı olduğundan burada tekrar etmeyeceğim. 

Bu kütüphaneye ve tüm kodlara bu linkten  ulaşabilirsiniz. AHT10 ile yapılan hesaplamaların karışık olduğunu düşünmüyorum bu nedenle uzun uzun değinmeye gerek yok. Bu arada birçok yerde ana fonksiyonlar yerine alt fonksiyonları kullandım bunun nedeni hafızadan kazanmaktır. Bu çalışmada da optimizasyon için OS seçildi aksi halde sığma şansı yok.

Bağlantı Şeması:



AVR- Attiny45 LCD Saat

 Malum ekonomik sıkıntılar her şeyi olduğu gibi hobimizi de etkiledi. MCU fiyatları uçtu gitti. Uygun fiyatlı olarak eski teknoloji 8051 türevleri ve Attiny MCU gördüm. ESP diyenler olacak ama onlar hakkında hiç deneyimim yok. İlerde belki deneme yapabilirim. Bir MCU öğrenmek için bir şeyler yapmayı en doğru yöntem olarak kabul ediyorum. Kızımın benden istediği saati yaparak öğrenmiş olurum dedim ve başladım.


Attiny45

Sadece öğrenmek için  datasheet e bakmadan aldım. İhtiyaç olan bir ürün araştırması yapıp seçmedim, tek kriter fiyatıydı. AVR az da olsa öğrendiğim için böyle rahat davrandım. İlk başta nasıl haberleştirmem gerektiğine baktım. Malum toplam 8 pin var 2 tanesi besleme bir tanesi reset onu da giriş/çıkış yaparsam bir daha kullanma şansım kalmaz. Geriye kalanlardan bir tanesi butonlara bir tanesi de pasif buzzer için ayrılacak bana 3 adet pin kalıyor. Bunlar ile saati okumam ve ekrana yazmam gerekiyor. Bu durumda SPI  kullanamıyorum tek seçenek olan I2C için okumaya başladım. Atmega serisinde birkaç farklı MCU kullandım. Register isimleri dışında neredeyse tamamen aynı birimlere sahipler. Attiny de ne yazık ki böyle değilmiş.

USI Birimi

Bir seri haberleşme birimi var ama alışık olduğum gibi değil. Henüz SPI ile bir çalışma yapmadım I2C ve yazılımla UART çalıştım ama SPI da muhtemelen I2C de olduğu gibi her şeyi yazılımla yaptırıyordur. Bir haberleşme birimi ama gerekli ayarları yapayım o arka planda işlemini yapsın diyebileceğiniz bir birim değil.
Birimin şeması bu şekilde kısaca registerlere değinip I2C için ne yaptığımı paylaşayım.

USIDR



USI biriminin veri kaydını tutan registerdir. Veri yazılır ve sola kaydırılarak çıkış verilir. Bu kaydırma işlemi için saat kaynağı seçimi yapabiliyoruz. Registerin 7 numaralı biti SPI için DO, I2C için SDA pinine bağlıdır. Çıkış açıksa bu bitin durumu çıkış pinine verilir. Ayrıca bu biti "0" yapmak bir Start koşulu oluşturuyor. Bu sadece timer0 kesmesinde oldu, diğer saat kaynaklarında "0" yaptığımda start oluşmadı.

USIBR



USI biriminin buffer registeridir. Aktarım tamamlanınca veri buraya yazılıyor. Buradan okumak da mümkün ama ne aplikasyon notlarında böyle bir şey gördüm ne de ben kullanma gereği duydum. SPI da belki gerekli olabilir.

USISR



USI biriminin durum registeridir. Oluşan koşulları tespit için bayraklar bulunur.
USISIF: Start koşulu algılandığında "1" olur.
USIOIF: USISR nin ilk 4 biti bir sayaç tutar. 15-0 arası değer alabilen bu sayaç taştığında USIOIF "1" olur. Kesme açıksa bir kesme oluşturur.
USIPF: Stop koşulu oluştuğunda "1" olur. Bir kesme oluşturmuyor.
USIDC: USIDR 7 numaralı bitiyle bağlı olduğu pinin değeri farklıysa "1" olur. Birden fazla master olduğunda oluşabilecek çakışma için uyarı verir.
USICNT: Bahsi geçen sayaç değerinin tutulduğu bitlerdir. Saat kaynağından gelen her darbede bir artar. 15 ten 0 a geçişte USIOIF "1" olur.I2C de start sonrası 8 saat darbesi veri 9. ise ACK için veriliyordu. Buradaki sayaç bu işlemi takip etmek için kullanılıyor.

USICR



USI biriminin kontrol registeridir. Kesmeler, saat kaynağı ve SCL çıkışı için gerekli ayarlamalar yapılır.
USISIE: Start koşulu gerçekleşince kesme oluşması için "1" yapılır. (USISIF)
USIOIE: Sayaç taşma kesmesi için "1" yapılır.(USIOIF)
USIWM[1:0]: USI biriminin çalışma modunu ayarlamak için bu bitler kullanılır. Sırasıyla [1,0] veya [1,1] yapılması durumunda USI birimi SCL ve SDA pinlerini kullanır yani I2C moduna geçer. İlk seçenek ikincisi arasındaki tek fark sayaç taşması sonrası sayaç bayrağı temizlenene kadar SCL "0" olarak tutulur. 
USICS[1:0]: USI biriminin saat kaynağını seçmek için bu bitler ayarlanır. Bu seçim için aşağıdaki tabloyu paylaşıyorum.

Harici (external) saat kaynağı kullanırken verinin çıkış hattına karşı kenarda yazılmasını sağlar. I2C de okuma yükselen kenarda olduğundan veri örneklenmesi düşen kenarda olmalıdır. Bu nedenle "positive edge" seçenekleri seçilmelidir. Böylece veri çıkışı yükselen kenarda olurken kaydırma işlemi düşen kenarda olur. Timer0 ve yazılım saati seçildiğinde çıkış ve kaydırma eş zamanlı olduğunda I2C de bu seçenekleri kullanamıyoruz.
USICLK: Yazılım saat kaynağıdır. Üstteki tabloda ikinci seçenek seçildiğinde USIC[1:0] "0" yapıldığında USIDR de yazılı veriyi kaydırır ve sayacı artırır. Bunun için USICLK "1" yapılır. Her "1" yapılışında bir kaydırılır. Bunun SCL üstünde bir etkisi olmaz. Verinin kaydırılması için harici kaynaklar seçildiğinde ve USICLK "1" yapıldığında hem sayaç hem veri kaydırma işlemini USITC yürütür. Böylece veri örneklenmesi için fazladan kod yükü ortadan kalkar.
Burada SCL hattına çıkış vermeden sadece USICLK ile verinin kaydırılmış hali görünmektedir.
USITC: SCL hattını tersleyerek çıkış darbesinin oluşturur. SCL "1" ise USITC "1" yapılınca SCL"0" olur. Aynı şekilde "0" olduğunda USITC "1" yapılınca "1" olur. Harici saat kaynağı seçili ve USICLK "1" olduğunda hem verin kaydırılması hem de sayaç değerinin artması için USITC kaynak seçilmiş olur.

USI ile I2C

I2C nin ne olduğuna çok girmeden kullanımına yönelik bilgi vermeye çalışacağım. Datashhetten alınma zamana göre çıkış durumunu gösteren bir şemadır. Bu şema üstünden ilerlemek doğru olacak.

A: Start koşuludur. SCL yüksekken SDA düşük olduğunda oluşur. Bunu ya PORTB pinleri değiştirerek ya da USIDR 7 numaralı biti"0" yaparak oluşturabiliriz. Bu şekilde oluşturmak için USICS1 "0" olmalı yani harici bir saat kaynağı seçilmemeli. Harici saat kaynağı seçildiğinde sadece PORT registeriyle start gerçekleşiyor. PORTB&=~(1<<SDA);


B: Start sonrası SCL düşük tutulur ve start bayrağı temizlenmelidir. SDA hattı için pull up direnç bağlantısı açılır. Böylece USIDR de kayıtlı veri SDA hattını değiştirir. Aksi durumda SDA "0" olarak kalır.
PORTB&=~(1<<SCL);
PORTB|=(1<<SDA);
USISR=(1<<USISIF)|(1<<USIOIF)|(1<<USIPF)|(1<<USIDC);
C: SCL hattı ilk saat darbesi için yükselir. Böylece slave cihaz yükselen kenarda USIDR deki veriyi SDA üstünden kaydeder.


D0 SDA ve D1 SCL hattıdır. Start koşulu sonrası bayraklar temizlenip USIDR ye adres ve okuma/yazma bilgisi girilir.
D: Sekiz saat darbesiyle birlikte USIDR kaydırılarak SDA ile aktarılır.

E: Son saat darbesiyle ACK bilgisi beklenir. Bundan önce SDA hattı giriş yapılır. Böylece slave SDA  düşük tutabilir. Eğer adres uyumsuzsa slave SDA serbest tutar ve NACK cevabı alınır. Veri okuma yaparken ACK master tarafından oluşturulur. Bu durumda son saat darbesinde SDA çıkış yapılır ve ACK için USIDR 7 numaralı biti "0" ve NACK için "1" ayarlanır.


Ben 0x55 girerek örneklenme zamanını göstermek istedim, yükselen kenarda veri okundu düşen kenarda yeni veri yazıldı. Son SCL darbesinde SDA yüksek olduğundan NACK cevabı alındı. Adresini 0x03 olarak ayarladığım slave ile alınan ACK bu şekildedir.

F:Stop koşulu SDA düşükken SCL hattının yüksek tutulmasıyla oluşur.


SCL hattını USITC ile kontrol ediyoruz. Her "1" yazmamızda tersleniyor. SDA hattının bu darbelerdeki durumuna USICLK ve USIC0 ile ayarlıyoruz. Tamamen yazılımla yaptığım 9 darbe sonrası görüntü bu şekildedir.

İlk olarak start oluştu SCL yükselen kenarda veri okundu, düşen kenarda sonraki veri yazıldı ve stop ile işlem tamamlandı. Yukarıdaki kodlarla birlikte yazılanlar aşağıdaki gibidir. Yazılımla yapabilmek için USIC[1:0] "0" ve USICLK "1" olmalıdır.
    SDA_LOW;
    SCL_LOW;
    SDA_HIGH;//start
    USISR=(1<<USISIF)|(1<<USIOIF)|(1<<USIPF)|(1<<USIDC);//bayrak temizlik
    USIDR=0x55;//adres yazıldı
    for (uint8_t i=0;i<8;i++){//8 tekrar   
    USICR|=(1<<USICLK);//veri kaydırma
    _delay_us(4);//100kHz için 
    USICR|=(1<<USITC);//scl toggle
    _delay_us(5);//100kHz için    
    USICR|=(1<<USITC);//scl toggle    
    }
    SDA_IN;//sda giriş yapıldı
    _delay_us(4);	
    USICR|=(1<<USITC);//ack için 9. saat darbesi
    _delay_us(5);	
    USICR|=(1<<USITC);
    SDA_LOW;
    SCL_HIGH;
    SDA_HIGH;//stop
USI birimiyle I2C bu şekildedir. Daha önce oluşturduğum I2C kütüphanesinde veri alıp yazma kısmını kesmelerle yürütmüştüm. Burada da aynı şekilde bir yol izledim ve donanımı mümkün olduğunca kullanmaya çalıştım. Bu kod parçasında kullanmadığım sayacı kullanılarak for döngüsü ve USICLK takibi yapmadım. Sayaç taşma kesmesiyle okuma/yazma işlemlerini yerine getirdim. Aplikasyon notlarında yalnızca slave cihaz için kesme kullanılmış bunun nedenini bilmiyorum. Ben kullandım ve gayet düzgün sonuç aldım ama boyutu büyük oldu. Aplikasyon notlarındaki kendi kütüphanesi daha az yer kapladığı için burada onu kullandım. Daha az yer kaplamasının nedeni  bu uygulamada kullanmadığım fonksiyonları silmemdir. (AVR310: Using the USI module as a I2C master) Kaynak kodu burada. Bu kütüphaneye eklemeler yaptım. Benim yazdığım kütüphane buradadır.

USI Kesmeler 

Start Kesmesi

Start için tanımlı fonksiyonda PORTB de gerekli ayarlamalar yapılıyor. Start oluşur oluşmaz kesme rutinine geçiyor. Kesme rutininde Sayaç taşma kesmesini açıyorum. Sonra bayrakların temizlenmesi ve sayaç değerinin sıfıra ayarlanmasını yapıyorum. Böylece sayaç 16 kez artarak gerekli olan 8 SCL darbesini takip etmiş olacak. USIDR registerine adres ve okuma/yazma bilgisini girip ACK kontrol durumuna geçiyorum. Tekrar start fonksiyonuna dönüp SCL hattının saat darbeleri için USITC "1" yapılıyor. Bu işlem taşma bayrağına kadar devam ediyor.

Sayaç Taşma Kesmesi

USITC ile birlikte veri hattının kaydırılması ve sayaç artması için USICS1 ve USICLK "1" yapıldı. Böylece harici saat kaynağı ve sayaç için USITC seçilmiş oldu. Sayaç taşınca yani "15" ten "0" a geçince kesme oluşuyor ve kesme rutini içine giriyoruz. Kesme rutini içinde Switch case yapısı kullandım. İlgili durumlara göre seçeneklere geçiş yapılıyor. Start kesmesinde ACK kontrol durumuna geçtiğinden bu seçeneğe giriyor. SDA giriş yapılıyor, bayraklar temizleniyor ve sayaç tek bir darbe oluşturmak için "14" e ayarlanıyor. Böylece 2 kez USITC "1" yazılınca bir SCL saat darbesi oluşuyor ve sayaç taşıyor. Burada okuma/yazma durumuna göre sonraki durum seçimi de yapılıyor.
 Okuma için ACK gelmişse USIDR belleğe yazılıyor. Yazma için yine ACK gelmişse bellekteki veri USIDR registerine yazılıyor. Bu geçişler tekrar edilerek iletişim gerçekleşiyor.

Belleğe yazma ve okumada Atmega için yazdığım kütüphaneyle aynı fonksiyonları kullanıyorum.

LCD Ekran (I2C)

Çok uzun zaman önce karakter LCD ekran kullanımı hakkında burada bilgi vermiştim. Aynı fonksiyon ve makroları kullandım. Tek fark veriyi 4 bit paralel olarak değil de I2C ile gönderdiğim veriye göre pin durumu değişen PCF8574 ile iletişimin gerçekleşmesidir. PCF8574 adresten sonra gelen 8 bit veriyi 8 adet çıkışına uyguluyor. Böylece sanki paralel bir bağlantı varmış gibi LCD ekranı kullanabiliyoruz. 
Daha önceki kodlarda sadece veri yazma kısmında değişiklik yaptım.
PCF8574 P7-P4 pinleri LCD data pinlerine P3-P0 pinleri ise sırasıyla arka aydınlatma, EN, RW ve RS ile bağlanmış. Gönderilen verinin ilk 4 biti kontrol pinlerine son 4 biti datan pinlerine bağlı olduğundan başlatma sırasında gönderdiğim veriler 0x03 yerine 0x30 oldu. Değişiklik olan fonksiyonların eski hali bu şekildedir.
void lcd_data(uint8_t  gelen){
    DATA_PORT=(DATA_PORT&0x0f)|(gelen & 0xF0);//4 bit veri
    EN_HIGH;//En yüksek
    _delay_us(1);
    EN_LOW;//EN düşük
    _delay_us(100);    
}
void lcd_kmt(uint8_t  cmd){
    RS_LOW;    
    lcd_data(cmd);    
    lcd_data(cmd<<4);
}
void lcd_yaz(uint8_t  data){
    RS_HIGH;        
    lcd_data(data);
    lcd_data(data<<4);    
}
I2C için yapılan değişiklik:
void lcd_data(uint8_t  gelen){
    i2c_adr(LCD_ADDR, I2C_WRITE);
    i2c_data(gelen|light);//4 bit ve ışık
    i2c_end();
    i2c_adr(LCD_ADDR, I2C_WRITE);
    i2c_data((gelen|light)|EN);// EN yüksek
    i2c_end();    
    i2c_adr(LCD_ADDR, I2C_WRITE);
    i2c_data((gelen|light)&~EN);//EN düşük
    i2c_end();
    
}
void lcd_cmd(uint8_t  cmd){
    //_delay_us(100);
    lcd_data((cmd&0xf0));//ilk 4 bit temizleniyor böylece komut bitleri karışmıyor
    lcd_data(((cmd<<4)&0xf0));    
}
void lcd_yaz(uint8_t  data){
    //_delay_us(100);
    lcd_data((data&0xf0)|RS);
    lcd_data(((data<<4)&0xf0)|RS);
}
Bu kadar az bir değişiklikle aynı fonksiyonlarla kullanmaya devam ediyorum. Ek olarak sadece arka aydınlatma için bir fonksiyon geldi.

DS3231

DS1302 ile yaptığım  bir uygulamayı paylaşmıştım. DS3231 daha doğru zamanlama dışında I2C ile haberleştiği için ve elimde bulunduğundan onu tercih ettim. İkisi arasında kullanım açısından fark yok. Sadece küçük birkaç değişiklikle saati okuyup yazabildim. DS1302 de olan okuma ve yazma fonksiyonlarına gerek kalmadı. I2C ile doğrudan adresi yazıp okuma ve yazma yaptım. Kodları aşağıdan indirebilirsiniz.

Bundan sonrası önceki saat konusuyla neredeyse aynı. Tek tek tüm fonksiyonları açıklamanın bir mantığı yok.

Saat gövde için dosyalar burada.

Bağlantı şeması:

Microchip Studio dosyalar.

Optimizasyon ayarını " Optimize for size" (Os) olarak seçmezseniz boyut hatası verecektir.








AVR- Buzzer ile Müzik



Sadece birkaç değişik "bip" sesi çıkartmak amacıyla başladığım iş biraz büyüdü. Bip sesi yerine farklı notaları farklı hızlarda çalsam derken bana göre güzel bir şey çıktı. Okuyan birçok kişi için basit gelebilir ama ben yine de paylaşmak istedim. Çünkü hem ilerde tekrar aynı şeyleri araştırmak zorunda kalmamak için yazmış olurum hem de ihtiyaç duyanlara bir kaynak olur. Bu arada gerçekten çocuk gibi eğlenerek yaptığım bir iş oldu. Arduino Tone() fonksiyonunu muhtemelen herkes kullanmıştır veya duymuştur. Onda bazı sorunlar var. Birden fazla kart ve timer ile çalışacak şekilde ayarlandığından oluşan bu sorunlar burada yok. Mesela Tone ile arada bekleme olmadan ses çıkartamazsınız benim paylaşımımda nota aralarına bekleme koymasanız da çalışır. Buna ek olarak aynı notayı farklı oktav değerlerinde çalmak isterseniz tüm notaları tekrar yazmanız gerekmez sadece parametre değiştirirsiniz. Tek tek anlatmak yerine konuya geçmek daha doğru olacak.

Notalar

Ses basit bir anlatımla bir yüzeyin belirli bir frekansla titreşimiyle oluşur. 20 ile 20000Hz arasındaki titreşimlerin çıkardığı sesi duyabiliyoruz. Yaşlandıkça bu üst sınır düşüyor. Çocuklarla test ettik, onların duyduğu sesi ne yazık ki ben duyamıyorum,  15000 Hz sonrası sessizlik. Bu geniş aralıkta duyulan ve müzik aletlerinin çıkardığı kulağa hoş gelen kısmını 0 dan başlayıp 8 e kadar oktav denilen bölümlere ayırmışlar. Tam ortada yer alan 4 oktav "LA" notasının frekansı 440 Hz kabul edilmiş ve her oktav 12 ye bölünmüş. La notasının frekansına göre bir sonraki notayı bulmak için 2^(1/12) ile çarpmak veya bölmek gerekiyor. Ayrıca La notasının bir alt veya üst oktav frekansı 2 katı veya yarısı şeklinde ilerliyor. Bunları öğrenince bir tablo ile tüm notaların frekanslarını hesapladım. Hazır olarak bulduğum değerlerde virgülden sonrası yeterli gelmedi.



La notası frekansı için 432Hz değerinin kullanıldığı enstrümanlar ve müzik türleri varmış ama ben o konulara girmedim, komplo teorilerine kadar gidiyor. Bu tabloda görülen Hz cinsinden frekansa göre benim çıkışı ayarlamam gerekecek.

Zamanlayıcı

Notalar için Timer1 birimini kullandım. Bunun nedeni timer1 in 16 bit olmasıdır. Bu sayede notaların gerçek frekansına yaklaşmış oldum. Başta PWM ile yaptım ama farklı pinleri seçme şansı vermek için CTC modu ve pin toggle yaparak çıkış vermek mantıklı geldi. Aksi halde iki pine mahkum olacaktım. MCU saat frekansı 16MHz ve prescaler kullanmazsam çok büyük rakamlara ulaşacak olan sayıcıda taşma oluşacaktır. Bunun ilave kodlarıyla uğraşmak istemedim. Seçtiğim prescaler çok büyük olursa yüksek frekanslarda çok küçük OCR1A değerleri çıkartacağından gerçek frekansa yaklaşma şansım azalacaktır. Tüm bunları düşünerek prescaler değerini "8" olarak seçtim. CTC modunda timer1 sayıcı (TCNT1) registeri OCR1A ile eşleştiğinde kesme oluşuyor ve TCNT1 sıfırlanıyordu. Bu konu hakkında daha önce paylaşımlarım oldu aynı konuyu tekrar etmeyeceğim. OCR1A değeri ile kesme oluşunca çıkışı pini "1" ve "0" olarak sürekli değişiyor. Bu değişimin aralığı (frekansı) değiştikçe çıkan ses de değişiyor. Aşağıda datasheetten alınma şemada bunu görebilirsiniz.

 

 

 



OC1A (OCna) frekansı için de yukarıdaki formülü kullanıyoruz. Bu formülde "N" prescaler değeri ve Fclk MCU çalışma frekansıdır. Frekans değerleri bir saniyedeki değişime göre belirleniyor, en düşük frekans "1" ve "0" zaman olarak arası en çok olandır. Notaların frekanslarına baktığımızda en düşük frekans bizim için en büyük OCR1A (OCRnA) değeri demektir. Bu nedenle "0" oktav da yer alan notaları baz alarak hesaplama yaptım. "0" dan "8" e kadar oktav değeri büyüdükçe frekans değeri de büyüyor ve OCR1A değeri de düşüyor. Bu değişim frekans olarak bir alt oktavın 2 katı artarken OCR1A değeri olarak aynı oranda azalacaktır. Büyük OCR1A değerini bölerek daha doğru değerler bulmuş oldum. "0" oktav tüm notalar için gerekli değerleri ön tanımlı olarak yüklemek ve diğer oktav değerlerini kod içinde hesaplatmak daha doğru geldi. Örnek olarak "0" oktav "DO" için OCR1A değerini bulalım.

Yukarıdaki formülde OC1A frekansını yerine yazıp OCR1A değerini formülden çekelim. OC1A frekansı bizim çıkış frekansımız yani notanın frekansıdır. Bu durumda formül OCR1A=(Fclk/fOC1A/(2*N))-1 şekline dönüşür. OCR1A=(16000000/nota Hz/(2*8))-1=(16000000/16,35160/16)-1=61155,103 olarak buluruz.

//0 oktav nota ocr değerleri prescaler 8
//datasheet CTC fOCR= f Clk/ (2*N*(1+OCR1A) N prescaler fOCR istenen frekans
// =((16000000/(Nota Hz)/2/8)-1) formülden OCRA değerini çekince bulunur.
#define DO    61155// 0 oktav DO 16,35159 Hz
#define DO_d  57723
#define RE_b  57723
#define RE    54483
#define RE_d  51425
#define MI_b  51425
#define MI    48539
#define FA    45814
#define FA_d  43243
#define SOL_b 43243
#define SOL   40816
#define SOL_d 38525
#define LA_b  38525//0 oktav 25,956 LA/2^(1/12)
#define LA    36363//0 oktav 27,50Hz ---tüm notalar 4 oktav 440Hz e göre hesaplandı 
#define LA_d  34322//0 oktav 29,135 LA*2^(1/12)
#define SI_b  34322
#define SI    32395
#define DO_I  30577

Timer Kesmesi

Bulduğumuz bu değere TCNT1 her ulaştığında yani OCR1A ile her eşleşmede kesme oluşur. Her kesme oluşumunda çıkış için ayarladığımız pin "1" veya "0" olur. Böylece istenen frekansa en yakın çıkışı sağlamış oluruz. Notaların frekansları kadar duyulma süresi yani vuruş zamanı da önemlidir. Aksi halde sadece gürültü olacaktır. Vuruş süresini delay veya benzeri bir şekilde ayarlamak yerine zaten kullanmakta olduğumuz timer ile bir sayaç kullanarak ayarlamak mantıklı olandır. Oktav değiştikçe değişen bir OCR1A değeriyle sabit bir sayaç kullanamayız. Bu nedenle her nota için istediğimiz vuruş süresini hesaplamak gerekir. Bu hesaba daha sonra değineceğim.

Kesme içinde bulduğumuz sayaç değerini kontrol edip sınıra gelince timer1 birimini durdurarak vuruş süresini de ayarlıyoruz. Toggle ile pin durumunu terslerken boşta ya da notalar arası beklemede çıkış vermemesi için her seferinde pin durumunu "1" yapıyorum.  OCR1A tek sayı olduğunda "1" ile başlayıp "1" ile bitirir ve sorun olmazken çift sayı olduğunda bir notadan diğerine geçerken pin durumu "0" olarak kalır ki bu durum çıkışı bozabilir. Pin "0" olduğunda  çıkış alacak şekilde ayarladım. Buzzer "+" ucu +5 volta bağlı, bu nedenle çıkış yokken pin "1" yapıldı. Anlamlı bir müzik için "sus" işaretleri de olmazsa olmazlardan. Sus için bir bayrak tanımlı ve "1" olduğunda toggle işlemi yapılmıyor.

ISR(TIMER1_COMPA_vect){	
	if (kesme_sayac<=nota_sayac){
		if (sus!=1){
			NOTA_PIN_T;//pin toggle
		}
		kesme_sayac++;
	}else{					
		nota_bitti=1;
		kesme_sayac=0;		
		TCCR1B&=~(1<<CS11);//timer1 durdu
		NOTA_PIN_H;//sonraki toggle işlemine high olarak başlar
		sus=0;		
	}	
}

Nota Fonksiyonu

Nota

Notaların güzel bir melodiye dönüşmesi için öncelikle doğru sesi çıkarması gerekir. Bunun için yukarıda gerekli işlemleri yaptım ve çok küçük farklar dışında en yakın frekansları elde ettim. İlk parametre olan notaya tekrar değinmeye gerek yok.

Oktav

Oktav değeri yükseldikçe notanın frekansı artarken OCR1A değeri azalır. Bunun için "0" oktav değeri neyse istenen oktav değeri kadar sağa kaydırarak notanın OCR1A değerini buluyoruz. Örnek olarak 1 oktav değeri bulmak için 0 oktav değerini 2 ye bölmek gerekir. Oktav 1 Nota(OCR1A)=Oktav 0 Nota(OCR1A)/2=Oktav 0 Nota(OCR1A)>>1 olur. Bu şekilde parametre alıp ayarlamanın daha doğru olduğunu düşünüyorum. Diğer türlü bir parçayı farklı değerde çalmak için nota listenizi yenilemeniz gerekir. Bu şekilde sadece nota listenizi girmeniz yeterli olur, tek yapmanız gereken parametreyi değiştirmektir. Bir parça içinde birden fazla oktavda çalınması gereken nota olması durumu için ince hali yani bir üst oktav bilgisini de kod içinde tanımladım. İki ya da daha fazla olması durumunda oktav için bir dizi tanımlanması daha doğru olur. Ben İstiklal marşı için bu şekilde yaptım.

Sonraki adım notaların bir araya gelerek vuruş süresi ve gereken yerlerde de sus ile bir ahenk yakalanmasıdır.

Vuruş Süresi

Notaların vuruş süresini frekans için kullandığımız timer1 ile belirleyeceğim. Bunun için bir sayaç tanımlıyorum ama dediğim gibi sabit bir değer veremiyorum. Bunun nedeni TCNT1 değerinin benim hangi oktav da olduğuma bakmaksızın bir mikro saniyede 2 kere artmasıdır. 4 Oktav LA için OCR1A değeri 2273, 5 Oktav LA için 1136 olur. Biz 4 oktav da kullandığımız sayaç değerinin iki katını 5 oktav da kullanırsak ancak aynı vuruş süresini buluruz. Bir saniye olarak baz aldığım ikilik bir LA için hesap yapalım. 4 Oktav La notasını elde etmek için TCNT1 2273 olmalı, OCR1A ile eşleşmeli ve kesme oluşmalıdır. Bunun için gereken 2273 saat darbesi 2273/2=1136,33 µs dir. (Prescaler 8 ve 16000000/8=2000000 saniye=2 µs) Bu hesaba göre OCR1A değerinin yarısı bize geçen zamanı verir. Her 1136µs bir kesme oluşur ve sayaç değeri bir artar. Bunu ms cinsine çevirelim 1136,33/1000=1,13633ms buluruz.

Kesme için gereken süreyi yani sayaç değerinin 1 artışındaki süreyi ms cinsinden bulduk. Bir milisaniye için sayaç değerini bulmamız gerekiyor. Bunun için de 1/1,13633=0,880 olur. Bu değer 4 oktav la için oluşan kesmelerle artan sayacın 1ms içindeki değeridir. Artık bir saniye=1000ms için sayaç değerini bulmak kolay 1000*0,880=880dir. Sayaç eğer 880 olursa 1 saniye geçmiş demektir. Aynı hesabı 5 oktav için yaparsak sayaç için: 1136/2=568µs=568/1000=0,568 ms sayacın bir artması için geçen süre, 1/0,568=1,760 bir ms deki sayaç değeridir. Bir saniye için sayaç=1,760*1000= 1760 buluruz.

Karışık olarak yaptığımız bu işlemleri bir formül haline getirelim. Sayaç= istenen süre(vuruş)*((2*1000)/OCR1A)=vuruş*2000/OCR1A olur. Bu üç parametre sonrası notanın sonuna kadar çalınabilmesi için zoraki bir döngü kullanmak gerekiyor. Bunun için de bir bayrak tanımladım.

Aşağıda kod içinde yeterli açıklamanın mevcut olduğunu düşünüyorum.

Not: Pasif buzzer kullanılması gerekiyor.


/*
* nota_timer_1.c
*
* Created: 17.02.2022 14:32:47
* Author : haluk
*/


#define F_CPU 16000000ul
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include <avr/pgmspace.h>



#define NOTA_PORT	PORTD
#define NOTA_DIR	DDRD
#define NOTA_PIN	PORTD5

#define NOTA_OUT	NOTA_DIR|=(1<<NOTA_PIN);
#define NOTA_PIN_H	NOTA_PORT|=(1<<NOTA_PIN);
#define NOTA_PIN_L	NOTA_PORT&=~(1<<NOTA_PIN);
#define NOTA_PIN_T	NOTA_PORT^=(1<<NOTA_PIN);

//0 oktav nota ocr değerleri prescaler 8
//datasheet CTC fOCR= f Clk/ (2*N*(1+OCR1A) N prescaler fOCR istenen frekans
#define DO    61155// =((16000000/(Nota Hz)/2/8)-1) formülden OCRA değerini çekince bulunur. 
#define DO_d  57723
#define RE_b  57723
#define RE	  54483
#define RE_d  51425
#define MI_b  51425
#define MI    48539
#define FA    45814
#define FA_d  43243
#define SOL_b 43243
#define SOL   40816
#define SOL_d 38525
#define LA_b  38525//0 oktav 25,956 LA/2^(1/12)
#define LA    36363//0 oktav 27,50Hz ---tüm notalar 4 oktav 440Hz e göre hesaplandı
#define LA_d  34322//0 oktav 29,135 LA*2^(1/12)
#define SI_b  34322
#define SI    32395
#define DO_I  30577

//1 oktav nota ocr değerleri prescaler 8
//her oktav değeri bir öncekinin yarısı
//her parçada oktav dizisi oluşturmamak için yazıldı çok gerekli değil.
// bkz. İstiklal marşı
#define I_DO_d  (DO_d>>1)
#define I_RE_b  (RE_b>>1)
#define I_RE	(RE>>1)
#define I_RE_d  (RE_d>>1)
#define I_MI_b  (MI_b>>1)
#define I_MI    (MI>>1)
#define I_FA    (FA>>1)
#define I_FA_d  (FA_d>>1)
#define I_SOL_b (SOL_b>>1)
#define I_SOL   (SOL>>1)
#define I_SOL_d (SOL_d>>1)
#define I_LA_b  (LA_b>>1)
#define I_LA    (LA>>1)
#define I_LA_d  (LA_d>>1)
#define I_SI_b  (SI_b>>1)
#define I_SI    (SI>>1)
#define I_DO_I  (DO_I>>1)

volatile uint8_t sus=0,nota_bitti=0;
volatile uint16_t onceki_nota=0;
volatile uint32_t nota_sayac=0, kesme_sayac=0;

void timer1_init();
void nota_cal(uint16_t nota,uint8_t oktav, uint32_t vurus);

const uint16_t YASASIN[]={//yaşasın okulumz
	DO,DO,SOL,SOL,LA,LA,SOL,0,
	FA,FA,MI,MI,RE,RE,DO,0,
	SOL,SOL,FA,FA,MI,MI,RE,0,
SOL,SOL,FA,FA,MI,MI,RE,0};
const PROGMEM uint16_t  GEZSEN[115] ={//gezsen anadoluyu
	DO,FA,MI,FA,MI,FA,SOL,RE,0,
	DO,FA,MI,FA,MI,DO,RE,0,
	DO,DO,FA,FA,MI,FA,SOL,RE,0,
	DO,FA,MI,FA,MI,DO,RE,0,
	SOL,LA,FA,SOL,LA,SOL,LA,FA,SOL,0,
	FA,SOL,MI,FA,SOL,FA,SOL,MI,FA,0,
	FA,MI,RE,DO,MI,FA,SOL,RE,0,
	DO,FA,MI,FA,MI,DO,RE,0,
	DO_I,DO_I,SI,DO_I,SI,DO_I,LA,0,
	SOL,LA,SOL,DO_I,LA,SI_b,SOL,0,
	SOL,FA,SOL,LA,0,
	FA,SOL,MI,MI,FA,RE,0,
	DO,FA,MI,FA,MI,DO,RE,0,
	DO,FA,MI,FA,MI,DO,RE,0
};
const PROGMEM uint16_t GEZSEN_vur[115] ={
	500,125,125,250,125,125,250,500,125,
	250,250,125,125,125,125,1000,125,
	250,250,250,250,125,125,250,500,125,
	250,250,125,125,125,125,1000,125,
	250,250,250,250,125,125,125,125,500,125,
	250,250,250,250,125,125,125,125,500,125,
	125,375,250,250,125,125,250,500,125,
	250,250,125,125,125,125,1000,125,
	250,250,125,375,375,125,500,125,
	250,250,250,250,375,125,500,125,
	1500,125,125,250,125,
	375,125,500,375,125,500,125,
	250,250,125,125,125,125,1000,125,
	500,500,250,250,250,250,1500,750
};
const PROGMEM uint16_t IZMIR[124]={//izmir marşı
	RE,LA,LA,LA,LA,LA,LA,SI_b,LA,SOL,SI_b,LA,0,
	RE,LA,LA,LA,LA,LA,LA,SI_b,LA,SOL,SI_b,LA,0,
	LA,I_RE,DO_I,SI_b,LA,SOL,LA,SOL,FA,MI,FA,0,
	FA,LA,SOL,FA,MI,RE,FA,MI,RE,DO_d,RE,0,
	RE,LA,LA,LA,LA,LA,LA,SI_b,LA,SOL,SI_b,LA,0,
	RE,LA,LA,LA,LA,LA,LA,SI_b,LA,SOL,SI_b,LA,0,
	LA,I_RE,DO_I,SI_b,LA,SOL,LA,SOL,FA,MI,FA,0,
	FA,LA,SOL,FA,MI,RE,FA,MI,RE,DO_d,RE,0,
	LA,I_RE,DO_I,SI_b,LA,SOL,LA,SOL,FA,MI,FA,0,
	FA,LA,SOL,FA,MI,RE,FA,MI,RE,DO_d,RE,0,
	
	
};
const PROGMEM uint16_t IZMIR_vur[124]={
	500,250,250,250,250,250,250,250,250,250,250,750,250,
	500,250,250,250,250,250,250,250,250,250,250,750,250,
	500,500,250,250,250,250,250,250,250,250,750,250,
	500,500,250,250,250,250,250,250,250,250,750,250,
	500,250,250,250,250,250,250,250,250,250,250,750,250,
	500,250,250,250,250,250,250,250,250,250,250,750,250,
	500,500,250,250,250,250,250,250,250,250,750,250,
	500,500,250,250,250,250,250,250,250,250,750,250,
	500,500,250,250,250,250,250,250,250,250,750,250,
	500,500,250,250,250,250,250,250,250,250,750,250,
};
const PROGMEM uint16_t CAVBELLA[67]={
	MI,LA,SI,DO_I,LA,0,
	MI,LA,SI,DO_I,LA,0,
	MI,LA,SI,DO_I,SI,LA,DO_I,//doI
	SI,LA,I_MI,I_MI,I_MI,0,
	I_MI,I_RE,I_MI,I_FA,I_FA,0,
	I_FA,I_MI,I_RE,I_FA,I_MI,0,
	I_MI,I_RE,DO_I,SI,I_MI,DO_I,//do
	I_RE,I_MI,0,
	LA,
	I_RE,I_MI,I_FA,I_FA,0,
	I_FA,I_MI,I_RE,I_FA,I_MI,0,
	I_MI,I_RE,DO_I,SI,I_MI,DO_I,//do
	SI,LA,0,
};
const PROGMEM uint16_t CAVBELLA_vur[67]={
	250,250,250,250,1000,125,
	250,250,250,250,1000,125,
	250,250,250,500,250,250,500,//doI
	250,250,500,500,250,125,
	250,250,250,250,1000,125,
	250,250,250,250,1000,125,
	250,250,250,500,500,500,//do
	500,1000,250,
	250,
	250,250,250,1000,125,
	250,250,250,250,1000,125,
	250,250,250,500,500,500,//do
	500,1000,750,
};
const PROGMEM uint16_t babyshark_nota[170]={
	RE,MI_b,0,
	RE,MI_b,0,
	RE,MI_b,0,
	RE,MI_b,0,
	RE,MI_b,0,
	RE,MI_b,0,
	RE,MI_b,0,
	RE,MI_b,0,
	SOL,FA,SOL,LA_b,SOL,FA,RE,FA,
	SOL,FA,SOL,LA_b,SOL,FA,RE,FA,
	SOL,FA,SOL,LA_b,SOL,FA,MI,SOL,
	RE,0,
	I_RE,I_MI,
	I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_RE,I_MI,
	I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_RE,I_MI,
	I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_FA_d,0,
	I_RE,I_MI,
	I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_RE,I_MI,
	I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_RE,I_MI,
	I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_FA_d,0,
	I_RE,I_MI,
	I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_RE,I_MI,
	I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_RE,I_MI,
	I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_FA_d,0,
	I_RE,I_MI,
	I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_RE,I_MI,
	I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_RE,I_MI,
	I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_SOL,I_FA_d,0,
};
const PROGMEM uint16_t babyshark_vur[170]={
	1000,125,875,
	1000,125,875,
	500,125,375,
	500,125,375,
	250,125,125,
	250,125,125,
	250,125,125,
	250,125,125,
	250,250,250,250,250,250,250,250,
	250,250,250,250,250,250,250,250,
	250,250,250,250,250,250,250,250,
	250,750,
	500,500,
	250,250,250,125,250,125,250,250,
	250,250,250,125,250,125,250,250,250,
	250,250,250,125,250,125,250,250,250,250,750,
	500,500,
	250,250,250,125,250,125,250,250,
	250,250,250,125,250,125,250,250,250,
	250,250,250,125,250,125,250,250,250,250,750,
	500,500,
	250,250,250,125,250,125,250,250,
	250,250,250,125,250,125,250,250,250,
	250,250,250,125,250,125,250,250,250,250,750,
	500,500,
	250,250,250,125,250,125,250,250,
	250,250,250,125,250,125,250,250,250,
	250,250,250,125,250,125,250,250,250,250,1750,
};
const PROGMEM uint16_t starwars[73]={
	SOL,SOL,SOL,MI_b,SI_b,
	SOL,MI_b,SI_b,SOL,0,
	I_RE,I_RE,I_RE,I_MI_b,SI_b,
	SOL_b,MI_b,SI_b,SOL,
	I_SOL,SOL,SOL,I_SOL,I_SOL_b,I_FA,
	I_MI,I_RE_d,I_MI,0,SOL_d,I_DO_d,DO_I,SI,
	SI_b,LA,SI_b,0,MI_b,SOL_b,MI_b,SOL_b,
	SI_b,SOL,SI_b,RE,0,
	I_SOL,SOL,SOL,I_SOL,I_SOL_b,I_FA,
	I_MI,I_RE_d,I_MI,0,SOL_d,I_DO_d,DO_I,SI,
	SI_b,LA,SI_b,0,MI_b,SOL,MI_b,SI_b,
	SOL,MI_b,SI_b,SOL,0,
	
};
const PROGMEM uint16_t starwars_vur[73]={
	500,500,500,375,125,
	500,375,125,1000,125,
	500,500,500,250,125,
	500,375,125,1000,
	500,375,125,500,375,125,
	125,125,250,250,250,500,375,125,
	125,125,250,250,250,500,375,125,
	500,375,125,1000,125,
	500,375,125,500,375,125,
	125,125,250,250,250,500,375,125,
	125,125,500,250,250,500,375,125,
	500,375,125,1000,750,
	
};
const PROGMEM uint16_t  cember[43]={//çemberimde gül oya
	MI,MI,MI,SOL,SOL,FA,MI,
	DO_I,DO_I,DO_I,DO_I,SI,LA,SI,LA,
	SOL,0,
	SOL,SOL,DO_I,DO_I,SI,LA,DO_I,DO_I,
	SI,SI,LA,SOL,SI,SI,SI,LA,SOL,
	LA,LA,DO_I,SI,LA,SOL,FA,
	MI,0,
};
const PROGMEM uint16_t  cember_vur[43]={
	250,250,250,250,250,250,750,
	375,125,250,250,250,250,500,250,
	1000,125,
	250,250,250,250,250,250,250,500,
	250,250,250,250,250,250,250,250,250,
	500,250,250,250,250,500,250,
	1000,750,
};
const PROGMEM uint16_t  panther[93]={
	RE_d,
	MI,0,FA_d,SOL,0,RE_d,
	MI,FA_d,SOL,DO_I,SI,MI,SOL,SI,
	SI_b,/*SI_b*/LA,SOL,MI,RE,MI,
	/*MI*/0,0,RE_d,
	MI,0,FA_d,SOL,0,RE_d,
	MI,FA_d,SOL,DO_I,SI,SOL,SI,I_MI,
	I_MI_b,/*I_MI_b*/0,RE_d,
	MI,0,FA_d,SOL,0,RE_d,
	MI,FA_d,SOL,DO_I,SI,MI,SOL,SI,
	SI_b,/*SI_b*/LA,SOL,MI,RE,MI
	/*MI*/,0,
	0,I_MI,I_RE,SI,LA,SOL,MI,
	SI_b,LA,SI_b,LA,SI_b,LA,SI_b,LA,
	SOL,MI,RE,MI,MI,/*MI*/
	SOL,MI,RE,MI,MI,/*MI*/
	SOL,MI,RE,MI,MI,/*MI*//*MI*/0,
	
};
const PROGMEM uint16_t  panther_vur[93]={
	125,
	500,375,125,500,375,125,
	375,125,375,125,375,125,375,125,
	1250,/*SI_b*/250,250,250,250,1250,
	/*MI*/500,375,125,
	500,375,125,500,375,125,
	375,125,375,125,375,125,375,125,
	3500,/*I_MI_b*/375,125,
	500,375,125,500,375,125,
	375,125,375,125,375,125,375,125,
	1250,/*SI_b*/250,250,250,250,1750
	/*MI*/,500,
	500,375,125,375,125,375,125,
	125,375,125,375,125,375,125,375,
	250,250,250,250,1250,/*MI*/
	250,250,250,250,1250,/*MI*/
	250,250,250,250,3250,/*MI*//*MI*/750,
	
};
const PROGMEM uint16_t  alibaba[38]={
	DO,
	FA,FA,FA,LA,
	SOL,FA,MI,FA,SOL,
	SOL,SOL,SOL,SI_b,
	LA,SOL,FA,SOL,LA,
	DO_I,0,DO_I,0,
	I_RE,DO_I,SI_b,LA,SI_b,
	SI_b,LA,SOL,FA,SOL,DO_I,
	LA,SOL,FA,0,
};
const PROGMEM uint16_t  alibaba_vur[38]={
	250,
	500,500,500,500,
	250,250,250,250,1000,
	500,500,500,500,
	250,250,250,250,1000,
	500,500,500,500,
	250,250,250,250,1000,
	250,250,250,250,500,500,
	500,500,750,750,
};
const PROGMEM uint16_t  benim[31]={//benim annem
	DO,RE,
	MI,MI,MI,FA,
	MI,RE,RE,MI,
	FA,LA,SOL,FA,
	MI,SOL,FA,
	RE,RE,MI,FA,
	LA,SOL,DO_I,LA,
	SOL,MI,SOL,FA,
	MI,0
};
const PROGMEM uint16_t  benim_vur[31]={//benim annem
	250,250,
	500,500,250,250,
	500,500,250,250,
	750,250,250,250,
	1000,250,250,
	500,500,250,250,
	500,500,250,250,
	500,500,250,250,
	1000,750
};
const PROGMEM uint16_t  istiklal[62]={//İstiklal marşı
	SI,
	MI,FA_d,SOL,RE_d,FA_d,
	MI,MI,
	LA,SI,DO,SI,SOL_d,SI,
	LA,SI,LA_d,SI,
	FA_d,FA_d/*FA_d*/,LA,SOL,RE_d,
	MI,FA_d,SOL,LA,SI,DO,RE,MI,
	RE,0,RE,DO_d,RE,SI,LA,
	SOL,SI,LA_d,SI,
	FA_d,SI,SI,LA,SOL,FA_d,SOL,
	MI,MI/*MI*/,RE,DO,SI,
	LA,SOL,FA_d,MI,SI,SI,
	MI,0,
};
const PROGMEM uint16_t  istiklal_oktav[62]={//İstiklal marşı
	-1,
	0,0,0,0,0,
	0,0,
	0,0,1,0,0,0,
	0,0,0,0,
	0,0/*FA_d*/,0,0,0,
	0,0,0,0,0,1,1,1,
	1,0,0,0,0,0,0,
	0,-1,-1,-1,
	0,-1,0,0,0,0,0,
	0,1/*MI*/,1,1,0,
	0,0,0,0,0,-1,
	0,0,
};
const PROGMEM uint16_t  istiklal_vur[62]={//İstiklal marşı
	500,
	500,500,500,375,125,
	1500,500,
	500,500,375,125,375,125,
	1500,250,250,250,
	500,875/*FA_d*/,125,375,125,
	375,125,375,125,375,125,375,125,
	500,125,250,250,250,500,500,
	1500,250,250,250,
	500,500,375,125,250,250,250,
	500,875/*MI*/,125,375,125,
	375,125,375,125,500,500,
	1500,1000,
};
const PROGMEM uint16_t  erikdali[89]={
	0,SI,SI,SI,SI,SI,DO_I,SI,LA,SI,LA,
	0,SI,SI,SI,SI,SI,DO_I,SI,LA,SI,LA,
	LA,LA,0,LA,SI,SOL,FA,MI,SOL,
	LA,LA,0,LA,SI,SOL,FA,MI,
	MI,SOL,LA,SOL,LA,SOL,LA,SI,SOL,FA,MI,SOL,
	LA,SOL,LA,SOL,LA,SOL,LA,SI,SOL,SOL,FA,MI,0,
	MI,SOL,LA,SOL,LA,SOL,LA,SI,SOL,FA,MI,SOL,
	LA,SOL,LA,SOL,LA,SOL,LA,SI,SOL,SOL,FA,MI,0
};
const PROGMEM uint16_t  erikdali_vur[89]={
	250,125,125,250,250,125,125,125,125,250,250,
	250,125,125,250,250,125,125,125,125,250,250,
	250,250,250,125,125,375,125,250,250,
	250,250,250,125,125,375,125,500,
	125,125,125,125,125,125,125,125,375,125,250,250,
	125,125,125,125,125,125,125,125,125,375,125,500,125,
	125,125,125,125,125,125,125,125,375,125,250,250,
	125,125,125,125,125,125,125,125,125,375,125,500,750
};
const PROGMEM uint16_t  dugme[54]={//kol düğmeleri
	MI,FA_d,SOL,
	SI,
	0,I_RE,DO_I,
	SI,DO_I,SI,LA,SI/*SI*/,
	0,MI,FA_d,SOL,
	SI,
	0,I_MI,I_RE,DO_I,
	SI,DO_I,SI,LA,SI/*SI*/,
	0,SOL,LA,SI,
	DO_I,SI,
	LA,DO_I,SI,LA,
	SI/*SI*/,LA,
	SOL,SOL,DO_I,SI,
	LA/*LA*/,SOL,
	LA,LA,I_RE,DO_I,
	SI,DO_I,SI,LA,SI/*SI*/,0
	
};
const PROGMEM uint16_t  dugme_vur[54]={//kol düğmeleri
	250,250,250,
	1500,
	750,500,250,
	62,62,62,62,1250/*SI*/,
	750,250,250,250,
	1500,
	750,250,250,250,
	62,62,62,62,1250/*SI*/,
	750,250,250,250,
	1250,250,
	750,250,250,250,
	1250/*SI*/,250,
	750,250,250,250,
	1250/*LA*/,250,
	750,250,250,250,
	62,62,62,62,1250/*SI*/,125
	
};
const PROGMEM uint16_t  bluey[88]={
	LA,SOL,FA,MI,0,DO,RE/*RE*/,
	FA,0,DO,RE,DO,0,
	LA,SOL,FA,MI,0,DO,RE/*RE*/,
	FA,0,
	LA,SOL,FA,MI,0,DO,RE/*RE*/,
	FA,0,DO,RE,DO,0,
	LA,SOL,FA,MI,0,DO,RE/*RE*/,
	FA,0,
	I_LA,I_SOL,I_FA,I_MI,0,DO_I,I_RE/*I_RE*/,
	I_FA,0,DO_I,I_RE,DO_I,0,
	I_LA,I_SOL,I_FA,I_MI,0,DO_I,I_RE/*I_RE*/,
	I_FA,0,
	I_LA,I_SOL,I_FA,I_MI,0,DO_I,I_RE/*I_RE*/,
	I_FA,0,DO_I,I_RE,DO_I,0,
	I_LA,I_SOL,I_FA,I_MI,0,DO_I,I_RE/*I_RE*/,
	I_FA,0,
	
};
const PROGMEM uint16_t  bluey_vur[88]={
	250,250,250,250,250,500,750/*RE*/,
	250,250,250,250,250,250,
	250,250,250,250,250,500,750/*RE*/,
	250,1250,
	250,250,250,250,250,500,750/*RE*/,
	250,250,250,250,250,250,
	250,250,250,250,250,500,750/*RE*/,
	250,1250,
	250,250,250,250,250,500,750/*I_RE*/,
	250,250,250,250,250,250,
	250,250,250,250,250,500,750/*I_RE*/,
	250,1250,
	250,250,250,250,250,500,750/*I_RE*/,
	250,250,250,250,250,250,
	250,250,250,250,250,500,750/*I_RE*/,
	250,1250,
	
};

ISR(TIMER1_COMPA_vect){
	if (kesme_sayac<=nota_sayac){
			if (sus!=1){
				NOTA_PIN_T;//pin toggle
			}
			kesme_sayac++;
		}else{
			nota_bitti=1;
			kesme_sayac=0;
			TCCR1B&=~(1<<CS11);//timer1 durdu
			NOTA_PIN_H;//sonraki toggle işlemine high olarak başlar
			sus=0;
	}
}
int main(void){
	NOTA_OUT;
	NOTA_PIN_H;
	timer1_init();
	nota_cal(DO,8,1);
	_delay_ms(500);
	while (1){
		for (uint8_t i=0;i<62;i++){
		nota_cal(pgm_read_word_near(istiklal+i),pgm_read_word_near(istiklal_oktav+i)+4,pgm_read_word_near(istiklal_vur+i));
		_delay_ms(10);
		}
		for (uint8_t i=0;i<124;i++){
		nota_cal(pgm_read_word_near(IZMIR+i),4,pgm_read_word_near(IZMIR_vur+i));
		_delay_ms(10);
		}
		for (uint8_t i=0;i<54;i++){
		nota_cal(pgm_read_word_near(dugme+i),3,pgm_read_word_near(dugme_vur+i));
		_delay_ms(10);
		}
		for (uint8_t i=0;i<43;i++){
		nota_cal(pgm_read_word_near(cember+i),4,pgm_read_word_near(cember_vur+i));
		_delay_ms(10);
		}		
		for (uint8_t i=0;i<115;i++){
		nota_cal(pgm_read_word_near(GEZSEN+i),4,pgm_read_word_near(GEZSEN_vur+i));
		_delay_ms(30);
		}
		for (uint8_t i=0;i<67;i++){
		nota_cal(pgm_read_word_near(CAVBELLA+i),4,pgm_read_word_near(CAVBELLA_vur+i));
		_delay_ms(30);
		}
		for (uint8_t i=0;i<89;i++){
		nota_cal(pgm_read_word_near(erikdali+i),4,pgm_read_word_near(erikdali_vur+i));
		_delay_ms(10);
		}
		for (uint8_t i=0;i<73;i++){
		nota_cal(pgm_read_word_near(starwars+i),3,pgm_read_word_near(starwars_vur+i));
		_delay_ms(10);
		}
		for (uint8_t i=0;i<93;i++){
		nota_cal(pgm_read_word_near(panther+i),4,pgm_read_word_near(panther_vur+i));
		_delay_ms(10);
		}
		for (uint8_t i=0;i<31;i++){
		nota_cal(pgm_read_word_near(benim+i),4,pgm_read_word_near(benim_vur+i));
		_delay_ms(10);
		}
		for (uint8_t i=0;i<32;i++){
		nota_cal(YASASIN[i],5,500);
		_delay_ms(10);
		}
		for (uint8_t i=0;i<38;i++){
		nota_cal(pgm_read_word_near(alibaba+i),4,pgm_read_word_near(alibaba_vur+i));
		_delay_ms(10);
		}
		for (uint8_t i=0;i<170;i++){
			nota_cal(pgm_read_word_near(babyshark_nota+i),4,pgm_read_word_near(babyshark_vur+i));
			_delay_ms(10);
		}
		for (uint8_t i=0;i<88;i++){
			nota_cal(pgm_read_word_near(bluey+i),4,pgm_read_word_near(bluey_vur+i));
			_delay_ms(10);
		}		
	}
}
void timer1_init(){
	cli();
	TCCR1B|=(1<<WGM12)|(1<<CS11);//ctc, pre 8
	TIMSK1|=(1<<OCIE1A);
	sei();
}
void nota_cal(uint16_t nota,uint8_t oktav, uint32_t vurus){	
	nota>>=oktav;//oktava göre ocr belirleniyor	
	if (nota!=0){
//Sayaç= istenen süre(vuruş)*((2*1000)/OCR1A) ms olarak istenen sürede oluşması gereken kesme sayısını verir
		nota_sayac=(vurus*2000)/nota;
		onceki_nota=nota;// önceki notanın değeri
		OCR1A=nota;//kesme aralığı
	}else{
		sus=1;
		nota_sayac=(vurus*2000)/onceki_nota;// es süresi
		OCR1A=onceki_nota;	//kesme aralığı önceki notaya göre aldım
	}
	TCCR1B|=(1<<CS11);//timer açıldı	
	while (nota_bitti==0);//nota bitene kadar bekler
	nota_bitti=0;	
}

 

 

 

 

 

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