PS2键盘也是一个经典的实验,可能不少人接触如何对通讯协议、时序编程就是从这个实验开始学习的。USB键盘已经很普及,如今市场上仍是有一些USB转PS2的转接头,还有一些转换芯片。这个实验虽然简单,不过不知道您有考虑过单按一次输出一个有效脉冲、短按、长按等这些是如何实现的么?这就涉及到一些时钟、边沿检测等设计问题。web
咱们见到的PS2的接口电路应该都是这样的:
编程
一根时钟线、一根数据线完成通讯,PS2通讯的帧格式以下所示,时钟的降低沿数据有效:svg
按键在被按下时,会发送一个字节,这个码就是通码;按键在释放时,会发送两个字节,这个码就作断码(固然也有例外)。每个按键都有惟一的通码和断码,据此进行判断按下的是哪一个键,从而执行对应的功能。如一部分按键的通码和断码以下所示:
学习
能够看出断码其实就是在通码前加了一个F0,好比A的通码是1C,则它的断码是F01C。另一些特殊功能的按键,在通码和断码前都会加个E0。PS2解码的代码以下所示:spa
//-----------------ps2_clk降低沿捕获--------------------
//clk至关于中间采样点的做用,第一个降低沿到来讲明起始位开始
reg ps2_clk0, ps2_clk1, ps2_clk2;//缓冲寄存器
wire ps2_clk_neg; //1表示检测到降低沿
reg ps2_state;
always @ (posedge clk or negedge rst_n)
if (!rst_n)
{ps2_clk0, ps2_clk1, ps2_clk2} <= 3'd0;
else
begin
ps2_clk0 <= ps2_clk;
ps2_clk1 <= ps2_clk0;
ps2_clk2 <= ps2_clk1;
end
assign ps2_clk_neg = ~ps2_clk1 & ps2_clk2;
//----------------------数据接收----------------------------
reg [3:0]num; //移位控制
reg [7:0]data_temp;//当前接收的数据
always @ (posedge clk or negedge rst_n)
if (!rst_n)
begin
num <= 4'd0;
data_temp <= 8'd0;
end
else if (ps2_clk_neg)
begin
if (num == 0) num <= num + 1'b1;//跳过起始位
else if (num <= 8) //数据位赋值
begin
num <= num + 1'b1;
data_temp[num-1] <= ps2_data;
end
else if (num == 9) num <= num + 1'b1;//跳过校验位
else num <= 4'd0; //清0
end
//--------------------按键按下/松开检测-------------------------
reg ps2_loosen;//1表示按键松开
reg [7:0]ps2_byte;//ps2一个字节数据
always @ (posedge clk or negedge rst_n)
if (!rst_n)
begin
ps2_state <= 1'b0;
ps2_loosen<= 1'b0;
end
//每接收完一个数据就进行按键检测
else if (num == 4'd10)
if (data_temp == 8'hf0) ps2_loosen <= 1'b1;//断码标识符
else
begin
if (ps2_loosen) //1表示按键松开,下一次接收数据后清0
begin
ps2_state <= 1'b0;
ps2_loosen<= 1'b0;
end
else //loosen变0后的下一个数据表示按键被按下
begin
ps2_state <= 1'b1;
ps2_byte <= data_temp; //把读取到的值赋给ps2_out
end
end
因为PS2通讯是在PS2时钟的降低沿有效,所以第一个always使用三个寄存器对PS2的CLK作一个降低沿捕获,并输出一个ps2降低沿的有效信号。设计
捕捉到了ps2时钟的降低沿,第二个always即是使用一个计数器在降低沿信号有效时读取并存储数据线上的数据。计数器的值正好对应着一帧中的通讯格式,所以在计数器为0时为通讯的起始位,1~8为数据位,9为校验位,10为中止位。计数器处于数据位期间内,将数据位依次存储到一个寄存器中。3d
获得了数据,第三个always进行的即是通讯数据的判断,这里进行的是断码的判断。每当完成一帧通讯时,即计数器计数到10(中止位)时,便对通讯数据作判断,若是是f0,则为断码的第一个码,那么下一次通讯来的必然是按键的键值码。所以将收到f0后的下一个通讯数据做为按键的键值码存到一个寄存器中,同时将按键有效信号ps2_state置高,表示按下一次按键。code
这样便完成了PS2的通讯。xml
设想一个问题,假设两个模块,他们的时钟是同样的,模块一用来进行PS2键盘检测,模块二根据按键按下的有效信号来决定是否执行对应的操做。若是模块二采用同步设计,即由时钟来控制(一般也是这么作的),若是模块一输出的按键有效信号不能作到刚好只维持一个时钟的脉冲宽度,那么模块二就会屡次检测到按键按下并触发屡次对应的控制操做。这也是新手常遇到的问题。blog
若是模块一的时钟是模块二时钟的两倍呢?若是这个时候模块一输出的按键有效信号仍然只有一个脉冲,那么模块二就会刚好检测不到。所以模块一输出的按键有效信号应该维持两个时钟的脉冲宽度。而这能够用一个计数器来控制。
我这里举一个只输出一个时钟长度的有效信号的例子:
reg ps2state_reg;
wire flag;
always @ (posedge clk)
ps2state_reg <= ps2_state;
assign flag = (ps2state_reg) & (~ps2_state);
//---------------------根据键盘扫描码输出按键有效信号?--------------------------
always @ (posedge clk or negedge rst_n)
if (!rst_n)
begin left <= 0; right <= 0; up <= 0; down <= 0; end
else if (flag) //每当松开按键时才进行输出
case (ps2_byte)
8'h1C: begin left <= 1; end //a
8'h23: begin right <= 1; end //d
8'h1D: begin up <= 1; end //w
8'h1B: begin down <= 1; end //s
default: begin left <= 0; right <= 0; up <= 0; down <= 0; end
endcase
else if (left) left <= 0; //有按键有效信号输出一个脉冲后立刻清零
else if (right) right <= 0;
else if (up) up <= 0;
else if (down) down <= 0;
首先对按键状态ps2_state作一级寄存,而后进行边沿检测,那么在alwasy中检测到边沿有效时则表示按键按下了一次,根据键码将相应的按键有效信号置1。而当检测到有效信号为高时,在下一个时钟立刻就拉低,从而实现只输出一个时钟的脉冲宽度。这样就不会引发错误的检测到屡次按下的问题。
咱们在玩游戏的时候还会碰到这种状况,须要长按一个键几秒钟才会有相应的反应,其实解决了上面的问题后咱们对这种短按、长按的控制思路就很清楚了。简而言之,在模块一中使用计数器来控制输出的有效信号的时钟长度,在模块二中使用相同的计数器对这个有效信号的时钟长度进行判断,进而识别这个键究竟是短按仍是长按,以选择不一样的操做。