第5天,我们尝试在屏幕上显示了字符串,并且显示了鼠标。
第5天的教程链接:
第5天的显示程序,让当前这个界面,有点像操作系统的样子了
但是,这个鼠标是静止的,所以。我们下一步就是先让鼠标动起来,让屏幕对鼠标的按键和移动操作有响应。
能对鼠标响应之后,再对键盘有响应。
好了,那我们的目标就确定了:让当前的屏幕对鼠标有响应,分别对鼠标的按键和移动有响应。
要做到对鼠标有响应,需要先设置寄存器GDTR, 并生成一个表GDT,然后设置IDTR,并生成一个表IDT。
这个咱们前一天的教程中已经完成了。
具体的,我们可以把获取鼠标按键,移动等代码放在IDT定义的中断函数中去实现,然后再想办法把移动后的鼠标显示到界面上来。
实际上这个IDT和GDT我们在第5天的代码中已经实现了,所以今天完成鼠标的˙中断函数,在中断函数里控制,响应鼠标动作。
只要能够响应鼠标的动作,就可以用鼠标的动作去改变操作系统显示的内容。比如让鼠标指针随着鼠标的动作而改变。
今天的内容会按照以下顺序展开:
- 初始化中断相关硬件:中断控制器
- 鼠标的中断函数实现分析
- C语言写的中断函数
- 对中断函数进行优化:将比较耗时的操作移出中断函数
使用中断功能:需要先初始化中断控制硬件
中断控制有专门的硬件:可编程中断控制器,programmable interrupt controller,简称PIC.
下面咱们直接看程序:
void init_pic(void) /* PIC初始化设定 */ { io_out8(PIC0_IMR, 0xff ); /* 禁止所有中断 */ io_out8(PIC1_IMR, 0xff ); /* 禁止所有中断 */ io_out8(PIC0_ICW1, 0x11 ); /* 边沿触发 */ io_out8(PIC0_ICW2, 0x20 ); /* IRQ0-7由INT20-27接收 */ io_out8(PIC0_ICW3, 1 << 2); /* PIC1由IRQ2接收 */ io_out8(PIC0_ICW4, 0x01 ); /* 无缓冲区模式 */ io_out8(PIC1_ICW1, 0x11 ); /* 边沿触发 */ io_out8(PIC1_ICW2, 0x28 ); /* IRQ8-15由INT28-接受 */ io_out8(PIC1_ICW3, 2 ); /* PIC1由IRQ2连接 */ io_out8(PIC1_ICW4, 0x01 ); /* 无缓冲区 */ io_out8(PIC0_IMR, 0xfb ); /* 11111011 PIC1以外全禁止 */ io_out8(PIC1_IMR, 0xff ); /* 11111111 禁止所有中断 */ return; }
程序解读:
PIC0_IMR, 第0个中断控制器的屏蔽寄存器。8位对应8路中断信号。如果某一位为1,那么该位所对应的信号就被屏蔽了,也就是说中断信号产生以后,CPU不会感知到。
这里我们要对中断控制器进行初始化,当然要把中断屏蔽了。所以,我们将PIC0_IMR设置位0xff,将PIC1_IMR设置位0xff.
就是将中断控制器0,中断控制器1的中断信号全部屏蔽了
下面的程序都是有关ICW的设定了。ICW,initial control word,初始化控制数据。也就是把数据写在ICW里,就是对中断控制器初始化。
每个PIC都有4个ICW,ICW1,ICW2,ICW3,ICW4,
ICW1,ICW4主要是设定配线方式。
ICW3设定主从连接方式的,也是8位,如果将其第2位设置位1,那么就是将其第二位作为主从连接用,来接受从中断控制器的中断信号。
ICW2设定中断号,如果设定为0x20,那么当前控制器产生的中断号为:0x20+0,0x20+1,0x20+...,0x20+7,即:0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27.
所以整个init_pic函数主要设定了两个中断控制器PIC0,PIC1,其中,PIC0是主,PIC1是从。
另外,这里使用了OUT指令对PIC的控制字进行操作,因为PIC是CPU的外部设别,CPU只要按照端口去对应写入到控制字就行了。
分别设定了这两个中断控制器的电器连接属性,中断号,以及主从连接属性。
鼠标的中断函数
完成PIC的初始化设定后,硬件上,CPU就可以感知到鼠标的动作了,我们编写中断函数,在感知到鼠标动作后,打印出一些内容就行了。
一般来说,鼠标的中断函数只能在汇编中写,但是这里我们既然用了C语言来写操作系统,自然就想用C语言来写中断函数。
要用C语言写中断函数,就必须用汇编来做桥梁,也就是说在汇编写的中断函数中调用C语言写的中断函数。
汇编写的中断函数只是做一些数据的传递工作,然后调用C语言写的中断函数,不做跟鼠标相关的内容。
我们先来看看汇编写的中断函数,如下:
以下代码摘自:
EXTERN _inthandler2c ; 引入C语言写的中断函数 _asm_inthandler2c: PUSH ES PUSH DS PUSHAD MOV EAX,ESP PUSH EAX MOV AX,SS MOV DS,AX ; make DS=SS MOV ES,AX ; make ES=SS CALL _inthandler2c POP EAX POPAD POP DS POP ES IRETD
可以看到,中间有一句:CALL _inthandler2c
这句代码调用了C语言写的中断函数。
这句代码之外的其他代码,基本上都是push,pop,栈的存入和取出。
也就是说,在这个汇编写的中断函数里,跟鼠标相关的操作,啥都没有做。
这个函数就是调用了C语言中写的中断函数。
如何把汇编中的中断函数放在C语言中实现
如上一段所示,用汇编写一个中断函数,在这个汇编写的中断函数里,调用C写的中断函数就行了。
汇编写的中断函数可以响应系统中断。
因为汇编写的中断函数调用了C语言写的中断函数,所以在它响应系统中断时,C语言写的中断函数就运行起来了。
我们使用C语言写的中断函数来完成对鼠标的响应。
那么C语言写的中断函数如下:
void inthandler2c(int *esp) /* 此函数受汇编中断函数调用 */ { struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO; boxfill8(binfo->vram, binfo->scrnx, COL8_000000, 0, 0, 32 * 8 - 1, 15); putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, "INT 2C (IRQ-12) : PS/2 mouse"); for (;;) { io_hlt(); } }
这个函数内部先是用boxfill8绘制了长方形,然后又在长方形上写了一串字符串:"INT 2C (IRQ-12) : PS/2 mouse"
那么,如果一切正常,将会显示如下画面:
但是,当我们编译完成后,发现并没有出现如上画面。无论是我们滑动鼠标也好,还是点击按钮,都没有反应。
这是因为,鼠标作为计算机的外部设备,也是需要先发送一些控制码,使与鼠标相关的硬件有效。
与鼠标有关的硬件其实就是键盘控制器和鼠标电路了,所以这里要分别给键盘控制器和鼠标发送控制码,如下:
#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(void) { /* 等待键盘控制器准备好 */ for (;;) { if ((io_in8(PORT_KEYSTA) & KEYSTA_SEND_NOTREADY) == 0) { break; } } return; } #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; }
将这些代码放在C语言中一起编译,然后运行,就会成功的到如下画面:
到现在为止,系统对鼠标可以响应了:显示字符:PS/2 mouse.
这个正是在我们用C写的中断函数里实现的。
提升中断函数的执行效率:
所谓中断处理,就是打断CPU本来的工作,加塞要求进行处理。
所以必须完成得干净利索。而且进行中断处理期间,不再接受新的中端。
所以如果中断处理程序运行的太慢,它会占用CPU,CPU就无法及时响应新产生的中断。
无法产生新的中断,鼠标如果有新的动作,CPU就无法捕捉到
这将会使CPU无法及时响应用户对鼠标,键盘的操作。
这种现象是我们要极力去避免的。
那么如何做呢?
原则就是中断程序内,只做最简单的事情。
显示一般比较耗时的,
所以,把最耗时的显示程序,放在中断程序外面。
中断程序内,就只是把鼠标的数据保存在某个变量里。
这样,中断程序只完成一个鼠标动作数据的传递功能,对数据的解读,显示等功能,交给main函数就行了。
这样,中断程序就会非常快速地完成,干净利索,cpu就可以更高的频率去捕捉鼠标的动作。
中断程序改成如下形式:
#define PORT_KEYDAT 0x0060 // 定义一个结构体 // 这个结构体用来存放鼠标数据 struct KEYBUF { unsigned char data, flag; }; struct KEYBUF keybuf; void inthandler2c(int *esp) { unsigned char data; io_out8(PIC1_OCW2, 0x64); /* 通知PIC1已经处理完成了IRQ-12的中断 */ io_out8(PIC0_OCW2, 0x62); /* 通知PIC1已经处理完成了IRQ-02的中断 */ data = io_in8(PORT_KEYDAT); if == 0) { keybuf.data = data; keybuf.flag = 1; } return; }
注意到,我们定义了一个结构体KEYBUF,专门用来存放鼠标的端口PORT_KEYDAT传过来的数据。
然后我们再在主程序中,把KEYBUF里的内容显示出来,代码如下:
// boo void HariMain(void) { ... ... ... // 前面是其他带代码 for (;;) { io_cli(); if == 0) { io_stihlt(); } else { i = keybuf.data; keybuf.flag = 0; io_sti(); sprintf(s, "%02X", i); boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31); putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s); } } }
以上代码就实现了把原本在中断函数inthandler2c中的显示部分代码,利用keybuf,转移到了HariMain函数中。
不仅实现了对鼠标动作的响应,还实现了中断函数内部的精简。
编译运行成功后,我们发现,我们对鼠标进行各种操作,屏幕上都会成功打印出一个字符出来。
移动鼠标,屏幕出现字符。
点击鼠标按键,屏幕出现字符。
鼠标的响应非常迅速。
但是,屏幕上的鼠标并没有移动。
要想让鼠标移动,就必须在鼠标移动后的位置,去重新绘制一鼠标,
也就是说,我们要完成两个步骤:
1. 获取鼠标的位置
2. 重新绘制鼠标。
那么,如何获取鼠标的位置?
要从keybuf中的data去找。
怎么找?
没办法找,因为data只有一个数值。
鼠标的位置坐标有连个数值:x和y.
所以,data如果是个数组的话,我们就可以把鼠标坐标相关的数据一次性的放入data了。
我们就把data改为数组。然后去按照这个数组来重新绘制鼠标就行了。
那么鼠标返回的数据,难道就只有x,y坐标么?还有其他值么?
有其他值的,比如鼠标按下的是左键,还是右键?鼠标移动时,是每隔一个很短的时候就发送一组x,y的,所以, 鼠标会在很短的时间发送大量的数据。
那么如此大量的数据,显然,如果data这个数组是需要把他们都存储下来的,所以我们需要把data数组优化为一个FIFO的缓冲区
FIFO缓冲区正式为应对鼠标这种短时间大量数据,来不及处理的情况而设计的数据结构,
今天的内容已经很多了,由于FIFO需要讲的比较多,所以我们放在下一天的教程中讲
总结:
今天我们开始对鼠标产生响应了,我们写了对鼠标硬件的控制程序,然后程序能够成功的响应鼠标的移动和按键。我们还为了提高中断函数的效率,把鼠标相关的显示程序放在了HariMain函数中。
但是我们还没有实现把鼠标的移动过程画出来。
因为要把鼠标移动的过程画出来,就要一次性接收大量的数据。我们需要把接收数据的变量data改造成FIFO数据缓冲区,然后再对数据逐一解读,解读出鼠标的坐标变化,然后才能依据解读出来的坐标,把鼠标指针画在屏幕上。
下一天的教程我们将会完成这个内容。