一、PS2键盘通信协议概述
PS2键盘采用双向同步串行通信协议,通过CLK(时钟线) 和DATA(数据线) 与主机通信,核心特性:
-
数据传输格式:11位/帧(1起始位(0) + 8数据位(LSB先发) + 1奇校验位 + 1停止位(1)),速率10-16.7kHz(位宽60-100μs)。
-
通信模式:键盘主动发送(通码/断码),主机被动接收;主机可发送命令(如复位、设置LED)。
-
扫描码 :104键键盘使用Make Code(通码,按下) 和Break Code(断码,释放),断码=通码最高位置1(如通码0x1C→断码0x9C),扩展码以0xE0前缀标识(如右Ctrl通码0xE014)。
二、硬件设计
2.1 核心组件
| 模块 | 说明 |
|---|---|
| 微控制器 | 51系列/STM8/STM32(GPIO控制) |
| PS2键盘 | 104键标准键盘(PS2接口) |
| 电路连接 | CLK→PA0(输入,上拉4.7kΩ),DATA→PA1(输入/输出,上拉4.7kΩ) |
三、软件设计(C语言,模块化实现)
3.1 头文件与宏定义
c
#include "reg52.h" // 以51单片机为例,其他平台需调整
#include "intrins.h"
// PS2引脚定义(可修改为实际硬件引脚)
sbit PS2_CLK = P1^0; // 时钟线(输入/输出)
sbit PS2_DAT = P1^1; // 数据线(输入/输出)
// 扫描码表(部分常用键,完整104键需扩展)
#define KEY_ESC 0x76
#define KEY_1 0x16
#define KEY_A 0x1C
#define KEY_SPACE 0x29
#define KEY_SHIFT_L 0x12
#define KEY_CTRL_L 0x14
#define KEY_EXT 0xE0 // 扩展码前缀
// 键值映射(通码→ASCII/功能码)
unsigned char code keyMap[128] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x00-0x0F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x10-0x1F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x20-0x2F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x30-0x3F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x40-0x4F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x50-0x5F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x60-0x6F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // 0x70-0x7F
};
3.2 核心函数实现
3.2.1 微秒级延时(时序控制)
c
void delay_us(unsigned int us) {
while (us--) {
_nop_(); _nop_(); _nop_(); _nop_(); // 12MHz晶振下,4个_nop_≈1μs
}
}
3.2.2 PS2数据接收(键盘→主机)
功能:按PS2协议接收1字节数据(含起始/数据/校验/停止位),返回接收值(0xFF表示错误)。
c
unsigned char ps2_receive(void) {
unsigned char data = 0, bit = 0, parity = 1; // 奇校验初始为1
unsigned int timeout = 1000; // 超时计数(防死等)
// 等待起始位(CLK高,DATA低)
while (PS2_CLK && timeout--); if (timeout == 0) return 0xFF;
delay_us(5); // 等待数据稳定
// 接收8位数据位(LSB先发)
for (bit=0; bit<8; bit++) {
while (!PS2_CLK && timeout--); if (timeout == 0) return 0xFF; // 等待CLK高
data >>= 1; // 右移(LSB先收)
if (PS2_DAT) { data |= 0x80; parity ^= 1; } // 记录数据位,更新校验
while (PS2_CLK && timeout--); if (timeout == 0) return 0xFF; // 等待CLK低
}
// 接收校验位
while (!PS2_CLK && timeout--); if (timeout == 0) return 0xFF;
if (PS2_DAT) parity ^= 1; // 校验位为1则翻转parity
while (PS2_CLK && timeout--); if (timeout == 0) return 0xFF;
// 接收停止位(应为1)
while (!PS2_CLK && timeout--); if (timeout == 0) return 0xFF;
if (!PS2_DAT) return 0xFF; // 停止位错误
while (PS2_CLK && timeout--); if (timeout == 0) return 0xFF;
// 校验结果(奇校验:1的个数为奇数)
if (parity != 1) return 0xFF; // 校验失败
return data;
}
3.2.3 PS2命令发送(主机→键盘)
功能:向键盘发送1字节命令(如复位0xFF、设置LED 0xED),返回键盘回应(0xFA为成功)。
c
unsigned char ps2_send(unsigned char cmd) {
unsigned char data = cmd, bit = 0, parity = 1;
unsigned int timeout = 1000;
// 主机控制CLK为低(请求发送)
PS2_CLK = 0;
delay_us(100); // 保持低电平>100μs
PS2_DAT = 0; // 起始位(0)
delay_us(10);
PS2_CLK = 1; // 释放CLK,由键盘控制
// 发送8位数据位(LSB先发)
for (bit=0; bit<8; bit++) {
while (PS2_CLK && timeout--); if (timeout == 0) return 0xFF; // 等待CLK低
PS2_DAT = (data & 0x01) ? 1 : 0; // 发送最低位
data >>= 1;
parity ^= (data & 0x01); // 更新校验(注意:此处需先取位再移位)
while (!PS2_CLK && timeout--); if (timeout == 0) return 0xFF; // 等待CLK高
}
// 发送校验位(奇校验)
while (PS2_CLK && timeout--); if (timeout == 0) return 0xFF;
PS2_DAT = parity;
while (!PS2_CLK && timeout--); if (timeout == 0) return 0xFF;
// 发送停止位(1)
while (PS2_CLK && timeout--); if (timeout == 0) return 0xFF;
PS2_DAT = 1;
while (!PS2_CLK && timeout--); if (timeout == 0) return 0xFF;
// 接收键盘回应(0xFA)
return ps2_receive();
}
3.2.4 键盘初始化
功能:发送复位命令,等待键盘回应(0xAA=自检通过,0x00=设备ID)。
c
bit keyboard_init(void) {
unsigned char ack, bat;
ps2_send(0xFF); // 发送复位命令
ack = ps2_receive(); // 应返回0xFA(确认)
if (ack != 0xFA) return 0;
bat = ps2_receive(); // 应返回0xAA(BAT通过)
if (bat != 0xAA) return 0;
ps2_receive(); // 接收设备ID(0x00)
return 1; // 初始化成功
}
3.2.5 扫描码处理(通码/断码→键值)
功能:维护修饰键状态(Shift/Ctrl),将扫描码转换为ASCII/功能码。
c
unsigned char shift_flag = 0, ctrl_flag = 0; // 修饰键状态
unsigned char key_value = 0; // 当前键值
void process_scancode(unsigned char code) {
static unsigned char ext_flag = 0; // 扩展码标志
if (code == KEY_EXT) { // 扩展码前缀
ext_flag = 1;
return;
}
if (ext_flag) { // 处理扩展码第二字节
ext_flag = 0;
switch (code) {
case 0x14: key_value = (shift_flag ? KEY_CTRL_R : KEY_CTRL_L); break; // 右Ctrl
// 其他扩展键(如右Alt、数字小键盘)需补充
}
return;
}
if (code & 0x80) { // 断码(释放)
code &= 0x7F; // 清除最高位
switch (code) {
case KEY_SHIFT_L: shift_flag = 0; break;
case KEY_CTRL_L: ctrl_flag = 0; break;
}
} else { // 通码(按下)
switch (code) {
case KEY_SHIFT_L: shift_flag = 1; break;
case KEY_CTRL_L: ctrl_flag = 1; break;
case KEY_A: key_value = (shift_flag ? 'A' : 'a'); break;
case KEY_1: key_value = (shift_flag ? '!' : '1'); break;
// 其他键映射需补充完整扫描码表
}
}
}
3.3 主程序流程
c
void main(void) {
unsigned char scancode;
// 初始化:引脚设为输入,上拉
PS2_CLK = 1; PS2_DAT = 1;
// 键盘初始化
if (!keyboard_init()) {
// 初始化失败(如LED报警)
while(1);
}
// 主循环:接收扫描码并处理
while (1) {
if (PS2_CLK == 0) { // 检测键盘发送数据(CLK低)
scancode = ps2_receive();
if (scancode != 0xFF) { // 接收成功
process_scancode(scancode);
// 输出键值(如LCD显示、串口发送)
// printf("Key: 0x%02X, Value: %c\n", scancode, key_value);
}
}
}
}
参考代码 104键PS2接口标准键盘程序(C语言) www.youwenfan.com/contentcss/161311.html
四、关键问题与解决方案
4.1 时序错误导致数据接收失败
-
原因:延时不足或CLK信号检测错误。
-
解决 :用示波器测量CLK/DATA波形,调整
delay_us参数,确保位宽符合10-16.7kHz要求。
4.2 扫描码解析遗漏扩展键
-
原因:未处理0xE0前缀的扩展码。
-
解决 :在
process_scancode中增加ext_flag状态机,记录扩展码前缀,分两次接收完整扫描码。
4.3 键盘无响应
-
原因:未通过复位命令初始化,或电源/接线错误。
-
解决 :用
ps2_send(0xFF)发送复位命令,检查键盘是否返回0xFA+0xAA,确保VCC(5V)、GND、CLK、DATA连接正确。
五、测试与验证
-
硬件连接:按2.1节连接键盘与微控制器,确保上拉电阻(4.7kΩ)接5V。
-
功能测试:按下字母键,通过串口助手观察输出的ASCII码(如按"A"显示0x41或0x61,取决于Shift状态)。
-
扩展测试:测试数字小键盘、功能键(F1-F12),补充完整扫描码表。
六、总结
基于PS2协议实现了104键键盘的驱动,核心是时序精确的收发函数和扫描码解析逻辑。通过模块化设计,可扩展支持全键扫描码映射、LED控制(如Num Lock指示灯)、连击检测等功能,适用于嵌入式设备的人机交互接口。