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.
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.
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.
Ç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.
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;
}
}
Önceki bölümde TWI biriminde bulunan registerler ile veri yazma ve okuma işlemini yaparak çalışma mantığını anlatmış oldum. Bu bölümde bu işlemleri birer fonksiyon halinde derli toplu bir hale getireceğim. TWI kesmesiyle yazma-okuma yaparak AHT10 ile sıcaklık ve nem bilgisini alacağım.
Fonksiyonlar
Başlatma, start ve stop için burada tekrar söyleyecek bir şey yok. Kesme kullanacağım için sadece TWIE bitini "1" yapacak kodu ekliyorum. Bu fonksiyonları aşağıda görebilirsiniz. Veri okuma ve yazma kısmında işler biraz farklı olduğundan bu fonksiyonlara geçiyorum.
Veri Yazma
Bunun için birkaç farklı fonksiyon tanımladım. Adres ve komut bilgisi sonrası göndereceğim veriyi bir tane veya birden fazla olacak şekilde göndermemi sağlayan fonksiyonlar oluşturdum. Fonksiyonların ilk parametresi slave aygıtın adresidir. Yazma olduğu için komut belli bunda bir parametre oluşturmaya gerek yok. Bir byte veri göndereceğim fonksiyonda gerekli değil ama birden fazla olacaksa ne kadar göndereceğimi de parametre olarak girmem gerekir. Dizi oluşturup gönderir ve boyutu için NULL (0x00) karakterine kadar gönder diyebilirim ama sensörler NOP (0x00) boş komut isteyebilirler. Bu durumda NOP gönderme şansım olmaz. Önceden tanımlı sensör komut dizisi göndermek için de bir fonksiyon oluşturdum. Bir diğer parametre tekrarlayan başlangıç (repeated start) için gereklidir. Stop koşulu oluşturmadan yazma veya okumaya devam etmek için kullanabiliriz.
Burada ilk veri 0x61 gittikten sonra stop koşulu gerçekleşmiş sonraki yazma komutu ve adres başlangıcı için start oluşmuş ve sonraki veri yazılmış.
Burada ilk veri 0x61 gittikten sonra stop koşulu gerçekleşmemiş sonraki yazma komutu ve adres başlangıcı için start oluşmuş ve sonraki veri yazılmış. Bu durumu gerçekleştirmek için bir değişken kullandım. İki tip fonksiyon için en önemli parametre yazacağımız veridir. Bir tane olduğunda sorun yok ama birden fazla olduğunda bu verileri bir bellekte tutmamız ve yazma işini oradan yürütmemiz gerekir. Belleğin neresinde olduğumuzu da bilememiz ve bunun için de bir işaretçi oluşturmamız gerekir.
Daha önce UART kullanımı ile ring buffer konusunu yazmıştım burada tekrar etmeyeceğim. Fonksiyona parametre olarak gelen verileri belleğe birer birer kaydederken verinin kayıt yerini tutan işaretçiyi de birer birer artırıyorum. Bu işlemin ne kadar olacağı ya NULL karaktere ya da dizinin boyutunu bildirdiğim ve parametre ile gösterdiğim boyutla sınırlı olacaktır. Bu ana fonksiyonlar bir önceki konuda alt başlıklarla ifade etmeye çalıştığım start, adres ve data gibi alt fonksiyonlardan oluşacaktır. İlk olarak adres ve komutu (R/W) parametrelere bağlı olarak bir değişkene kaydediyorum. Sonraki adım olarak veriyi belleğe alıyorum. En son olarak start koşulu oluşturarak TWI birimi iletişimi başlatıyor.
Alt Fonksiyonlar
void i2c_start(){//start koşulu için gerekli bitler 1 yapıldı
TWCR=(1<<TWEN)|(1<<TWSTA)|(1<<TWINT)|(1<<TWIE);
while(!(TWCR & (1<<TWINT)));// twint beklendi
}
void i2c_adr(uint8_t adr,i2c_cmd_t cmd){
Sladr_RW=((adr<<1)|cmd);//adres bir bit sola kaydırıldı ve komutla değişkene yazıldı
}
void i2c_data(uint8_t data){
i2c_tx_bas=(i2c_tx_bas+1)&I2C_Tx_Mask;//veri işaretçisi artırıldı
i2c_tx_ring[i2c_tx_bas]=data;//işaretçinin gösterdiği belleğe veri yazıldı
}
void i2c_end( i2c_rep_t repst){
i2c_rep=repst;//repeated start kontrol
i2c_start();//start koşulu
}
void i2c_stop(){
TWCR=(1<<TWEN)|(1<<TWSTO)|(1<<TWINT)|(1<<TWIE);// stop koşulu
}
Sadece bu fonksiyonlar ile veri göndermek mümkün. Sırasıyla adres, data (veri miktarı kadar) ve end fonksiyonu çağrılarak işlem yapılır. Burada görünmeyen asıl işlemler TWI kesme rutini içindedir. Ana fonksiyonlara göz atıp kesme içinde neler olduğunu anlatmaya çalışacağım. Bir sensörün veya farklı bir entegrenin komut setinde çeşitli veriler olacaktır. Bunları söylediğim sırayla tek tek yazmak yerine bir fonksiyon halinde birleştirmek daha doğru olur. Bu amaçla üç tane fonksiyon oluşturdum. Bunlardan biri tek bir veri için, diğer ikisi birden fazla veri göndermek içindir. Karakter dizisi göndermek için tanımladığım fonksiyona belki hiç gerek olmayacak.
Ana Fonksiyonlar
void i2c_send_data(uint8_t adr, uint8_t data, i2c_rep_t repst){
i2c_adr(adr, I2C_WRITE);
i2c_data(data);
i2c_end(repst);
}
void i2c_send(uint8_t adr, uint8_t* str, uint8_t len, i2c_rep_t repst){
i2c_adr(adr, I2C_WRITE);
for (uint8_t i=0;i<len;i++){//dizi boyutu kadar
i2c_data( str[i]);// belleğe yazar
}
i2c_end(repst);
}
void i2c_send_str(uint8_t adr, const char* str, i2c_rep_t repst){
i2c_adr(adr, I2C_WRITE);
while (*str){//0x00 bulana kadar
i2c_data(*str++);// belleğe yazar
}
i2c_end(repst);
}
Veri Okuma
Okuma kısmında bir fonksiyon yeterli oluyor. Hangi adresten ne kadar okuyacağımızı parametre olarak aktardığımız fonksiyon ilk olarak adres ve komutu (R/W) değişkene yazıyor. Veri miktarını tutan bir başka değişkene kayıt yapıp start koşuluna gidiyor. Burada tüm iş kesme rutini içinde yapılıyor. Gelen veri belleğe kaydediliyor. Bu kayıt işlemi içinde bellekteki yeri tutan işaretçi artırılıyor. Bu sayede verinin alındığını öğreniyoruz. Bellekten okuma yapmak için de farklı fonksiyonlar oluşturdum.
TWI okuma
void i2c_read(uint8_t adr, uint8_t len){
i2c_adr(adr, I2C_READ);
i2c_rx_len=(i2c_rx_len+len)&I2C_Rx_Mask;//okunacak veri sınırı
i2c_start();
}
Bellekten Okuma
uint8_t i2c_gelen(){//bellek işaretçileri eşit değilse yeni veri gelmiştir
if (i2c_rx_son==i2c_rx_bas) return 0;
return 1;
}
uint8_t i2c_oku(){// bellek işaretçileri eşit olana kadar bellekten veri alır.
i2c_rx_son=(i2c_rx_son+1)&I2C_Rx_Mask;
return i2c_rx_ring[i2c_rx_son];
}
Bu fonksiyonlardan sonra kesme rutini içinde neler oluyor onlara değinelim.
TWI Kesme(Write)
Ana program içinde farklı işlemler yapmak için yoklama yerine kesmeleri tercih ediyorum. Bu sayede MCU daha verimli şekilde kullanılmış oluyor. Burada kullanmamam gerektiği halde while döngüleri kullandım. TWI kesme vektörü son sıralarda olduğu için biraz rahat davrandım. Başka bir işlemi engelleme olasılığı daha az olacaktır. Ayrıca bu döngülerde bekleme süresi çok düşük. Eğer arka arkaya veri okuma yazma yapmayacaksanız aralarda farklı işlemler ve hatta bekleme süreleri olacaksa bu döngülere kesme kullandığımız için gerek yoktur. Öncelikle alttaki lejantı paylaşayım.
Datasheetteki şemalar için hazırlanmış bir lejant, şemalar oldukça anlaşılır. Bunların dışında adım adım neler yapılması gerektiğini gösteren farklı tablolar da mevcut. Ama bunun yerine bu şemaları paylaşmak daha mantıklı geldi.
Bu şemada hangi işlem sonrası hangi durum kodunun oluştuğu görülmektedir. Bu kodlara göre kesme rutini içinde yapacağımız işlemlere karar vereceğiz. TWI birimi her işlem sonunda bir durum kodu oluşturduğu gibi kesme bayrağını da "1" yapıyor. Bunu elle temizlememiz gerektiğini önceki konuda söylemiştim. Her seferinde kesme oluyor biz siliyoruz. Bu şekilde yukarıda görülen adımlar arsında sürekli kesme oluşuyor. Kesme rutini içine girdiğimizde hangi adımda olduğumuzu durum kodu ile belirlemek için Switch-case kullanılmış ben de önceki konularda benzer şeyler yapmıştım. Burada Switch ifadesi olarak TWSR registerini 3-7 bitlerine bakacağız.
I2C_STATUS kesmeye girdiğimizde hangi seçeneğe gideceğimizi yukarıdaki şemaya göre şekillendirecektir.
Start
İlk adım olan Start ile 0x08 değerini alır. Bu bize start koşulunun gerçekleştiğini söyler. Bu durumda adres ve komutu TWDR ye yazmamız gerekir. Ardından TWINT "1" yaparak işlemi tamamlarız. Yukarıda bahsettiğim tekrarlayan başlangıç durum kodu 0x10 dur. Bu gerçekleştiğinde farklı bir durum kodu olmasına rağmen yapılması gereken işlemler aynıdır. Start ve Rep_Start için aynı kodu ayrı ayrı yazmak yerine iki seçeneği alt alta yazıp 0x08 içine break; yazmıyorum. Böylece kesme içine girdiğimiz zaman durum kodu 0x08 (START) veya 0x10 (REP_START) olsa da tek bir işlem yapılıyor.
case (I2C_START):
case (I2C_REP_START):
TWDR=Sladr_RW;//adres ve komut yazıldı
TWCR=(1<<TWEN)|(1<<TWINT)|(1<<TWIE);//OR ile twint "1" yapmak da yeterlidir.
while(!(TWCR & (1<<TWINT)));// iletişim aralıkları sık değilse gereksiz.
break;
TWINT "1" yapıldıktan sonra kesme while döngüsü olmazsa kesme içinden çıkıp ana döngüye geri döner. Eğer döngüyle beklenirse sonraki adıma kadar kesmede kalır ve sonraki adım için oluşan kesme gerçekleşir. Fakat iletişim koparsa sonsuz döngüye girer burada en hızlı haliyle denemek için böyle yaptım.
Adres
Slave aygıt adresi algılayıp ACK ile dinlemeye başladığını belirtmiş olur. Bu gerçekleşmezse NACK koşulu oluşur. ACK için ( (MTR_ADR_ACK) 0x18, NACK için (MTR_ADR_NACK) 0x20 kodu oluşturulur. NACK durumunda hattı meşgul etmemek için stop koşuluna geçilebilir veya aynı iletişimi tekrar deneyebiliriz. Bu tekrarı bir değişkenle kontrol etmesek sonsuz bir döngüye girebiliriz. ACK durumundaysa veriyi hatta göndermemiz gerekir. Veri tek bir byte ile sınırlıysa burada yapılacak işlemler vardır ama birden çok olduğunda veri yazma sonrası farklı durum kodu oluşur ve burada yapılacak işlemin aynısı orada da yapılması gerekir. Yine kod kalabalığı olmaması için adres ACK ve data ACK alt alta yazıp 0x18 içine break; yazmıyorum. Bunun yerine case (I2C_MTR_ADR_ACK | I2C_MTR_DATA_ACK): başka bir durum koduyla çakışmamış olsaydı yazabilirdik. Fakat bu çakışma olduğu için bu yöntemi yapamıyorum.
Data
Adres sonrası ACK ile data sonrası ACK işlemleri aynıdır. Burada öncelikli olarak bellekte göndereceğimiz verinin sonuna gelip gelmediğimizi kontrol ediyoruz. Sona gelmemişsek TWDR ye gidecek olan veriyi bellekteki yeriyle eşitleyerek yazıyoruz. Ardından yine TWINT "1" ve döngüyle kontrol ediyoruz. Haliyle veri hatta yazılıyor ve yine kesme oluşuyor. Burada datanın alınması veya alınmamasına bağlı olarak ACK (MTR_DATA_ACK) 0x28 veya NACK (MTR_DATA_NACK) 0x30 koşulu oluşuyor. NACK durumunda adres NACK ile aynı şeyleri yapıyoruz. Data ACK durumunda yine aynı yere gelip tekrar bellekteki veriyi kontrol ediyoruz. Sona geldiğimizde yani gidecek veri kalmadığında iki seçenek belirliyoruz. Eğer bir Rep_Start ayarlıysa start koşulu oluşturuyoruz. tekrar bir başlangıç ayarlı değilse stop ile işlemi sonlandırıyoruz.
case (I2C_MTR_ADR_ACK):
case (I2C_MTR_DATA_ACK):
if (i2c_tx_son!=i2c_tx_bas){//bellek sonu kontrol ediliyor
i2c_tx_son=(i2c_tx_son+1)&I2C_Tx_Mask;
TWDR=i2c_tx_ring[i2c_tx_son];//veri yazılıyor
TWCR |=(1<<TWINT);
while(!(TWCR & (1<<TWINT))){
timeout++;
if (10>=timeout){
timeout=0;
break;;
}
}
}
else if (i2c_rep==1){//repeated start
i2c_rep=0;
TWCR|=(1<<TWSTA);
}
else{
i2c_stop();
}
break;
case I2C_MTR_ADR_NACK:
//tekrar dene, dur v.b
break;
case I2C_MTR_DATA_NACK:
//tekrar dene, dur v.b
break;
Master mod veri yazma kesmelerle bu şekildedir. Arbitration çoklu master olan bir hat için geçerli olduğundan bu konuya girmiyorum. Hattın kontrolünü başka bir master aygıt aldığında yapılması gerekenler için bu kodlara göre işlem yapılır.
TWI Kesme(Read)
Yazma kısmında olduğu gibi öncelikle datashette yer alan şemayı paylaşıyorum. Burada yine her adımda oluşturulan durum kodlarını ve neler yapılması gerektiği görünüyor.
Veri okumak için adresi TWDR ye yazıp kaç adet veri okumak istediğimizi belirleyip start koşulu ile iletişimi başlatıyorduk. Bu start veya rep_start bölümüyle ortaktır. Durum kodları da aynıdır.
Adres
Adres ve komutu (Sladr_RW) start-rep_start seçeneğinde yazıyorduk. Bu komut okuma olduğu için sonrasında adres ACK -NACK durum kodu farklı olacaktır. Yazmada bu adımdan sonra 0x18 veya 0x20 durum kodları oluşturulurken, okumada adres ACK (MRD_ADR_ACK) 0x40 ve adres NACK (MRD_ADR_NACK) 0x48 durum kodu oluşturulur. NACK durumunda hattı meşgul etmemek için stop koşuluna geçilebilir veya aynı iletişim tekrar deneyebiliriz. ACK durumunda yani slave aygıtın cevap vermesi durumunda veriyi okumaya başlamamız gerekiyor. Bunun için TWINT "1" yapıyor ve kontrol ediyoruz. Veri alındıkça ACK koşulunu oluşturan bu sefer master cihaz olacaktır. Son sıradaki veri alınınca NACK koşulu oluşturarak iletişimi sonlandıracağız. Bu nedenle TWINT ile beraber TWEA da "1" yapılıyor. Tek bir byte veri alacaksak adres ACK cevabından sonra alınacak veri adetini sorgulamamız gerekir. Sonraki adımda veriyi almaya başlıyoruz. Tekrar bir sorgulama yapmak gerekeceğinden veri alma koşulunun adres ACK koşulunun üstüne yazarak komut tekrarı yapmıyoruz.
Data
Adres ACK sonrası data ACK (MRD_DATA_ACK) 0x50 durumu oluşur TWDR belleğe kaydedilir. TWINT ve TWEA "1" yapılır. Yukarıda bahsettiğim gibi bir veya birden fazla olma durumunu kontrol için aynı kodları yazmak yerine adres ACK durumuna geçilir. Son veri alınınca NACK koşulu oluşturulur. Bu durumda data NACK (MRD_DATA_NACK) 0x58 durum kodu oluşur ve TWDR den son veri belleğe alınarak iletişim sonlandırılır.
case I2C_MRD_DATA_ACK:
i2c_rx_bas=(i2c_rx_bas+1)&I2C_Rx_Mask;//bellek işaretçisi artıldı
i2c_rx_ring[i2c_rx_bas]=TWDR;// veri belleğe alındı
case I2C_MRD_ADR_ACK:
if (i2c_rx_bas!=i2c_rx_len){//okunacak veri sınırı kontrolü
TWCR=(1<<TWEN)|(1<<TWINT)|(1<<TWIE)|(1<<TWEA);//ACK
while(!(TWCR & (1<<TWINT)));
}else{
TWCR=(1<<TWEN)|(1<<TWINT)|(1<<TWIE);//NACK
while(!(TWCR & (1<<TWINT)));
}
break;
case I2C_MRD_ADR_NACK:
i2c_stop();
break;
case I2C_MRD_DATA_NACK:
i2c_rx_bas=(i2c_rx_bas+1)&I2C_Rx_Mask;
i2c_rx_ring[i2c_rx_bas]=TWDR;
i2c_stop();
break;
Bundan sonraki durum kodları slave aygıt ve hata kodları içindir. Aşağıda toplu halde tüm durumlara ve başlık dosyasına ulaşabilirsiniz.
AHT10
Üreticinin sitesinde datasheet yok. Güncellendiği yazılı başka kaynaklardan çevrilmiş halini buldum buradan ulaşabilirsiniz. Bu çevirilerde eksik bilgiler var. Bu nedenle biraz daha araştırdım ve üreticinin sayfasında yönlendirmediği bir Çince datasheet buldum. Burada eksik kalan komutlara ulaştım. AHT10 ±0,3 °C hata payı, 0,01 °C çözünürlükte ve -40+85 °C aralığında sıcaklık ölçebiliyor. ±2 %RH hata payı, 0,24%RH çözünürlükte ve 0-100%RH aralığında nem ölçümü yapabiliyor. Sıcaklık ve nem belli bir aralık dışına çıkınca hata payı artıyor. AHT10 en fazla 3,6 V ile çalışabiliyor ama ben modül olarak aldım ve modül 6V a kadar çalışabiliyor. Modül üstünde SDA-SCL pinleri TWI iletişimi için ve VIN-GDN besleme pinleri olarak çıkartılmış. ADR pini lehimli bu nedenle modülün adresi 0x38, lehimli değilse adres 0x39 olur.
İletişim
I2C (TWI) ile bağlantı kurulabilen bu modülün komut sayısı az veya datasheette yazılı değil. Bu kanıya nereden vardığımı merak edenler için. Modülün durum kodlarında ismi geçen komut, çevrim ve normal (CMD,CYC ve NOR) modlara nasıl geçildiği yazılı değil. Bunlar ölçüm anında da geçilen değişen modlar değil. Bu durumu kontrol ettim daima normal mod dönüşü aldım. İngilizce datasheette init kısmı da eksikti bu bölümü Çince datasheetten aldım.
Datasheeten alıntı tablo ile devam edersek üç adet komut bulunmaktadır. İlk elektrik verildikten sonra 40ms bekleyip init komutu ile başlatma tavsiye edilmiş. Modülün kalibrasyonu için State bit 3 kontrol edilmeli denilmiş. Bu kontrol yerine soft reset ile başlıyorum. Bunun için önce adres 0x38 ve yazma komutu sonra reset komutu 0xBA gönderiyoruz. Bundan sonra başlatma komutu göndereceğiz. ilk olarak adres 0x38 ve yazma komutu sonra init komutu 0xE1, ilk parametre 0x08 ve son parametre 0x00 gönderiyoruz. Ölçüm için tetikleme komutu 0xAC adres sonrası gönderiyoruz. İlk parametre 0x33 ve son olarak 0x00 ile tetikleme başlatılıyor. Bundan sonra 75ms beklememiz öneriliyor. Bu beklemeyi yapmadan da okumak mümkün sadece bir önceki çevrimin sonuçlarını almış oluruz.
Okuma yapmak için adres ve okuma komutu gönderip 6 byte veri alıyoruz. ilk byte veri[0] durum kodlarını içeriyor. Durum kodlarından bit 7 meşgul bayrağıdır. Tetik sonrası bekleme yapmadan okuduğunuz zaman "1" bekleme sonrası okuduğunuz zaman "0" olacaktır. Bit 6-5 numaralı mod gösterir ama hep "00" olarak gördüm değişim için bilgi verilmemiş. Bit 3 kalibrasyon durumunu gösterir "0" ise kalibrasyon tamamlanmamış demektir. İnit komutu ile başlatma önerilmiş. Sonraki iki byte veri[1],veri[2]ve 3. byte veri[3]yüksek değerli 4 biti nem için ayrılmış. 3. Byte veri[3] düşük değerli 4 biti ile son iki byte veri[4],veri[5] sıcaklık içindir. Bu şekilde okunan veriyi bir değişkene kaydedip sıcaklık-nem dönüşüm hesabı için kullanacağız.
Sıcaklık
3. byte yarısı ve son iki byte ile aldığımız veriyi 32 bit bir sayı olarak saklıyoruz. Bunun için son byte veri[5] en sağda olacak. Veri[4] 8 bit sola kaydırıp veri[3] ilk 4 biti 16 bit sola kaydırıp sayımızı oluşturuyoruz.
Bu formüle göre işlemi yaparak sıcaklık değerini buluyoruz. St okuduğumuz veri değeridir. Sıcaklık için virgülden sonrası önemli değilse 20 bit sağa kaydırıp işlem yapabiliriz. Önemliyse bölme işlemini yaparak float değer olarak sonucu sıcaklık değişkenine atıyoruz.
Nem
Nem için de sıcaklık gibi sayımızı oluşturmamız gerekiyor. Bunun için son byte veri[3] en sağda olacak. Veri[2] 8 bit sola kaydırıp veri[1] 16 bit sola kaydırıp sayımızı oluşturuyoruz. Veri[3] ilk 4 biti sıcaklık için ayrılmış bu nedenle oluşan sayıyı 4 bit sağa kaydırmamız gerekiyor.
Yine formüle göre işlemi yaparak nem değişkenine atıyoruz. Modül hakkında söylenecek başka bir şey kalmadı. Her şeyi toplu olarak aşağıda paylaşıyorum.
Güncelleme Notu:
Bir önceki konuda arka arkaya veri yazma ve okuma konusunda sorun olduğu için while döngüsü kullandığımı yazmıştım. Birkaç yerde birden kullanmak yerine tek bir döngü kullanarak aynı sonucu elde ettim. Bunun için bir meşgul bayrağı tanımladım. Tüm iletişimin bitmesini bekleyip bundan sonra sıradaki iletişimi başlattım.
Bunun dışında tekrar başlatma kısmında start koşulunu kaldırdım. Meşgul bayrağı "0" olunca fonksiyon start koşulunu oluşturacak. Burada kesmeyi de kapattım. Sürekli olarak kesme içine giriyor ve 400kHz de sorun oluyordu.
Son haliyle dosyalar aşağıdaki gibidir.
/*
* aht10_i2c.c
*
* Created: 24.01.2022 00:56:31
* Author : haluk
*/
//#include <xc.h>
#include <avr/io.h>
#include <util/twi.h>
#include "uart.h"
#include "i2c_master.h"
#include <util/delay.h>
#include <stdio.h>
uint8_t aht_adr=0x38;//aht10adres
uint8_t aht_res=0xBA;//soft reset
uint8_t aht_init[]={0xE1,0x08,0x00};//init
uint8_t aht_trig[]={0xAC,0x33,0x00};//ölçüm başlatma
uint8_t aht_data[6];
uint32_t aht_temp=0;
uint32_t aht_hum=0;
float sicaklik=0.0;
float nem=0.0;
char str_data[20];
int main(void)
{
uart_basla(115200);
i2c_init();
i2c_send_data(aht_adr,aht_res, N_REPEAT);//soft reset
i2c_send(aht_adr,aht_init,3, N_REPEAT);//init komutu
uint8_t i=0;
while (1){
i2c_send(0x38,aht_trig,3,N_REPEAT);//ölçme tetik
//_delay_ms(75);//75ms bekleme önerilmiş
i2c_read(aht_adr,6);// veri okuma
while (i2c_gelen()){//okunan veri alındı
aht_data[i++]= i2c_oku();
}
i=0;
aht_temp=((uint32_t)(aht_data[3]&0x0F)<<16)|((uint16_t)aht_data[4]<<8)|(aht_data[5]);
aht_hum=(((uint32_t)aht_data[1]<<16)|((uint16_t)aht_data[2]<<8)|aht_data[3])>>4;
sicaklik=((float)aht_temp/5242.88)- 50;//formülün sadeleştirilmiş hali
//sicaklik=((aht_temp*25)>>17)- 50;//virgülden sonrası önemsizse
nem=((float)aht_hum/10485.76);//formülün sadeleştirilmiş hali
//nem=(aht_hum*25)>>18;//virgülden sonrası önemsizse
sprintf(str_data,"%.2f--%.2f\n",sicaklik,nem);//sonuçları ekrana yazmak için
uart_dizi(str_data);
//_delay_ms(500);//ölçüm aralığı
}
}