PS/2 键盘
16.1 概述
正如我们之前看到的,UART 是一种双向通信协议,可用于接收和发送数据。它可以用来接收来自另一台计算机的字符,并解释为直接输入到我们的 8 位计算机中。
然而,这引发了几个问题。UART 主要用于发送 ASCII 字符(或原始数据字节),没有标准方式发送特殊的键盘按键,如 Ctrl、Alt 或功能键。此外,我们始终需要另一台计算机,或至少一台监视器来接收输入。
安装标准的 PS/2 键盘接口可以解决这两个问题。
16.2 PS/2 协议
PS/2 协议基于两条双向 漏极开路 线:一条时钟线和一条数据线。空闲时两条线均被上拉,因此当时钟信号处于下降沿时,数据线已准备好且稳定。
每当键盘上按下某个键时,键盘将开始切换时钟线,同时输出一个起始位、对应于按键代码的 8 位数据、一个停止位和一个奇偶校验位。因此,每次传输由 11 位组成。键盘会在时钟为高时准备好要发送的位,并在时钟为低时保持其稳定。下图展示了字节如何发送到主机:
当键盘上的按键释放时,键盘会向计算机发送一个额外的事务。该事务由一个 break 码(0xF0)后跟释放按键的代码组成。如果按下的键具有以 0xE0 开头的 2 字节扫描码,则 break 码 0xF0 紧跟在 0xE0 之后。
每个按键的代码取决于键盘扫描集编号。大多数 PS/2 键盘使用扫描集 2,我们将在本节其余部分以此为例。下表显示了每个按键的扫描码:
| 按键名称 | 按下码 | 释放码 | 按键名称 | 按下码 | 释放码 |
|---|---|---|---|---|---|
| ESC | 76 | F0 76 | K | 42 | F0 42 |
| F1 | 05 | F0 05 | L | 4B | F0 4B |
| F2 | 06 | F0 06 | ; | 4C | F0 4C |
| F3 | 04 | F0 04 | ' | 52 | F0 52 |
| F4 | 0C | F0 0C | 回车 | 5A | F0 5A |
| F5 | 03 | F0 03 | Shift (左) | 12 | F0 12 |
| F6 | 0B | F0 0B | Z | 1A | F0 1A |
| F7 | 83 | F0 83 | X | 22 | F0 22 |
| F8 | 0A | F0 0A | C | 21 | F0 21 |
| F9 | 01 | F0 01 | V | 2A | F0 2A |
| F10 | 09 | F0 09 | B | 32 | F0 32 |
| F11 | 78 | F0 78 | N | 31 | F0 31 |
| F12 | 07 | F0 07 | M | 3A | F0 3A |
| Prt Scr | E0 12 E0 7C | E0 F0 7C E0 F0 12 | , | 41 | F0 41 |
| Scroll Lock | 7E | F0 7E | . | 49 | F0 49 |
| Pause | E1 14 77 E1 F0 14 E0 77 | 无 | / | 4A | F0 4A |
| ` | 0E | F0 0E | Shift (右) | 59 | F0 59 |
| 1 | 16 | F0 16 | Ctrl (左) | 14 | F0 14 |
| 2 | 1E | F0 1E | Windows (左) | E0 1F | E0 F0 1F |
| 3 | 26 | F0 26 | Alt (左) | 11 | F0 11 |
| 4 | 25 | F0 25 | 空格 | 29 | F0 29 |
| 5 | 2E | F0 2E | Alt (右) | E0 11 | E0 F0 11 |
| 6 | 36 | F0 36 | Windows (右) | E0 27 | E0 F0 27 |
| 7 | 3D | F0 3D | 菜单 | E0 2F | E0 F0 2F |
| 8 | 3E | F0 3E | Ctrl (右) | E0 14 | E0 F0 14 |
| 9 | 46 | F0 46 | Insert | E0 70 | E0 F0 70 |
| 0 | 45 | F0 45 | Home | E0 6C | E0 F0 6C |
| - | 4E | F0 4E | Page Up | E0 7D | E0 F0 7D |
| = | 55 | F0 55 | Delete | E0 71 | E0 F0 71 |
| Backspace | 66 | F0 66 | End | E0 69 | E0 F0 69 |
| Tab | 0D | F0 0D | Page Down | E0 7A | E0 F0 7A |
| Q | 15 | F0 15 | 上箭头 | E0 75 | E0 F0 75 |
| W | 1D | F0 1D | 左箭头 | E0 6B | E0 F0 6B |
| E | 24 | F0 24 | 下箭头 | E0 72 | E0 F0 72 |
| R | 2D | F0 2D | 右箭头 | E0 74 | E0 F0 74 |
| T | 2C | F0 2C | Num Lock | 77 | F0 77 |
| Y | 35 | F0 35 | / (小键盘) | E0 4A | E0 F0 4A |
| U | 3C | F0 3C | * (小键盘) | 7C | F0 7C |
| I | 43 | F0 43 | - (小键盘) | 7B | F0 7B |
| O | 44 | F0 44 | 7 (小键盘) | 6C | F0 6C |
| P | 4D | F0 4D | 8 (小键盘) | 75 | F0 75 |
| [ | 54 | F0 54 | 9 (小键盘) | 7D | F0 7D |
| ] | 5B | F0 5B | + (小键盘) | 79 | F0 79 |
| \ | 5D | F0 5D | 4 (小键盘) | 6B | F0 6B |
| CapsLock | 58 | F0 58 | 5 (小键盘) | 73 | F0 73 |
| A | 1C | F0 1C | 6 (小键盘) | 74 | F0 74 |
| S | 1B | F0 1B | 1 (小键盘) | 69 | F0 69 |
| D | 23 | F0 23 | 2 (小键盘) | 72 | F0 72 |
| F | 2B | F0 2B | 3 (小键盘) | 7A | F0 7A |
| G | 34 | F0 34 | 0 (小键盘) | 70 | F0 70 |
| H | 33 | F0 33 | . (小键盘) | 71 | F0 71 |
| J | 3B | F0 3B | 回车 (小键盘) | E0 5A | E0 F0 5A |
16.3 硬件实现
Zeal 8-bit Computer 提供了一个 mini DIN-6 连接器,在 PCB 上标记为 J4,用于连接标准 PS/2 键盘。其数据和时钟线通过标记为 RN1、阻值为 1kΩ 的电阻网络上拉。
对于 Zeal 8-bit Computer,我们选择在硬件层面而非软件层面进行解码,并且仅使用 7400 系列逻辑芯片来实现。 虽然使用微控制器进行解码是可行且更直接的方式,但这会损害主板真实的复古感。此外,选择逻辑芯片而非微控制器确保了主板的透明度更高,因为微控制器可被视为具有固有复杂性的黑盒。
解码的第一部分由两个 74HC595 串行转并行移位寄存器组成,在 PCB 上标记为 U11 和 U12。
它们负责在按键按下或释放时存储来自键盘的传入字节。尽管它们总共可以存储 16 位,但最终只有 8 位被使用并连接到 CPU 数据总线。确实,使用全部 16 位将需要更多组件和更多的胶合逻辑。
此外,这些寄存器一次只能存储最多 8 位,它们内部没有任何 FIFO 或 RAM。这意味着如果 Z80 CPU 没有足够快地读取传入的字节,一旦发生另一个键盘事件,该字节就会丢失。
移位寄存器引脚排列
这些移位寄存器的引脚排列如下:
信号说明:
- $DATA\ 0$ 到 $DATA\ 7$:来自 PS/2 键盘的 8 位数据。这些直接连接到 Z80 CPU 数据总线。因此,只有当 $\overline{KB\_ENABLED}$ 有效(低电平)时,接收到的 8 位数据才会输出到总线上。
- $SER\ OUT$:这两个引脚连接在一起。当第一个移位寄存器
U11中移入超过 8 位时,最高位从该组件移出以接收新的位。该位通过此引脚输出。因此,该最高位成为第二个移位寄存器U12的最低位。 - $Parity$, $Stop$, $Start$:当 $\overline{KB\_ENABLED}$ 有效(低电平)时,这些引脚分别包含奇偶校验位、1 和 0。这些引脚在 Zeal 8-bit Computer 上未连接到任何地方。
- $PS/2\ DATA$:来自 PS/2 键盘的数据线。
- $KB\_CLOCK$:反相的 PS/2 时钟信号。由于 74HC595 需要高电平有效的时钟来移位输入数据,而 PS/2 时钟线是低电平有效,因此使用一个标记为
U7的反相器来反相时钟信号并将其连接到移位寄存器。 - $KB\_LATCH$:触发将移位后的位锁存到内部缓冲器的信号。该信号高电平有效,因此锁存在上升沿发生。有关更多信息,请查看下面的互补信号生成小节。
- $\overline{KB\_ENABLED}$:来自逻辑胶合电路的低电平有效信号。当 CPU 尝试读取当前存储在移位寄存器中的位时,该线为低电平。
- $\overline{CLR}$:低电平时清除内部存储的数据。在我们的情况下硬连线到 5V,不要尝试将其连接到任何其他信号。
- $5V$ 和 $Gnd$:电源。
- $NC$:未连接。
互补信号生成
需要为上述组件生成的第一个信号是 $KB\_LATCH$。确实,即使位已经移入寄存器,仍不足以将它们输出到数据总线上。需要将它们锁存到另一个内部缓冲器中,该缓冲器在 $\overline{KB\_ENABLED}$ 变为有效时将其内容输出到 $DATA\ 0$...$DATA\ 7$ 线上。
在 Zeal 8-bit Computer 上,$KB\_LATCH$ 通过一个电阻-电容电路(RC 电路)和一个反相器生成,该反相器使信号保持低电平,直到最后一位传输完成。下图展示了传输过程中锁存信号的样子:
一旦锁存信号变为高电平(上升沿),移位寄存器会将移位后的位锁存到其内部 8 位缓冲器中。
还有一个与移位寄存器不直接相关但仍然由 PS/2 解码电路生成的信号:$\overline{KB\ Signal}$,这是一个连接到 Z80 PIO 系统端口的低电平有效信号,用于在键盘有新字节移入并准备读取时通知 PIO。
为了生成此信号,使用了一个微分器,也由一个电容器和一个电阻器组成,以及一个同样标记为 U7 的反相器。
框图
以下框图总结了 PS/2 键盘、解码器和 PIO 之间的交互:
无源元件
为便于维护 PS/2 解码电路,下表总结了所使用的必要元件,包括它们在 PCB 上的参考标记及其各自的值:
| 元件 | 参考标记 | 值 | 描述 |
|---|---|---|---|
| 二极管 | D2 | 1N4148 | 用于 $KB\_LATCH$ |
| 二极管 | D3 | 1N4148 | 用于 $\overline{KB\ Signal}$ |
| 电容器 | C17 | 100nF | 用于 $KB\_LATCH$ |
| 电容器 | C18 | 100nF | 用于 $\overline{KB\ Signal}$ |
| 电阻器 | R8 | 2kΩ | 用于 $KB\_LATCH$ |
| 电阻器 | R9 | 470Ω | 用于 $\overline{KB\ Signal}$ |
| 电阻器 | R10 | 20kΩ | 用于 $\overline{KB\ Signal}$ |
16.4 时序
如前所述,当按键按下或释放时,键盘将切换时钟线并将实际的按键扫描码传输给主机。PS/2 协议规定时钟频率在 10KHz 到 16.7KHz 之间,这意味着一个时钟周期在 60µs 到 100µs 之间。
由于 $KB\_LATCH$ 信号是由时钟信号本身生成的,它也会受到频率的影响。然而,它的上升沿总是在最后一个时钟周期之后出现,因此它的持续时间永远不会短于一次传输。
类似地,$\overline{KB\_SIGNAL}$ 的下降沿总是发生在 $KB\_LATCH$ 变为高电平时。无论 PS/2 时钟频率如何,它都会保持低电平约 18µs。
下图总结了键盘解码器的时序,请注意这并非 100% 精确,因为时序取决于电路板上的电容和电阻:
其中:
- $p$ 是 PS/2 时钟信号的周期,$60µs ≤ p ≤ 100µs$
- $e$ 是时钟第一个下降沿与锁存信号下降沿之间的延迟,$e ≃5µs$
- $d$ 是最后一个时钟上升沿与锁存信号上升沿之间的延迟,实际约为 $104µs$
- $s$ 是 $\overline{KB\_SIGNAL}$ 的持续时间,$s ≃18µs$
注意:时序是在 13.15KHz 的 PS/2 键盘上测量的。
16.5 局限性
双向通信
PS/2 协议规定时钟线和数据线采用漏极开路配置,Zeal 8-bit Computer 也是如此。其主要目的是能够实现主机到设备的通信。实际上,主机应能够向键盘发送命令,告知其复位、重发上一个字节、打开/关闭 LED 等。
然而,由于 Zeal 8-bit Computer 上解码的方式(仅使用逻辑芯片)以及空间原因,通信是单向的,只能从键盘到 PIO/CPU。因此,无法告知键盘点亮"Num Lock"或"Caps Lock"等 LED。
键盘中断源
当键盘发送一个字节时,它会通过 $\overline{KB\_SIGNAL}$ 信号触发中断。然而,当 CPU 读取传入的字节时,该信号不会自行清除。因此,如果来自 PIO 系统端口的另一个中断源到来,PIO 将不会触发第二个中断,即使 CPU 已经完成并从中断服务程序(ISR)返回。
第一个中断的发生是因为 $\overline{KB\_SIGNAL}$ 信号变为有效(低电平),CPU 读取 PIO 系统端口的状态(值为 0x7F),处理键盘中断并通过 reti 指令从 ISR 返回。
之后,V-blank 信号变为有效,但由于 $\overline{KB\_SIGNAL}$ 仍然有效,PIO 不会触发第二个中断。因此,V-blank 中断将丢失且永远不会被处理。
📝 注意
假设 PIO 系统端口配置为位控制/逻辑或/低电平有效模式。
软件解决方法
解决此问题的最简单方法是每次只启用一个中断源,这样当中断发生时,就能明确是哪个设备触发的。
另一种解决方案是让 ISR 足够长,使得当 CPU 从中断返回时,键盘信号已无效(高电平)。这也意味着 ISR 必须检查在此期间是否有其他中断源变为有效。
键盘信号持续约 18µs,对应于 Zeal 8-bit Computer 上的 180 个 T 状态。
假设键盘和 V-blank 中断都已启用,ISR 将如下所示:
; Interrupt handler invoked by the CPU in mode 2
pio_isr:
; Use the CPU alternate registers for the ISR
ex af, af'
exx
; Read the state of the PIO system port to determine which line is low.
in a, (0xD1)
; Check for KB_SIGNAL (Bit 7)
bit 7, a
call z, keyboard_isr
; If we entered keyboard_isr routine, A contains the latest state of PIO System port
; Check for V-blank (Bit 6)
bit 6, a
call z, vblank_isr
; Return from the interrupt
exx
ex af, af'
ei
reti
keyboard_isr:
; Read byte received by the keyboard
in a, (0xE0)
; Handle the byte...
[...]
; Wait for the signal to go high
_keyboard_isr_low:
in a, (0xD1)
bit 7, a
ret nz
jr _keyboard_isr_low
vblank_isr:
; Handle V-Blank ISR, do not alter A
; Clear the V-blank interrupt!
ret
硬件解决方法
在硬件上,可以通过在二极管 D3 的阴极和移位寄存器 U12 的引脚 13($\overline{KB\_ENABLED}$)之间添加一个二极管(1N4148 类型)来克服此问题。
添加此二极管将使 $\overline{KB\_SIGNAL}$ 在 CPU 尝试读取刚接收到的字节时自动清除。
因此,ISR 不再需要等待键盘信号变为高电平:
; Interrupt handler invoked by the CPU in mode 2
pio_isr:
; Use the CPU alternate registers for the ISR
ex af, af'
exx
; Read the state of the PIO system port to determine which line is low.
in a, (0xD1)
; Only keep the bits we are interested in
and 0xc0
pio_isr_check:
; Check for KB_SIGNAL (Bit 7)
bit 7, a
call z, keyboard_isr
; Check for V-blank (Bit 6)
bit 6, a
call z, vblank_isr
; Check if any signal changed since the beginning
ld b, a
in a, (0xD1)
and 0xc0
cp b
; If any bit changed, handle the new signals
jr nz, pio_isr_check
; Return from the interrupt
exx
ex af, af'
ei
reti
keyboard_isr:
; Read byte received by the keyboard, this will
; automatically clear the signal
in a, (0xE0)
; Handle the byte...
[...]
ret
vblank_isr:
; Handle V-Blank ISR, do not alter A
; Clear the V-blank interrupt!
ret