讓滑鼠漫天飛舞:在核心中實現滑鼠的中斷處理
上一節,我們成功實現了鍵盤按鍵的中斷響應,本節,我們看如何響應滑鼠的中斷訊號,並做相應處理。
如果大家還記得描述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,是滑鼠被啟用時傳送過來的,滑鼠傳送的資料,需要連續三個位元組一起解讀,解讀的辦法會在後面介紹。
現在,我們核心已經能夠接收滑鼠資料了,下一步就是解讀資料,重新繪製滑鼠了。