在单片机系统中,经常使用的键盘都是专用键盘.此类键盘为单独设计制作的,成本高、使用硬件连接线多,且可靠性不高,这一状况在那些要求键盘按键较多的应用系统中更为突出.与此相比,在PC系统中广泛使用PS/2键盘具有价格低、通用可靠,且使用连接线少(仅使用2根信号线)的特点,并可满足多种系统的要求.因此在单片机系统中应用PS/2键盘是一种很好的选择.
文中在介绍PS/2协议和PS/2键盘工作原理与特点的基础上,给出了一个在单片机上实现对PS/2键盘支持的硬件连接与驱动程序设计实现.该设计实现了在单片机系统中对PS/2标准104键盘按键输入的支持.使用Keil C51开发的驱动程序接口和库函数可以方便地移植到其他单片机或嵌入式系统中.所有程序在Keil uVision2上编译通过,在单片机AT89C51上测试通过.
1 PS/2协议
目前,PC机广泛采用的PS/2接口为mini-DIN 6pin的连接器,如图1所示.
PS/2设备有主从之分,主设备采用Female插座,从设备采用Male插头.现在广泛使用的PS/2键盘鼠标均在从设备方式下工作.PS/2接口的时钟
与数据线都是集电极开路结构,必须外接上拉电阻(一般上拉电阻设置在主设备中).主从设备之间数据通信采用双向同步串行方式传输,时钟信号由从设备产生.
1.1 从设备到主设备的通信
当从设备向主设备发送数据时,首先检查时钟线,以确认时钟线是否为高电平.如果是高电平,从设备就可以开始传输数据;反之,从设备要等待获得总线的控制权,才能开始传输数据.传输的每一帧由11位组成,发送时序及每一位的含义如图2所示.
每一帧数据中开始位总是为0,数据校验采用奇校验方式,停止位始终为1.从设备到主设备通信时,从设备总是在时钟线为高时改变数据线状态,主设备在时钟下降沿读人数据线状态.
1.2 主设备到从设备的通信
主设备与从设备进行通信时,主设备首先将时钟线和数据线设置为“请求发送”状态,具体方式为:首先下拉时钟线至少100us抑制通信,然后下拉数据线“请求发送”,最后释放时钟线.在此过程中,从设备在不超过10us的间隔内必须检查这个状态,当设备检测到这个状态时,它将开始产生时钟信号.此时数据传输的每一帧由12位构成,其时序和每一位含义如图3所示.
与从设备到主设备通信相比,其每帧数据多了一个ACK位.这是从设备应答接收到字节的应答位,由从设备通过拉低数据线产生,应答位ACK总是为0.主设备到从设备通信过程中,主设备总是在时钟线为低电平时改变数据线的状态,从设备在时钟上升沿读人数据线状态.
2.1 PS/2键盘的编码
目前,PC机使用的PS/2键盘都默认采用 第2套扫描码集 .扫描码有两种不同的类型:“通码(make code)”和“断码(break code)”.当一个键被按下或持续按住时,键盘会将该键的通码发送给主机;而当一个键被释放时,键盘会将该键的断码发送给主机.根据键盘按键扫描码的不同,可将按键分为3类:
第1类按键 通码为一个字节,断码为0xF0+通码形式.如A键,其通码为0x1C;断码为0xF0 0x1C.
第2类按键 通码为两字节0xE0+0xXX形式,断码为0xE0+0xF0+0xXX形式.如Right Ctrl键,其通码为0xE0 0x14;断码为0xE0 0xF0 0x14.
组合按键扫描码的发送是按照按键发生的次序,如按下面顺序按左Shift十A键:① 按下左Shift键;② 按下A键;③ 释放A键;④ 释放左Shift键,那么计算机上接收到的一串数据为0x12 0x1C 0xF0 0x1C 0xF0 0x12.
在文中的驱动程序设计中,就是根据按键的分类对其分别进行处理.
2.2 PS/2键盘的命令集
主机可通过向PS/2键盘发送命令对键盘进行设置或者获得键盘的状态等操作.每发送一个字节,主机都会从键盘获得一个应答0xFA(“重发resend”和“回应echo”命令例外).驱动程序在键盘初始化过程中所用的指令:0xED,主机在该命令后跟随发送一个参数字节,用于指示键盘上Num Lock,Caps Lock,Scroll Lock Led的状态;0xF3,主机在这条命令后跟随发送一个字节参数定义键盘机打的速率和延时;0xF4,用于当主机发送0xF5禁止键盘后,重新使能键盘.
3 PS/2键盘与单片机的连接电路
PS/2键盘与AT89C51单片机的连接方式如图4所示.P1.0接PS/2数据线;P3.2(INT0)接PS/2时钟线.因为单片机的P1,P3口内部是带上拉电阻的,所以PS/2的时钟线和数据线可以直接与单片机的P1,P3相连接.
驱动程序的开发使用Keil C51语言以及KeiluVision2编程环境.PS/2 104键盘驱动程序主要任务是实现单片机与键盘间PS/2通信,同时将接收到的按键扫描码转换为该按键的键值KeyVal,提供给系统上层软件使用.
4.1 单片机与键盘间PS/2通信的程序设计
在PS/2通信过程中,主设备(文中是单片机)是在时钟信号为低时发送和接收数据信号.因为单片机向键盘发送的是指令,需要键盘回应,所以这部分程序采用查询方式;而单片机接收键盘数据时,数据线上的信号在时钟为低时已经稳定,所以这部分程序采用中断方式,且不需要在程序中加入延时程序.
单片机向PS/2键盘发送数据程序代码为:
void ps2_sentchar(unsigned char sentchar){//ps2主设备向从设备发送数据
unsigned char sentbit_cnt= 0x00;
unsigned char sentchar_chk = 0x00;
EX0=0; //关外部中断0
//发起一个传送,发起始位
PS2_SGN_CLOCK = 0; //将时钟线拉低并保持100 us
delay100us();
PS2_SGN_DATA= 0; //起始位
PS2_SGN_CLOCK = 1;
//发送DATA0-7
for(sentbit_cnt=0;sentbit_cnt< 8;sentbit_cnt++){
while(PS2_SGN_CLOCK) _nop_(); //等待时钟线变为低
PS2_SGN_DATA = sentchar& 0x01;//发送数据
if(PS2_SGN_DATA) sentchar_chk++; //计算校验
while(!PS2_SGN_CL0CK) _nop_(); //等待时钟线变高
sentchar>>=1; //待发送数据右移一位
}
//发送校验位
while(PS2_SGN_CLOCK) _nop_(); //等待时钟线变低
switch(sentchar_chk){
case 0:
case 2:
case 4:
case 6:PS2_SGN_DATA =1;break;//奇校验
case 1:
case 3:
case 5:
case 7:PS2_SGN_DATA = 0;break;//奇校验
default;break;
)
while(!PS2_SGN_CLOCK) _nop_(); //等待时钟线变高
while(PS2_SGN_CLOCK) _nop_(); //等待时钟线变低
PS2_SGN_DATA =1;//发送停止位,停止位总为1
while(!PS2_SGN_CLOCK) _nop_(); //等待时钟线变高
while(PS2_SGN_CLOCK) _nop_(); //等待时钟线变低
//接收ACK
//if(PS2_SGN_DATA) error();
//ACK信号由键盘发出,总为低电平
while(!PS2_SGN_CLOCK) _nop_(); //等待时钟线变高
EX0= 1; //开外部中断0
}
单片机由PS/2键盘接收数据程序:外部中断0设置为下降沿触发
void int0() interrupt 0 using 0 {//
EX0=0; //关外部中断0
switch(ps2_revchar_cnt){
case 1:
……
case 8:mcu_revchar<<=1;
if(PS2_SGN_DATA) mcu_revchar |= 0x01;
ps2_revchar_cnt++;
break;
case 0:ps2_revchar_cnt++;break; //开始位,
case 9:ps2_revchar_cnt++;break; //校验位,可添加校验程序
case 10: _nop_();//停止位
ps2_revchar_cnt= 0;
revchar_flag=1;//置接收到数据标识位
break;
default:break;
}
EX0=1;//开外部中断0
}
4.2 键盘扫描码转换程序设计
由于键盘扫描码无规律可循,因此由键盘扫描码获得相应按键的键值(字符键为其ASCII值,控制键如F1,Ctrl等为自定义值),只能通过查表的方式获得.由于按键的3种类型及部分按键对应着两个键值(如A键的键值根据Caps和Shift键状态有0x41(A)和0x61(a)两种),因此综合考虑查表转换速度和资源消耗,设计中使用4个键盘表:键盘扫描码转换基本集和切换集(kb_plain_map[NR_KEYS]与kb_shift_map[NR_KEYS]);包含E0前缀的键盘扫描码转换基本集和切换集(kbeO_plain_map[NR_KEYS]与kbe0_shiftmap[NR_KEYS]).PS/2 104键盘按键扫描码最大值为0x83,所以设置NR_KEYS为132.所有4个键盘表的定义均为如下形式:KB_MAP[MAKE CODE]=KEYVAL,如果扫描码对应的按键为空(如KB_MAP[0x00]),则定义相应键值为NULL_KEY(0x00).以下是键盘扫描码基本集的部分代码实例:
kb_plain_map[NR_KEYS]={……
NULL_KEY;0x2C;0x6B;0x69;0x6F;0x30;0x39;
NULL_KEY; //扫描码0x40~0x47
//对应按键空,逗号,K,I,O,0,9,空
//对应键值0x00,',','k','i','o','O','9',0x00…… };
如此设计键盘转换表的另一个好处在于,以后如需扩展支持有ACPI、Windows多媒体按键键盘时,只需要将键表中相应处修改即可,如ACPI
Power按键通码为0xE0 0x37,修改kbe0_plain_map[0x37]=KB_ACPI_PWR即可.
特殊按键Pause使用单独程序处理,如果接收到0xE1就转入这段程序.而Print Screen键则将其看作是两个通码分别为0xE0 0x12和0xE0 0x7C
的“虚键”的组合键处理.在驱动程序中设定如下全局变量:led_status记录Scroll Lock Led,Num Lock Led和Caps Lock Led的状态(关为0,开为1);agcs_status记录左右Shift Ctrl Gui Alt状态,相应键按下则对应位为1,释放为0.E0_FLAG接到0xE0置1;E1_FLAG接收到0xE1置1;F0_FLAG接收到0xF0置1.按键键值通过KeyVal提供上层程序使用.PS/2键盘扫描码键值转换程序ps2_codetrans()流程框架如图5所示.
第1类按键的扫描码键值转换程序代码。
if(F0_FLAG){//接收扫描码为断码
switch(mcu_revchar){//处理控制键
case 0x11:ages_status&=0xF7;break;//左alt释放
case 0x12:ages_status&=0xFE;break;//左shift释放
case 0x14:agcs_status&=0xFD;break;//左ctrl释放
case 0x58;if(led_status&0x04) led_status &= 0x03; //caps lock
else led_status |=0x04;
ps2_ledchange();
break;
case 0x59: agcs_status &= 0xEF;break;//右shift释放
case 0x77: if(led_status&0x02)led_status&=0x05;//num lock
else led_status |=0x02;
ps2_ledchange();
break;
case 0x7E:if(led_status&0x01) led_status&=0x06;//scroll lock
else led_status |=0x01;
ps2_ledchange();
break;
default;break;
}
F0_FLAG=0;
}
else{//接收扫描码为通码
if(led_status&0x04) caps_flag=1;else caps_flag = 0;
if(led_status&0x02) num_flag =1;else num_flag =0;
if(agcs_status&0x11) shift_flag = 1;else
shift_flag=0;
//扫描码键值转换
if((caps_flag == shift_flag) || (!num_flag)) KeyVal=kb_plain_map[mcu_revchar];
else KeyVal = kb_shift_map[mcu_revchar];
switch(mcu_revchar)(//处理控制键或状态键
case 0x11:agcs_status|= 0x08;//左alt按下
Case 0x12:agcs_status|= 0x01;//左shift按下
case 0x14:agcs_status|= 0x02;//左ctrl按下
case 0x59:agcs_status|= 0x10;//右shift按下
default:break;
}
}
第2类按键的扫描码键值转换程序与上面相似.注意:在退出该程序段时,对E0_FLAG和F0_FLAG标识清0.Pause键的处理程序,如果接收到0xE1,置E1_FLAG=1,然后顺次将后续接收到的7个字节数据和Pause的通码后7个字节比较,一致则返回KeyVal=KB_PAUSE;在比较完所有7个字节后清除E1_FLAG标识.键盘初始化程序kb_init()流程为:
① 上电后,接收键盘上电自检通过信号0xAA,或者自检出错信号0xFC.单片机接收为0xAA则进入下一步,否则进行出错处理.
② 关LED指示,单片机发送0xED,然后接收键盘回应0xFA,接着发送0x00接收0xFA.
③ 设置机打延时和速率:单片机发送0xF3,接收0xFA,发送0x00(250 ms,2.0 cps),接收0xFA.
④ 检查LED,发送0xED,接收0xFA,发送0x07(开所有LED),接收0xFA.发送0xED,接收0xFA,发送0x00(关LED),接收0xFA.
⑤ 允许键盘,发送0xF4,接收0xFA.键盘LED改变ps2_ledchange()函数流程:发送0xED;接收0xFA;发送led_status;接收0xFA.
5 结语
该驱动程序经Keil uVision2 编译,在AT89C51单片机上运行通过,实现了对PS/2 104键盘的支持,实现了对字符按键大小写切换,Num Lock切换、控制键及组合按键的支持.同时该程序对其他嵌入式或单片机系统中PS/2键盘的应用也有借鉴意义