Full picture
16

PS/2 键盘

16.1 概述

正如我们之前看到的,UART 是一种双向通信协议,可用于接收和发送数据。它可以用来接收来自另一台计算机的字符,并解释为直接输入到我们的 8 位计算机中。

然而,这引发了几个问题。UART 主要用于发送 ASCII 字符(或原始数据字节),没有标准方式发送特殊的键盘按键,如 Ctrl、Alt 或功能键。此外,我们始终需要另一台计算机,或至少一台监视器来接收输入。

安装标准的 PS/2 键盘接口可以解决这两个问题。

16.2 PS/2 协议

PS/2 协议基于两条双向 漏极开路 线:一条时钟线和一条数据线。空闲时两条线均被上拉,因此当时钟信号处于下降沿时,数据线已准备好且稳定。

每当键盘上按下某个键时,键盘将开始切换时钟线,同时输出一个起始位、对应于按键代码的 8 位数据、一个停止位和一个奇偶校验位。因此,每次传输由 11 位组成。键盘会在时钟为高时准备好要发送的位,并在时钟为低时保持其稳定。下图展示了字节如何发送到主机:

PS/2 设备到主机

当键盘上的按键释放时,键盘会向计算机发送一个额外的事务。该事务由一个 break 码(0xF0)后跟释放按键的代码组成。如果按下的键具有以 0xE0 开头的 2 字节扫描码,则 break0xF0 紧跟在 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 上标记为 U11U12。 它们负责在按键按下或释放时存储来自键盘的传入字节。尽管它们总共可以存储 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 电路)和一个反相器生成,该反相器使信号保持低电平,直到最后一位传输完成。下图展示了传输过程中锁存信号的样子:

PS/2 锁存信号

一旦锁存信号变为高电平(上升沿),移位寄存器会将移位后的位锁存到其内部 8 位缓冲器中。

还有一个与移位寄存器不直接相关但仍然由 PS/2 解码电路生成的信号:$\overline{KB\ Signal}$,这是一个连接到 Z80 PIO 系统端口的低电平有效信号,用于在键盘有新字节移入并准备读取时通知 PIO。

PS/2 就绪信号

为了生成此信号,使用了一个微分器,也由一个电容器和一个电阻器组成,以及一个同样标记为 U7 的反相器。

框图

以下框图总结了 PS/2 键盘、解码器和 PIO 之间的交互:

键盘框图
PS/2 解码电路框图

无源元件

为便于维护 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µs100µ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)返回。

键盘时序
PS/2 解码器中断时序图

第一个中断的发生是因为 $\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 类型)来克服此问题。

键盘修复
PS/2 解码器硬件解决方法

添加此二极管将使 $\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
EN | 中文Beta