AVR Dış Kesme Kullanımı




Arduino ile tanışma sonrasında C ile Avr öğrenmeye başlamamın nedenlerinden bir tanesi (interrupt) kesmelerdir. Led yakma ile başlayan serüven buton ilavesiyle ilerledi. Delay süresi uzadıkça ya da farklı döngüler kullandıkça, butona bastığınız halde denetleyicinin cevap vermediğini görmüşsünüzdür. Daha önce kısaca değindiğim kesmeler bu sorunu da ortadan kaldırıyor. Ben çok detaya inmeden kullanımına dönük bilgi vermeye çalışacağım. Unutmayın ben bir amatörüm ve öğrendiklerimi yine benim gibi amatörler için paylaşıyorum. Asla bu işin eğitimini almış insanlara yönelik olduğunu söyleyemem, anlatım bu kişilere basit gelebilir. Amacım bu konuda araştırma yapan, öğrenmek isteyen insanlara kolaylık sağlamak.


Dış Kesme
Atmega328p datasheet'i incelersek  dış kesme (INT) ve pin değişiklik kesmesi (PCINT) olduğunu görürüz. PCINT kesmesinde ayarlanan pin "1" ya da "0" olduğunda kesme oluşturur. Pull-up ya da pull-down yaparak bu kesmeleri de yükselen ya da düşen kenar olarak kullanabiliriz. INT kesmesi  pin değişikliği, yükselen kenar "1" ve düşen kenar "0" durumuna göre ayarlanabilen kesme oluşturur. Aşağıda dış kesme için kullanılan pinleri gösteren şema bulunmaktadır. 


Gördüğünüz gibi PD2-INT0 ve PD3-INT1 kesmesi için kullanılıyor. 


EIMSK|=(1<<INT0);//pd2 kesme aktif
EIMSK|=(1<<INT1);//pd3 kesme aktif
EICRA|=(1<<ISC00)|(1<<ISC01);//pd2 yükselen kenar kesme oluşur
EICRA|=(1<<ISC10)|(1<<ISC11);//pd3 yükselen kenar kesme oluşur
 Gerekli ayarlamalar ve kesme başlatma bu şekilde yapılmaktadır. EICRA yazmacında ISC0(1:0)-ISC1(1:0) bitleri değiştirilerek kesme durumu ayarlanabilir. Bu konu için detaylı anlatım için bağlantıyı yukarıda paylaştım. Yapacağımız uygulamada INT0 yani PORTD2 pinini kullanacağım. Dahili pull-up dirençlerini devreye alarak kesme başlatma fonksiyonu oluşturuyorum. Bu işlemi Main fonksiyonu içinde yapabilirsiniz, ben bu şekilde fonksiyon olarak ayarlamanın daha doğru olduğunu düşünüyorum.

void pin_kesme(){
       DDRD&=~(1<<PORTD2);//pd2 giriş yapıldı
       PORTD|=(1<<PORTD2);//pd2 dahili pull-up
       EIMSK|=(1<<INT0);//pd2    
       EICRA|=(1<<ISC01);//pd2 düşen kenar    
       sei(); //tüm kesmeler açıldı}
Buton İle Kesme Kullanımı
Bu basit uygulama kesmenin nasıl işlediğini ve buton kullanırken karşılaşacağımız sorunları görmek için yeterli olacaktır. Öncelikle uygulamada bir butona basıldığında kesme rutini gerçekleşmektedir. Kesme oluştuğunu anlamak için led kullanacağız. Kesme için tek led yeterli olurken ark (bounce) oluşumunu görmek için ikinci bir led daha ekledim. Gerekli kütüphanelerden sonra kolaylık olması açısından bazı makrolar oluşturdum. Bu tanımların detayına inmiyorum. Buton dahili pull-up yapıldığı için Gnd ile PD2 arasına bağlanacak, basıldığında PD2 "0" yapılacak. Ledler sırasıyla PD3, PD4 ve PD5 ile bağlanacak (220ohm dirençle). Bu kadar basit bir bağlantı sonrası kodlar aşağıda.

/*
 * buton_kesme.c
 *
 * Created: 14.04.2019 16:15:48
 * Author : haluk
 */

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

#define DONGU_LED PORTD3
#define KESME_LED1 PORTD4
#define KESME_LED2 PORTD5
#define LED_PORT PORTD
#define LED_OUT DDRD|=(1<<DONGU_LED)|(1<<KESME_LED1)|(1<<KESME_LED2)
#define DONGU_LED_HIGH LED_PORT|=(1<<DONGU_LED)
#define DONGU_LED_LOW LED_PORT&=~(1<<DONGU_LED)
#define KESME_LED1_HIGH LED_PORT|=(1<<KESME_LED1)
#define KESME_LED1_LOW LED_PORT&=~(1<<KESME_LED1)
#define KESME_LED2_HIGH LED_PORT|=(1<<KESME_LED2)
#define KESME_LED2_LOW LED_PORT&=~(1<<KESME_LED2)

void pin_kesme(){
       DDRD&=~(1<<PORTD2);//pd2 giriş yapıldı
       PORTD|=(1<<PORTD2);//pd2 dahili pull-up
       EIMSK|=(1<<INT0);//pd2    
       EICRA|=(1<<ISC01);//pd2 düşen kenar    
       sei(); //tüm kesmeler açıldı
}
ISR (INT0_vect){
       DONGU_LED_LOW;
       KESME_LED1_HIGH;
       _delay_ms(100);     //ledin yandığını görebilmek için eklendi
       KESME_LED1_LOW;
       KESME_LED2_HIGH;
       _delay_ms(100);//ledin yandığını görebilmek için eklendi
       KESME_LED2_LOW;           
}
int main(void)
{
       LED_OUT;
       pin_kesme();
    while (1)    {
             DONGU_LED_HIGH;                  
       }
}

Yukarıdaki kodu yüklediğinizde teorik olarak butona basınca kesme oluşacak denetleyici tüm işini bırakacak (döngü led sönecek) kesme rutini içinde yer alan işlemler yapılacak (kesme led1 yanacak sonra sönecek kesme led2 yanacak ve sönecek) sonrasında kaldığı yerden (döngü led yanacak) devam edecek. Ama bu böyle olmuyor ne yazık ki butona basınca ark oluşmakta ve arka arkaya PD2 "0" ve "1" olmaktadır. Videoda gördüğünüz gibi basınca ve bırakınca ark oluşmaktadır. Bir kesme oluşunca kesmeler devre dışı bırakılır yeni bir kesme oluşmaz ama kesme sıraya alınır. Bu nedenle ikinci kesme oluşmuş gibi döngü ledi yanmadan kesme led1 ve led2 tekrar yanmaktadır.


Kesme içine eklenecek bekleme, kesmenin sıraya alınması nedeniyle işe yaramayacaktır. Aşağıda bu denemeyi görebilirsiniz. Bekleme süresi 1sn yapıldı ama bir değişiklik olmadı. Butona basarken ve bırakırken ark oluştu ikinci bir kesme gerçekleşti.


ISR (INT0_vect){
       DONGU_LED_LOW;
       KESME_LED1_HIGH;
       _delay_ms(1000); //ark önlemek için artırıldı 
       KESME_LED1_LOW;
       KESME_LED2_HIGH;
       _delay_ms(1000); //ark önlemek için artırıldı
       KESME_LED2_LOW;           
}



Oluşan bu arkın önlenmesine geçmeden bir konuya açıklık getirmek istiyorum. Kesmelerin sıraya alınması kesmelerin önceliğine göre değişir. Aşağıdaki sıralama aynı zamanda öncelik sıralamasıdır.


INT0, INT1 ve PCINT0 kesmelerini aynı anda gerçekleştirdiğinizde ana program durur, mevcut durumunu kaydeder ve sırasıyla INT0, INT1 ve PCINT0 kesmelerini yürütür sonra ana programda kaldığı yerden devam eder. Tekrar etmek gerekirse ark için bekleme kullanılması bu sıraya alınma işlemi nedeniyle işe yaramaz. Bekleme ark oluşumunu 2 adet ile sınırlar :) Aşağıda kesme rutini içindeki bir sayaç değişkeninin seri  ekran görüntüleri bulunmakta. Sonuçlara bakarsanız beklemenin 2 şer artış yaptığını göreceksiniz. Hiç bekleme kullanılmadan aşağıda paylaşacağım çözüm ile daha düzgün bir sonuç alınmaktadır.
Buton Arkı Önleme (Debounce)

Bu konuda birçok çözüm şekli gördüm bazıları donanımla bazıları yazılımla çözülmüş. Eğer çok hassas bir işlem yapıyorsanız "schmitt trigger" konusunda araştırma yapabilirsiniz. Bu uygulamada sadece 100ohm direnç ve 100nf kapasitör yeterli. Bu değerlerin nasıl bulunduğu konusu internette ayrıntılı şekilde yazılıdır. Devre şeması basit aşağıda görebilirsiniz.

Dahili pull-up dirençlerin bağlanması durumunda R1 ve Vcc bağlantısı yapılmayacak. Donanım çözümünü uyguladığımızda sonuç bu şekilde oluyor.



Kesme yürütülürken oluşacak kesmenin sıraya alınmasından bahsetmiştim. Ark konusu artık gördüğünüz gibi gözle görünür bir sorun çıkarmıyor. Bekleme sürelerini artırıp kesme oluşunca tekrar butona basacağım. Kesme içindeki bekleme sürelerinin arkı engellemediğini ve oluşan kesmenin sıraya alınması durumunu deneyerek görmüş olalım.


Donanım olarak çözüm bu şekilde, yazılım olarak çeşitli versiyonlar var. Ben kesme içinde buton halen basılı mı? Şeklinde bir sorgu ve bunun sonunda kesme içinde yapılacak işlemin gerçekleştirilmesi yöntemini kullanıyorum.
ISR(INT0_vect){
       _delay_ms(50);// ark önlemez ama ark durumunda yanlış bayrak oluşmaz
       if (!(PIND&(1<<PIND2)))    {//PD2 "0" ise
             bayrak=1;
       }
}

Buraya kadar olan kısım dış kesme ile buton kullanımı ve ark önlenmesini içeriyordu. Dış kesme kullanılarak yapılabilecek bir uygulamaya daha değinmek istiyorum. Birçok yerde uzun uzun anlatılan rotary encoder kullanımı.
Rotary Encoder Kullanımı
Rotary encoder tanımı yapmayacağım, kendi projemde kullandığım şeyleri araştırıp öğrenmeye çalışıyorum ve elimden geldiğince öğrendiklerimi paylaşıyorum. Kullanımını anlatacağım basit encoderler, gelişmiş modelleri için birazdan söyleyeceklerim geçerli olmayabilir. Birçok yerde araştırma yaptım çok farklı tarz uygulamalar gördüm. Gray kodlar, önceki durum kontrolleri, timer ile yoklamalar ve kesme üstüne kesmeler. Bir uzman değilim ama bu uygulamaların çoğu bana göre gereksiz ya da yetersiz. Özellikle Arduino için yapılan örneklerin hatalı olduğunu düşünüyorum. Bir programda her şeyi yoklama yöntemiyle kontrol ederseniz işlemci başka bir işle meşgul olduğunda değişiklikleri kaçırırsınız. Ayrıca bu yoklama yöntemi işlemci israfıdır. Encoder ile yapmak istediğimiz şey neyse onu kullandığımızda ne eksik ne de fazla değer dönmesini isteriz bunun kolay ve doğru yöntemi kesme kullanmaktır.



Bu konuda araştırma yaptığınızda yukarıda yaptığım şemaya benzer şemalar göreceksiniz. Encoder modül haldeyse eğer modül üstünde pull-up dirençler bulunur. Gnd ortak uçtur ve çevirdiğinizde dönüş yönüne göre A ve B uçlarını sırasıyla "0" yapar. A ve B uçlarına led bağlarsanız ve yanan ledlere "1" dersek çevirdiğinizde şöyle bir dizilim görürsünüz.  Saat yönünde "00-10-11-01-00" ve saatin tersi yönünde "00-01-11-10-00" ikilik sistemdeki "1" ve "0" bunları onluk sistemde yazarsak. "0-2-3-1-0" ve "0-1-3-2-0" şekline gelir buna gray kod deniyor. Encoderi bu koda göre yorumlayıp kullananlar var.

Ben bu şekilde kullanmayacağım. Yukarıdaki şemada saat yönünde A "0" olduğunda B'nin "1" ve tersi yönünde A "0" olduğunda B'nin "0" olduğunu görebilirsiniz. A çıkışını dış kesme pinine bağlayıp düşen kenar ayarlarsam encoderin yönünü bulmak için tek yapmam gereken B'nin durumunu kontrol etmek olacaktır. Bunu yapmadan bir uyarı yapmam gerekiyor. Aynı butonda olduğu gibi burada da çevirme esnasında ark oluşmaktadır. Önlemek için aşağıdaki bağlantıyı yapmamız gerekiyor.

Buraya kadar okuduysanız son bir paylaşım daha yapıp örnek kodu paylaşacağım. Kesme içinde uzun işlemler yapmayın. Başka fonksiyonları çağırmayın. Kesme gerektiren işlemler yaptırmayın çalışmayacaktır. Mümkün olduğunca çabuk ana programa dönün oluşabilecek kesmelerin beklemesine neden olmamak önemli (Ram yığılma konusunu araştırabilirsiniz.) Kesme içinde "volatile" değişken kullanın.


/*
 * rotary_kesme.c
 *
 * Created: 13.04.2019 18:47:17
 * Author : haluk
 */
#define F_CPU 16000000ul
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include <stdio.h>

volatile int8_t rotary=0;
volatile uint8_t bayrak=0;
char crotary[5];
void uart_basla(uint32_t baud){
       uint16_t baudRate=F_CPU/baud/16-1;
       UBRR0H=(baudRate>>8);
       UBRR0L=baudRate;
       UCSR0B|=(1<<RXEN0)|(1<<TXEN0);
       UCSR0C|=(1<<UCSZ01)|(1<<UCSZ00);
}
uint8_t uart_oku(){
       while(!(UCSR0A & (1<<RXC0)));
       return UDR0;
}
void uart_gonder(uint8_t uData){
       while(!(UCSR0A & (1<<UDRE0)));
       UDR0=uData;
}
void uart_dizi(char*str){
       uint8_t i=0;
       while(str[i]){
             uart_gonder (str[i]);
             i++;
       }
}
ISR(INT0_vect){    
       if (!(PIND&(1<<PIND2)))    {//PD2 "0" ise
             if (!(PIND&(1<<PIND4))){ //PD4 "0" ise
                    rotary++;
             }else{
                    rotary--;}
             bayrak=1;
       }           
}
int main(void)
{
       DDRD&=~(1<<PIND4)|(1<<PIND2);    
       EIMSK|= (1<<INT0);
       EICRA|= (1<<ISC01);
       uart_basla(19200);
    sei();
       while (1)
    {       
             if (bayrak==1){
                    sprintf(crotary, "%d",rotary);
                    uart_dizi(crotary);
                    uart_gonder('\n');
                    bayrak=0;                 
             }
    }
}