Gömülü sistemlerde giriş/çıkış olaylarının nasıl yönetildiği ve Interrupt-based yöntem hakkında detaylı notlarımı bu yazımda paylaşıyorum. Mikrodenetleyicilerde genel olarak giriş/çıkış olaylarının yönetimi 2 şekilde yapılır.
1 — Polling-based I/O : CPU’nun bir pini veya durumu sürekli kontrol etmesidir. Genelde while(1) döngüsü içinde sürekli if ile kontrol edilerek çalışan bir yöntemdir. Olay gelmese bile her döngüde bu kontroller yapılır, bu da başka olaylara verilen tepki süresinin garanti edilememesi problemine yol açar.
2 — Interrupt-Based I/O : Donanımın CPU’ ya bildirim vermesi ile ana kod durdurularak belirtilen interrupt çalıştırılması ve ardından geri ana koda dönülerek programın çalışmaya devam etmesi olarak özetlenebilir. Yani loop içinde sürekli olarak butona basıldığını kontrol etmek yerine sadece ilgili butona basıldığında ilgili kodları çalıştırmaktır.
Interrupt-based yöntemini kullanmak, zaman-kritik(time-critical) ve deterministik tepki gerektiren işlemler için kullanılması gereken bir yöntemdir.
Mesela bir LED blink projesi düşünelim, main içindeki while(1) döngüsü içinde LED 1 sn boyunca blocking Delay() kullanılarak yakılsın ve sonra 1 sn boyunca söndürülsün. Bu senaryoda eğer yine while(1) içinde bir butona basılıp basılmadığının kontrolü de yapılıyorsa bu kontrol tam olarak anlık çalışmaz. Kullanıcı butona basıldığı an CPU Delay() satırlarında olabilir, bu durumda kullanıcının butona basması aksiyonu fark edilemez. Doğru anda butonun kontrolünü okumanın tek ihtimali CPU’nun execute sırasında okuduğu satır buton kontrolü satırına gelindiği anda butona basılması ile sağlanabilir ki bu da çok zor bir ihtimaldir ve yanlıştır. Kullanıcı deneyimini kötü etkiler, gecikmelere sebep olur.
Bunun yerine interrupt kullanımı ile butona basıldığı an CPU elindeki işi bırakır ve hangi interrupt tetiklenmişse o işlemleri hemen yapar ve tekrar yaptığı işe geri döner. Bu da anlık bir tepki sağlar, gerçek zamanlı olarak kontrol sağlar. CPU gereksiz kontroller yapmaz, sadece gerektiğinde istenenler yapılır.
Interrupt Nasıl Çalışır ?
Normal execution flow devam ederken her instruction tamamlanmasının sonunda interrupt olup olmadığının kontrolü yapılır. Eğer interrupt varsa o anki execution state(Program Counter) Stack’e kaydedilir ki program interrupt sonrası kaldığı yerden devam edebilsin, sonra bu interrupt adresine gidilir ve ilgili kod parçası execute edilir. Ardından tekrar kaydedilen execution state’den kod çalıştırılmaya devam eder.
PIC16F887 için interrupt’ların çalışabilmesi için bazı register’ların set edilmesi gerekir. Bu register’lar haricinde her bir donanımın kendine ait bir enable ve flag register’ları vardır. Bu görsel interrupt’ların nasıl kontrol edileceğini ve erişileceğini gösteriyor.

CPU doğrudan bir register’a erişemez , ilk önce GIE(global interrupt enable) kapısının set edilmesi gerekir. Ardından bazı register’lar direkt kendi enable flag’leri set edilerek erişebilir, bazılarına erişebilmek için ise ek olarak birde PEIE(peripheral interrupt enable) kapısının da set edilmesi gerekir. Yani CPU bir interrupt’a erişebilmesi için önündeki tüm kapıların set edilmiş olması gerekmektedir, aksi halde ilgili register flag’i set edilse bile interrupt mekanizması çalışmaz.
Default olarak tüm bu kapılar 0 ‘dır.
ISR(void) Nedir ?
Birçok MCU için Vector-Interrupt Handling yapısı vardır. Program Counter sırasında hangi interrupt türü gelmişse bu interrupt’ın hangi kategorideki(hangi ISR()deki) interrupt olduğu bilgisini tutan tablo indexi bilgiside alınır. Bu index ile Interrupt — Vector Table içinde hangi tür interrupt(ISR()) olduğu öğrenilir ve bu öğrenilen interrupt adresine gidilir ve execute edilir.
PIC ise Non-Vector Interrupt Handling yapısı ile çalışır. Yani tek bir interrupt türü(ISR() ) olduğu için hangi interrupt’ın tetikleneceğini bulmak için bu interrupt içinde if statement’lar ile bu interrupt’ın flag’i kontrol edilir.
Örnek Interrupt Kullanımı(Timer0)
// Timer0 için configuration
void init_timer0(void){
INTCONbits.GIE = 1; // Global interrupt enable yap
INTCONbits.T0IE = 1; // Timer0 interrupt enable yap
OPTION_REGbits.T0CS = 0; // clock source seçimi - 0 : dahili clock, 1: harici clock
OPTION_REGbits.PSA = 0; // prescaler hangisi için kullanılacağı - 0: Timer0, 1: Watchdog
OPTION_REGbits.PS = 0b001; // Prescaler oranı seçimi - 1:4 (sayacın her bir flag için tam bir ms sayması için)
TMR0 = 0; // Timer0 sayacının başlangıç değeri
INTCONbits.T0IF = 0; // Timer0 interrupt flag i reset et
}Bu Timer0 ayarlaması için oluşturulan fonksiyon main() içerisinde üst satırlarında çağrılması gerekir.
// FOSC/4 için clock sinyali frekansı , pic için de aynı değer seçilmelidir.
#define _XTAL_FREQ 4000000
volatile unsigned char tmr1_overflow_count = 0; // 1 sn için overflow sayacı‚
volatile unsigned int timer1 = 0; // global sn sayacı
volatile unsigned int timer0 = 0; // global ms sayacı
void __interrupt() ISR(void) // __interrupt özel bir kelimedir, compiler tarafından anlaşılan özel bir komuttur.
{
// Timer0 Interrupt Control
if(INTCONbits.T0IF && INTCONbits.T0IE){ // Timer0 overflow olup flag set edilmiş mi - aktif mi kontrol et
INTCONbits.T0IF = 0; // flag i tekrar resetle
// Timer0 8 bit(256 değer) tutabildiği için ve 1000/4 = 250 olduğundan tam 1 ms lik ölçüm için clock sayacını 6 dan başlat
TMR0 = 6;
timer0++; // ms sayacını arttır.
}
// Timer1 Interrupt Control
if (TMR1IF && TMR1IE) // Timer1 overflow olup flag set edilmiş mi - aktif mi kontrol et
{
TMR1IF = 0; // Flag reset et, Tekrar flag i temizle ki diğer flag geldiğini kontrol edebilelim
// 1:8 oranı kullanıldığı için 2 kez overflow gerekiyor, tam olarak 1 sn elde edebilmek için bu değerden başlatıldı
TMR1H = 3036 >> 8; // Sayacın tam 1 sn olarak sayabilmesi için başlangıç değeri set etme
TMR1L = 3036 & 0xFF;
tmr1_overflow_count++; // overflow sayacı , 2 tanesi 1 sn yapıyor - prescaler oranı ile tekrar ayarlanabilir
if (tmr1_overflow_count >= 2) // 2 sn de bir timer1 sayacı artsın, her artışta overflow sayacı da sıfırlansın
{
tmr1_overflow_count = 0;
timer1++; // SADECE HABER VER // Ana sn sayacı
}
}
// Other interrupt Controls here
}Bu kodlar asıl interrupt kodlarıdır. Main() üzerinde kullanılarak global olmaları sağlanır ve bu sayede sn ve ms türündeki sayaçlara her yerden erişilebilir. Burada da görüldüğü gibi tek bir __interrupt() fonksiyonu bulunuyor, yani tek bir interrupt giriş yeri bulunur ve tüm interrupt’lar aynı adrese giderler. Tüm interrupt türleri de bu fonksiyon içerisinde if statement’ları ile flag’leri kontrol edilerek ilgili register için işlemler yapılıyor.
Burada __interrupt() içinde de LED veya Buton işlemleri yapılabilir fakat bu işlemler gecikmelere yol açabileceği için bu __interrupt() fonksiyonu içerisinde sadece temel işlemler yapılmalı ve global değişkenlere aktarılmalıdır, sonrasında bu değişkenler üzerinden asıl işlemler yapılmalıdır.
// timer0 ile buton debounce kontrolü ve led blink
// kontrol değişkenleri
unsigned int timer0_copy = 0; // butona basıldığı andan itibaren geçen sürenin tutulacağı değişken
unsigned int timer1_copy = 0;
char button_pressed = 0; // butona basılıp basılmadığı flag i
char button_debounced = 0; // buton bırakıldığının flag i
while(1){
if(RC6 == 1 && button_pressed == 0){
button_pressed = 1;
timer0_copy = timer0; // basılma zamanını kaydet, debounce beklemesi için
}
if(RC6 == 1 && button_pressed == 1 && button_debounced == 0 && (timer0 - timer0_copy) > 20){
button_debounced = 1;
timer1_copy = timer1;
}
if(RC6 == 0 && button_debounced == 1){
RB2 = !RB2;
button_debounced = 0;
button_pressed = 0;
}
}Main() içerisinde de yukarıdaki gibi bir kullanım ile global olan ve __interrupt() içerisinde update edilen değişkenler kullanılabilir.
Daha detaylı anlatım için : https://youtu.be/LGrd-4W1EYc?si=HocQcfjOSy-u7iSe
Kodlar ve devre şeması için : https://github.com/Mehmetdora/PIC16F887_Embedded_C_Projects
