《30天自制操作系統》筆記(05)——啟用鼠標鍵盤
進度回顧
從最開始的(01)篇到上一篇為止,已經解決了開發環境問題和OS項目的頂層設計問題,並且知道了如何在320*200像素的模式下使用顯示器。這意味着處理和輸出部分已經有了最基本的版本,因此本篇來完成輸入功能,即啟用鍵盤和鼠標。
以前基於.net做app的時候,必須了解一些.net虛擬機、AppDomain、.net類庫、socket、面向對象等相關的知識。現在要基於物理機做一個被稱為"操作系統"的app,當然要對物理機有一些認識。
本篇將整理一些關於CPU的知識點。整理這些的目的是實現對硬件的封裝(寫一些供C語言調用的函數),對硬件進行封裝的目的當然是隱藏硬件細節,為寫操作系統這個app服務了。不過大動干戈地封裝不宜在此時進行,因為我對后續的內存管理、多任務、窗口這些東西還沒有概念。放到整個操作系統完成后進行重構時再仔細封裝比較穩妥。
事件,小名中斷
Windows窗體編程基於事件機制。簡單來說,就是鼠標單擊時,應用程序會執行某個函數;你可以自行隨意編寫這個函數的實現代碼。這個機制自然是操作系統提供給應用程序的。
但是,操作系統又是如何獲取鼠標的單擊事件的?一種方式是讓CPU死循環不停地查詢,但這太浪費,而且CPU就沒時間處理應用程序的邏輯運算了。那么唯一的可能就是計算機在硬件層次上提供給操作系統一種類似的事件機制,也就是中斷(interrupt,簡寫做INT)。也就是說,硬件能夠產生鼠標點擊的事件(物理事件),並調用一個由操作系統指定的函數A,函數A則通知相應的應用程序"喂,發生了鼠標點擊事件M"(實際上稍微復雜一點,函數A是將M放入消息隊列,間接地讓應用程序獲知了M);當CPU執行該應用程序時,就會根據M執行應用程序的事件函數。
因此,中斷可以看做幼兒期的事件,通過操作系統、應用程序之間的傳遞過程,就被封裝成能干活的大人了。
傳個紙條
根據這一原理,操作系統只需給硬件寫個紙條說"CPU同志,發生鼠標點擊事件時,請你調用函數M"。CPU用幾個特定的寄存器和一點點內存保存好這個紙條就可以了。
這個紙條用代碼寫出來,就是下面這個樣子的。
1 void init_gdtidt(void) 2 { 3 struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT; 4 struct GATE_DESCRIPTOR *idt = (struct GATE_DESCRIPTOR *) ADR_IDT; 5 int i; 6 7 /* GDT的初始化 */ 8 for (i = 0; i <= LIMIT_GDT / 8; i++) { 9 set_segmdesc(gdt + i, 0, 0, 0); 10 } 11 set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, AR_DATA32_RW); 12 set_segmdesc(gdt + 2, LIMIT_BOTPAK, ADR_BOTPAK, AR_CODE32_ER); 13 load_gdtr(LIMIT_GDT, ADR_GDT); 14 15 /* IDT的初始化 */ 16 for (i = 0; i <= LIMIT_IDT / 8; i++) { 17 set_gatedesc(idt + i, 0, 0, 0); 18 } 19 load_idtr(LIMIT_IDT, ADR_IDT); 20 21 /* IDT的設定 */ 22 set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32); 23 set_gatedesc(idt + 0x27, (int) asm_inthandler27, 2 * 8, AR_INTGATE32); 24 set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32); 25 26 return; 27 } 28 29 void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar) 30 { 31 if (limit > 0xfffff) { 32 ar |= 0x8000; /* G_bit = 1 */ 33 limit /= 0x1000; 34 } 35 sd->limit_low = limit & 0xffff; 36 sd->base_low = base & 0xffff; 37 sd->base_mid = (base >> 16) & 0xff; 38 sd->access_right = ar & 0xff; 39 sd->limit_high = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0); 40 sd->base_high = (base >> 24) & 0xff; 41 return; 42 } 43 44 void set_gatedesc(struct GATE_DESCRIPTOR *gd, int offset, int selector, int ar) 45 { 46 gd->offset_low = offset & 0xffff; 47 gd->selector = selector; 48 gd->dw_count = (ar >> 8) & 0xff; 49 gd->access_right = ar & 0xff; 50 gd->offset_high = (offset >> 16) & 0xffff; 51 return; 52 }
其中在"/* IDT的設定 */"這行注釋下面的三行,就是告訴了CPU發生鼠標、鍵盤事件時應該調用的函數。"asm_inthandler21"對應鍵盤事件,"asm_inthandler27"對應鼠標事件,"asm_inthandler2c"對應……額我不記得什么硬件的事件了。這三個函數只能用匯編寫,其內容神似,只說鍵盤事件即可。
"asm_inthandler21"代碼如下,它的功能就是調用C語言寫的"inthandler21"函數。這樣就可以用C語言來實現操作系統層的邏輯了。
1 _asm_inthandler21: 2 PUSH ES 3 PUSH DS 4 PUSHAD 5 MOV EAX,ESP 6 PUSH EAX 7 MOV AX,SS 8 MOV DS,AX 9 MOV ES,AX 10 CALL _inthandler21 11 POP EAX 12 POPAD 13 POP DS 14 POP ES 15 IRETD
C語言編寫的asm_inthandler21()代碼如下,它的功能是將獲取到的鍵盤按鍵信息保存到一個隊列keyinfo里供主函數HariMain使用。
1 #define PORT_KEYDAT 0x0060 2 3 struct FIFO8 keyfifo; 4 5 void inthandler21(int *esp) 6 { 7 unsigned char data; 8 io_out8(PIC0_OCW2, 0x61); /* 通知PIC,說IRQ-01的受理已經完成 */ 9 data = io_in8(PORT_KEYDAT); 10 fifo8_put(&keyfifo, data); 11 return; 12 }
操作系統處理消息隊列
處理鍵盤消息隊列
操作系統這個App的主函數HariMain()在死循環里讀取keyfifo這個隊列里的數據,根據是否讀到數據來決定是否通知應用程序。當然此時還不存在基於當前這個操作系統的應用程序,那么我們就僅僅在屏幕上顯示出讀到的鍵盤信息好了。
1 for (;;) { 2 io_cli(); 3 if (fifo8_status(&keyfifo) == 0) { //鍵盤沒有被按下或彈起 4 io_stihlt();//硬件系統掛起,直到發生了某個中斷 5 } else { 6 i = fifo8_get(&keyfifo); //獲取按鍵編碼 7 io_sti(); 8 sprintf(s, "%02X", i); //顯示按鍵編碼 9 boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31); 10 putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s); 11 } 12 }
處理鼠標消息隊列
在HariMain()里,對鼠標事件的處理與鍵盤是神似的。只不過,一個鼠標事件會發生三次中斷,即鼠標消息隊列mouseinfo里會有三個數據,所以處理起來會稍微麻煩一點,代碼如下。
1 for (;;) { 2 io_cli(); 3 if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) == 0) {//鍵盤和鼠標都沒有被按下或彈起 4 io_stihlt();//硬件系統掛起,直到發生了某個中斷 5 } else { 6 if (fifo8_status(&keyfifo) != 0) { 7 //同上,略 8 } else if (fifo8_status(&mousefifo) != 0) { 9 i = fifo8_get(&mousefifo); 10 io_sti(); 11 if (mouse_decode(&mdec, i) != 0) { 12 /* 數據的3個字節都齊了,顯示出來 */ 13 sprintf(s, "[lcr %4d %4d]", mdec.x, mdec.y); 14 if ((mdec.btn & 0x01) != 0) { 15 s[1] = 'L'; 16 } 17 if ((mdec.btn & 0x02) != 0) { 18 s[3] = 'R'; 19 } 20 if ((mdec.btn & 0x04) != 0) { 21 s[2] = 'C'; 22 } 23 boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 32 + 15 * 8 - 1, 31); 24 putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s); 25 /* 鼠標指針的移動 */ 26 boxfill8(binfo->vram, binfo->scrnx, COL8_008484, mx, my, mx + 15, my + 15); /* 隱藏鼠標 */ 27 mx += mdec.x; 28 my += mdec.y; 29 if (mx < 0) { 30 mx = 0; 31 } 32 if (my < 0) { 33 my = 0; 34 } 35 if (mx > binfo->scrnx - 16) { 36 mx = binfo->scrnx - 16; 37 } 38 if (my > binfo->scrny - 16) { 39 my = binfo->scrny - 16; 40 } 41 sprintf(s, "(%3d, %3d)", mx, my); 42 boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 0, 79, 15); /* 隱藏鼠標 */ 43 putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, s); /* 顯示坐標 */ 44 putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16); /* 描畫鼠標 */ 45 } 46 } 47 } 48 }
為了應對鼠標事件的復雜狀態,這里已經把分析鼠標事件封裝為mouse_decode()函數,其實現如下。其實是個小小的狀態機吧。
1 int mouse_decode(struct MOUSE_DEC *mdec, unsigned char dat) 2 { 3 if (mdec->phase == 0) { 4 /* 等待鼠標的0xfa階段 */ 5 if (dat == 0xfa) { 6 mdec->phase = 1; 7 } 8 return 0; 9 } 10 if (mdec->phase == 1) { 11 /* 等待鼠標第一字節的階段 */ 12 if ((dat & 0xc8) == 0x08) { 13 /* 如果第一字節正確 */ 14 mdec->buf[0] = dat; 15 mdec->phase = 2; 16 } 17 return 0; 18 } 19 if (mdec->phase == 2) { 20 /* 等待鼠標第二字節的階段 */ 21 mdec->buf[1] = dat; 22 mdec->phase = 3; 23 return 0; 24 } 25 if (mdec->phase == 3) { 26 /* 等待鼠標第三字節的階段 */ 27 mdec->buf[2] = dat; 28 mdec->phase = 1; 29 mdec->btn = mdec->buf[0] & 0x07; 30 mdec->x = mdec->buf[1]; 31 mdec->y = mdec->buf[2]; 32 if ((mdec->buf[0] & 0x10) != 0) { 33 mdec->x |= 0xffffff00; 34 } 35 if ((mdec->buf[0] & 0x20) != 0) { 36 mdec->y |= 0xffffff00; 37 } 38 mdec->y = - mdec->y; /* 鼠標的y方向與畫面符號相反 */ 39 return 1; 40 } 41 return -1; /* 應該不會到這兒來 */ 42 }
有圖有真相
用Vmplayer虛擬機加載此時的Haribote.img,如下圖所示。

對了,我稍微修改了下光標的樣子,個人感覺這樣看起來更美觀舒服一點。初始化光標的代碼如下。其實只是修改了原作者設定的cursor[16][16]數組里的內容而已。
1 void init_mouse_cursor8(char *mouse, char bc) 2 /* マウスカーソルを準備(16x16) */ 3 { 4 static char cursor[16][16] = { 5 "*...............", 6 "**..............", 7 "*O*.............", 8 "*OO*............", 9 "*OOO*...........", 10 "*OOOO*..........", 11 "*OOOOO*.........", 12 "*OOOOOO*........", 13 "*OOOOOOO*.......", 14 "*OOOO*****......", 15 "*OO*O*..........", 16 "*O*.*O*.........", 17 "**..*O*.........", 18 "*....*O*........", 19 ".....*O*........", 20 "......*........." 21 }; 22 int x, y; 23 24 for (y = 0; y < 16; y++) { 25 for (x = 0; x < 16; x++) { 26 if (cursor[y][x] == '*') { 27 mouse[y * 16 + x] = COL8_000000; 28 } 29 if (cursor[y][x] == 'O') { 30 mouse[y * 16 + x] = COL8_FFFFFF; 31 } 32 if (cursor[y][x] == '.') { 33 mouse[y * 16 + x] = bc; 34 } 35 } 36 } 37 return; 38 }
總結
本篇只寫了啟用鼠標鍵盤的相關代碼和最基本的原理。對於初始化鍵盤鼠標的具體原理沒有任何說明。這是因為太麻煩了,涉及太多的硬件細節,我看了將近一周的書才看明白,要我自己寫一遍很費勁且不必要。目前的首要任務是先把后續的內存管理、多任務、窗體、應用程序、異常機制等過一遍,之后再詳細琢磨如何簡潔明快地解釋GDT、IDT這些東西。
鍵盤鼠標的初始化工作是一個由於細節冗長繁雜造成的難點。這之后,其它硬件相關的初始化工作也就接近尾聲了。下一步准備內存管理,為多任務奠定基礎。
請查看下一篇《《30天自制操作系統》筆記(06)——CPU的32位模式》
