本篇向着移动鼠标的目标继续前进。先对中断处理进行一些补充说明,然后建立完善缓冲区来实现键盘数据接收。最后是在此基础上的初始化鼠标控制电路与鼠标的数据接收。
1. 中断处理程序补充说明
前面的处理中,接收到键盘中断后只是显示一行信息,现在把按键的信息也一并显示出来。将中断处理函数修改如下:
void inthandler21(int *esp)
{
struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
unsigned char data, s[4];
io_out8(PIC0_OCW2, 0x61); /* 通知PIC IRQ-01已经处理完毕 */
data = io_in8(PORT_KEYDAT);
sprintf(s, "%02X", data);
boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
return;
}
这段程序中需要注意的点有两个。
首先是语句
io_out8(PIC0_OCW2, 0x61);
这里是通知PIC,中断IRQ1已处理,这样PIC才会继续监控IRQ1中断,否则后续即使IRQ1中断再次产生,PIC也不进行上报。
此外还有语句
data = io_in8(PORT_KEYDAT);
这里PORT_KEYDAT定义为0x0060,是一个端口号,对应的设备就是键盘。从这里读出的8位信息就是按键编码。
运行程序,按下按键或者松开按键,都会显示出一个十六进制数的按键编码。
按键编码显示出来了,但是还是要对中断处理程序做一些优化。
因为中断处理打断了CPU的正常工作流程,相当于CPU停下了手头全部的工作来处理这一中断,而且此过程中不再接收其他的中断。如果中断处理的时间过长,CPU就无法及时响应其他的中断,造成鼠标运动不连续,不能从网上接收数据等问题。
而字符显示要执行大量的if语句进行判断,执行大量的内存写入指令,花费的时间明显较长。因此这里将接收到的编码先存放在变量中,并不在中断程序中执行显示的动作,而是在Harimain函数中以轮询的方式查询变量的值,查询到变量中存放了值就将其显示出来。中断处理函数修改如下:
void inthandler21(int *esp)
{
unsigned char data;
io_out8(PIC0_OCW2, 0x61); /* 通知PIC IRQ-01已经处理完毕 */
data = io_in8(PORT_KEYDAT);
if (keybuf.flag == 0) {
keybuf.data = data;
keybuf.flag = 1;
}
return;
}
在中断程序中,如果接收到了按键值,就将keybug结构体中的flag置为1,将按键值存放在data中。
HariMain函数如下:
void HariMain(void)
{
struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
char s[40], mcursor[256];
int mx, my, i;
init_gdtidt();
init_pic();
io_sti();
io_out8(PIC0_IMR, 0xf9);
io_out8(PIC1_IMR, 0xef);
init_palette();
init_screen8(binfo->vram, binfo->scrnx, binfo->scrny);
mx = (binfo->scrnx - 16) / 2;
my = (binfo->scrny - 28 - 16) / 2;
init_mouse_cursor8(mcursor, COL8_008484);
putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16);
sprintf(s, "(%d, %d)", mx, my);
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, s);
for (;;) {
io_cli();
if (keybuf.flag == 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);
}
}
}
可以看到,主函数完成前期的初始化之后,就进入for循环检测。由于没有循环判断条件,for循环会一直执行下去。在处理keybuf之前,需要先屏蔽中断,防止此时响应中断造成混乱。接下来对keybuf.flag进行检测,如果为0,说明没有收到按键值,不做处理,可以继续响应中断;如果置1,则说明收到了按键值,将按键值读取出来。按键值取出之后,再响应新的中断就不会造成混乱了,此时就可以放开中断,然后将按键值显示出来。
当然了,这里只能读取一个数据,如果数据处理期间又产生了中断,则无法进行处理,新产生的中断数据会被丢弃。
- 缓冲区:从初步到完善
2.1 数据移位的缓冲区
那么问题就来了。按下右Ctrl键时,会连续产生两次中断,发送了两个字节的按键值,对于上面处理能力只有一个字节的程序,就无法收到第二个字节了。
于是我们需要一定长度缓冲区,能够在中断处理完成前保存一定的数据,而不是只能处理一个字节。于是有下面的中断处理函数:
struct KEYBUF {
unsigned char data[32];
int next;
};
void inthandler21(int *esp)
{
unsigned char data;
io_out8(PIC0_OCW2, 0x61);
data = io_in8(PORT_KEYDAT);
if (keybuf.next < 32) {
keybuf.data[keybuf.next] = data;
keybuf.next++;
}
return;
}
这里将缓冲区设置为32个字节的长度,并用next变量记录下一个数据存放的位置。保险起见,next达到32之后的数据就丢弃不使用了。相对应的主程序修改如下:
for (;;) {
io_cli();
if (keybuf.next == 0) {
io_stihlt();
} else {
i = keybuf.data[0];
keybuf.next--;
for (j = 0; j < keybuf.next; j++) {
keybuf.data[j] = keybuf.data[j + 1];
}
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);
}
}
在主程序中,一次for循环只能处理1字节的数据。这里每次从keybuf.data数组中取出一个数据,都要将其他的数据依次向前移动一个位置,以保证每次执行循环体时data[0]都是最新需要被处理的数据。
2.2 “环形”的缓冲区
这样的处理似乎只是逻辑上看起来略显复杂,实际不止于此。采用这种数据移动的方式仍然要执行很多条指令,主程序要在禁止中断时进行这些操作,可能导致无法及时响应中断。
于是继续对缓冲区做优化。维护两个位置信息,一个是中断程序写入数据的位置,一个是主程序读出数据的位置。
在初始过程中,读出数据的位置落后于写入数据的位置:
这样一直写入,会有写入的位置到达缓冲区末尾时的情况:
这时只需要将写入位置重置为缓冲区起始位置,从起始位置重新开始写入。
从图中可以看出,只要写入的位置没有“追上”读出的位置,那么这段缓冲区就可以一直重复利用。
根据以上的思路,程序整理如下。
首先是定义缓冲区的结构体:
struct FIFO8 {
unsigned char *buf;
int p, q, size, free, flags;
};
其中buf即是可自定义长度的缓冲区,p与q分别 用于表示写入的位置与读取的位置,size和free分别表示缓冲区的总长度与剩余可写入的长度。缓冲区的初始化函数:
void fifo8_init(struct FIFO8 *fifo, int size, unsigned char *buf)
/* 初始化FIFO缓冲区 */
{
fifo->size = size;
fifo->buf = buf;
fifo->free = size; /* 缓冲区的大小 */
fifo->flags = 0;
fifo->p = 0; /* 下一个数据的写入位置 */
fifo->q = 0; /* 下一个数据的读出位置 */
return;
}
向缓冲区写入数据的函数与从缓冲区读取数据的函数:
int fifo8_put(struct FIFO8 *fifo, unsigned char data)
/* 向FIFO传送数据并保存 */
{
if (fifo->free == 0) {
/* FIFO中无空余空间,丢弃 */
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)
/* 从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_put向缓冲区写入一个数据;主程序中,每执行一次循环体,调用fifo8_get从缓冲区读取一个数据。
void inthandler21(int *esp)
{
unsigned char data;
io_out8(PIC0_OCW2, 0x61); /* 通知PIC, IRQ-01受理已经完成 */
data = io_in8(PORT_KEYDAT);
fifo8_put(&keyfifo, data);
return;
}
char s[40], mcursor[256], keybuf[32];
fifo8_init(&keyfifo, 32, keybuf);
for (;;) {
io_cli();
if (fifo8_status(&keyfifo) == 0) {
io_stihlt();
} else {
i = fifo8_get(&keyfifo);
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);
}
}
3. 鼠标:从控制电路初始化到接收鼠标数据
优化了以上中断处理程序,再次回到鼠标上来。
前一篇中鼠标的中断处理程序之所以未能生效,是因为有两方面的初始化没有做。一是鼠标的控制电路,二是鼠标本身,其中鼠标控制电路的初始化要在鼠标本身的初始化之前完成。
鼠标的控制电路包含在键盘的控制电路中,键盘的控制电路初始化完成后,鼠标的控制电路也就初始化完成了。
#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;
}
void init_keyboard(void)
{
/* 初始化键盘控制电路 */
wait_KBC_sendready();
io_out8(PORT_KEYCMD, KEYCMD_WRITE_MODE);
wait_KBC_sendready();
io_out8(PORT_KEYDAT, KBC_MODE);
return;
}
wait_KBC_sendready函数用于CPU确认键盘控制电路已经做好准备,可以接收指令。而确定键盘控制电路处于可以接收指令的状态后,发送设定模式的指令,键盘控制电路的初始化就完成了。
最后还需要激活鼠标本身,同样是向键盘控制电路发送指令。
#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; /*成功状态下键盘控制器会返回ACK(0xfa) */
}
向键盘控制器写入0xd4后,下一条指令就会被发送给鼠标,这样就可以发送激活鼠标的指令了。
以上工作全部完成后,运行程序,鼠标的中断信息也终于可以显示出来了。
既然中断处理函数正常,那么继续来接收鼠标的数据。
struct FIFO8 mousefifo;
void inthandler2c(int *esp)
/* 来自PS/2鼠标的中断 */
{
unsigned char data;
io_out8(PIC1_OCW2, 0x64); /* 通知PIC1,IRQ-12已受理完成 */
io_out8(PIC0_OCW2, 0x62); /* 通知PIC0,IRQ-02已受理完成 */
data = io_in8(PORT_KEYDAT);
fifo8_put(&mousefifo, data);
return;
}
由于鼠标的IRQ12在从PIC上,在通知从PIC后还需要通知主PIC的IRQ2,否则主PIC会忽略之后的IRQ2中断。
for (;;) {
io_cli();
if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) == 0) {
io_stihlt();
} else {
if (fifo8_status(&keyfifo) != 0) {
i = fifo8_get(&keyfifo);
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);
} else if (fifo8_status(&mousefifo) != 0) {
i = fifo8_get(&mousefifo);
io_sti();
sprintf(s, "%02X", i);
boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 47, 31);
putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);
}
}
}
从键盘获取数据与从鼠标获取数据的方式相同,这里将二者进行合并。在中断处理函数中,键盘和鼠标的数据分别写入了不同的缓冲区,这里也从不同的缓冲区读取。运行程序,移动鼠标或按下键盘,会分别产生响应:
距离移动鼠标只有一步之遥了!下一篇继续,敬请期待!