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.