AVR -Led Yakma (delay olmadan, farklı yöntemlerle)

Birçok yerde Arduino eğitimi amacıyla bu başlığı gördünüz. C dili kullanılarak AVR denetleyiciler için de yeterince led örneği var.  Örneklerin alt alta delay kullanılarak yapılması, birden fazla led uygulamasında da aynı şekilde devam etmesini doğru bulmuyorum. Arduino eğitimi için de bu şekilde yapılması uygun değil. Bütün bu örneklerin içinde delay(); kullanmadan yapılanları da var. Bu yazıyı onlardan ayıran yöntem ve yapılan işin anlatılması olacaktır.  Zamanlayıcı (Timer) birimi, millis() ve birçok yerde hayat kurtaran dizilerin (look up table) kullanımını aktarmaya çalışacağım.

Timer/Counter 0 (TC0)

TCNT0

Zamanlayıcılara buradan erişebilirsiniz. Bu yazıdaTimer0 birimiyle işlem yapacağız. Yukarıdaki linkte detaylı anlatım mevcut ama kısaca değinmek istiyorum. TC0 şemasına baktığımızda kristalden gelen saat darbesiyle artarak sayım yapan TCNT0 registeri bulunur. (not: “register” kullanmak ve “rejıstır” yerine “register” olarak okumak daha kolay geliyor)
TCNT0 ile karşılaştırma yapılan OCR0A, OCR0B registerleriyle bu karşılaştırma sonucuna göre çıkış verilen (PWM) OC0A ve OC0B pin bağlantıları görülmektedir. Ayrıca tüm TC0 birimi ayarlamaların yapıldığı TCCR0A ve TCCR0B registerleri ile saat darbesinin değerinin bölündüğü prescaler bulunur.

TCNT0 saat darbesiyle artar ve yapılan ayarlamaya bağlı olarak üst değere (255.0xFF) ulaşıp sıfırlanır veya sıfıra doğru azalır. 16MHz bir kristalin bağlı olduğu denetleyicide 1sn de 16.000.000, 1ms de 16.000 saat darbesi oluşur. 8bit veri tutabilen TCNT0 62.500 defa üst değere ulaşır. Bu çok büyük bir rakam olduğundan Prescaler ile gelen saat darbesini bölerek okunabilir değerlere getireceğiz.

Prescaler tablosunda gördüğünüz gibi 1,8,64,256 ve 1024 gibi değerler bulunmaktadır. Bu değerleri kullanarak bölme işlemini yapacağız. Buradaki bölme işlemi 2′ nin katları olduğundan bit kaydırma şeklindedir. Önceki yazılarımda bahsettiğim gibi onluk sistemde “10” a bölmek demek basamak değerinin azaltmak (sola virgül kaydırmak/”0″ silmek) ve “10” ile çarpmak basamak değerini artırmak (sağa virgül kaydırmak/”0″ eklemek) demektir. İkilik sistemde böyledir. “8” e bölmek değerimizi “3” bit sağa kaydırmak demektir. (16)0B 0001 0000 /8= 0B 0000 0010(2)

Bu değerler içinde “64” bizim için uygundur. Küçük olanlar sayıyı yeterince küçültmez (16.000.000/8=2.000.000/sn ve 2.000/ms) ve büyük olanlarda buçuklu değerlere neden olur. (16.000.000/256=62.500/sn ve 62,5/ms) “64” kullandığımızda TCNT0 16.000.000/64= saniyede 250.000 ve milisaniyede 250 saat darbesi alarak artar. Bu değer taşmaya neden olmaz ve tespiti kolay olur.

OCR0A

TCNT0 için uygun bölücü değerini bulduk fakat biz bir önlem almazsak TCNT0 sürekli artarak sıfırlanacaktır. Bize lazım olan “250” darbeyi tespit etmemiz ve TCNT0 sıfırlamamız gerekir aksi halde 1ms  zamanı belirleyemeyiz. Bunu yapmanın yolu TCNT0 ile istediğimiz bir değeri karşılaştırmak, sonuca göre işlem yapmaktır. Bu karşılaştırma işlemi OCR0A ve OCR0B registerleridir. Bu iki register “çıkış karşılaştırma yazmaçlarıdır.” PWM için de bu registerler kullanılır. Biz eşleşme ile kesme oluşsun veTCNT0 sıfırlansın istiyoruz. Bilgi sayfasında TC0 için üç adet kesme görülüyor. Bunlardan ikisi karşılaştırma (OCR0A, OCR0B) ve TCNT0 taşma kesmesidir. Karşılaştırma yapmamız gerektiğinden OCR0A ve OCR0B registerini kontrol eden  TIMER0_COMPA(OCR0A) ve  TIMER0_COMPB(OCR0B) kesmelerini not alıyoruz.
TC0 çalışma modlarını incelediğimizde normal, PWM, fast PWM ve CTC modu görünmektedir. Bu modlar arasında CTC karşılaştırma sonucu zamanlayıcıyı temizleyen mod olarak tam istediğimiz şeyi yapmaktadır.

 

Grafikte görüldüğü gibi eşleşme olduğunda kesme oluşuyor, kesme bayrağı aktif oluyor ve TCNT0 sıfırlanıyor. Bilgi sayfasında CTC modunda OCR0A kullanıldığı yazılı yine aynı şekilde aşağıdaki tabloda CTC ile OCR0A kullanıldığı görülmektedir. Bu nedenle TIMER0_COMPA kesmesini kullanacağız.

Buraya kadar zamanlayıcı birimini nasıl kullanacağımıza değindik. Buna göre ayarlamaları yapmamız gerekir. Bu ayarlamaları TCCR0A ve TCCR0B kontrol registerlerini kullanarak yapacağız.

TCCR0A-TCCR0B ve TIMER0_COMPA

 

Yukarıdaki inceleme sonucunda CTC modu kullanacağız, Prescaler değeri “64” olacak ve OCR0A Kesmesini kullanacağız. CTC modu için yukarıdaki tabloya göre WGM00,WGM01 ve WGM02 isimli bitlerinden WGM01 “1” diğerleri”0″ olacaktır. Bilgi sayfasına göre WGM01 TCCR0A registerinin 1 numaralı bitidir. TCCR0A|=(1<<WGM01); şeklinde ayarlanır.

Prescaler değerini “64” yapmak için yukarıdaki tabloya göre CS00, CS01 ve CS02 isimli bitlerinden CS00 ve CS01 bitlerini “1” yapacağız. CS00 ve CS01 bitleri TCCR0B kontrol registerinin “0” ve “1” numaralı bitleridir. TCCR0B|=(1<<CS00)|(1<<CS01); şeklinde ayarlanır.

Kesme için bilgi sayfasına göre TIMSK0 registeri ayarlanmalıdır. Bu registerin “1” numaralı biti OCIE0A isimli bit “1” yapılmalıdır. TIMSK0|=(1<<OCIE0A); şeklinde ayarlanır. Kesme bu şekilde ayarlandı, kesmenin oluşması için TCNT0ve OCR0A eşitliği gereklidir. Yukarıda “250” saat darbesinin 1ms de gerçekleşeceğini bulduk. Eşleşme sonrası TCNT0 “0” olacağından “0” dahil “250” darbe saymalıyız. Buna göre OCR0A değeri “249” almamız gerekir. OCR0A=249; şeklinde ayalarız.

Bu işlemleri main() içinde yaptığımızda taşınabilir olmaz. Farklı bir çalışma yapmak istersek karıştırma ihtimali olabilir. Bunun yerine bir fonksiyon içinde tanımlamak daha doğru olur. Kesme oluşunca yani her 1ms de bir yapılacak olanları da kesme rutini içine yazacağız. Kesme oluşunca belirlediğimiz değişkeni artıracağız.

 

Milisaniye()

Arduino millis(); fonksiyonu TCNT0 taşma oluşunca kesme oluşturur bu da fazladan “6” saat darbesi hata ve 41,6 ms de bir düzeltme ihtiyacı çıkartır. Arduinoda  bunu PWM için böyle yapılmıştır aksi halde zamanlayıcı PWM ayarlandığında millis(); çalışmayacaktır. Bu kısa açıklama sonrası millis fonksiyonuyla aynı işi yapacak olan milisaniye fonksiyonuna geçelim.

Kesme oluşunca artan değişkenimizi ana programda istediğimiz an çağırıp işlem yapamayız. Bunu yapmak için başka bir değişkene eşitleyeceğiz. Yine kesme içi değişkenimiz sürekli değişeceğinden bu eşitleme öncesi kesmeleri iptal edeceğiz. Kesmeleri öylece kaldırmak faklı kesmeler ayarlıysa sorun çıkartacaktır.

 

 

Bunu önlemek için SREG registerini kopyalayıp kesmeleri kaldırmalı ve yine SREG yazılmalıdır. Bunu yapan hazır makro (ATOMIC_RESTORESTATE)olduğundan makro kullanacağım. Bu makro ve benzeri kütüphanelere buradan ulaşabilirsiniz. Artık kesmeleri durdurup güven içinde aktif edebildiğimize göre istediğimiz an sayaç (zaman0) değerini öğrenebiliriz.  Sayaç 1ms de bir artacağından 1000ms de 1sn hesabına göre istediğimiz işlemi yapabiliriz. Örnek olarak 1sn gecikme istediğimizde yapmamız gereken sayacın önceki değeriyle 1000ms sonraki değerinin farkını kullanmamız gerekir. Şimdiki değeri daha büyük olacağından bunu şu şekilde yazarız. Şimdiki değer-önceki değer >=1000 ise 1sn geçmiş demektir. Bir önemli nokta bu sayaç değeri “4.294.967.295” sonrası sıfırlanır. Önceki ve şimdiki değerleri karşılaştıracağımız için bu hataya neden olabilir. Bunun için ya sayaç değeri sıfırlanmalı ya da örnek olarak (uint32_t)(simdiki-onceki) şeklinde imzasız değişkenler ile hesap yapılmalıdır.

Artık 1ms de bir oluşan kesme, bu kesme ile artan bir sayaç ve ana program içinde bu sayaç değerini kopyalamaya yarayacak fonksiyon için gerekli bilgileri gözden geçirdik buna göre milisaniye fonksiyonu ile 1sn de bir artarak bekleme (delay) kullanmadan led yakmamıza olanak veren “if” şartını yazalım. Kod içinde her dönüşüm yaptığımızda bu yoklamayı yapmak zorunda kalacağız. Bunun yerine bir fonksiyon halinde çözmek doğru olacaktır. Sürekli bir kodu tekrar etmemiz gerekiyorsa bunu fonksiyon olarak düzenlemek doğru olur.

 

Port Kontrol

Atmega328P de D portu (Arduino D0-D7  arası pinler) üstünde işlem yapacağım. Bu pinlerin çıkış ayarı için DDRD registerinde istenen bitleri “1” yapacağız. Tüm port için 0B11111111 veya 0xFF yazarak DDRD|=0B11111111; şeklinde ayarlarız. PORTD registerinde hangi biti “1” yaparsak o bitin gösterdiği pin “1” (HIGH) çıkış verecektir. (“0” LOW) Bunun için örnek olarak PORTD|=0B11110000; şeklinde ayarlarsak ilk 4 pin (D0-D3) sönükken son 4 pin (D4-D7) led yanacaktır. İstenen ledin yanıp sönmesi için XOR (^) operatörü ile toggle  PORTD^=(1<<PORTD7); kullanabiliriz. Led “0” yapmak için AND,NOT (ve ,&-değil,~)(PORTD&=~(1<<PORTD7); ) ve “1” için OR (veya, |)(PORTD|=(1<<PORTD7);) operatörleriyle işlem yapabiliriz.

Port üstündeki ledleri sırayla yakmak için left shift (<<) ve right shift (>>) operatörleriyle sağa veya sol kaydırma kullanabiliriz. PORTD=(PORTD<<1); şeklinde kullanılır. Bu yazım ile bu PORTD<<=1; aynı şeydir. Burada yapılan  0000 0001 şeklinde olan PORTD bit değerlerini her adımda bir bit 0000 0010, 0000 0100 şeklinde sola kaydırılır. PORTD>>=1;  şeklinde de sağa kaydırılır.

Port çıkışını dizileri kullanarak da yönetebiliriz. Bunun için oluşturacağımız 8 bitlik dizinin elemanlarını PORTD registerine eşitleyeceğiz. Bu eşitleme işleminin yine sayaç değerini kullanarak yapacağız. Sayaç değeri belirlediğimiz süre (1000ms) doldukça değişecek böylece istediğimiz beklemeyi yaparak çıkışı düzenleyeceğiz. dizi[0] dediğimizde dizinin ilk elemanı dizi[1] ikinci elemanı işaret eder. Bu şekilde dizinin tüm elemanlarını seçebiliriz. Buraya kadar olan kısım aşağıdaki halde çalışır durumdadır.

 

Fonksiyonlar

Ledlerin farklı düzenlerde yanması için sayaçla port çıkışını belirli koşullara bağladık. Gördüğünüz gibi her seferinde sorgu yapmak zorunda kaldık. Koşul içinde de fazladan sorgu yaptık. Tüm bunlara tekrar tekrar yapmamak için fonksiyon oluşturmalıyız. Bekleme isminde bir fonksiyon oluşturacağım. Yukarıdaki koşul ile aynı sorguyu yapan ve değer döndüren bir fonksiyon olacak. Parametre ile belirlenen süre şartı sağlanmışa “1” sağlanmamışsa “0” döndürecek. While döngüsü içinde bu fonksiyonu sorgulayarak işlem yapacağız.

Bekleme fonksiyonu ile ledlerin nasıl yanacağına karar vereceğimiz koşulları sadeleştirerek bir fonksiyon oluşturacağız. Önceki örnekte bir koşuldan diğerine geçmemesi için büyük-küçük kontrolü yaptık. Bu örnekte sadece küçüktür koşulu kontrol edeceğiz. Ayrı bir fonksiyon olduğu için işi biten koşul sonrası fonksiyondan (return) çıkabileceğiz. Bunu daha önce kullanamazdık while sonsuz döngüsünden çıkardık.
Birçok kişi Arduino eğitimi için sürekli aynı şeyleri yapıyor, anlatıyor ve yazıyor. Bir sürü delay() alt alt yığılıp eğitim veriliyor. Bir amatör olarak bundan rahatsız oluyorum ve bir adım öteye taşımaya çalışıyorum. Profesyonellerin de bu işe el atıp daha ileri taşımaları bilmediğimiz yöntemleri bize öğretmesini bekliyorum. AVR için son halli bu şekilde.

Not: kod içine buton kontrolü koyup ledlerden bağımsız işlem yapabilirsiniz.

Bu yazı Haluk ŞİMŞEK tarafından yazılmıştır.

 

 

 

 

 

 

 

 

 

 

You may also like...

Bir cevap yazın

E-posta hesabınız yayımlanmayacak.