到目前为止,我们的操作系统只能输出而不能输入。本章将要实现的是键盘驱动,其能让我们的操作系统接收键盘输入。
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 |
从上表可以看出:
- 所有的通码与断码之间都相差
0x80
- 键盘只负责产生键盘扫描码,不处理大小写,上挡键等。这部分功能由键盘驱动完成
15.2 键盘驱动的实现
键盘驱动的实现分为以下三个步骤:
- 向8259A发送中断响应信号
- 从
0x60
端口读取键盘扫描码 - 实现一个函数,处理键盘扫描码。本章中,键盘驱动的目标是打印输入的键(如果输入的键是可打印字符的话)
请看本章代码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键混合在一起的逻辑比较复杂,描述如下:
- Shift键影响所有的键
- CapsLock键只影响字母键
- 这两个键之间是异或关系,只能二选一。例如:如果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
用于测试键盘驱动。