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
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)
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
İş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
İş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
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;
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ı: