让鼠标漫天飞舞:在内核中实现鼠标的中断处理

时间:2022-08-03 14:45:33

代码的调试和运行,以及更详细的讲解,请参看视频:
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,是鼠标被激活时传送过来的,鼠标发送的数据,需要连续三个字节一起解读,解读的办法会在后面介绍。

现在,我们内核已经能够接收鼠标数据了,下一步就是解读数据,重新绘制鼠标了。