一个操作系统的设计与实现——第15章 键盘驱动(上)

发布时间 2023-11-12 09:56:33作者: 樱雨楼

到目前为止,我们的操作系统只能输出而不能输入。本章将要实现的是键盘驱动,其能让我们的操作系统接收键盘输入。

15.1 键盘驱动的原理

当按下键盘上的键时,发生了什么呢?原来,每当按下键盘上的键,键盘都会发起至少一次键盘中断;每当一个键弹起时,键盘又会发起至少一次键盘中断;如果一直按住一个键不松手,键盘就会连续不断的发起键盘中断。

键盘接在8259A主片的第二个接口上,所以,想要接收到键盘中断,就需要取消对这个接口的中断屏蔽。

当一个键被按下或弹起后,可以从0x60端口读取到一个数字,其被称为键盘扫描码(Keyboard scancode)。0x60端口是一个8位的端口,但键盘扫描码不一定是8位的,还有可能是16位的,甚至更多。对于此类多字节的键盘扫描码,键盘会连续多次发起中断,每个字节发起一次。

在计算机的发展过程中,键盘扫描码一共出现了三套,但我们无需关注此事,这是因为不管键盘实际使用的是哪一套键盘扫描码,其最终都会被转码为第一套键盘扫描码,然后存储到0x60端口以供读取。

上文提到,键盘上的键被按下和弹起时,都会发起中断。对于同一个键,其被按下和弹起时产生的键盘扫描码是不同的,分别被称为通码(Make code)和断码(Break code)。

完整的键盘扫描码表可以参考这个网页:https://wiki.osdev.org/PS/2_Keyboard#Scan_Code_Set_1。此外,笔者发现一些书籍和互联网上关于```/~``这个键的断码常有误,请读者知悉。

我们的操作系统只支持主键盘上的键盘扫描码,如下表所示:

按键 通码 断码
ESC 0x1 0x81
1 0x2 0x82
2 0x3 0x83
3 0x4 0x84
4 0x5 0x85
5 0x6 0x86
6 0x7 0x87
7 0x8 0x88
8 0x9 0x89
9 0xa 0x8a
0 0xb 0x8b
- 0xc 0x8c
= 0xd 0x8d
Backspace 0xe 0x8e
Tab 0xf 0x8f
Q 0x10 0x90
W 0x11 0x91
E 0x12 0x92
R 0x13 0x93
T 0x14 0x94
Y 0x15 0x95
U 0x16 0x96
I 0x17 0x97
O 0x18 0x98
P 0x19 0x99
[ 0x1a 0x9a
] 0x1b 0x9b
Enter 0x1c 0x9c
Left Ctrl 0x1d 0x9d
A 0x1e 0x9e
S 0x1f 0x9f
D 0x20 0xa0
F 0x21 0xa1
G 0x22 0xa2
H 0x23 0xa3
J 0x24 0xa4
K 0x25 0xa5
L 0x26 0xa6
; 0x27 0xa7
' 0x28 0xa8
` 0x29 0xa9
Left Shift 0x2a 0xaa
\ 0x2b 0xab
Z 0x2c 0xac
X 0x2d 0xad
C 0x2e 0xae
V 0x2f 0xaf
B 0x30 0xb0
N 0x31 0xb1
M 0x32 0xb2
, 0x33 0xb3
. 0x34 0xb4
/ 0x35 0xb5
Right Shift 0x36 0xb6
*(小键盘) 0x37 0xb7
Left Alt 0x38 0xb8
Space 0x39 0xb9
CapsLock 0x3a 0xba

从上表可以看出:

  1. 所有的通码与断码之间都相差0x80
  2. 键盘只负责产生键盘扫描码,不处理大小写,上挡键等。这部分功能由键盘驱动完成

15.2 键盘驱动的实现

键盘驱动的实现分为以下三个步骤:

  1. 向8259A发送中断响应信号
  2. 0x60端口读取键盘扫描码
  3. 实现一个函数,处理键盘扫描码。本章中,键盘驱动的目标是打印输入的键(如果输入的键是可打印字符的话)

请看本章代码15/Keyboard.h

第5行,声明了keyboardDriver函数。

接下来,请看本章代码15/Keyboard.hpp

第7~15行,定义了__KEYBOARD_MAP_LIST变量,该变量定义了键盘扫描码和字符之间的关系。这是一个二维数组,第一维的索引值使用键盘扫描码;第二维的索引值使用0或1,表示上档状态。对于那些不可打印的字符,如Shift键等,在表格中以{'\0', '\0'}占位。

第17~18行,定义了两个布尔值,分别用于表示Shift键和CapsLock键的状态。

keyboardDriver函数是键盘驱动的核心,其用于处理键盘扫描码。

第22~25行,处理Shift键。Shift键有左右两个,其扫描码不同;并且,无论是通码还是断码,都意味着Shift键的状态发生了一次改变。

第26~29行,处理CapsLock键。CapsLock键与Shift键不同,它是按一下切换一次状态。所以,只需要关注CapsLock键的通码。

第30~41行,处理其他键。Shift键与CapsLock键混合在一起的逻辑比较复杂,描述如下:

  1. Shift键影响所有的键
  2. CapsLock键只影响字母键
  3. 这两个键之间是异或关系,只能二选一。例如:如果CapsLock键已经被按下,再按住Shift键,打出的字母就是小写字母

第32~35行,使用一个很长的逻辑表达式,将键盘扫描码转换成ASCII码。

第37~40行,判断这个ASCII码是否可打印,如果是,就打印这个字符。

接下来,请看本章代码15/Int.s

第5行,声明了外部链接的keyboardDriver函数。

第37行,将发送给8259A主片的中断屏蔽掩码从0xfe改成了0xfc,这样就打开了键盘中断。

第93行,删除了intTmpl 0x21宏展开,其将被intKeyboard函数代替。

intKeyboard函数是键盘中断处理函数。

第157~159行,向8259A发送中断响应信号。

第161~164行,从0x60端口读取键盘扫描码,然后调用keyboardDriver函数。

第168行,使用iret指令从中断返回。

第249行,将intKeyboard函数安装在intList中,从而,键盘驱动就会被Int.hpp中的__installIDT函数安装到IDT中。

15.3 测试

本章代码15/Kernel.c用于测试键盘驱动。