一、 实验目的
实现采用PS2协议的PC机键盘的解码,最终将PS2键盘作为FPGA系统的一个标准输入设备。
二、 实验原理
PS2键盘的内部结构我们不需要过多的去关注,我们只需要关心其接口协议,正确的解码其发送过来的按键信息即可,至于解码到的按键信息该怎么处理,不同的应用有不同的处理方式,这里就不做过多的介绍。PS2协议的简单描述如下:
PS2协议总共由两根线组成,一根时钟线和一根数据线。这里我们将采用PS2协议的键盘称为从机,将控制和解码PS2协议的一方成为主机,生活中最常见的主机便是我们的PC机。PS2总线协议的两根线中,时钟线传输时钟信号,该时钟信号始终由从机,即键盘产生。PS2协议发送一个字节的数据总共有11位,分别为
1位起始位
8位数据位
1位校验位
1位停止位
PS2从机发送一个完整数据包的时序图如下所示:
图2-1 PS2从机发送数据时序图
相信熟悉UART协议的同学一眼就能看出来,PS2协议和我们最熟悉的UART通信协议非常相似,那么我们的工作就简单了,既然键盘按照这个协议发送数据,我们FPGA作为主机,只需要正确的实现该协议的解码,将其中的8位数据位读取出来即可。由图可知,数据在时钟的下降沿处是稳定的,因此我们只需要去捕获时钟信号的下降沿,并在检测到这个下降沿时去读取数据线上的电平,就能够正确的读到数据。
以上是PS2协议中从机到主机的一个通讯过程,实际在PS2协议中,包含了从机到主机和主机到从机的通信时序,只是在我们进行PS2键盘的解码时,可以不需要进行主机到从机的通信,因此这部分内容小梅哥就不在这里讲解了,如果大家以后要解码PS2鼠标的话,就会用到主机到从机的通信了。
上面只是简单的介绍了PS2从机到主机的通信协议,我们知道了键盘是一个字节一个字节的往主机发送数据的,但是,每个字节代表了什么内容呢,我们还需要对照键盘编码对照表来查看。
键盘扫描码分为第一套扫描码、第二套扫描码和第三套扫描码,我们日常生活中常见的扫描码绝大多数采用第二套扫描码,因此这里小梅哥就只附上第二套扫描码的内容。
实际一个按键由按下到释放时键盘将按照如下的规定往主机发送数据:
只要一个键被按下,这个键的通码(MAKE)就被发送到计算机。通码只表示键盘上的一个按键,它不表示印刷在按键上的那个字符。 这就意味着在通码和ASCII码之间没有已定义好的关联, 直到主机把扫描码翻译成一个字符或命令。
虽然多数第二套通码都只有一个字节宽,但也有少数“ 扩展按键”的通码是两字节或四字节宽,这类的通码第一个字节总是为E0H(如“END”、“HOME”……)。正如键按下通码就被发往计算机一样, 只要键一释放, 断码就会被发送。每个键都有它自己唯一的通码,它们也都有唯一的断码。 幸运的是,你不用总是通过查表来找出按键的断码―― 在通码和断码之间存在着必然的联系。 多数第二套断码有两字节长,它们的第一个字节是F0H, 第二个字节是这个键的通码。
例如,当我们按下键盘上的“A”键时,键盘就会往主机发送”A”键对应的通码(MAKE)”1C”,如果你一直按下这个按键不释放,那么在一个短暂的延时之后,键盘会再次开始,以一定的速率持续向主机发送”1C“,直到该按键被释放。在该按键被释放后,键盘将会向主机发送“A ”的断码(Break),即首先发送“F0”,然后下一个字节再马上发送“1C”。
如果我们按下的是“END”、“PAGE UP”等扩展按键时,会怎么样呢?这里,以”END”键举例。当”END“键被按下后,键盘会首先向主机发送“E0”,发送完成后,又会接着发送下一个字节“69”,释放时也与前面的普通按键类似,先发送“E0”,然后发送“F0”,最后再发送“69”。
由上述分析可知,我们FPGA在解码到一次数据后,还需要对这个数据进行分析判断,判断该数据是否为断码标志“F0”以及扩展码标志”E0“。
三、 硬件设计
PS2电路结构简单,因此这里就略去。
四、 架构设计
本实验我们进行PS2键盘的解码实验,并将解码到的结果显示在数码管上,同时,为了使我们更加直观的感受到按键被按下和释放,小梅哥在这里增加了一个蜂鸣器,当有按键按下或释放(包括长按时每接收到一个通码)时,蜂鸣器发出一个短暂的响声,来反馈给操作者,让我们知道我们的解码模块解码到了按键信息。设计结构如下图所示:
图4-1 led实验模块组织结构图
详细端口名及其意义如下
表4-1 独立按键检测实验端口说明
因为存在模块间的连接,因此有部分内部信号,下表为内部信号的名称和功能说明
表4-2 独立按键检测实验内部信号说明
五、 代码分析
这里,解码的关键是PS2接口的时钟信号,该时钟为异步时钟,我们需要通过边沿检测的方式来检测其下降沿,以便根据下降沿的个数来确定每个时钟我们因该做什么,边沿检测的电路,前面几个实验已经讲过很多次了,这里便不再做过多的解释,贴上代码即可:
以下是代码片段:
regPS2_Clk_Tmp0,PS2_Clk_Tmp1,PS2_Clk_Tmp2,PS2_Clk_Tmp3;
wire nedge_PS2_Clk; /*PS2从机时钟下降沿检测标志信号*/
always @ (posedge Clk or negedge Rst_n)
if(!Rst_n) begin
PS2_Clk_Tmp0 <= 1'b0;
PS2_Clk_Tmp1 <= 1'b0;
PS2_Clk_Tmp2 <= 1'b0;
PS2_Clk_Tmp3 <= 1'b0;
end
else begin
PS2_Clk_Tmp0 <= PS2_Clk;
PS2_Clk_Tmp1 <= PS2_Clk_Tmp0;
PS2_Clk_Tmp2 <= PS2_Clk_Tmp1;
PS2_Clk_Tmp3 <= PS2_Clk_Tmp2;
end
/*-------获取PS时钟信号的下降沿-------------*/
assign nedge_PS2_Clk = !PS2_Clk_Tmp0 & !PS2_Clk_Tmp1 & PS2_Clk_Tmp2 & PS2_Clk_Tmp3;
一个PS2的数据包总共由11位组成,因此会有11个时钟下降沿,因此我们必须对下降沿的个数准确计数,才能保证我们能够解码得到正确的数据,这里,使用我们的PS2时钟下降沿标志信号来使能我们的计数器自加,当计数器加到11后,表示一个数据包接收完成,将计数器清零,等待下一个下降沿的到来,相关代码如下:
以下是代码片段:
/*------------PS2时钟下降沿个数计数器-----------------------*/
always @(posedge Clk or negedge Rst_n)
if(!Rst_n)
Cnt1 <= 4'd0;
else if(Cnt1 == 4'd11)
Cnt1 <= 4'd0;
else if(nedge_PS2_Clk)
Cnt1 <= Cnt1 + 1'b1;
接下来,就是根据时钟下降沿的计数个数,来读取对应位的数据了,因为采用了非阻塞赋值的方式,因此,PS2时钟下降沿到来时,此时Cnt1执行自加1操作,但同时如果也来用Cnt1的值来确定数据位数,就一定会造成错误,因为此时,Cnt1的加1操作并没有执行,而是会在下一个时钟上升沿到来之时才变,因此,为了保证我们使用的Cnt1的数据是已经更新了的,我们需要在Cnt1已经变化之后再来使用其值做判断,即在PS2时钟下降沿检测成功后,滞后一个系统时钟周期后再来读取PS2_Din上的值,比较简单的操作方式就是将PS2时钟下降沿检测标志信号再用寄存器打一拍,对应代码如下:
以下是代码片段:
always @(posedge Clk)nedge_PS2_Clk_Shift <= nedge_PS2_Clk;
可能这里相对比较难以理解,希望大家结合仿真结果自学揣摩体会。
接下来就是根据Cnt1的计数值来读取每一位的数据了,这部分代码很简单,如下所示:
以下是代码片段:
/*--------------读取8位数据位---------------*/
always @ (posedge Clk or negedge Rst_n)
if(!Rst_n)
Data_tmp <= 8'd0;
else if(nedge_PS2_Clk_Shift) begin
case(Cnt1)
4'd2:Data_tmp[0] <= PS2_Din;
4'd3:Data_tmp[1] <= PS2_Din;
4'd4:Data_tmp[2] <= PS2_Din;
4'd5:Data_tmp[3] <= PS2_Din;
4'd6:Data_tmp[4] <= PS2_Din;
4'd7:Data_tmp[5] <= PS2_Din;
4'd8:Data_tmp[6] <= PS2_Din;
4'd9:Data_tmp[7] <= PS2_Din;
default:Data_tmp <= Data_tmp;
endcase
end
else
Data_tmp <= Data_tmp;
通过以上操作,我们就能正确的解码PS2键盘发送过来的每一个字节的数据了,但是,这些数据代表了什么呢,如果是断码标志,或者是长码标志,我们又该如何进行操作呢,这里,小梅哥先贴上我的处理代码:
以下是代码片段:
always @ (posedge Clk or negedge Rst_n)
if(!Rst_n) begin
Break_r <= 1'b0;
Key_Valve <= 10'd0;
Key_Flag <= 1'b0;
Long_Code_r <= 1'b0;
end
else if(Cnt1 == 4'd11) begin
if(Data_tmp == 8'hE0) /*判断是否为长码*/
Long_Code_r <= 1'b1; /*将长码标志置1*/
else if(Data_tmp == 8'hF0) /*判断是否为断码*/
Break_r <= 1'b1; /*将断码标志置1*/
else begin /*检测到的数据为通码*/
Key_Valve <= {Break_r,Long_Code_r,Data_tmp};/*将长码标志、断码标志和解码到的按键码输出*/
Key_Flag <= 1'b1; /*产生解码成功标志信号*/
Long_Code_r <= 1'b0; /*清零长码标志*/
Break_r <= 1'b0; /*清零断码标志*/
end
end
else begin
Key_Valve <= Key_Valve;
Key_Flag <= 1'b0;
Break_r <= Break_r;
Long_Code_r <= Long_Code_r;
end
这里,小梅哥使用了两个标志寄存器,当检测数据完成后,即Cnt1=11时,就对解码到到数据进行判断,如果Data_tmp == 8'hE0,则解码到到数据为长码(扩展码)标志,此时便将长码标志寄存器置1,如果Data_tmp == 8'hF0,则解码到到数据为断码标志,此时便将断码标志寄存器置1。然后,当解码到其他数据(如单字节通码或双子节通码的第二个字节)后,便将解码到的数据连同断码和长码标志寄存器的状态输出,并给出按键检测成功标志(Key_Flag置1)。
六、 仿真分析
为了对小梅哥设计的PS2键盘解码驱动进行验证,小梅哥编写了一个Testbench来模拟键盘发送数据,通过观察键盘解码驱动的输出来验证该解码模块的正确性,关于模拟键盘发送数据,在一份介绍PS2协议的手册中有如下描述:
我推荐仿真键盘/鼠标采用下面的过程发送一字节的数据到主机:
1) 等待Clock线为高电平, 即等待主机释放Clock线;
2) 延时50us;
3) 判断Clock线是否为高电平?
No―― 跳到第1步;
4) Data线是否为高电平?
No―― 放弃(跳到从主机读取字节的程序中) 。
5) 延迟20us,输出起始位(0) , 然后延迟20us, 再拉低Clock线保持40us后释放Clock线, 形成一个脉冲;
6) 延时20us, 测试Clock线是否为高电平?No―― 跳到第1步;
7) 输出第1个数据位, 然后延时20us, 再拉低Clock线保持40us后释放Clock线, 形成一个脉冲;
8) 重复6-7步发送剩下的7个数据位和校验位;
9) 延时20us, 测试Clock线是否为高电平?
No―― 跳到第1步;
因此,我们的模拟键盘发送数据的过程只需要依照上面的流程来即可,这里贴上小梅哥编写的testbench:
以下是代码片段:
`timescale 1ns/1ns
module PS2_Key_Board_Driver_tb;
reg Clk;/*system clock*/
reg Rst_n;/*复位信号*/
reg PS2_Din;/*PS2键盘数据线*/
reg PS2_Clk;/*PS2键盘时钟线*/
wire Key_Flag;/*解码得到键值标志信号*/
wire [9:0] Key_Valve;/*解码结果,其中最高位为通/断码识别位,0为通码,1为断码,低八位为码值*/
PS2_Key_Board_Driver u1(
.Clk(Clk),
.Rst_n(Rst_n),
.PS2_Din(PS2_Din),
.PS2_Clk(PS2_Clk),
.Key_Flag(Key_Flag),
.Key_Valve(Key_Valve)
);
initial begin
Clk = 1;
Rst_n = 0;
PS2_Din = 1;
PS2_Clk = 1;
#200;
Rst_n = 1;
Key_Event(8'h1A); /* Z */
#400;
Key_Event(8'h35); /* X */
#800;
Key_Event(8'h44); /* O */
#1320;
Key_Event(8'h4D); /* P */
#2560;
Key_Event(8'h24); /* E */
#1230;
Key_Event(8'h31); /* N */
#20000;
Long_Key_Event(8'h70); /* "INSERT" */
#400;
Long_Key_Event(8'h6c); /* "HOME" */
#800;
Long_Key_Event(8'h7d); /* "PAGE UP" */
#1320;
Long_Key_Event(8'h71); /* "DELETE" */
#2560;
Long_Key_Event(8'h69); /* "END" */
#1230;
Long_Key_Event(8'h7a); /* "PAGE DOWN" */
#2000000;
$stop;
end
/*---------生成工作时钟-----------*/
always #10 Clk = ~Clk;
/*----任务:以PS2协议发送一个字节的数据-----*/
task Send_data;
input [7:0]Data;
begin
PS2_Din = 0; /*发送起始位*/
#20000;PS2_Clk = 0;
#40000;PS2_Clk = 1;
#20000;PS2_Din = Data[0];/*发送第0位*/
#20000;PS2_Clk = 0;
#40000;PS2_Clk = 1;
#20000;PS2_Din = Data[1];/*发送第1位*/
#20000;PS2_Clk = 0;
#40000;PS2_Clk = 1;
#20000;PS2_Din = Data[2];/*发送第2位*/
#20000;PS2_Clk = 0;
#40000;PS2_Clk = 1;
#20000;PS2_Din = Data[3];/*发送第3位*/
#20000;PS2_Clk = 0;
#40000;PS2_Clk = 1;
#20000;PS2_Din = Data[4];/*发送第4位*/
#20000;PS2_Clk = 0;
#40000;PS2_Clk = 1;
#20000;PS2_Din = Data[5];/*发送第5位*/
#20000;PS2_Clk = 0;
#40000;PS2_Clk = 1;
#20000;PS2_Din = Data[6];/*发送第6位*/
#20000;PS2_Clk = 0;
#40000;PS2_Clk = 1;
#20000;PS2_Din = Data[7];/*发送第7位*/
#20000;PS2_Clk = 0;
#40000;PS2_Clk = 1;
#20000;PS2_Din = 0;/*暂时忽略校验位*/
#20000;PS2_Clk = 0;
#40000;PS2_Clk = 1;
#20000;PS2_Din = 1;/*停止位*/
#20000;PS2_Clk = 0;
#40000;PS2_Clk = 1;
end
endtask
/*-----任务:模拟按下按键的操作------*/
task press_key;
input [7:0]Key_Number;
begin
Send_data(Key_Number);
#50000;
end
endtask
/*-----任务:模拟释放按键的操作------*/
task release_key;
input [7:0]Key_Number;
begin
Send_data(8'hF0);
#50000;
Send_data(Key_Number);
#50000;
end
endtask
/*----任务:模拟一次短码的按下和释放操作-----*/
task Key_Event;
input [7:0]Key_Number;
begin
press_key(Key_Number);
#30000;
release_key(Key_Number);
end
endtask
/*----任务:模拟一次长码的按下和释放操作-----*/
task Long_Key_Event;
input [7:0]Key_Number;
begin
press_key(8'he0);
#30000;
press_key(Key_Number);
#30000;
press_key(8'he0);
#30000;
release_key(Key_Number);
end
endtask
endmodule
testbench中使用了一个主任务来模拟键盘的数据发送,并使用了其他几个基于此任务的任务来模拟按键按下、按键释放、普通按键按下+释放、扩展按键按下+释放的过程,通过模拟进行部分按键的按下和释放操作,来观察解码模块的结果,便可获知解码是否成功。
以下为小梅哥的仿真结果,与我发送的数据一致,因此表明我的PS2解码是成功的。
七、 下板验证
这里,小梅哥在至芯科技ZX2的板子上验证通过,如下图:
其中,第三个数码管,为0表示普通按键通码,为2表示普通按键断码,为1表示扩展按键通码,为3表示扩展按键断码。