本章节所有代码托管在miniOS_32
章节任务介绍
在上一节中,我们介绍了操作系统的同步机制互斥锁的内容,并手动实现了互斥锁,同时实现了线程安全的屏幕打印。 至此,我们算是基本完成了操作系统的“输出”功能,但目前为止我们的输入仍旧依赖于程序,而不是用户操控的键盘 本节我们将正式完成操作系统的“输入”
任务简介
本节的主要任务有:
- 键盘驱动测试
- 编写键盘驱动程序
- 基于环形缓冲区的键盘驱动程序
前置知识
键盘输入原理简介
键盘编码介绍
- 一个键的状态要么是按下,要么是弹起,因此一个键有两个编码,这两个编码统称扫描码,一个键的扫描码由通码和断码组成。
- 按键被按下时的编码叫通码,表示按键上的触点接通了内部电路,使硬件产生了一个码,故通码也称为makecode。
- 按键在被按住不松手时会持续产生相同的码,直到按键被松开时才终止,因此按键被松开弹起时产生的编码叫断码,也就是电路被断开了,不再持续产生码了,故断码也称为breakcode。
- 断码=0x80+通码
以下是各个键的扫描码
- 无论是通码还是断码,它们基本都是一字节大小
- 最高位也就是第7位的值决定按键的状态,最高位若值为 0,表示按键处于按下的状态,否则为1的话,表示按键弹起。
- 但有些按键的通码和断码都以0xe0开头,它们占2字节
8048芯片
无论是按下键,或是松开键,当键的状态改变后,键盘中的 8048 芯片把按键对应的扫描码(通码或断码)发送到主板上的 8042 芯片,8042处理后保存在自己的寄存器中,然后向 8259A 发送中断信号,这样处理器便去执行键盘中断处理程序,将 8042处理过的扫描码从它的寄存器中读取出来,继续进行下一步处理。
因此,8048是键盘上的芯片**,负责监控键盘哪个键被按下或者松开,并将扫描码发送给8042芯片
8042芯片
8042芯片负责接收来自键盘的扫描码,将它转换为标准的字符编码(如ASCII码),并保存在自己的寄存器中,然后向 8259A 发送中断信号,这样处理器便去执行键盘中断处理程序,将 8042处理过的扫描码从它的寄存器中读取出来,继续进行下一步处理。
如下所示,8042共有4个8位寄存器,这4个寄存器共用2个端口
8042是连接8048和处理器的桥梁,8042存在的目的是:为了处理器可以通过它控制8048的工作方式,然后让 8048的工作成果通过 8042回传给处理器。此时8042就相当于数据的缓冲区、中转站,根据数据被发送的方向,8042的作用分别是输入和输出。
键盘中断处理程序测试
本节我们将简单编写一个键盘驱动程序,用以测试键盘输入过程。我们首先梳理一下逻辑
- 当键盘键入按键后,8048芯片就将扫描码发送给8042,然后8042触发中断信号,接着触发中断处理程序
- 因此我们需要做的其实就是,编写键盘中断对应的中断处理程序,程序的逻辑就是读取8042接收到的扫描码,然后将其按照扫描码与键盘的对应关系显示在键盘上
- 由于最终目的是要编写键盘中断处理程序,我们需要首先在中断描述符表中添加键盘中断的中断描述符
- 要添加中断描述符,就要知道键盘中断对应的中断向量号
故而,我们的逻辑其实很简单
- 添加键盘中断的中断向量号
- 添加键盘中断处理程序
- 构建中断描述符
- 打开键盘中断
/kernel/kernel.S
首先在intr_entry_table
中添加键盘中断的中断处理程序入口,键盘中断的中断号是0x21,为方便后续代码编写,以下添加了所有的中断号
|
|
/kernel/interrupt.c
修改中断描述符的总数量,原来只有33个中断描述符
|
|
然后打开键盘中断
|
|
接下来编写键盘驱动程序
/device/keyboard.h
|
|
/device/keyboard.c
下述键盘驱动程序代码只做测试用,无论键盘的哪个按键被按下或者松开,都会只显示字符k
,并未对键盘按键的情况做处理,后续我们再修改键盘驱动程序
|
|
添加键盘中断初始化
/kernel/init.c
|
|
修改main.c测试键盘中断
|
|
编译运行
|
|
结果如下所示,我们按下了空格键
和字母q键
,由于按下和松开都会触发中断,因此每个按键会显示两次字符k
,故共有四个字符k
编写键盘驱动程序
上一小节,我们测试了键盘驱动程序的流程,在这一小节,我们修改键盘驱动程序,以实现当按键被按下时,屏幕上会显示对应的字符
/device/keyboard.c
数据准备与定义
按照扫描码表格定义每个扫描码对应的按键情况
|
|
其中不可见字符以及控制字符的显式定义宏为
|
|
接下来定义控制字符的通码和断码
|
|
由于控制字符常常和其余键作为组合键使用,因此需要记录控制字符的状态
|
|
键盘驱动程序逻辑
首先从8042芯片中读取扫描码,需要注意的是,8042
每次只接受一个字节的扫描码,但是有些按键的扫描码是两个字节,因此会触发两次中断,并向8042依次发送这两个字节的数据
因此,需要根据第一次接受到的扫描码是否是0xe0
,如果是,说明该按键的扫描码是由两个字节组成的,需要再次接受一个字节的扫描码,然后才能拼接出完整的两个字节的扫描码
|
|
接受到扫描码后需要判断是断码还是通码,然后分别进行处理,而由以上我们知道
断码=0x80+通码
因此有
|
|
如果是断码,说明松开了按键
- 如果松开的按键是字母键,则不进行处理
- 如果松开的是控制按键,则清除对应控制按键的状态(因为控制按键在按下的时候我们会置状态位,因此松开的时候需要清除)
|
|
以下是通码对应的处理逻辑
- 判断按键是否是控制键(ctrl、alt、shift、大写锁定键):如果是,说明用户可能在使用组合键,因此首先记录该控制按键的状态是被按下了,然后返回接受下一个中断的按键(这里我们并没有实现具体的组合键处理情况)
- 判断按键是否是特殊两个字母的键(和shift可以组合使用的键):如果是,则判断shift按键的状态是否被按下,如果被按下就打印转换的字符,如果shift状态没有被按下,就直接打印对应字符即可
- 判断正常字母按键:正常字母按键可能会和shift或者大写锁定键组合使用,但只有一个会起作用,但无论是哪个起作用,都将shift状态位置为1,表示接下来该字母输出的是大写
|
|
编译运行
如下是运行的结果,我在键盘输入的是“nihao hello world
”
可以看到程序正常显示了我的按键情况
环形输入缓冲区
到现在,我们的键盘驱动仅能够输出咱们所键入的按键,这还没有什么实际用途。
在键盘上操作是为了与系统进行交互,交互的过程一般是键入各种shell 命令,然后shel 解析并执行。
shell 命令是由多个字符组成的,并且要以回车键结束,因此咱们在键入命令的过程中,必须要找个缓冲区把已键入的信息存起来,当凑成完整的命令名时再一并由其他模块处理。
本节咱们要构建这个缓冲区
- 环形缓冲区本质上是用数组进行表示,并使用模运算实现区域的回绕
- 当缓冲区满时,要阻塞生产者继续向缓冲区写入字符
- 当缓冲区空时,要阻塞消费者取字符
以下是具体代码,实现较为简单,不再赘述细节
/device/ioqueue.h
|
|
/device/ioqueue.c
|
|
接下来我们需要进行测试
- 生产者自然是键盘驱动程序
- 为了模拟消费者,我们在main函数中添加两个子线程,两个线程都用于从缓冲区中取字符
- 由于有两个线程取字符,因此每次按下键盘后,字符可能由不同的线程接收并显示在屏幕,我们在代码中显示每次显示的字符是由哪个线程打印的
之前为了测试键盘中断,我们关闭了时钟中断,仅打开了键盘中断,而此时由于要使用子线程,因此我们需要开启时钟中断
/kernel/interrupt.c
|
|
/kernel/main.c
|
|
编译运行
|
|
以下是运行结果