键盘种类很多,XV6 里使用的是 QEMU 仿真出来的 PS/2 键盘。XV6 只是简单地实现了键盘输入的基本功能,因此对键盘控制器的设置保持默认设置,主要是检测是否有按键并读入按键扫描码。
在经过初始化之后,键盘的主要作用就是扫描按键动作,如果有按键动作则将其扫描码发送到键盘控制器 8042 芯片上。每个字母、数字、符号的按键扫描码并不能通过公式简单推算出来,必须通过查表才能知道。
每次按下一个键,将发送一个扫描码(make code);放开按键后,再发送一个扫描码(break code)。大多数 make code 是一个字节的,少部分两字节扫描码是 E0 开头的,还有一个四字节的扫描码(对应 pause 键)。大多数 break code 是将 F0 加上对应的 make code 而组成,扩展键则是 E0 + F0 再加上对应的 make code 组成。 下表是几个按键的 make code 和 break code。
Key | Make Code | Break Code |
---|---|---|
A | 1C | F0, 1C |
5 | 2E | F0, 2E |
F10 | 09 | F0, 09 |
Right Arrow | E0, 74 | E0, F0, 74 |
Right CTRL | E0, 14 | E0, F0, 14 |
对于一次按键 shift + g
动作,将由按下 shift(0x12)、按下 g(0x34)、释放 g(0xF0, 0x34)、 释放 shift(0xF0, 0x12)四个动作构成,即序列 0x12、0x34、0xF0、0x34、0xF0、0x12。如果持续按键,则会按一定的频率重复发送扫描码。如果有多个按键长时间按下,则会将重复发送最后一个按键的扫描码。
上述扫描码我们称为键盘扫描码集合 2(Keyboard Scan Codes: Set 2),键盘控制器 8042 接收到 Set 2 的扫描码后会转换成 Set 1 的扫描码, PC 驱动程序读到的扫描码是后者 Set 1 扫描码。
幸运的是在 XV6 的代码中无需处理扫描码的细节信息,因为我们是和键盘控制器 8042 通 信,而 8042 芯片会帮忙接收和处理按键的扫描码。8042 芯片有 4 个寄存器,分别是:
这些寄存器的 IO 端口地址请参见下表:
Port | Read / Write | Function |
---|---|---|
0x60 | Read | Read Input Buffer |
0x60 | Write | Write Output Buffer |
0x64 | Read | Read Status Register |
0x64 | Write | Send Command to Port |
其中对 0x64 的写操作可以发出 Read Command Byte 读操作,也可以发出 Write Command Byte 写操作。无论是往 0x64 里面写出 “读” 或 “写” 命令,都不会将数值写到哪个寄存器中,而是由 8042 芯片解释并执行,如果命令中有参数则通过 0x60 端口传送,反之如有返回数据也从 0x60 端口读回。
状态寄存器的 8 位标志定义如所示。
位 | 名称 | 说明 |
---|---|---|
7 | PERR | Parity Error,0 正常,1 奇偶检验错 |
6 | TO | Receive Timeout, 0 正常,1 表示键盘对命令的响应超时 |
5 | MOBF | Transmit Timeout,0 表示正常,1 表示 15 ms 内未收到键盘时钟信号 |
4 | INH | Inhibit flag,0 表示进制与键盘通信,1 表示可以与键盘通信 |
3 | A2 | Address line A2,8042 内部使用,0 表示上次写入 0x60,1 表示上次写入的是 0x64 |
2 | SYS | System flag,0 表示上电启动中,1 表示收到 BAT(Basic Assurance Test)自检结果 |
1 | IBF | Input Buffer Full,1 表示满,可以读入数据 |
0 | OBF | Output Buffer Full ,1 表示满,不可写出数据 |
例如从状态寄存器读入 0x64=00010100b
,则说明没有禁止键盘通信、8042 已经完成了 BAT(Basic Assurance Test) 自测。
当 8042 接收一个有效的扫描码后,转换成另一种扫描码存放在 0x60 端口,并设置 IBF 标志,发出 IRQ1 中断。8042 在接收到一个扫描码后,将停止键盘时钟禁止接收其他按键扫描码 ,直到输入寄存器清空。
IRQ1 中断对应 0x09 中断向量,驱动程序将从 0x60 读入按键扫描码,处理后写入键盘输入缓冲区。
键盘控制命令需要写入到 0x64 端口,如果有参数则需要随后写入到 0x60 中。只有当 8042 的 OBF 标志清零后才能发送下一个命令或参数。命令包括:
命令字节的定义如下表:
位 | 名称 | 说明 |
---|---|---|
7 | ||
6 | XLAT | |
5 | _EN2 |
Disable Mouse,0 表示使能鼠标 Auxillary PS/2 接口正常,1 表示禁止 |
4 | _EN |
Inhibit flag,0 表示禁止与键盘通信,1 表示可以与键盘通信 |
3 | Address line A2,8042 内部使用,0 表示上次写入 0x60,1 表示上次写入的是 0x64 | |
2 | SYS | System flag,0 表示让POST 电路上电自测,1 表示收到 BAT 后的热启动 |
1 | INT2 | Mouse Input Buffer Full Interruption,0 表示禁止鼠标 Auxillary IBFvsdr ,1 表示使能 |
0 | INT | Input Buffer Full Interruption,0 表示禁止 IBF 中断,1 表示使能 IBF 中断 |
驱动对键盘控制器发送命令是通过写端口 64h 实现的,共有 12 条命令,分别为:
8042 可以向键盘设备发送命令,例如可以将键盘上的 LED 等点亮等。共有 10 条命令,分别为:
00h/FFh 当击键或释放键时检测到错误时,则在 Output Bufer 后放入此字节,如果 Output Buffer 已满,则会将 Output Buffer 的最后一个字节替代为此字节。使用 Scan code set 1 时使用 00h,Scan code 2 和 Scan Code 3 使用 FFh。
AAh BAT 完成代码。如果键盘检测成功,则会将此字节发送到 8042 Output Register 中。
EEh Echo 响应。Keyboard 使用 EEh 响应从 60h 发来的 Echo 请求。
F0h 在 Scan code set 2 和 Scan code set 3 中,被用作 Break Code 的前缀。
FAh ACK。当 Keyboard 任何时候收到一个来自于 60h 端口的合法命令或合法数据之后,都 回复一个 FAh。
FCh BAT 失败代码。如果键盘检测失败,则会将此字节发送到 8042 Output Register 中。
FEh Resend。当 Keyboard 任何时候收到一个来自于 60h 端口的非法命令或非法数据之后, 或者数据的奇偶交验错误,都回复一个 FEh,要求系统重新发送相关命令或数据。
83ABh 当键盘收到一个来自于 60h 的 F2h 命令之后,会依次回复 83h,ABh。83AB 是键 盘的 ID。
Scan code 除了上述那些特殊字节以外,剩下的都是 Scan code。
端口的读写操作,驱动中使用函数 READ_PORT_UCHAR 进行读操作,READ_PORT_UCHAR 中使用 CPU 读端口指令 in
。驱动中使用函数 WRITE_PORT_UCHAR 进行写操作, WRITE_PORT_UCHAR 中使用 CPU 写端口指令 out
。
读取状态寄存器
读取状态寄存器的方法,对 64h 端口进行读操作。
读数据
需要读取的数据有,i8042 从 i8048 得到的按键的扫描码,i8042 命令的 ACK,i8042 从 i8048 得到的 i8048 命令的 ACK,需要命令重发的 RESEND,一些需要返回结果的命令得到的结果。
当有数据需要被驱动读走的时候,数据被放入输出缓冲器,同时将状态寄存器的 bit0 (OUTPUT_BUFFER_FULL)置 1,引发键盘中断(键盘中断的 IRQ 为 1)。由于键盘中断,引起 由键盘驱动提供的键盘中断服务例程被执行。在键盘中断服务例程中,驱动会从 i8042 读走数 据。一旦数据读取完成,状态寄存器的 bit0 会被清 0。
读数据的方法,首先,读取状态寄存器,判断 bit0,状态寄存器 bit0 为 1,说明输出缓冲 器中有数据。保证状态寄存器 bit0 为 1,然后对 60h 端口进行读操作,读取数据。
这里我们要谈一点很有用的题外话,前面提到的 IRQ,是 Interrupt Request line,中断请 求线,是一个硬件线,它和中断向量是不同的。中断向量是用来在中断描述符表(IDT)中查找中 断服务例程的那个序号。键盘的 IRQ 是 1,键盘中断服务例程对应的中断向量可不是 1。这点 要弄清楚。
向 i8042 发命令
当命令被发往 i8042 的时候,命令被放入输入缓冲器,同时引起状态寄存器的 Bit1 置 1, 表示输入缓冲器满,同时引起状态寄存器的 Bit2 置 1,表示写入输入缓冲器的是一个命令。
向 i8042 发命令的方法,首先,读取状态寄存器,判断 bit1,状态寄存器 bit1 为 0,说明 输入缓冲器为空,可以写入。保证状态寄存器 bit1 为 0,然后对 64h 端口进行写操作,写入命 令。
间接向 i8048 发命令
向 i8042 发这些命令,i8042 会转发 i8048,命令被放入输入缓冲器,同时引起状态寄存器 的 Bit1 置 1,表示输入缓冲器满,同时引起状态寄存器的 Bit2 置 1,表示写入输入缓冲器的 是一个命令。这里我们要注意,向 i8048 发命令,是通过写 60h 端口,而后面发命令的参数, 也是写 60h 端口。i8042 如何判断输入缓冲器中的内容是命令还是参数呢,我们在后面发命令 的参数中一起讨论。
向 i8048 发命令的方法,首先,读取状态寄存器,判断 bit1,状态寄存器 bit1 为 0,说明输 入缓冲器为空,可以写入。保证状态寄存器 bit1 为 0,然后对 60h 端口进行写操作,写入命令。
发命令的参数
某些命令需要参数,我们在发送命令之后,发送它的参数,参数被放入输入缓冲器,同时 引起状态寄存器的 Bit1 置 1,表示输入缓冲器满。这里我们要注意,向 i8048 发命令,是通过 写 60h 端口,发命令的参数,也是写 60h 端口。i8042 如何判断输入缓冲器中的内容是命令还 是参数呢。i8042 是这样判断的,如果当前状态寄存器的 Bit3 为 1,表示之前已经写入了一个 命令,那么现在通过写 60h 端口放入输入缓冲器中的内容,就被当做之前命令的参数,并且 引起状态寄存器的 Bit3 置 0。如果当前状态寄存器的 Bit3 为 0,表示之前没有写入命令,那 么现在通过写 60h 端口放入输入缓冲器中的内容,就被当做一个间接发往 i8048 的命令,并且 引起状态寄存器的 Bit3 置 1。
向 i8048 发参数的方法,首先,读取状态寄存器,判断 bit1,状态寄存器 bit1 为 0,说明输 入缓冲器为空,可以写入。保证状态寄存器 bit1 为 0,然后对 60h 端口进行写操作,写入参数。
变量定义均在 kbd.h。
normalmap[256] 对应直接按键。
shiftmap[256] 对应 shift 组合键
ctlmap[256] 对应 CTRL 组合按键。
这三个表都是以键盘扫描码集合 1(Keyboard Scan Codes: Set 1)转换为按键的 ASCII 码的。例如, Set 1 中按键 1
的 make 码是 0x02,break 码是 0x82(0x02 + 0x80); 按键 A
的 make 码是 0x1E,break 码是 0x9E(0x1E + 0x80)。
扫描码到 ASCII 转换的码表有三个,normalmap[256]
、shfitmap[256]
和 ctlmap[256]
,分别 对应于单键动作、SHIFT 组合按键、CTRL 组合按键的 ASCII 码。
CAPSLOCK、NUMLOCK 和 SCROLLLOCK 三个按键每按一次状态反转一次,因此使用 toggledmap[] 码表进行转换,结果记录到 shift 变量中。
这些转换表都是根据 set 1 扫描码而产生的,set 1 扫描码的示例如下图:
Key | Make | Break Code |
---|---|---|
A | 1E | 9E |
1 | 02 | 82 |
L SHIFT | 2A | AA |
R SHIFT | 36 | B6 |
L ALT | 38 | B8 |
R ALT | E0, 38 | E0, B8 |
L CTRL | 1D | 9D |
R CTRL | E0, 1D | E0, 9D |
例如按键 1
的 set 1 扫描码是 0x02,因此 normalmap[02]=’1’
,同理有按键 A
对应normalmap[0x1E]=’a’
。 也就说,将 set 1 扫描码作为数组下标,就能从 normalmap[]
中读取到相应按键的 ASCII 码值。
功能实现均在 kdb.c 。
kdb.c
中就只有两个函数,一个是键盘中断服务函数 kdintr(),另一个就是读取按键的 kbdgetc()。
kbdgetc()
函数读取的按键扫描码保存在 data 中,SHIFT / CTRL 等按键状态保存在 shift 静态变量中,根据情况不同需要做不同处理:
shift |= E0ESC
记录,返回 0 值。data & 0x7F
取扫描码的低 7 位(相当于转换成 make 码)。清除 shift 对应的 SHIFT、CTL、ALT 位,清除 shift 中的 E0ESC 标志。data |= 0x80
,然后清除 shift 中的 E0ESC 标志。从上面可以看出 E0 扫描码仅能使 shift 的 E0ESC 位有效一次,然后会被清除。
如果通过 togglecode[] 码表发现有 CAPSLOCK、NUMLOCK 和 SCROLLLOCK 按键,则进行变换。
通过 shiftcode[] 码表检查和记录 shift
、crtl
和 alt
的按键情况。
将处理后的扫描码经过 charcode[][] 表转换成 ASCII 码。
最后如果 CAPSLOCK 标记有效且 ASCII 字符是英文字符,则进行大小写转换,a~z
转换成 A~Z
,反之如果扫描码转换后是 A~Z
(例如 shift+’a’
~ shift+’z’
)则转换成 a~z
。