PS/2 协议详解
目前市面上的资料,大多数是对PS/2键盘的通信过程做了介绍,但是PS/2和Windows在开机阶段是怎么握手上的,却没有详细地介绍,本文弥补了这一点。
参考网站
- 通信协议——PS2
- PS2 设备 通信协议
- pdf 第一章 PS/2鼠标、键盘协议
- PS/2键盘的编码与命令集
- OS Dev.org PS/2 Keyboard
- EC 所需知識 - PS2
- PS2_Keyboard.pdf
- 进阶项目(12)PS2键盘驱动程序设计讲解
- PS2 键盘在win10下开机过程 收发的数据
- pdf 第一章 PS/2 鼠标 键盘协议 中文版f:\0003_DATA\datasheet\ps2键盘鼠标协议中文版.pdf
1 通信概述
PS/2 协议使用两根线 CLK(时钟线)和 DAT(数据线),与 IIC 协议有一定相似性,是一种串行通信协议。PS/2 设备主要包括键盘和鼠标,本文重点介绍键盘,但键盘和鼠标在通信协议上大致相同。
在 PS/2 通信中,主机和设备之间可以进行双向数据传输,即主机可以向设备发送数据,设备也可以向主机发送数据。CLK 和 DAT 在空闲状态下都为高电平,只有当它们处于高电平时,才能够开始数据的发送,否则会出现通信抑制的情况,此时不能发送数据。
在设备主动发送数据时,设备会先拉低 DAT 线,再拉低 CLK 线,然后由设备产生时钟信号来发送数据;而主机主动发送数据时,主机会先拉低 CLK 线,再拉低 DAT 线,之后由设备产生时钟信号,主机进行数据发送。
对于数据的读取时机,设备主动发送的数据在 CLK 的下降沿被读取,主机主动发送的数据在 CLK 的上升沿被读取。并且,一般不会在边沿产生时立刻开始读取或发送数据,而是在边沿产生后延时一段时间再进行操作,以确保数据的稳定。具体来说,设备主动发送时,在 CLK 低电平时,DAT 线上的数据是稳定的;而主机主动发送时,在 CLK 高电平时,DAT 线上的数据是稳定的。
2 时序详解
PS/2 协议的数据传输时序包括 1 个起始位、8 个数据位、1 个校验位、1 个停止位和 1 个应答位,具体如下:
- 起始位:低电平
- 数据位:LSB,先发送低位
- 校验位:奇校验
- 停止位:高电平
- 应答位:低电平,设备接收到主机数据后才需要应答,主机接收到设备数据无需应答
2.1 主机主动发送
发送一个字节数据过程:
- CLK 先拉低,延时,然后拉低 DAT
- 释放 DAT
- 等待设备
设备主动发送
发送一个字节数据过程:
- 等待 CLK 为高电平后,检查数据线是否为高电平,如果是,继续下面的操作
- 延时 40us,发送起始位 0
- 由低位开始发送 8 位的数据位
- 发送奇校验位
- 发送停止位
发送一个位的过程:(保证在拉低 CLK 的时候 DAT 已经准备好了)
- DAT 准备好数据位,延时 10us
- 拉低 CLK,延时 20us
- 释放 CLK,延时 10us
- 检查 CLK 是否为高(空闲状态),如果是,则返回正确
3 硬件连接
在硬件连接方面,对于主机(例如 x86 设备),一般通过一个 SIO 芯片提供两个引脚(CLK 和 DAT)来进行 PS/2 通信。这两个引脚的资源在 BIOS 中需要被配置为 PS/2 模式,这样在开机初始化时系统才会尝试和设备进行握手操作。同时,CLK 和 DAT 线应进行外部上拉处理。如果使用单片机模拟键盘,接入 CLK 和 DAT 的引脚要配置为开漏模式。
4 通信过程
4.1 初始化
PS/2 设备和主机之间需要进行握手操作,只有握手成功后,设备才能正常使用。需要注意的是,PS/2 设备不支持热拔插。
以 Windows 10 主机接入 PS/2 键盘,开机初始化的过程为例: 主机会先尝试告知设备其要发送数据,具体操作是先下拉 CLK 线进行通讯抑制,然后下拉 DAT 线表示要发送数据,接着释放 CLK 线,等待设备产生 CLK 线上的时钟信号。主机根据设备产生的时钟信号进行数据发送,通常发送一个字节的信息,例如 0xEE。设备接收到主机发送的这个字节后,需要进行应答。应答的数据一般是 0xFA,但存在一些例外情况:如果接收到的信息是 0xEE,则需要应答 0xEE(EE 表示 echo);如果接收到 0xFF,则需应答 0xFA,并在 500ms 之内设备发送 0xAA(FF 是复位命令,该命令的正确应答对于键盘能否初始化成功至关重要)。
4.2 常态
在设备和主机握手成功之后,进入常态。在常态下,设备可以向主机发送键码,告知主机用户按下了键盘的哪个键,同时主机也可以向设备发送数据进行一些设置操作。
5 bios 是否配置 或 引脚是否正常判断
将正常的 PS/2 键盘接入系统,正常情况下键盘应该能够正常使用,且在设备管理器中的键盘一项可以看到 PS/2 键盘。可以使用示波器监视 CLK 和 DAT 线,在系统开机过程中,主机应该有尝试发送数据的动作(即先拉低 CLK 线然后拉低 DAT 线),如果没有这个动作,则可能是 BIOS 没有正确配置或者 SIO 芯片存在虚焊等问题。
6 初始化成功判断
如果握手没有成功,在进入系统后,CLK 线可能会被 SIO 主动拉低,从而抑制通信。如果握手成功了,在设备管理器中(以 Windows 系统为例)的键盘选项中,可以看到一个 PS/2 的设备。
7 键码
目前使用的 PS/2 键盘大多默认采用第 2 套扫描码集(可参考https://wiki.osdev.org/PS/2_Keyboard)。PS/2 键盘的键码有通码和断码之分,按下按键时发送通码,松开按键时发送断码。
例如,要模拟 Win 键的触发,使用正常的 PS/2 键盘进行测试时可以发现,按下 Win 键必须在松开后才会弹出开始菜单,这意味着首先要模拟 Win 键的通码发送,然后再模拟断码发送。通过查表可以得到,需要先发送 0xE0,0x1F,然后发送 0xE0,0xF0,0x1F。
需要先发送 0xE0,0x1F
8 代码
- 下面代码是 STC8 模拟 PS/2 键盘的示例,包括开机初始化键盘的握手,以及键码的发送
注意 delay 函数延时的准确性,如果不准确会造成一些 bug,这方面和编译优化选项也有关系,最好使用示波器实际校准下。 KDAT 和 KCLK 为 STC8 连接到 PS/2 接口的管脚。
8.1 读取单个位
unsigned char ReadBit() {
Delay_us(20);
KCLK = 0; // 设备拉低时钟线
Delay_us(40);
KCLK = 1; // 释放时钟线
Delay_us(20);
return KDAT; // 返回数据线状态
}
8.2 发送单个位
char SendBit(unsigned char b) {
KDAT = b; // 准备数据位
Delay_us(20);
KCLK = 0; // 拉低时钟线
Delay_us(40);
KCLK = 1; // 释放时钟线
Delay_us(20);
return KCLK ? 0 : -1;// 检测时钟抑制
}
8.3 计算校验位
unsigned char CalculateParity(unsigned char dat) {
unsigned char parity = 1, i;
for(i=0; i<8; i++) parity ^= (dat >> i) & 1;
return parity;
}
8.4 发送应答位
void SendAck() {
Delay_us(15);
KDAT = 0; // 拉低数据线
Delay_us(5);
KCLK = 0; // 拉低时钟线
Delay_us(40);
KCLK = 1; // 释放时钟线
Delay_us(5);
KDAT = 1; // 释放数据线
}
8.5 读取一个字节
char RecvByte() {
unsigned char b, parity, stop;
unsigned char tmp = 0;
unsigned char i;
// 等待时钟线高电平(主机释放时钟)
// while(!KCLK);
unsigned int wait_start = systick;
while(!KCLK) {
if (systick - wait_start > 5) { // 等待5ms超时
return -1; // 超时退出
}
}
Delay_us(300);
// if(!KCLK) return -1;
// 检查起始位(数据线必须为低)
if(KDAT != 0) return -1;
// 读取8位数据
for(i=0; i<8; i++) {
b = ReadBit();
if(!KCLK) return -2; // 检测时钟抑制
tmp |= (b << i);
}
// 读取校验位
parity = ReadBit();
// 读取停止位
stop = ReadBit();
// 处理停止位错误
if(stop != 1) {
while(!(stop = ReadBit())); // 持续读取直到高电平
return -3;
}
// 发送应答信号
SendAck();
// // 校验数据完整性
// if(CalculateParity(tmp) != parity) return -4;
// 最终延时
Delay_us(45);
recvData = tmp; // 将接收到的数据存储到全局变量
// *data = tmp;
return 0;
}
8.6 发送一个字节
char SendByte(unsigned char dat) {
unsigned char i;
// 等待时钟线高电平
// while(KCLK == 0);
unsigned int wait_start = systick;
while(!KCLK) {
if (systick - wait_start > 5) { // 等待5ms超时
return -1; // 超时退出
}
}
Delay_us(40);
if(KCLK == 0) return -1; // 检测时钟线状态
// 检查数据线状态
if(KDAT == 0) return -1;
// 发送起始位
Delay_us(5);
if(SendBit(0)) return -1;
// 发送数据位
for(i=0; i<8; i++) {
if(SendBit((dat >> i) & 1)) return -1;
}
// 发送校验位
if(SendBit(CalculateParity(dat))) return -1;
// 发送停止位
if(SendBit(1)) return -1;
Delay_us(50);
return 0;
}
8.7 接收数据与应答(可用于初始化的握手)
bit last_KDAT = 1; // 上次KDAT状态
unsigned char recvData = 0; // 接收数据存储变量
void main(){
last_KDAT = KDAT;
while(1){
// 接受主机发送的数据
if(KDAT==0 && last_KDAT==1){//KDAT被拉低,主机可能要发送数据
if(RecvByte()==0){
SendByte(0xFA); // 发送确认
if(recvData == 0xFF){
Delay_ms(360);
SendByte(0xAA); // 发送复位确认
}
}
}
last_KDAT = KDAT;
}
}
8.8 键码的发送
- 以发送 win 键为例
- 通码和断码之间延时可适当拉长
void SendWinKey() {
SendByte(0xE0); // 扩展标记
Delay_ms(3);
SendByte(0x1F); // 左Win键通码
Delay_ms(3);
SendByte(0xE0);
Delay_ms(3);
SendByte(0xF0);
Delay_ms(3);
SendByte(0x1F);
}
9 bug 说明
9.1 bug1——没有等待主机进入数据发送模式
-
具体描述: 在主机主动发送数据时,主机先拉低 CLK 线,再拉低 DAT 线,然后释放 CLK 线示意设备可以产生时钟信号,设备产生时钟信号后主机发送数据。但从主机释放 CLK 线后,如果设备立刻产生时钟信号,主机可能来不及转换到数据发送模式,从而导致通信出现问题。
-
解决方式: 设备检测到主机释放 CLK 后,等待 300us,再检测起始位,然后进行数据的读取(数据读取时设备发送时钟)。
-
参考 patch:
diff --git a/main.c b/main.c
index 9954251..a4ac5b7 100644
--- a/main.c
+++ b/main.c
@@ -269,6 +269,7 @@ char RecvByte() {
// 接收完整数据包
char RecvByte() {
unsigned char b, parity, stop;
unsigned char tmp = 0;
unsigned char i;
// 等待时钟线高电平(主机释放时钟)
while(!KCLK);
+ Delay_us(300);
// 检查起始位(数据线必须为低)
if(KDAT != 0) return -1;
// 读取8位数据
for(i=0; i<8; i++) {
b = ReadBit();
if(!KCLK) return -2; // 检测时钟抑制
tmp |= (b << i);
}
// 读取校验位
parity = ReadBit();
9.2 bug2——握手超时导致主机死机
-
具体描述: 在 S5 关机或者 S4 休眠唤醒后,系统没有出现死机或者卡顿现象,而在 S3 睡眠唤醒后,出现了死机和卡顿情况。经排查,发现该问题与 PS/2 设备有关。原因是设备在接收到主机发送的 FF 复位信号后,虽然回复了 FA 和 AA,但是 AA 距离 FF 的时间超过了 500ms。PS/2 协议规定,设备在接收到主机发送的 FF 复位信号后,应该在 500ms 内回复 AA。推测是主机在发送 FF 后,收到了设备的 FA,但是由于 AA 超时,主机没有接收到。虽然按常理即使超时也不应该导致卡顿,但可能存在 PS/2 相关中断的 bug。
-
解决方式: 设备接收到主机发的 FF 复位命令后,及时回复,并且保证在 500ms 之内回复 AA
-
参考 patch:
diff --git a/main.c b/main.c
index 5fea6f4..1eea803 100644
--- a/main.c
+++ b/main.c
@@ -402,7 +402,7 @@ void main()
SendByte(0xFA); // 发送确认
if(recvData == 0xFF){
- Delay_ms(500);
+ Delay_ms(360);
SendByte(0xAA); // 发送复位确认
}
}
9.3 bug3——没有超时检测机制导致的死循环问题
-
具体描述: 设备检测到 DAT 线被拉低,意味着主机准备接受数据,所以设备要等待主机释放时钟线。但有时候主机没有释放时钟线,单纯使用 while 循环会导致程序一直在等待时钟线的释放,从而进入死循环。
-
解决方式:加入超时机制
-
参考 patch:
// while(!KCLK);
unsigned int wait_start = systick;
while(!KCLK) {
if (systick - wait_start > 5) { // 等待5ms超时
return -1; // 超时退出
}
}
10 实际波形分析
以下波形为真实的 PS/2 键盘的波形。
10.1 S3 睡眠唤醒
- 主机发送 FF -> 键盘应答 FA
- 360ms 后
- 键盘主动发送 AA
- 主机发送 F3 -> 键盘应答 FA
- 主机发送 10 -> 键盘应答 FA
- 主机发送 ED -> 键盘应答 FA
- 主机发送 01 -> 键盘应答 FA
- 主机发送 ED -> 键盘应答 FA
- 主机发送 02 -> 键盘应答 FA

10.2 S4 休眠唤醒和 S5 相同
- 主机发送 ED -> 键盘应答 FA
- 主机发送 02 -> 键盘应答 FA
- 5S 后
- 主机发送 FF -> 键盘应答 FA
- 360ms 后
- 键盘主动发送 AA
- 主机发送 F3 -> 键盘应答 FA
- 主机发送 80 -> 键盘应答 FA
- 主机发送 ED -> 键盘应答 FA
- 主机发送 80 -> 键盘应答 FA
- 主机发送 ED -> 键盘应答 FA
- 主机发送 未知 -> 键盘应答 FA
- 主机发送 F3 -> 键盘应答 FA
- 主机发送 04 -> 键盘应答 FA
- 主机发送 F3 -> 键盘应答 FA
- 主机发送 10 -> 键盘应答 FA
- 主机发送 F3 -> 键盘应答 FA
- 主机发送 20 -> 键盘应答 FA
- 主机发送 ED -> 键盘应答 FA
- 主机发送 01 -> 键盘应答 FA
10.3 win7 下发送 Windows 键的键码

留下评论