第7天 FIFO与鼠标控制

发布时间 2023-12-11 00:17:09作者: RainbowMagic

获取按键编码

当中断程序处理完毕之后需要相8259A发送一个处理完毕的信号,这样8259A才知道中断已经处理完毕,可以接收下一个中断信号了,不然的话,我们的键盘中断一直阻塞在哪里没办法进行处理下一个按键操作。
io_out8(PIC0_OCW2, 0x61);就是为了满足这个操作的。键盘的中断是IRQ1,如果想要通知8259AIRQ2处理完毕的话,那么就得向OCW2发送0x62到端口中。
当键盘被按下后,会将按键数据存储到0x60这个端口中,我们只要读取0x60这个端口的值就可以知道按下了什么按键,如下代码所示。

void inthandler21(int *esp)
/* 来自PS/2键盘的中断 */
{
	unsigned char data, s[4];

	// 获取中断键盘码	
	data = io_in8(0x0060);
	sprintf(s, "%02X", data);
	
	struct Boot_info *binfo = (struct Boot_info *) 0x0ff0;
	boxfill8(binfo->vram, binfo->scrnx, 0x07, 0, 0, 32 * 8 - 1, 15);
	printFont8_ascii(binfo->vram, binfo->scrnx, 10, 10, 0x07, s);
	
	io_out8(PIC0_OCW2, 0x61);	/* 通知PIC IRQ-01 已经受理完毕 */

	return;
}

ESC按下会输出0x81
image

加快中断处理

中断是以加塞的方式进行处理的,当一个中断处理的较慢时,会将其他中断进行阻塞的,例如如果键盘处理较慢会将网卡鼠标处理起来看起来一卡一卡的。我们使用vram在屏幕上画图时需要大量的if判断才能画数据到屏幕中,所以我们可以添加个缓冲区,先将读取到的按键存储到一个位置之后,之后慢慢进行处理也可以,这样就不会阻塞调其他中断了。类似简单的发布订阅模式,当有数据时单独异步进行处理。
我们先简单的做处理,判断缓冲区中是否有缓冲数据如果有则丢弃新中断的数据,如果没有则将数据保存到缓冲区中,这样做有个弊端,就是如果有大量键盘中断发生的话,中断来不急处理会造成大量的数据丢失。
在正式往屏幕中写数据时首先将中断关闭,避免我们在处理的过程中被打断,在处理完缓冲区数据之后再将中断打开。但是这样会有一个问题,有些按键是两个字节的按键码,而CPU发送数据只能发送一个字节到8259A中,这时就会拆成两次中断来进行发送数据,如果按我们之前的那样来处理中断的话,肯定会有一个字节的数据被舍弃。
代码如下所示:

struct KeyBoardBuffer
{
	// 键盘缓冲区
	unsigned char buffer;
	// 如果有数据flag等于1,否则flag等于0
	unsigned short flag;
};

struct KeyBoardBuffer keyBoardBuffer;

void inthandler21(int *esp)
/* 来自PS/2键盘的中断 */
{


	if (keyBoardBuffer.flag == 0)
	{
			// 获取中断键盘码
		keyBoardBuffer.flag = 1;
		keyBoardBuffer.buffer = io_in8(0x0060);;
	}

	io_out8(PIC0_OCW2, 0x61); /* 通知PIC IRQ-01 已经受理完毕 */

	return;
}

void HariMain(void)
{
	struct Boot_info *boot_info = (struct Boot_info *)0x0ff0;

	init_gdt_idt();
	init_pic();
	io_sti();

	init_palette();
	init_screen(boot_info->vram, boot_info->scrnx, boot_info->scrny);

	char mour[256];

	int mx = (boot_info->scrnx - 16) / 2; /* 计算画面的中心坐标*/
	int my = (boot_info->scrny - 28 - 16) / 2;

	init_mouse_cursor8(mour, 0x0f);

	io_out8(PIC0_IMR, 0xf9); /* 开放PIC1和键盘中断(11111001) */
	io_out8(PIC1_IMR, 0xef); /* 开放鼠标中断(11101111) */
	
	unsigned char s[4];

	for (;;)
	{
		// 判断缓冲区是否有数据
		if (keyBoardBuffer.flag == 1)
		{
			// 关闭中断 避免程序被打断
			io_cli();

			sprintf(s, "%02X", keyBoardBuffer.buffer);

			boxfill8(boot_info->vram, boot_info->scrnx, 0, 3, 15, 31, 0x0f);
			print_font8_ascii(boot_info->vram, boot_info->scrnx, 0, 3, 0x07, s);

			// 处理完数据将缓冲区flag设置为0
			keyBoardBuffer.flag = 0;
		} else 
		{
			// 冲区数据处理完毕 开启中断
			io_sti();
		}
	}
}

缓存区FIFO

为了避免数据丢失,所以需要一个先进先出的队列来保存缓冲区数据,这样就不会丢失大量的键盘中断数据了。在处理缓冲区数据时,每处完一个字节数据,缓冲区整体向前移动一位,如下图所示:
image
有中断时直接将next + 1并在队列后面添加数据即可,next的大小就是队列缓冲区数据的大小,如果键盘中断未处理数据大于缓冲区大小,避免缓冲区溢出那么就不做处理。
缓冲区需要存储数据字符串列表了,缓冲区最大大小为32个字符

struct KeyBoardBuffer
{
	// 键盘缓冲区
	unsigned char buffer[32];
	// 下一个未读取的数据下标
	unsigned short next;
};

在读取数据的时候直接向队列末尾添加数据并下标 + 1即可。

void inthandler21(int *esp)
/* 来自PS/2键盘的中断 */
{
	unsigned char data = io_in8(0x0060);
	if (keyBoardBuffer.next < 32)
	{
		// 获取中断键盘码
		keyBoardBuffer.buffer[keyBoardBuffer.next] = data;
		// 下标 + 1
		keyBoardBuffer.next++;
	}

	io_out8(PIC0_OCW2, 0x61); /* 通知PIC IRQ-01 已经受理完毕 */
	return;
}

void HariMain(void)
{
	struct Boot_info *boot_info = (struct Boot_info *)0x0ff0;

	init_gdt_idt();
	init_pic();
	io_sti();

	init_palette();
	init_screen(boot_info->vram, boot_info->scrnx, boot_info->scrny);

	char mour[256];

	int mx = (boot_info->scrnx - 16) / 2; /* 计算画面的中心坐标*/
	int my = (boot_info->scrny - 28 - 16) / 2;

	init_mouse_cursor8(mour, 0x0f);

	io_out8(PIC0_IMR, 0xf9); /* 开放PIC1和键盘中断(11111001) */
	io_out8(PIC1_IMR, 0xef); /* 开放鼠标中断(11101111) */

	unsigned char s[4];
	for (;;)
	{
		// 判断缓冲区是否有数据
		if (keyBoardBuffer.next != 0)
		{
			// 关闭中断 避免程序被打断
			io_cli();
			// 每次读取缓冲区第一位数据 因为是队列 肯定读第一位
			unsigned char data = keyBoardBuffer.buffer[0];
			sprintf(s, "%02X", data);
			boxfill8(boot_info->vram, boot_info->scrnx, 0, 3, 15, 31, 0x0f);
			print_font8_ascii(boot_info->vram, boot_info->scrnx, 0, 3, 0x07, s);

			int i = 0;
			keyBoardBuffer.next--;
			// 获取到数据之后缓冲区集体向前移动1位覆盖已读取过的数据
			for (i = 0; i < keyBoardBuffer.next; i++)
			{
				keyBoardBuffer.buffer[i] = keyBoardBuffer.buffer[i + 1];
			}

			// 缓冲区的数据读取完毕就可以继续接收其他中断了,例如键盘中断新数据会继续保存到缓冲区中 并不会对我们的程序有什么影响
			io_sti();
		}
		else
		{
			// 缓冲区数据处理完毕 开启中断
			io_sti();
		}
	}
}

缓冲区优化

每次读写都要移动元素对于性能来说消耗太大了,所以我们可以维护两个指针,一个指针指向读的元素,一个指针指向写的元素,如下图所示:
image
如果读写满了则从开始继续读写数据。
image
从开始读写数据的前提是缓冲区的大小没有满,不然会覆盖没有读取的数据的。

struct KeyBoardBuffer
{
	// 键盘缓冲区
	unsigned char buffer[32];
	// 读下标
	unsigned short read;
	// 写下标  
	unsigned short write;
	unsigned short len;
};

void inthandler21(int *esp)
/* 来自PS/2键盘的中断 */
{
	unsigned char data = io_in8(0x0060);
	// 判断缓冲区是否已满,如果未满则可以加数据,如果已满则不加数据
	if (keyBoardBuffer.len < 32)
	{
		// 获取中断键盘码
		keyBoardBuffer.buffer[keyBoardBuffer.read] = data;
		// 元素下标 + 1
		keyBoardBuffer.len++;
		// 读下标 + 1
		keyBoardBuffer.read++;

		if (keyBoardBuffer.read == 32) {
			keyBoardBuffer.read = 0;
		}
	}

	io_out8(PIC0_OCW2, 0x61); /* 通知PIC IRQ-01 已经受理完毕 */
	return;
}

void HariMain(void)
{
	struct Boot_info *boot_info = (struct Boot_info *)0x0ff0;

	init_gdt_idt();
	init_pic();
	io_sti();

	init_palette();
	init_screen(boot_info->vram, boot_info->scrnx, boot_info->scrny);

	char mour[256];

	int mx = (boot_info->scrnx - 16) / 2; /* 计算画面的中心坐标*/
	int my = (boot_info->scrny - 28 - 16) / 2;

	init_mouse_cursor8(mour, 0x0f);

	io_out8(PIC0_IMR, 0xf9); /* 开放PIC1和键盘中断(11111001) */
	io_out8(PIC1_IMR, 0xef); /* 开放鼠标中断(11101111) */

	unsigned char s[4];
	for (;;)
	{
		// 判断缓冲区是否有数据
		if (keyBoardBuffer.len != 0)
		{
			// 关闭中断 避免程序被打断
			io_cli();
			// 读到哪就从哪的下标获取元素
			unsigned char data = keyBoardBuffer.buffer[keyBoardBuffer.read];
			// 读下标 + 1
			keyBoardBuffer.read++;
			// 元素总数下标 -1
			keyBoardBuffer.len--;


			sprintf(s, "%02X", data);
			boxfill8(boot_info->vram, boot_info->scrnx, 0, 3, 15, 31, 0x0f);
			print_font8_ascii(boot_info->vram, boot_info->scrnx, 0, 3, 0x07, s);

			int i = 0;
			// 缓冲区的数据读取完毕就可以继续接收其他中断了,例如键盘中断新数据会继续保存到缓冲区中 并不会对我们的程序有什么影响
			io_sti();
		}
		else
		{
			// 缓冲区数据处理完毕 开启中断
			io_sti();
		}
	}
}

鼠标中断

从计算机发展历程来看,鼠标是一种很新的设备,从它的IRQ编号就可以看出来,鼠标的IRQ编号是12号。
由于鼠标设备是较新的设备,所以在早期,大多数操作系统都不指出鼠标中断,如果在不支持鼠标中断的计算机中插入了鼠标,那么鼠标只要轻轻晃动就会引发中断,这样是不利于用户使用的。所以即便是主板有鼠标用的电路,只要操作系统没有激活,那么就不会发生鼠标中断。
如下图所示,我们需要做的是打通CPU与鼠标电路已经鼠标电路与鼠标才能正常使用鼠标中断。
img
由于鼠标控制电路已经继承到键盘控制电路里了,所以我们只要初始化键盘集成电路自然而然也初始化好了鼠标的集成电路。

PS/2控制器文档: https://wiki.osdev.org/"8042"_PS/2_Controller#Data_Port
(io_in8(PORT_KEYSTA) & KEYSTA_SEND_NOTREADY) != 0 会校验键盘控制器是否通过自检,如果通过自检则 & 后等于0,如下文档所示0x60端口获取到的数据含义, bit 2代表PS/2控制器自检完成:
img
可恶的是,书作者提供的源代码是错误的,使用for(;;)编写的死循环命令无法编译成功,最终只能使用goto语句来实现死循环操作了。

init_keyboard函数就是用来初始化键盘的了,init_keyboard会先校验PS/2控制器是否自检完成,如果自检完成才会向控制器发送指令。

如果想要初始化PS/2控制器需要先向发一个out 0x64, 0x60的命令,0x64端口表示控制命令,0x60表示将下一个字节写入到RAM的0个字节位置,0字节各位含义如图2所示,之后将0x47写入到写缓冲区0x64即可, 0x47的二进制为0100 0111。
img
img

#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)
{

// 死循环编译错误 只能使用给goto语句来实现死循环
start:
	if ((io_in8(PORT_KEYSTA) & KEYSTA_SEND_NOTREADY) != 0)
	{
		goto start;
	}

	return;
}


/**
 * 初始化键盘电路
*/
void init_keyboard(void)
{
	wait_KBC_sendready();
	io_out8(PORT_KEYCMD, KEYCMD_WRITE_MODE);
	wait_KBC_sendready();
	io_out8(PORT_KEYDAT, KBC_MODE);
}

0xd4表示第二个PS/2控制器也就是鼠标,0xf4表示允许获取鼠标发送过来的数据,如下图所示:
img
img

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

之后处理就和键盘处理差不多了,由于鼠标PIC芯片与两个IRQ有关,所以需要发送数据两次,之后读取0x0060就能获取到鼠标中断事件,之后在压入栈中

void inthandler2c(int *esp)
/* 来自PS/2鼠标	的中断 */
{
	unsigned char data;
	io_out8(PIC1_OCW2, 0x64); /* 通知PIC IRQ-12 的受理已经完成*/
	io_out8(PIC0_OCW2, 0x62); /* 通知PIC IRQ-02 的受理已经完成*/
	data = io_in8(0x0060);

	fifo8_put(&mouseBuffer, data);

	return;
}

然后死循环源源不断获取鼠标键盘发送过来的数据。

	fifo8_init(&keyboardBufffer, 32, keyBuffer);
	fifo8_init(&mouseBuffer, 128, mouseBuf);

	unsigned char s[4];
	for (;;)
	{
		// 判断缓冲区是否有数据
		if (fifo8_status(&keyboardBufffer) != 0)
		{
			// 关闭中断 避免程序被打断
			io_cli();
			unsigned char data = fifo8_get(&keyboardBufffer);

			sprintf(s, "%02X", data);
			boxfill8(boot_info->vram, boot_info->scrnx, 0, 3, 15, 31, 0x0f);
			print_font8_ascii(boot_info->vram, boot_info->scrnx, 0, 3, 0x07, s);
		}

		// 处理鼠标中断
		if (fifo8_status(&mouseBuffer) != 0)
		{
			io_cli();
			unsigned char data = fifo8_get(&mouseBuffer);

			sprintf(s, "%02X", data);
			boxfill8(boot_info->vram, boot_info->scrnx, 0, 3, 15, 31, 0x0f);
			print_font8_ascii(boot_info->vram, boot_info->scrnx, 0, 3, 0x07, s);
		}

		// 缓冲区的数据读取完毕就可以继续接收其他中断了,例如键盘中断新数据会继续保存到缓冲区中 并不会对我们的程序有什么影响
		// 判断鼠标 键盘缓冲区都为空了,那么就将程序挂起
		if (fifo8_status(&keyboardBufffer) == 0 && fifo8_status(&mouseBuffer) == 0)
		{
			io_sti();
			io_hlt();
		}
	}