代码的调试和运行,以及更详细的讲解,请参看视频:
Linux kernel Hacker, 从零构建自己的内核
上一节,我们成功实现了键盘按键的中断响应,本节,我们看如何响应鼠标的中断信号,并做相应处理。
如果大家还记得描述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,是鼠标被激活时传送过来的,鼠标发送的数据,需要连续三个字节一起解读,解读的办法会在后面介绍。
现在,我们内核已经能够接收鼠标数据了,下一步就是解读数据,重新绘制鼠标了。