1,PS2接口描述
6脚Mini-DIN(PS/2):1 数据;2 未实现,保留;3 电源地;4 电源+5V;5 时钟;6:未实现。重要的是1脚和5脚2,时序图
注意点:1)PS2的数据格式与UART十分相似,但是对数据的读取是是时钟的下降沿;2)PS2的时钟频率比较慢,大约是10kHz
3,编码键盘
键盘的编码分为通码make和断码break。通码对应某按键的按下事件,断码对应某按键的释放事件。例如按下A键,会输出0x1C的通码,释放A键,会输出0xF0,0x1C的断码。每个按键具有唯一的通码和断码。编码键盘的一个规则:一次只能有一个输出,若同时按下多个按键,只有其中一个有效。民用键盘都是采用第二套编码方式。通常通码只有一个字节(短码),少部分扩展按键的通码是两字节或四字节(长码)。多数断码是两字节,第一个字节是8’hf0,第二个字节是按键的通码。扩展按键的断码有三字节,前两个字节是8’he0,8’hf0,最后一字节是按键的通码。
4,代码设计思路
1)检测PS2时钟信号的下降沿;2)仿顺序操作,对每帧数据(11位)解码,判断是通码还是断码,忽略断码之后的通码;
3)输出一个字节的数据。
5,代码
ps2_decode.v
module ps2_decode(
input wire sclk,
input wire ps2_clk,
input wire ps2_data,
output wire[7:0] make_data,
output wire ps2_done
);
reg [2:0] ps2_clkr = 3'b111;
reg [4:0] i = 0;
reg [7:0] ps2_datar = 0;
reg flag_done = 0;
wire nege_ps2_clk;
//ps2_clk的下降沿
always@(posedge sclk)
begin
ps2_clkr <= {ps2_clkr[1:0],ps2_clk};
end
assign nege_ps2_clk = ~ps2_clkr[1] & ps2_clkr[2];
always@(posedge sclk)
begin
case(i)
0:begin//起始位
if(nege_ps2_clk)begin
i <= i + 1;
end
end
1,2,3,4,5,6,7,8:begin//数据位,低位在前
if(nege_ps2_clk)begin
ps2_datar[i-1] <= ps2_data;
i <= i + 1;
end
end
9,10:begin//校验位和停止位
if(nege_ps2_clk)begin
i <= i + 1;
end
end
11:begin
if(ps2_datar == 8'hf0)begin//判断是否是断码
i <= i + 1;
end
else begin
i <= 23;
end
end
12,13,14,15,16,17,18,19,20,21,22:begin//忽略断码之后的通码
if(nege_ps2_clk)begin
i <= i + 1;
end
end
23:begin
flag_done <= 1;
i <= i + 1;
end
24:begin
flag_done <= 0;
ps2_datar <= 0;//使得每次接收寄存器的初值都为零
i <= 0;
end
endcase
end
assign make_data = ps2_datar;
assign ps2_done = flag_done;
endmodule
该程序对通码和断码都适用,接收通码,对于断码忽略其后的通码,体现在i=12,13,14,15,16,17,18,19,20,21,22时直接自加1.具体分析见仿真截图。代码参考了《FPGA那些事儿--verilog HDL建模设计》中PS2解码部分并结合了自己的思考编写,欢迎交流指正。
6,仿真
tb_ps2.v
//ps2_clk的频率为10KHz
`timescale 1ns / 1ns
module tb_ps2();
reg sclk,ps2_clk,ps2_data;
wire[7:0] make_data;
wire ps2_done;
ps2_decode ps2_decode_inst(
.sclk (sclk),
.ps2_clk (ps2_clk),
.ps2_data (ps2_data),
.make_data (make_data),
.ps2_done (ps2_done)
);
initial begin
sclk = 0;
forever #10 sclk=~sclk;//50MHz
end
initial begin
ps2_clk=1;
ps2_data=1;
#50000;
key_event(8'h1C);//发送A
#50000;
key_event(8'h32);//发送B
#50000;
key_event(8'h21);//发送C
#50000;
end
task key_event;
input [7:0] key_number;
begin
data_send(key_number);//通码
#50000;
data_send(8'hf0);//断码
#50000;
data_send(key_number);
#50000;
end
endtask
task data_send;
input [7:0]data;
begin
ps2_data=0;#50000;//发送起始位
ps2_clk=0;#50000;//ps2_clk=0保持20000ns,注:延时操作是对前一句的赋值语句进行延时
ps2_clk=1;
ps2_data=data[0];#50000;//发送第0位数据
ps2_clk=0;#50000;
ps2_clk=1;
ps2_data=data[1];#50000;//发送第0位数据
ps2_clk=0;#50000;
ps2_clk=1;
ps2_data=data[2];#50000;//发送第0位数据
ps2_clk=0;#50000;
ps2_clk=1;
ps2_data=data[3];#50000;//发送第0位数据
ps2_clk=0;#50000;
ps2_clk=1;
ps2_data=data[4];#50000;//发送第0位数据
ps2_clk=0;#50000;
ps2_clk=1;
ps2_data=data[5];#50000;//发送第0位数据
ps2_clk=0;#50000;
ps2_clk=1;
ps2_data=data[6];#50000;//发送第0位数据
ps2_clk=0;#50000;
ps2_clk=1;
ps2_data=data[7];#50000;//发送第0位数据
ps2_clk=0;#50000;
ps2_clk=1;
ps2_data=0;#50000;//发送校验位,未设置,保留
ps2_clk=0;#50000;
ps2_clk=1;
ps2_data=1;#50000;//发送停止位
ps2_clk=0;#50000;
ps2_clk=1;
end
endtask
endmodule
仿真截图:
分析:箭头所指处ps2_done为1,表示完成一次ps2解码操作。下面一张图是方框的放大部分,可以看出在读取结束位的时候把ps2_done拉高一个时钟周期,解码数据(8'h1c)输出,下一个时钟周期解码数据清零,使得每次接收寄存器的初值为零。