Interrupt etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster
Interrupt etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster

10 Aralık 2020 Perşembe

Keyboard Scan Code ve Keyboard Handler


Merhaba. Bu yazının konusu klavyenin nasıl çalıştığı. Bu kez başlık İngilizce çünkü internette "klavye tarama kodları" diye arattığımda yalnız bir kaynak bulunduğu için terimleri kafama göre Türkçeleştirmenin doğru olmadığını düşündüm. Bunun doğruluğu/yanlışlığına dair ayrı bir yazı çıkar, o yüzden bir kenara bırakıyorum.


Keyboard Scan Code
Klavyede bir tuşa basıldığında bilgisayara 'N' tuşuna basıldı '5' tuşuna basıldı gibi bir bilgi iletilmez. Zaten öyle olsaydı Türkçe, İngilizce, Rusça vs. klavye arasında geçiş yapamazdık. Bunun yerine klavye bilgisayara 1. tuşa basıldı, 8. tuşa basıldı gibi bir sıra numarası gönderir. Bu sıra numaralarına tuşların scan kodları denir. Kodlar, yalnızca tuşun (İngilizce) klavyedeki sıra numarasıdır. Örn. ESC: 1, 1'den Backspace'e kadar olan üst sıra 2 .. 14, Tab ile başlayıp Enter ile sonlanan ikinci sıra 15 .. 28 vb. Türkçe klavyedeki " (çift tırnak) karakteri üst sırada olsa da scan kodu 41'dir çünkü standart AT klavyede bu tuş orta sırada Enter'ın yanında bulunur. 

ESC'in AT klavyenin solunda olmasına rağmen scan kodunun 1 olması, aslında öncülü XT klavyede birinci sırada olmasından kaynaklanır. Dikkat edilirse iki klavyede de F11 ve F12 tuşları bulunmuyor. Buna sonradan da değineceğim.


Klavyenin Gelişimi
Bugün kullandığımız klavyenin standartları 1981'de piyasaya sürülen IBM PC'nin klavyesine (Model F) kadar gidiyor. Mart 1983'te piyasaya sürülen IBM PC XT'de de aynı klavye bulunuyor. Bu modellerde özelleşmiş bir klavye denetleyicisi (keyboard controller) bulunmuyor, dolayısıyla klavyeler programlanabilir değil. Programlanabilirlikten kasıt donanımın kendini test etmesi, resetlemesi veya tuş tekrarlama frekansı gibi parametlerin ayarlanması. Bu klavye yalnızca, anakarttaki 8048 entegresine basılan tuşun scan kodunu iletiyor.

1984'te piyasaya sürülen PC AT'de kullanılan klavyeyle (yukarıda görseli olan) birlikte IBM, önceden kullandığı kodları değiştiriyor ve anakarta 8042 klavye denetleyicisini ekliyor. Bu denetleyici yalnız klavyeden sorumlu ve klavyeye yukarıda saydığım programlanabilir özellikler ekleniyor. Ayrıca geriye doğru uyumluluk için klavyeye eklenen bir anahtarla klavye XT protokolünde çalıştırılabiliyor. SysRq tuşu da bu modelde ekleniyor.

Nisan 1986'da standart 101 tuşlu Model M klavye piyasaya sürülüyor. Insert, Home gibi tuşlarla F11, F12 tuşları ekleniyor. 


IBM Model M klavye (Kaynak: https://en.wikipedia.org/wiki/File:IBM_Model_M.png)

Ve son olarak Microsoft, Windows 95'le birlikte klavyelere 3 tuş daha ekliyor: Sağ ve sol Windows tuşuyla, menü tuşu.


Basma Kodları, Bırakma Kodları, Farklı Scan Kod Setleri
Klavye, bilgisayarla 60h ve 64h portu üzerinden haberleşir. 60h verinin, 64h denetleyici komutlarının iletildiği porttur. Klavyede bir tuşa basıldığında, anakarttaki denetleyiciye (8042) tuşun scan kodunu gönderir. Bu koda basma kodu (make code) adı verilir. Tuş bırakıldığındaysa bırakma kodu (break code) gönderilir. Ancak üç farklı kod standardı bulunur. 

Not: 60h aynı zamanda PS/2 farenin de bağlandığı porttur.

Set 1 de denen birinci standart, PC XT'de bulunan yukarıda açıkladığım standarttır. Bu, uyumluluk modu olarak bırakılmıştır. PC AT ile ortaya ikinci standart (set 2) çıkar. 8042 denetleyicisi, set 2 kodlarını set 1'e dönüştürür ve işletim sistemine iletir ancak bu davranış değiştirilebilir. Son olarak set 3, Ekim 1983'te IBM PC 3270 ile çıkmış ve set 2 ile kısmen uyumlu kodlardır. Set 3 de set 2 gibi 8042 tarafından birinci kod setine çevrilir. Kısaca, donanımın ilettiği farklı set kodlar işletim sistemi tarafında tek bir set gibi görünür. Klavyenin veri pinine osiloskop bağladığımda gördüğüm sinyalle 60h portundan okuduğum değer aynı değildir (XT klavyem yoksa).

Set 1'deki basma kodlarına yukarıda kısaca değindim. Bırakma kodları, basma kodlarının 80h ile VEYA işleminden geçirilmesiyle elde edilir. Örn. basma kodu 01h olan ESC'in bırakma kodu 81h, basma kodu 1Ch olan Enter'ın bırakma kodu 9Ch'dır. Bu hesapla bir klavyede maksimum 127 (sıfırıncı kod yok) tuş olabileceği düşünülebilir. Fakat bazı tuşlar için klavye birden fazla scan kod üretir. Genişletilmiş scan kod (extended scan code) adı verilen kodlar 0E0h ile başlar. Örn. nümerik klavyedeki Enter ve '/' (bölü) tuşu genişletilmiş scan kod üretir. Bunlara basıldığında sırasıyla 0E0h 1Ch ile 0E0h 35h üretilir. Genişletilmiş scan kodlarda, bırakma kodu ikinci byte'ın 80h ile VEYA işlemine sokulmasıyla elde edilir. Yani 0E0h 0B5h veya 0E0h 9Ch gibi. Bu arada genişletilmiş kodların hemen hemen tamamı 0E0h ile başlasa da bütün bir 0EXh bloğu aslında bunlara ayrılmıştır ve pratikte az sayıda da olsa 0E1h ile başlayan kod dizisi vardır.

Bu kodlar linux'ta showkey -s ile görülebilir. Örn:


Kodları DOS'ta görüntülemek için basit bir kod yazdım. Öncelikle belirtmeliyim ki, VirtualBox genişletilmiş scan kod setini tanımıyor. Kodu vmware'de geliştirdim ve test ettim. Kod, DOSBox'ta da düzgün çalışıyor ama DOSBox'ta TurboC'de kodu yazarken bazı garip hatalar aldım (bu benden de kaynaklanıyor olabilir). Ancak kod DOSBox'ta vmware'den bile stabil çalışıyor.

Yukarıdaki ekran görüntüsünde sırayla Enter, F10, F11 ve nümerik klavyedeki '/' ile Enter'a bastım. Bu çıktıdakiler elbette birinci sete çevrilmiş kodlar. ESC'e basarak programı sonlandırdım. Kodda 64h portuna yazdığım iki denetleyici komutu var. 0ADh klavyeyi durduruyor, 0AEh tekrar aktifleştiriyor. Bu arada klavyenin tarihinden bahsederken XT klavyelerde on fonksiyon tuşu olduğunu, F11 ile F12'nin AT'lerle birlikte eklendiğini söylemiştim. İşte bu nedenle F1 .. F10 arası tuşların scan kodları 3Bh ile 44h arasıyken F11'in scan kodu 57h ve F12'ninki 58h'dır. Bu programın kaynak kodu aşağıda:

#include <stdio.h>

unsigned char readport()    {
    unsigned char r;
    asm    {
        mov     dx, 0x0064   // klavyeyi disable et
        mov     al, 0xAD
        out     dx, al
        push    dx

        mov     dx, 0x0060   // klavye portundan oku
        in      al, dx
        mov     r, al

        pop     dx
        mov     al, 0xAE     // klavyeyi enable et
        out     dx, al
    }
    return r;
}


int  main(int argc, char* argv[])    {
    unsigned char r, r0;

    for( ;; )    {
        asm { cli }     // interruptlari kapat
        r = readport();
        if((r ^ 0xE0) < 0x10)   {
            // eğer porttan extended kod okunuyorsa
            printf("--- Ext. Key: %02X  ", r);
            // bir byte daha oku:
            printf("Escaped Char: %02X  \n", r0 = readport());
        }
        else if (r0 != r)       {
            // önceki basilan tustan farkliysa scan kodunu yaz
            printf("%02X\n", r);
        }

        if(r == 0x01)   {       // ESC ile cik
            asm { sti }
            break;
        }

        r0 = r;
    }

    return 0;
}

Çalıştırılabilir dosya ve kaynak kod buradan indirilebilir. 

Set 2'de, basılan tuş bir byte'lık kodlarla temsil edilir. Bu sette ESC 76h, '1' 16h, '2' 1Eh şeklinde karışık gider. Kodların klavyede nasıl göründüğü aşağıdadır: 

Kaynak: Wikibooks

Bu görseldeki 0F0h ile başlayan kodlar, set 2'deki bırakma kodlarıdır. Anlaşılacağı gibi bırakma kodu, basma kodundan önce gelen 0F0h ile temsil edilir. ESC 76h ise bırakma kodu 0F0h 76h'dır. Genişletilmiş kodlar bu sette de 0E0h ile başlar. Örn. sol windows tuşunun basma kodu 0E0h 1Fh ve bırakma kodu 0E0h 0F0h 1Fh'dir. 0E0h, set 1'deki gibi bırakma kodundan önce gelir. 

Set 3'te ilginç şekilde genişletilmiş tuşlar bulunmaz. Bunların görüntüsüne bu bağlantıdan; bu setlerle ilgili ayrıntılı bilgi ve dönüşüm tablosuna bu bağlantıdan ulaşılabilir. 

8042 klavye denetleyicisi normalde klavyenin türünü bulur ve klavyeden gelen tarama kodlarını dönüştürür. Güncel işletim sistemlerinin hiçbiri artık birinci seti kullanmamaktadır. Örn. linux'ta atkbd.c klavye sürücüsünde atkbd_select_set( ... ) fonksiyonu 2 veya 3 değerini döndürür. Bu fonksiyonun döndürdüğü değer atkdb struct'ı içerisindeki set elemanına yazılır.

8042'nin gelen veriyi hangi sete dönüştüreceği ayarlanabilir. 60h portuna 0F0h, 0 yazılarak mevcut set sorgulanır. Birinci, ikinci ve üçüncü setler için sırayla 43h, 41h ve 3Fh değerleri döner. Eğer 60h portuna 0F0h'ten sonra 1, 2 veya 3 yazılırsa 8042, istenen kod setinde veri göndermeye başlar. Elbette bunu DOS'ta yapınca, klavye sürücüsü başka setle gelen kodları anlamlandırmaya çalışacak ve yeniden başlatılıncaya kadar sistem kullanılmaz duruma gelecektir. Bu arada Linux'ta showkey -s komutu geçici olarak klavyeyi raw moda geçirir.


Klavye ile İletişim
Klavyeler, IBM PC AT'lere kadar beş pinli DIN konektörle bağlanıyordu. Bu klavyelerden bulmak sanıyorum bit pazarı haricinde imkansız. IBM, 1987'de PS/2 konektörünü geliştirdi. Bunu hatırlayanlar olacaktır, altı pinli, klavye için mor, fare için yeşil renkli bir konektördü. Bunlardan hala çöpe atılmamışsa, kilerde veya depoda tek tük bulunabilir. Günümüzdeyse hemen hepsinin USB bağlantısı var. 

PS/2 ve DIN konektörün sinyalleri tamamen aynıdır. İkisinde de Vcc, GND'nin yanısıra CLK ve DATA pinleri vardır. DATA'daki veri, CLK sinyaliyle senkron olarak (çift yönlü seri senkron iletim) gider. Her tuş basımında klavye CLK'yla senkron şekilde scan kodlarını DATA pininden seri olarak sürer. 

Bu bilgi, bilgisayarda birşey yapmak için gerekli olmasa da Arduino veya başka geliştirme kartlarıyla klavyeyi kullanmak için lazım olabilir.


Bir Tuşa Basıldığında Neler Olur?
Klavyede bir tuşa basıldığında scan kodunun denetleyiciye gönderildiğinden yukarıda bahsettim. Peki işlemci seviyesinde neler olur, ona bakalım. 

Öncelikle klavye denetleyicisi, birincil 8259 kesme denetleyicisinin (IRQ Controller) on dokuzuncu pinine (IRQ 1 bacağına) ulaşarak kesme isteğini işlemciye bildirir. İşlemci 8086 modundaysa Int 9'u çağırır, korumalı modda PIC programlanarak başka bir kesme çağırılır. 

Int 9 kodunu normalde BIOS yükler. İşletim sistemi gerekirse buna wrapper yazar veya tamamen değiştirebilir. BIOS'un çalışması bittiğinde (boot öncesi) Int 09 vektörü F000:E987 adresindedir. Bu kesme klavyeyi anlık olarak kapatır ve 60h portundan scan kodunu okur. Bundan sonra Int 15h/4Fh çağırılır. Int 15/4Fh, normalde bir tuşun scan kodunun değiştirilmesi gerekiyorsa kullanılır (örn. Fn tuşu ile Ctrl'nin yerini değiştirmek)*. Sonraki adımlar basılan tuşa göre değişkenlik gösterir. 
 
Shift, CTRL ve Alt tuşları için BIOS veri alanında (BIOS Data Area - BDA) bir flag bulunur. Bu flag 0040h:0017h ve 0018h adreslerindedir. Bu adreste CapsLock, NumLock, Pause vb. tuşlar için de flaglar bulunur.

Kaynaklar: Ralf Brown Int. List ve lowlevel.eu


Benzeri bir flag daha 0040h:0096h ile 0097h adreslerinde bulunur: 
 
Kaynak: Ralf Brown Interrupt List

Eğer basılan tuş yukarıda adı geçen özel tuşlardan biriyse, int 9 gerekli bitleri ayarlar. Örn. sağ shift'e basıldığında 36h scan kodu okunur ve BIOS 0417h'daki 0. biti set eder. Bu tuş bırakıldığında 0B6h scan kodu okunur ve BIOS 0417h'daki 0. biti resetler. 

Özel amacı olmayan bir tuşa basıldığında yalnız bu tuşa mı basılıyor yoksa bir tuş kombinasyonuyla mı basılıyor, önemlidir. Tuş kombinasyonları yukarıdaki flag'larla anlaşılır. Int 9, bir tuşa basıldığında bunun ASCII kodunu hesaplamak için bir tablodan yararlanır. Örn. basılan tuş yalnız 'A' ise, okunan scan kod 1Eh ve buna karşılık gelen ASCII kodu 'a'ya karşılık gelen 61h'dır. Ancak Shift+A basıldığında yine 1Eh scan kodu okunmasına rağmen shift flag aktiftir (öncesinde shift scan kodu gelmektedir ve flag set edilmiştir) ve int 9 bunun ASCII kodunu 41h ('A') olarak yorumlar. Aynı şey, shift flag aktif değilken CapsLock açıksa da gerçekleşir. Eğer Ctrl+A basılıyorsa, ASCII kodu 01h olarak dönüştürülür. Dolayısıyla BIOS'daki tablo 128 scan kod için 128 satırdan oluşur. Tuşun kendi değeri, Shift ile değeri, CTRL ile değeri için ayrı ayrı sütunlar eklenir. 

Scan kod - ASCII dönüşümü, hemen hemen her tuş için yapılır ve basılan tuş klavye ara belleğinde (keyboard buffer) depolanır. Fonksiyon tuşları, Home, PgUp gibi tuşların ASCII kodu olmadığından ara bellekte ASCII kodları 0 olarak depolanır. Ctrl, Alt, NumLock gibi tuşlarsa depolanmaz. Klavye arabelleği BDA'da 1Eh offsetinde bulunur ve 32 byte uzunluktadır. Her bir tuş için bir scan kod ve bir ASCII kod depolandığından 16 tuşa kadar burada saklanır. Bu veri yapısı bir "ring buffer" (Türkçesi olmayan bir terim daha) olarak kurulmuştur. Ring buffer'ın başlangıç adresi 1Ah ve bitiş adresi 1Ch adresinde bulunur. Bu bir ring buffer olduğundan 1Ah'daki adres 1Ch'deki adresten büyük olabilir. Bir tuş buraya alınacağı zaman tuşun scan ve ASCII kodundan oluşan word, 1Ch'nin gösterdiği adrese yazılır ve 1Ch'deki gösterici 2 arttırılır. Bu arttırım sonucu oluşan değer, ring buffer'ın sonu olan 3Eh adresini aşıyorsa bu değerden 20h çıkartılır. O zaman gösterici tekrar 1Eh adresini göstermeye başlar. Sonradan basılan tuş bellekte, önce basılan tuştan daha küçük offsette yer alabilir. Eğer arttırım sonucunda 1Ch'deki değer 1Ah'daki değere eşit olacaksa klavye arabelleği taşmıştır: Kullanıcı 16 tuşa basmış, BIOS bunları sıraya almış ancak işletim sistemi bunları bellekten oku(ya)mamıştır. Bazı BIOS'lar bu durumda anakarttaki minik hoparlörden sesle kullanıcıyı uyarırlar. Her okuma sonucu 1Ah'daki değer iki eksiltilir.

Buraya kadar scan kod porttan okunmuş, ASCII koduna çevrilmiş ve arabelleğe yazılmıştır. Bundan sonra klavye tekrar aktifleştirilir ve kesmeden çıkılarak ana programa geri dönülür.

Yukarıda anlattığım adımları VirtualBox'ın kaynak kodundan derledim. VBoxBiosAlternative386.asm VBox BIOS'unun assembly kod dökümü. Dosyanın en sağ sütununda, komutun olduğu bellek adresi ve kaynak kodunun olduğu dosya var. Int 9 kesme adresi olan F000h:E987h, bu dosyanın 18 268. satırından başlıyor ve kaynak kodu orgs.asm dosyasının 974. satırı. Klavyenin kapatılması, int 15h/AH=4Fh çağrısı burada gerçekleşiyor ve genişletilmiş scan kodlar için flag'lar burada ayarlanıyor (1011. satır). Sonra assembly kodundan, keyboard.c dosyasındaki int09_function fonksiyonu (371. satır) çağırılıyor. Bu fonksiyonun assembly kodu VBoxBiosAlternative386.asm dosyasında 7501. satırda. Bu fonksiyon, bir switch/case içinde, eğer basılan tuş, özel görevi olan bir tuşsa gerekli flag'ları ayarlıyor, normal bir tuşsa keyboard.c:90'da tanımlanan scan_to_scanascii yapısından tuşun ASCII ve scan kod karşılıklarını buluyor. Bu tablodaki her satır sırayla, tuşun normal, Shift'le, CTRL ile ve Alt ile basılması durumundaki kodları içeriyor. Son eleman 40h ise tuşun CapsLock'la, 20h ise NumLock'la durumunu değiştirdiğini belirtiyor. 604. satırda enqueue_key fonksiyonu çağırılarak çevrilen tuş klavye arabelleğine atılıyor. Bu fonksiyon da aynı dosyanın 339. satırında bulunuyor (VBoxBiosAlternative386.asm:7454).

Aslında BDA'nın 80h ve 82h offsetinde klavye arabelleği için birer tane daha başlangıç ve bitiş göstericileri var. Bu alan, XT BIOS'un 16 tuşluk kısıtlı ara belleğine alternatif olarak AT BIOS'larda tanımlandı. Ancak AT'ler çıktığında piyasada o kadar çok program eski ara belleği hardcoded olarak kullanıyordu ki, bu yeni göstericiler zamanla işlevsiz kaldı. Öyle ki, VBox bile bu göstericileri dikkate almıyor.

* DOS'ta bir klavye düzeni (örn. Türkçe klavye) yüklendiğinde Int 15h/4Fh, scan kodunu alıp ilgili klavye düzenine (layout) ait scan kod - ASCII kod karşılık tablosunda bulunup bulunmadığına bakar. Eğer kod bu tabloda varsa int 9 yerine kendisi tuşun scan ve ASCII kodlarını klavye arabelleğine sürer ve çıkışta carry flag'ı resetleyerek int 9'u bypass eder.


Klavye Arabelleğinden Okumak
Yukarıda herhangi bir şekilde klavye arabelleğinden okumaktan, basılan tuşu kullanıcı programına veya işletim sistemine iletmekten söz etmedim. Klavye portuna doğrudan erişim bir çok nedenle etkin bir yöntem değildir. Örn. klavye standart olmayan bir dilde olabilir ve tüm Alt, CapsLock vb. görevli tuşları kontrol etmeye çalışmak kodu gereksiz yere karmaşık hale getirir. BIOS, basılan tuşları arabelleğe attığı gibi arabellekten okumak için de fonksiyonlar sunar. Bu fonksiyonlar int 16h altında sunulur. 

Fonksiyonlardan ilki, AH=00 ile çağırılan sıfırıncı fonksiyondur. Görevi, klavye arabelleğinde tuş varsa, oradan bunu silmek ve AX'te döndürmektir. Arabellek boşsa fonksiyon, bir tuşa basılana kadar bekler. AH=01 ile çağırılan birinci fonksiyon, arabellekte tuş varsa bunu AX'te döndürür ancak arabellekten silmez. Tuş yoksa da sıfırıncı fonksiyonun tersine, girdi beklemeden sonlanır. Arabellekte tuş olup olmadığını zero flag aracılığıyla geri döndürür.

Yukarıdaki iki fonksiyon XT fonksiyonları olup XT tuş setiyle uyumludur. Başka bir deyişle AT klavyeyle gelen F11, F12, Home, Insert gibi tuşlar bu fonksiyonlarla okunamaz. AT serisiyle birlikte int 16h'ya 10h ve 11h fonksiyonları eklenmiştir. 10h fonksiyonunun görevi 00 ile aynı olup AT tuş takımındaki tuşları da okur. Aynı şekilde 11h fonksiyonu da 01 fonksiyonuyla uyumludur. 00 fonksiyonu çağırılıp F12'ye basılırsa herhangi birşey olmazken 10h fonksiyonu çağırılırsa F12'nin scan kodunu döndürecektir. 

Benzer biçimde 20h ve 21h fonksiyonları 122 tuş klavyeler için varsa da bu klavyelerin yaygın olmaması nedeniyle bu fonksiyonlar bir çok bilgisayarda bulunmaz.


Küçük Bir Uygulama
Klavye arabelleğiyle ilgili yazdıklarımı açıklamak için aşağıda küçük bir kod yazdım. Program sonsuz bir döngüde BDA'yı ekrana basıyor. Klavye arabelleği burada olduğundan, basılan tuşların arabelleğe girişi gerçek zamanlı görülebiliyor. Q, programı sonlandırıyor ve ESC arabelleği temizliyor. Kaynak kodu aşağıda:

#include<stdio.h>
#include<stdlib.h>

#define MEMLINES 10
#define MEMSIZE (MEMLINES * 16)

// return current page
unsigned char getpage()     {
    unsigned char p;
   
    asm     {
        mov     ah, 0x0F
        int     0x10
        mov     p, bh
    }

    return p;
}

// return current cursor position
void getline(unsigned char *x, unsigned char *y)    {
    unsigned char tx, ty;
    unsigned char p = getpage();

    asm     {
        mov     ah, 0x03
        mov     bh, p
        int     0x10
        mov     tx, dl
        mov     ty, dh
    }

    *x = tx;
    *y = ty;
}

// set given cursor position
void setline(unsigned char x, unsigned char y)      {
    unsigned char p = getpage();

    asm     {
        mov     ah, 0x02
        mov     bh, p
        mov     dl, x
        mov     dh, y
        int     0x10
    }
}

// listen on port 60h and return keypress and key release values
char keypress()     {
    unsigned char r;

    asm     {
        in      al, 0x60
        mov     r, al
    }

    return r;
}

// clear keyboard buffer by copying last key pointer over first
// key pointer i.e. 0040h:001Ch <-- 0040h:001Ah
void clearKBbuffer()    {
    unsigned char far *startKBbuffer = (unsigned char far *)0x0041C;
    unsigned char far *endKBbuffer = (unsigned char far *)0x0041A;

    *startKBbuffer++ = *endKBbuffer++;
    *startKBbuffer   = *endKBbuffer;

}


int main(int argc, char* argv[])        {
    int i, j;
    unsigned char curX, curY;
    // memblock points to the start of BIOS Data Area
    unsigned char far *memblock = (unsigned char far *)0x00400;

    getline(&curX, &curY);

    while(1)    {

        for(i = 0; i < MEMLINES; i++)   {
            // print offsets here:
            printf("%06X  ", memblock + (i << 4));

            // hexadecimal block is printed below:
            for(j = 0; j < 16; j++)     {
                printf("%02X ", memblock[(i << 4) + j]);
                if(j == 7)  printf("- ");
            }

            printf("  ");

            // char block is printed below:
            for(j = 0; j < 16; j++)     {
                    if((memblock[(i << 4) + j] < 32) ||
                       (memblock[(i << 4) + j] > 128))
                        printf(".");
                    else
                        printf("%c", memblock[(i << 4) +j]);
            }

            printf("\n");
        }
       
        setline(curX, curY);

        if (keypress() == 0x10)     // if Q key is pressed
            break;                  // break the infinite loop
        if (keypress() == 1)    {   // if ESC key is pressed
            clearKBbuffer();        // clear keyboard buffer
            // break;
        }
    }
   
    return 0;
}


Programı TurboC'de yazdım. TurboC'yi bulmak zor olabileceği için kaynak kodla çalıştırılabilir dosyası buradan indirilebilir. Program hem sanal makinada hem de DosBox'ta çalıştırılabiliyor (DosBox'ta prompt en alt satırdayken çalıştırınca getline() ve setline() düzgün çalışmıyor. cls komutundan sonra çalıştırınca çıktı düzgün görünüyor).

Programı ilk çalıştırdığımdaki ekran görüntüsü yanda. 1Ah ve 1Ch adreslerinde 38h bulunuyor. Birbirine eşit olmaları arabelleğin boş olduğu anlamına geliyor. Programın adını yazarken kullandığım readmem karakterleri arabellekte duruyor. Hatta 1Eh adresinde 'o' karakteri ve sonra ASCII kodu 9, scan kodu 0Fh olan Tab tuşu görülüyor. Komutu yazarken "project" yerine "pro" yazıp tamamlaması için Tab'a basmışım (FreeDOS). Ardından "readmem" yazıp tekrar Tab'a basmışım ama aynı dizinde hem readmem.exe hem de readmem.obj dosyası olduğundan yalnız nokta karakteri çıkmış. Ben de tekrar 'e' ve tekrar Tab'a basıp ardından 1Ch tuşuna yani Enter'a basarak programı çalıştırmışım. 1Ch, 37h offsetinde, bu nedenle de göstericiler 38h'yı gösteriyor. 

Sırayla 'wertyu' tuşlarına bastım. 'wer' 38h'dan arabelleğin sonu olan 3Eh'ya kadar olan alanda bulunuyor. Ardından ring buffer başa dönüyor, geri kalan 'tyu' tuşları 1Eh ile 24h arasında bulunuyor. Esc'e basarak arabelleği sıfırlayıp (1Ah'daki adresi 1Ch'deki adrese yazarak) sonrasında 'asdf'ye basıyorum (aşağıdaki görsel). Bunu yapmamla 'asd' tuşları önceden bastığım 'wer'in üzerine yazılıyor ve 'f' yine 1Eh adresine geliyor. Son olarak arabelleği temizlemeden 'Q'ya basıp ve programı sonlandırıyorum. Int 16h'yla tuşları okumadığım için program sonlanınca, bastığım asdf ve q tuşları DOS tarafından okunarak prompt'ta gösteriliyor.


Klavye bu kadarla sınırlı değil. Burada anlatılanlar yalnızca bir giriş olabilir. Klavyeyi açma ve kapatma komutlarının yanında daha onlarca denetleyici komutu bulunuyor. Bunlarla örn. klavye resetlenip donanım testi yapılabiliyor. PS/2 fare de belirttiğim gibi 60h portundan bağlı. Farenin tek farkı, klavye denetleyicisi IRQ1 oluştururken farenin IRQ12 oluşturması ancak farenin aktifleştirilmesi veya pasifleştirilmesi yine 64h portuna komut göndererek yapılıyor.


Kaynaklar: 
Metin içinde bağlantısı verilen kaynakların yanısıra:

8 Ağustos 2012 Çarşamba

Programlamabilir Kesme Denetleyicisi Gerçekten Programlanabilir mi?


Programlanabilir kesme denetleyicisi Türkçe windows'un çevirisinden alınma. Kulağa ileri okuma eniyileştirme gibi bir tını veriyor. İngilizcesi programmable interrupt controller yani PIC. Yazarken asıl mikrodenetleyici olan PIC serisiyle karıştırmamaya özen göstereceğim.

Programlanabilir kesme denetleyicisi, standartları neredeyse milattan öncesinden kalmış gibi gelen IBM PC uyumlu bilgisayarlarda mikroişlemci dışındaki aygıtlardan gelen kesme isteklerine cevap verebilmek için kullanılan 8259A çipine verilen ad. Bugün üretilen hiçbir anakart bu entegreyi kullanmıyor. Bunun iki nedeni var; birincisi zaten anakartların üzerinde1990'lardan beri kesme denetleyicisi için ayrı, DMA için ayrı, klavye denetleyicisi için ayrı ayrı entegreler bulunmuyor. Bunun yerine bunların hepsini kapsayan Southbridge çipseti içerisinde bütün entegrelerin karşılıkları var. İkincisi de 2000'lerde kesme çakışması yada başka bir deyişle IRQ çakışması hepimizi üzen bir sorundu. Eğer anakartınızda çok fazla genişleme kartı takılıysa mutlaka başınıza geliyordu çünkü toplamda 15 tane IRQ, zamanla gelişen bilgisayar teknolojisinin artan IRQ ihtiyacına yetmiyordu. Üstelik bir kaç seneye kadar artık masaüstü bilgisayarlarında bile birden fazla işlemci kullanılması kaçınılmaz görünüyordu ama 8259A ile bunu desteklemenin imkanı yoktu. Dolayısıyla Intel gelişmiş kesme denetleyicisini yani APIC'i çıkardı. APIC, 240 tane IRQ'yu destekleyecek şekilde üretilmişti ve mikroişlemcilerin birlikte çalışırken, cache bellek güncellemesi sırasında diğerinin çalışmasını durdurmasına olanak veren işlemciler arası kesmeyi (inter-processor interrupt -- IPI) destekliyordu. Ben genel olarak PIC'i anlatacağım kadar bildiğim kadarıyla APIC'in farklarından da bahsedeceğim.

Yalnız hepsinden önce birbirine benzeyen üç tane terimin bence açıklığa kavuşması gerekiyor:
  1. IRQ (Interrupt Request -- Kesme İsteği): Mikroişlemcinin dışındaki aygıtlar bir durum değişikliği yaşadıkları zaman "yahu birşeyler oldu, çalışmana bi ara ver de benimle ilgilen, ne olduğunu anla" anlamında mikroişlemciyi dürterler. Buna IRQ denir. En açıklayıcı örnek klavyeden bir tuşa basıldığında klavye denetleyicisi PIC'e bir kesme isteği iletir ki basılan tuş sistem tarafından okunsun, kullanılmayacaksa bile BIOS'un basılan tuşları tuttuğu tampon belleğine aktarılabilsin. Henüz kesme isteği PIC'dedir. PIC bunu mikroişlemciye iletir.
  2. INT (Interrupt -- Kesme): Mikroişlemciye PIC aracılığıyla iletilen kesme isteği eğer mikroişlemci daha önemli bir şeyle meşgul değilse bir kesme oluşturması gerekir. Kesme oluşursa adı gereği yapılan işlem kesilir, hangi kesme çağırılacaksa onun numarasına karşılık gelen kesme vektörü tablodan okunur ve CS:(E)IP'e yüklenir. 
  3. ISR (Interrupt Service Routine -- Kesme Hizmet Programı): Bir kesme gerçekleştiğinde devreye giren kesme koduna ISR denir. CS:(E)IP'ye yüklenen adresin başlangıcındaki koddan IRET (Interrupt Return) opkoduna kadar olan bloktur.

Kesme vektörleri Intel x86 işlemcilerinde gerçek modda (real mode) belleğin en başında bulunurlar. Bunlar aslında uzak göstericilerden (far pointer) başka birşey değildir. Örneğin resimde sıfırıncı kesme (sıfıra bölme kesmesi) gerçekleştiği anda mikroişlemci CS yazmacına (code segment) 00A7h değerini ve IP yazmacına (instruction pointer) 1068h değerini yükleyecektir.

Bu arada bir gereksiz bilgi daha: Aslında sıfırıncı kesme yalnızca sıfıra bölme hatası değil işlem sonucunda bulunan bölümün, bu bölümün depolanması için ayrılmış alana sığmaması sonucu da oluşur. DIV ve IDIV komutları bölen 8 bitlikse AX'deki değeri böler, bölümü AL'ye kalanı da AH'ye depolar; 16 bitlikse DX:AX'deki değeri böler ve bölümü AX'e kalanı da DX'e depolar. Örneğin aşağıdaki kod parçası sıfıra bölme olmamasına rağmen sıfırıncı kesmeyi tetikler:

mov dx, 0FFF
mov ax, dx
mov bx, 0001
idiv bx

Bunu yazmakla 'int 0' kodunu çağırmak yada yukarıdaki resim için 'jmp 00A7:1068' yazmak aynı şeydir. Turbo Pascal'ın CRT.TPU birimi, birim zamanda saniye üzerinden bir döngüye girip bir sayacı arttırıyor sonra da bulduğu değer üzerinden bölme işlemi yapıyordu. Pascal'ın CRT birimini kullanan programlar çok hızlı bilgisayarlarda (333 Mhz falan) çalışmamaya başladılar, sıfıra bölme hatası verip çıkıyorlardı. Çünkü bölmenin sonucu yazmaca sığmıyordu. (Sene 90'ların sonu) Sonradan bir CRT yaması çıktı da düzelmişti.

Bir gereksiz bilgi de, elbette ki yukarıdaki resim Windows XP altında alındığından, orada bir işlemcinin gerçek çalışma modundan bahsedemeyiz. Korumalı modda kesme vektörleri yerine kesme betimleyici tabloda (interrupt descriptor table -- IDT) bu vektörler tutuluyor.

Konudan yeterince uzaklaştıktan sonra tekrar PIC'e dönelim. Normalde IBM/PC için hangi IRQ'nun hangi INT tarafından karşılanacağı kabaca bellidir:
  • IRQ0: Programmable Interval Timer (PIT) Zamanlayıcı kesmesi. İstenirse PIT programlanarak belli zaman aralıklarında kesme üretmesi sağlanabilir. Eskiden anakarta bağlı hoparlörden bunu kullanarak ses çıkarırdık.
  • IRQ1: Klavye denetleyicisi. Daha önce de söyledim, her tuşa basıldığında bu kesme çağırılır.
  • IRQ2: Bu kullanılan bir kesme değildir. Aslında 8259A entegresi 8 tane kesmeye kadar bakabiliyordu ama bir tane daha 8259A buna bağlanarak (slave PIC) 15 tane kesmeye bakması sağlanıyordu. İkincil PIC birinciye bu IRQ2 bacağından bağlandığından gerçek anlamda bir IRQ2 bulunmamaktadır.
  • Geri kalan IRQ'lar seri ve paralel portlar, disk denetleyicisi ve ses kartı tarafından kullanılır. Bunları kullanmadığımdan hakkında pek bilgim yok.
IRQ0 oluştuğunda INT 08h çağırılır, IRQ1 oluştuğunda INT 09h çağırılır. İlk IRQ7'ye kadar bu sırada gider. İkincil PIC'in baktığı IRQ8--IRQ15 arasıysa INT 70h -- INT 77h arasındadır. Örneğin BIOS açılışta zamanlayıcıyı saniyenin 18.2'sinde IRQ0 oluşturacak şekilde programlar. (PIT'in zamanlayıcı taşması (Timer Overflow) bacağı PIC'in IRQ0 bacağına bağlıdır.) Her saniye 18.2 kere INT 08h çağırılır. BIOS aynı zamanda INT 08h'ya ait ISR'yi de belleğe ekler. ISR her çağırıldığında 0000:046Ch adresindeki DWORD'u bir arttırıp INT 1Ch'yi çağırır. DOS altında düzenli olarak çağırılan bir TSR yazarken INT 08h'nın kodunu değiştirmek sistemin kararlılığını etkileyeceğinden pek önerilmez, bunun yerine programın INT 1Ch'ya asılması önerilir.

Yalnız kutsal kitap Ralf Brown Interrupt List'e gözatınca INT 08h'nın aynı zamanda çifte hata (double exception) için de çağırıldığını görürüz. Benzer şekilde INT 09h matematik işlemci hatalarında "exception handler" olarak CPU tarafından INT 00h'ya benzer biçimde çağırılmaktadır. Özellikle IRQ5'i karşılaması gereken INT 0Dh aynı zamanda kullanıcılara çok tanıdık gelen genel koruma hatası (general protection fault -- GPF) ve yine IRQ6'yı karşılaması gereken INT 0Eh de aynı zamanda sayfa hatası (page fault) için çalışmaktadır. Şimdi Windows 95'in verdiği mavi ekranda "bir istisna 0E oluştu" ifadesinin ne anlama geldiği daha açıkça anlaşılabiliyordur sanırım.

Çok kötü birşeyler olmuş (Image credit)


Daha önce hizalama hatasından bahsederken aslında bu kesmenin de BIOS kesmeleriyle çakıştığını söylemiştim. Peki bu nasıl olabiliyor?

Birincisi sonradan eklenen INT 08h ile INT 11h arasındaki mikroişlemci istisnaları dikkat edilirse çoğunlukla korumalı moda ait hatalarda tetikleniyor. Dolayısıyla kodu, gerçek mod kodu olan INT 10h ve INT 11h kesmelerini zaten korumalı mod altında kullanmayacağımızdan yada INT 10h ile INT 11h gerçek modda tetiklenecek bir hata olmadığından gerçek moddayken güvendeyiz. Ama gerçek moddan korumalı moda geçtiğimizde bir IRQ5 gerçekleştiğinde ben gerçekte koruma hatası mı alıyorum yoksa sabit disk bir kesme mi istiyor bunu ayırt edebilmem gerçekten çok zor olacaktır. İşte programlanabilir kesme denetleyicisinin programlanabilir olduğu yer burası. Siz kesme denetleyicisine G/Ç portlarından ulaşarak yeniden programlayabilirsiniz. Kesme denetleyicisinin portları birinci denetleyici için 20h ikinci denetleyici için 0A0h'dır.
Bu portlardan denetleyiciyi programlayan kodu aşağıda veriyorum. Kod benim değil, zamanında Utrecht Üniversitesi'nin korumalı mod e-posta listesinde paylaşılmış basit bir kod:

mov    al, 00010001b ; Input Control Word (ICW) 1
out    20h, al       ; Birinci PIC
out    0A0h, al      ; Ikinci PIC

mov    al, 20h       ; IRQ0'i karsilayacak INT
out    21h, al
mov    al, 28h       ; IRQ8'i karsilayacak INT
out    0A1h, al
 
mov    al, 00000100b ; ICW3
out    21h, al       ; Ikinci PIC hangi bacaga bagli?
mov    al, 2
out    0A0h, al

mov    al, 00000001b ; ICW4
out    21h, al
out    0A1h, al

mov    al, 11111011b ; Output Control Word (OCW)1: bir olan IRQ'lari maskele
out    21h, al
mov    al, 11111111b ; hepsini maskele
out    0A1h, al

Benzeri kodlar http://wiki.osdev.org/8259_PIC adresinde de bulunabilir.

PIC'i programlarken dikkat edilmesi gereken ICW1 20h portuna yazılırken ICW2, ICW3 ve ICW4 komutları ICW1'in yazılması bittikten hemen sonra 21h portuna yazılması gerekiyor.

ICW1'in ilk dört biti 0001 olması gerekiyor. Üçüncü bit PIC'i kenar tetiklemeli (edge-triggered) moda alıyor, ikinci bit kesme vektörlerinin 32 bit olduğunu bildiriyor, birinci bit PIC'in birlikte çalıştığı başka bir PIC daha bulunduğunu bildiriyor. Sıfırıncı bitse ICW4'ün verileceğini bildiriyor.

ICW2, birinci ve ikinci PIC'lerin oluşturacağı IRQ'ları mikroişlemcide hangi kesmenin karşılayacağını bildiriyor.

ICW3, birinci PIC'in hangi bacağına bağlı olduğunu bildiriyor. (IRQ2) İkinci PIC içinse ICW3 bu PIC'e bir numara vermemizi sağlıyor. Bu numara birden fazla PIC ardarda bağlanırsa hepsini numaralandırabilmeye olanak tanıyor. IBM PC uyumlularında yalnızca iki tane olduğundan çok da önemli değil.

ICW4'ün sıfırıncı biti 8086 modunda kesmeler üretmesini sağlıyor.

Kodun son parçasında OCW aracılığıyla birinci PIC'de IRQ2 haricindeki IRQ'lar iptal ediliyor ve ikinci PIC'de de bütün IRQ'lar iptal ediliyor. Diğer bitlerin değerleri ve anlamları için yine Ralf Brown kesme listesine bakılabilir.

Elbette ki bu kod çalışırken herhangi bir kesme gerçekleşse bile dikkate alınmaması gerekiyor bu nedenle bu kodun hemen öncesinde bir CLI komutu çalıştırılmalı ve sonrasında gerekmiyorsa (tabi ISR'ler hazırlandıktan sonra) kesmeler yeniden STI ile geri getirilmelidir.

Kesme denetleyici entegreye bakınca 8 bitlik bir veriyolu bağlantısının olduğu görülür. Bir IRQ gerçekleştiği zaman kesme denetleyicisi mikroişlemcinin INTR (Interrupt Request) bacağını set edip veriyoluna kesmenin numarasını sürer. Bundan sonrasında mikroişlemcinin keyfi olursa o kesmeyi çağırır ve INTA bacağını resetler (INTA active low olması lazım) yada kesme isteğine cevap vermez.

Son olarak sinyallemenin daha rahat anlaşılması için google görsellerde "Cascaded 8259" aratıldığında bağlantının nasıl yapıldığını anlatan güzel görseller bulunabiliyor. Ben, başkasının çizimleri olduğundan buraya eklemek istemedim.