【读书笔记-《30天自制操作系统》-6】Day7

本篇向着移动鼠标的目标继续前进。先对中断处理进行一些补充说明,然后建立完善缓冲区来实现键盘数据接收。最后是在此基础上的初始化鼠标控制电路与鼠标的数据接收。
在这里插入图片描述

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,则说明收到了按键值,将按键值读取出来。按键值取出之后,再响应新的中断就不会造成混乱了,此时就可以放开中断,然后将按键值显示出来。
当然了,这里只能读取一个数据,如果数据处理期间又产生了中断,则无法进行处理,新产生的中断数据会被丢弃。

  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);
			}
		}
	}

从键盘获取数据与从鼠标获取数据的方式相同,这里将二者进行合并。在中断处理函数中,键盘和鼠标的数据分别写入了不同的缓冲区,这里也从不同的缓冲区读取。运行程序,移动鼠标或按下键盘,会分别产生响应:
在这里插入图片描述
距离移动鼠标只有一步之遥了!下一篇继续,敬请期待!

  • 11
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值