《30天自制操作系统》笔记(05)——启用鼠标键盘
《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位模式》
微信扫码,自愿捐赠。天涯同道,共谱新篇。
微信捐赠不显示捐赠者个人信息,如需要,请注明联系方式。 |