电子说
PS2键盘也是一个经典的实验,可能很多人接触如何对通信协议、时序编程就是从这个实验开始学习的。USB键盘已经很普及,现在市场上还是有一些USB转PS2的转接头,还有一些转换芯片。这个实验虽然简单,不过不知道您有考虑过单按一次输出一个有效脉冲、短按、长按等这些是如何实现的么?这就涉及到一些时钟、边沿检测等设计问题。
PS2协议实现
我们见到的PS2的接口电路应该都是这样的:
一根时钟线、一根数据线完成通信,PS2通信的帧格式如下所示,时钟的下降沿数据有效:
按键在被按下时,会发送一个字节,这个码就是通码;按键在释放时,会发送两个字节,这个码就做断码(当然也有例外)。每一个按键都有唯一的通码和断码,据此进行判断按下的是哪个键,从而执行对应的功能。如一部分按键的通码和断码如下所示:
可以看出断码其实就是在通码前加了一个F0,比如A的通码是1C,则它的断码是F01C。另外一些特殊功能的按键,在通码和断码前都会加个E0。PS2解码的代码如下所示:
//-----------------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为停止位。计数器处于数据位期间内,将数据位依次存储到一个寄存器中。
得到了数据,第三个always进行的便是通信数据的判断,这里进行的是断码的判断。每当完成一帧通信时,即计数器计数到10(停止位)时,便对通信数据做判断,如果是f0,则为断码的第一个码,那么下一次通信来的必然是按键的键值码。因此将收到f0后的下一个通信数据作为按键的键值码存到一个寄存器中,同时将按键有效信号ps2_state置高,表示按下一次按键。
这样便完成了PS2的通信。
PS2按键判断
设想一个问题,假设两个模块,他们的时钟是一样的,模块一用来进行PS2键盘检测,模块二根据按键按下的有效信号来决定是否执行对应的操作。如果模块二采用同步设计,即由时钟来控制(通常也是这么做的),如果模块一输出的按键有效信号不能做到恰好只维持一个时钟的脉冲宽度,那么模块二就会多次检测到按键按下并触发多次对应的控制操作。这也是新手常遇到的问题。
如果模块一的时钟是模块二时钟的两倍呢?如果这个时候模块一输出的按键有效信号仍然只有一个脉冲,那么模块二就会恰好检测不到。因此模块一输出的按键有效信号应该维持两个时钟的脉冲宽度。而这可以用一个计数器来控制。
我这里举一个只输出一个时钟长度的有效信号的例子:
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。而当检测到有效信号为高时,在下一个时钟马上就拉低,从而实现只输出一个时钟的脉冲宽度。这样就不会引起错误的检测到多次按下的问题。
我们在玩游戏的时候还会碰到这种情况,需要长按一个键几秒钟才会有相应的反应,其实解决了上面的问题后我们对这种短按、长按的控制思路就很清楚了。简而言之,在模块一中使用计数器来控制输出的有效信号的时钟长度,在模块二中使用相同的计数器对这个有效信号的时钟长度进行判断,进而识别这个键到底是短按还是长按,以选择不同的操作。
全部0条评论
快来发表一下你的评论吧 !