一个操作系统的实现:第七篇——输入/输出系统

键盘敲击的过程:

在键盘中存在一枚叫做键盘编码器(Keyboard Encoder)的芯片,它通常是Intel 8048以及兼容芯片,作用是监视键盘的输入,并把适当的数据传送给计算机。另外,在计算机主板上还有一个键盘控制器(Keyboard Controller),用来接收和解码来自键盘的数据,并与8259A以及软件等进行通信。

一个操作系统的实现:第七篇——输入/输出系统_第1张图片

敲击键盘有两个方面的含义:动作和内容。动作可以分解成三类:按下、保持按住的状态以及放开;内容则是键盘上不同的键,字母键还是数字键,回车键还是箭头键。所以,根据敲击动作产生的编码,8048既要反映“哪个”按键产生了动作,还要反映产生了“什么”动作。
敲击键盘所产生的编码被称作扫描码(Scan Code),它分为Make Code和Break Code两类。当一个键被按下或者保持住按下时,将会产生Make Code,当键弹起时,产生Break Code。除了Pause键之外,每一个按键都对应一个Make Code和一个Break Code。
扫描码总共有三套,叫做Scan codeset 1、Scan codeset 2和Scan codeset 3。Scan codeset 1是早期的XT键盘使用的,现在的键盘默认都支持Scan code set 2,而Scan code set 3很少使用。
当8048检测到一个键的动作后,会把相应的扫描码发送给8042,8042会把它转换成相应的Scan code set 1扫描码,并将其放置在输入缓冲区中,然后8042告诉8259A产生中断(IRQ1)。如果此时键盘又有新的键被按下,8042将不再接收,一直到缓冲区被清空,8042才会收到更多的扫描码。

 8042的寄存器:

一个操作系统的实现:第七篇——输入/输出系统_第2张图片

对于输入和输出缓冲区,可以通过in和out指令来进行相应的读取操作。也就是说,一个in al, 0x60指令就可以读取扫描码了。

Scan code set1扫描码:

一个操作系统的实现:第七篇——输入/输出系统_第3张图片

一个操作系统的实现:第七篇——输入/输出系统_第4张图片

带*号的为Windows多媒体键盘专有按钮
注意: 由于a和A是同一个键,所以它们的扫描码是一样的(事实上根本就不是“它们”而是“它”,因为是同一个键),如果按下“左Shift+a”,将得到这样的输出:0x2A0x1E0x9E0xAA,分别是左Shift键的Make Code、a的Make Code、a的Break Code以及左Shift键的Break Code。所以,按下“Shifta”得到A是软件的功劳,键盘和8042是不管这些的,在你自己的操作系统中,甚至可以让“Shift+a”去对应S或者T,只要你习惯就行。同理,按下任何的键,不管是单键还是组合键,想让屏幕输出什么,或者产生什么反应,都是由软件来控制的。虽然增加了操作系统的复杂性,但这种机制无疑是相当灵活的。

 屏幕上每一个字符对应的2字节:

一个操作系统的实现:第七篇——输入/输出系统_第5张图片

可以看到,低字节表示的是字符本身,高字节用来定义字符的颜色。颜色分前景和背景两部分,各占4位,其中低三位意义是相同的,表示颜色,但最高位作用不同。如果前景最高位为1的话,字符的颜色会比此位为0时亮一些;如果背景最高位为1,则显示出的字符将是闪烁的(注意是字符闪烁而不是背景闪烁)。

字符属性位颜色详解:

一个操作系统的实现:第七篇——输入/输出系统_第6张图片

mov ah, 0Ch ; 0000: 黑底 1100: 红字
mov al, 'P'
mov [gs:edi], ax

VGA寄存器:

一个操作系统的实现:第七篇——输入/输出系统_第7张图片

寄存器名称全部使用的是英文,这样做不但避免了翻译的偏差,而且有个好处是,我们可以通过Register这个单词是否使用复数来判断寄存器是否只有一个。比如CRT Controller Registers这一组,其中的Data Registers使用的是复数,说明数据寄存器不止一个,如下表所示。

CRTController DataRegisters:

一个操作系统的实现:第七篇——输入/输出系统_第8张图片

这么多寄存器,只有一个端口0x3D5,怎么来操作其中某一个呢?这就用到Address Register了。我们看到上表中每一个寄存器都对应一个索引值,当想要访问其中一个时,只需要先向Address Register写对应的索引值(通过端口0x3D4),然后再通过端口0x3D5进行的操作就是针对索引值对应的寄存器了。如果我们把Data Registers看作一个数组,那么 Address Register就相当于数组的下标。
举个例子,假如想把索引号为idx的寄存器的值改为new_value,可以这样来做:
out_byte(0x3D4, idx);
out_byte(0x3D5, new_value);

TTY任务示意图:

一个操作系统的实现:第七篇——输入/输出系统_第9张图片

 TTY任务代码示意:

一个操作系统的实现:第七篇——输入/输出系统_第10张图片

多控制台示意图:

一个操作系统的实现:第七篇——输入/输出系统_第11张图片

输入缓冲区和控制寄存器都是可写的,但它们的作用是不同的,写入输入缓冲区用来往8048发送命令,而写入控制
寄存器是往8042本身发送命令。
我们的目的是往8048发送命令,使用端口0x60。设置LED的命令是0xED。当键盘接收到这个命令后,会回复一个ACK(0xFA),然后等待从端口0x60写入的LED参数字节,这个参数字节定义如下图所示。

设置LED的参数字节:

一个操作系统的实现:第七篇——输入/输出系统_第12张图片

要注意的是,在向8042输入缓冲区写数据时,要先判断一下输入缓冲区是否为空,方法是通过端口0x64读取状态寄存器。状态寄存器的第1位如果为0,表示输入缓冲区是空的,可以向其写入数据。

 设置LED:

#define LED_CODE 0xED
#define KB_ACK 0xFA

/*======================================================================*
				 kb_wait
 *======================================================================*/
PRIVATE void kb_wait()	/* 等待 8042 的输入缓冲区空 */
{
	u8 kb_stat;

	do {
		kb_stat = in_byte(KB_CMD);
	} while (kb_stat & 0x02);
}


/*======================================================================*
				 kb_ack
 *======================================================================*/
PRIVATE void kb_ack()
{
	u8 kb_read;

	do {
		kb_read = in_byte(KB_DATA);
	} while (kb_read =! KB_ACK);
}

/*======================================================================*
				 set_leds
 *======================================================================*/
PRIVATE void set_leds()
{
	u8 leds = (caps_lock << 2) | (num_lock << 1) | scroll_lock;

	kb_wait();
	out_byte(KB_DATA, LED_CODE);
	kb_ack();

	kb_wait();
	out_byte(KB_DATA, leds);
	kb_ack();
}

区分任务和用户进程:

一个操作系统的实现:第七篇——输入/输出系统_第13张图片

增加一个系统调用的过程:

一个操作系统的实现:第七篇——输入/输出系统_第14张图片

printf调用过程示意图:

一个操作系统的实现:第七篇——输入/输出系统_第15张图片

你可能感兴趣的:(一个操作系统的实现:第七篇——输入/输出系统)