1. 程式人生 > >讓滑鼠漫天飛舞:在核心中實現滑鼠的中斷處理

讓滑鼠漫天飛舞:在核心中實現滑鼠的中斷處理

上一節,我們成功實現了鍵盤按鍵的中斷響應,本節,我們看如何響應滑鼠的中斷訊號,並做相應處理。

這裡寫圖片描述

如果大家還記得描述8259A中斷控制器那一小節的話,滑鼠傳送中斷訊號的資料線在從8259A晶片的IRQ4訊號線,因此,為了接收滑鼠中斷訊號,我們在初始化中斷控制晶片時,必須啟用該訊號線,同時,從8259A晶片是通過主8259A的IRQ2訊號線連線在一起的,所以,也必須同時啟動主8259A晶片的IRQ2訊號線,這樣,我們在核心中要對init8259A程式碼段做一些改動:

init8259A:
...
mov  al, 11111001b ;允許鍵盤中斷
out  021h, al
call io_delay

mov  al, 11101111b ;允許滑鼠中斷
out  0A1h, al
call io_delay

ret

mov al, 11111001b 這一句指令,啟用了主8259A晶片的IRQ1和IRQ2兩根訊號線,mov al, 11101111b 這句指令啟用了從8259A的IRQ4訊號線,這根訊號線就是用來發送滑鼠訊號的。
我們上幾節說過,只要是外接硬體,要想使用,就得對其進行配置和初始化,就像我們前面看到的,硬體的初始化,一般就是對給定埠傳送幾個資料而已,滑鼠自然也不例外。

滑鼠電路的初始化

滑鼠電路對應的一個埠是 0x64, 通過讀取這個埠的資料來檢測滑鼠電路的狀態,核心會從這個埠讀入一個位元組的資料,如果該位元組的第二個位元位為0,那表明滑鼠電路可以接受來自核心的命令,因此,在給滑鼠電路傳送資料前,核心需要反覆從0x64埠讀取資料,並檢測讀到資料的第二個位元位,知道該位元位為0時,才著手傳送控制資訊,程式碼如下:

#define  PORT_KEYDAT  0x0060
#define  PORT_KEYSTA  0x0064
#define  PORT_KEYCMD  0x0064
#define  KEYSTA_SEND_NOTREADY  0x02
#define  KEYCMD_WRITE_MODE  0x60
#define  KBC_MODE     0x47

void  wait_KBC_sendready() {
    for(;;) {
        if ((io_in8(PORT_KEYSTA) & KEYSTA_SEND_NOTREADY) == 0) {
            break
; } } }

for迴圈一直從埠讀取資料,然後檢測位元位,只有對應位元位是0時,才返回。大家看到,上面程式碼中,居然有一個埠是 0x60, 你可能會困惑,0x60不是鍵盤電路的埠嗎?沒錯,滑鼠的初始化,就是得通過鍵盤電路來實現的,當對應位元位為0,也就是滑鼠可以接收資料了,這時候,我們就得通過向埠0x60傳送資料來配置滑鼠:

void init_keyboard(void) {
    wait_KBC_sendready();
    io_out8(PORT_KEYCMD, KEYCMD_WRITE_MODE);
    wait_KBC_sendready();
    io_out8(PORT_KEYDAT, KBC_MODE);
    return;
}

上面程式碼中,先等待0x64埠返回可寫訊號,然後繼續向埠傳送一個位元組資料,這個位元組數值是 0x60, 該資料讓鍵盤電路進入資料接收狀態。緊接著向埠0x60傳送一個位元組的資料0x47, 這個資料要求鍵盤電路啟動滑鼠模式,這樣,滑鼠硬體所產生的資料資訊,都可以通過鍵盤電路埠0x60讀到,至於為什麼滑鼠會跟鍵盤電路勾搭在一起,我也不清楚,也不知道當時IBM的設計人員是怎麼想的。

當我們想向滑鼠傳送資料時,先向埠傳送一個位元組的資料,改資料的值是0xd4,完成這一步後,任何向埠0x60寫入的資料都會被傳送給滑鼠:

#define KEYCMD_SENDTO_MOUSE 0xd4
#define MOUSECMD_ENABLE 0xf4

void enable_mouse(void) {
    wait_KBC_sendready();
    io_out8(PORT_KEYCMD, KEYCMD_SENDTO_MOUSE);
    wait_KBC_sendready();
    io_out8(PORT_KEYDAT, MOUSECMD_ENABLE);
    return;
}

io_out8(PORT_KEYCMD, KEYCMD_SENDTO_MOUSE); 這一句就是向埠0x64寫入一個位元組的資料,即0xd4, 然後o_out8(PORT_KEYDAT, MOUSECMD_ENABLE); 這一句是向埠0x60寫入一位元組資料,該資料的數值為0xf4,這個資料會被鍵盤電路傳送給滑鼠,該資料的作用是對滑鼠進行啟用,滑鼠一旦接收到該資料後,立馬向CPU傳送中斷訊號,如果這時候,我們設定好滑鼠的中斷處理函式的話,相關函式的程式碼就會被CPU執行,我們先看看如何設定滑鼠的中斷函式:

LABEL_IDT:
%rep  33
    Gate  SelectorCode32, SpuriousHandler,0, DA_386IGate
%endrep

.021h:
    Gate SelectorCode32, KeyBoardHandler,0, DA_386IGate

%rep  10
    Gate  SelectorCode32, SpuriousHandler,0, DA_386IGate
%endrep

.2CH:
    Gate SelectorCode32, mouseHandler,0, DA_386IGate

我們在初始化8259A晶片時,將主8259A的初始中斷向量設定為0x20,把從8259A的初始中斷向量設定為0x28, 由於滑鼠中斷訊號線是從8259A的IRQ4,所以滑鼠的中斷向量就是0x28 + 4 = 0x2C, 從上面程式碼看來,滑鼠的中斷處理函式叫mouseHandler, 我們看看它的程式碼:

_mouseHandler:
mouseHandler equ _mouseHandler - $$
     push es
     push ds
     pushad
     mov  eax, esp
     push eax

     call intHandlerForMouse


     pop  eax
     mov  esp, eax
     popad
     pop  ds
     pop  es
     iretd

在這段程式碼中,我們又呼叫了來自C語言實現的函式叫intHandlerForMouse, 我們再看看其實現:

void intHandlerForMouse(char* esp) {
    char*vram = bootInfo.vgaRam;
    int xsize = bootInfo.screenX, ysize = bootInfo.screenY;
    showString(vram, xsize, 0, 0, COL8_FFFFFF, "PS/2 Mouse Handler");   
    for(;;) {
        io_hlt();
    }
}

它的實現很簡單,列印一個字串後進入死迴圈。

當上面的程式碼編譯入核心後,執行結果如下:
這裡寫圖片描述

由此可見,滑鼠啟用後,立馬向CPU傳送中斷訊號,CPU正確呼叫了我們的中斷處理函式。

資料快取機制的改進

滑鼠啟用後,只要滑鼠稍微有點移動,它都會向CPU傳送大量的座標資料,因此核心要能夠把滑鼠傳送的資料合適的儲存起來,以便中斷函式進行相應的處理。我們上面提供的快取機制不夠靈活,因為快取空間只限定在32位元組,這對滑鼠來說是不夠用的,這裡我們對原有機制進行改進,以便用於處理滑鼠傳送的資訊:

struct FIFO8 {
    unsigned char* buf;
    int p, q, size, free, flags;
};

void fifo8_init(struct FIFO8 *fifo, int size, unsigned char *buf) {
    fifo->size = size;
    fifo->buf = buf;
    fifo->free = size;
    fifo->flags = 0;
    fifo->p = 0;
    fifo->q = 0;
    return ;
}

#define FLAGS_OVERRUN 0x0001
int fifo8_put(struct FIFO8 *fifo, unsigned char data) {
    if (fifo->free ==0) {
        fifo->flags |= FLAGS_OVERRUN;
        return -1;
    }

    fifo->buf[fifo->p] = data;
    fifo->p++;
    if (fifo->p == fifo->size) {
        fifo->p = 0;
    }

    fifo->free--;
    return 0;
}

int fifo8_get(struct FIFO8 *fifo) {
    int data;
    if (fifo->free == fifo->size) {
        return -1;
    }

    data = fifo->buf[fifo->q];
    fifo->q++;
    if (fifo->q == fifo->size) {
        fifo->q = 0;
    }

    fifo->free++;
    return data;
}

int fifo8_status(struct FIFO8 *fifo) {
    return fifo->size - fifo->free;
}

FIFO8 是用於資料快取的結構體,裡面的buf可以根據不同的需求進行變換。如果用於鍵盤緩衝,可以通過fifo8_init設定32位元組的記憶體,如果用於鍵盤快取,也可以通過fifo8_init設定128位元組的快取用於滑鼠。

FIFO8裡面的p 對應於原來的next_w, q對應於原來的next_r.

上述修改後的程式碼可見程式碼目錄中的write_vga_desktop_fifo.c。

從滑鼠接收資料

完事具備後,我們的核心就可以源源不斷的從滑鼠接收資料並進行相應處理了,在原有的滑鼠中斷處理函式中做如下改進:

static struct FIFO8 keyinfo;
static struct  FIFO8 mouseinfo;

static char keybuf[32];
static char  mousebuf[128];

void CMain(void) {
    ....
    fifo8_init(&mouseinfo, 128, mousebuf);
    ....

    int data = 0;
    for(;;) {
       io_cli();
       if (fifo8_status(&keyinfo) + fifo8_status(&mouseinfo)  == 0) {
           io_stihlt();
       } else if(fifo8_status(&keyinfo) != 0){
           io_sti();
           data = fifo8_get(&keyinfo);
           char* pStr = charToHexStr(data);
           static int showPos = 0;
           showString(vram, xsize, showPos, 0, COL8_FFFFFF, pStr);
           showPos += 32; 

       } else if (fifo8_status(&mouseinfo) != 0) {
           show_mouse_info();
       }
    }
}

void  show_mouse_info() {
    char*vram = bootInfo.vgaRam;
    int xsize = bootInfo.screenX, ysize = bootInfo.screenY;
    unsigned char data = 0;

    io_sti();
    data = fifo8_get(&mouseinfo);
    char* pStr = charToHexStr(data);
    static int mousePos = 16;
    if (mousePos <= 256) {
        showString(vram, xsize, mousePos, 16, COL8_FFFFFF, pStr);
        mousePos += 32;
    }
}

void intHandlerForMouse(char* esp) {
    unsigned char data;
    io_out8(PIC1_OCW2, 0x20);
    io_out8(PIC_OCW2, 0x20);

    data = io_in8(PORT_KEYDAT);
    fifo8_put(&mouseinfo, data);
}

在入口函式CMain 中,先初始化滑鼠的快取結構體,在intHandlerForMouse中,PIC1_OCW2 的值是0xA0, 也就是從8259A晶片的埠,PIC_OCW2是主8259A晶片的埠,前面提到過,每當中斷處理後,要想再次接收中斷訊號,就必須向中斷控制器傳送一個位元組的資料,這個位元組資料叫OCW2, 它值得我們詳細瞭解下:
OCW2[0-2] 用來表示中斷的優先順序,OCW2[3-4]這兩位必須設定為0,OCW[5]這一位稱之為End of Interrupt, 這一位設定為1,表示當前中斷處理結束,控制器可以繼續呼叫中斷函式處理到來的中斷訊號,要想下一次繼續處理中斷訊號,這一位必須設定為1,OCW2[6-7]這兩位我們不用關心,設定為0即可,我們程式碼中傳送OCW2時的數值是0x20,也就是僅僅把OCW[5]設定為1即可。

接著把滑鼠傳送的資料從埠0x60讀取,並通過fifo8_put寫入到滑鼠緩衝區中。

在CMain中,通過if (fifo8_status(&keyinfo) + fifo8_status(&mouseinfo) == 0)判斷鍵盤或滑鼠緩衝區是否有資料到達,如果有,上面的if判斷就會成立,成立後,要進一步判斷是資料在鍵盤緩衝區還是滑鼠緩衝區,如果是在滑鼠緩衝區,則呼叫show_mouse_info將資料顯示到桌面上。

show_mouse_info的實現也簡單,先將滑鼠傳送的資料轉換成16進位制的字串,然後顯示到桌面上,由於滑鼠一次傳送的資料太多,我在實現裡簡單的做了限制,一個字元顯示時要佔用8個畫素,一個十六進位制字串例如,”0x12”,它的顯示寬度是32個畫素,我在實現裡把字串的畫素寬度限制在128,大家拿到程式碼後,可以自己修改。

上面的程式碼編譯如核心,載入後執行效果如下:
這裡寫圖片描述

在系統被虛擬機器啟動後,把滑鼠放入虛擬機器,然後滑動滑鼠,螢幕上第二行資料就是滑鼠滑動後給核心傳送的資料,這裡的資料有必要提到的是,滑鼠傳送的第一個資料0xFA,是滑鼠被啟用時傳送過來的,滑鼠傳送的資料,需要連續三個位元組一起解讀,解讀的辦法會在後面介紹。

現在,我們核心已經能夠接收滑鼠資料了,下一步就是解讀資料,重新繪製滑鼠了。