PS/2 接口
介绍
PS/2 是一种常见的键盘接口(快淘汰了),它使用两根信号线,一根传输时钟信号 PS2_CLK,另一根传输数据 PS2_DAT。PS2_CLK 主要用于指示数据线上的比特位在什么时候是有效的。 键盘和主机之间可以进行双向的数据传递,本实验只讨论键盘向主机发送数据。
数据传输方式
当用户按键/松开时,键盘以每帧 11 位比特的格式串行传递数据给主机,同时在 PS2_CLK 时钟信号上传输对应的时钟。 这 11 位分别是:
-
开始位 0
-
8 位数据位
-
1 位奇偶校验位
-
停止位 1
键盘上每一个按键都有一个 8 位的“通码”(特殊的有 16 位),比如 w 的通码是“8’h1D”,当按下这个键时,键盘会向主机发送一个中间 8 位是“1D”的 11 位串数据,如果按下不松,键盘将一直不停地发送这个 11 位串。 每一个按键又都有一个“断码”,一般情况下,断码=F0+通码,比如 W 的断码是 16 位串“F01D”,当你松开 w 时,键盘向主机发送一个 F0,之后发送一个 1D,再之后就不发送数据了。 所以如果你按下 W,保持一段时间,再松开,接收到数据串是这样的:“1D-1D-1D-1D-F0-1D” 当有多个按键被按下时,将逐个的传递数据,“按下”这个过程总是有先后的。 按下 shift,开始传递 12、12、12,再按下 w,不再传递 12,而是开始不断传递 w 的通码 1D,当松开 shift 时,传递 F012,再松开 w,传递 F01D。
使用 Verilog 语言编写程序接受键盘信号
老师提供的控制器代码:
//键盘控制器
module ps2_keyboard(clk,clrn,ps2_clk,ps2_data,data,ready,nextdata_n,overflow);
input clk,clrn,ps2_clk,ps2_data;
input nextdata_n;
output [7:0] data;
output reg ready;
output reg overflow; // fifo overflow
// internal signal, for test
reg [9:0] buffer; // ps2_data bits
reg [7:0] fifo[7:0]; // data fifo
reg [2:0] w_ptr,r_ptr; // fifo write and read pointers
reg [3:0] count; // count ps2_data bits
// detect falling edge of ps2_clk
reg [2:0] ps2_clk_sync;
always @(posedge clk)
begin
ps2_clk_sync <= {ps2_clk_sync[1:0],ps2_clk};
end
wire sampling = ps2_clk_sync[2] & ~ps2_clk_sync[1];
always @(posedge clk)
begin
if(clrn == 0)
begin // reset
count <= 0; w_ptr <= 0; r_ptr <= 0; overflow <= 0; ready<= 0;
end
else
begin
if(ready)
begin // read to output next data
if(nextdata_n == 1'b0) //read next data begin
begin
r_ptr <= r_ptr + 3'b1;
if(w_ptr==(r_ptr+1'b1)) //empty
ready <= 1'b0;
end
end
if(sampling)
begin
if(count == 4'd10)
begin
if((buffer[0] == 0) && // start bit
(ps2_data) && // stop bit
(^buffer[9:1]))
begin // odd parity
fifo[w_ptr] <= buffer[8:1]; // kbd scan code
w_ptr <= w_ptr+3'b1;
ready <= 1'b1;
overflow <= overflow | (r_ptr == (w_ptr + 3'b1));// for next
end
count <= 0;
end
else
begin
buffer[count] <= ps2_data; // store ps2_data
count <= count + 3'b1;
end
end
end
end
assign data = fifo[r_ptr]; //always set output data
endmodule
input clk:使用 DE10 开发板自带的 CLOCK_50。
input clrn:使能端,接入 SW[0]。
input ps2_clk:键盘时钟,使用 DE10 的 PS2_CLK 引脚。
input ps2_data:键盘数据,使用 DE10 的 PS2_DAT 引脚。
output data:键盘发来的 11 位串中的 8 位键码,在时钟信号附近短时间内有效。
output ready:当 ready 为 1 时,data 有效,其他模块可以接收 data。
input nextdata_n:当 nextdata_n 为 0 时,控制器模块继续读取下一个数据。为 1 时暂停读取。
注意:实验要求在自己的处理模块(下面的 solve)中处理完一个数据后,将 nextdata_n 置为 0,这样控制器模块才能继续读取并发送数据,且只能置空 1 个时钟周期。(否则,solve 模块将有可能重复接收 data)
overflow:其他模块处理数据太慢,控制器设置的队列溢出。
自行完成的数据处理代码:
基本功能:单个按键
module solve(clk,clrn,data,ready,nextdata_n,count,state,key_data,sig,sig_cap)
input clk;//CLOCK_50
input clrn;//使能端,SW[0]
input [7:0] data;//控制模块输出的8位键码
input ready;//控制模块给的是否能够读取的信号
output reg nextdata_n;//本模块输出的信号,控制模块用它判断是否应该继续读数
output reg [7:0] count;//用于记录按键次数
output reg [1:0] state;//设置状态机
output reg [7:0] key_data;//在data有效时,接收data
……
状态机设计:
00:初始化状态,数码管不显示。
01:一直按键状态,数码管持续显示键码和 ASCII 码。
10:按键松开状态,数码管不显示。
在 00 状态下,若遇到通码,则跳转 01。
在 01 状态下,若接收通码,则仍然跳转 01;若接收到 F0(断码的前 8 位),则跳转 10。
在 10 状态下,直接跳转至初始化状态 00。
这样,就可以实现单个按键的信号接收过程了。
拓展功能:shift&ctrl
要实现组合键,状态机就变得复杂了。
把上面基础功能的状态机当成一个三角形,那么这里组合键的出现将状态机升级为两个三角形衔接的组合。
总共有 5 个状态。
A:接收到通码就跳转 B,记录下这个通码为 f。
B:接收到 F0 就跳转 E。如果 f 是 12(shift)/14(ctrl),接收到其他通码,就跳转 C;否则,留在 B。
C:若继续接收其他非 12/14 的通码,则留在 C。若接收到 F0,跳转 D。
D:直接跳转回 B。
E:直接跳转 A。
caps_lock
这个功能就比较简单了,接收到断码的时候,判断一下下一位是不是 caps_lock 的键码,是的话就把 sig_cap 取反。
最终输出的时候,根据 sig_cap 的值,判断是否输出大写。
反思
这次实验,老师已经提供了键盘控制器模块的代码,只需要自行完成状态机处理数据即可。主要是为了学习 PS/2 接口键盘的数据传输原理,而且还是只学了键盘到主机单向传值的原理。
之前的实验都是小实验,模块很少,这次最大的难度就在于,信号很多,变量很多,模块很多,模块与模块之前是有时序关系的,很难处理。
比如说,ps2_keyboard 模块接收键盘的信号,同时提供数据 data 给 solve 模块,但却要根据 solve 模块输出的 nextdata_n 进行是否读数的判断。在 solve 模块处理完数据后,display 模块要把处理的结果显示在数码管上,这两者是有先后顺序的,必须先处理,再显示。但 solve 模块是时序电路,用的是非阻塞语句。这样就会出现一个问题:在一个 clk 触发后,solve 要处理数据 key_data,要把它从 A 变成 B,display 则要显示数据 B,这两个模块如果同时进行会怎样?display 到底显示的是 A 还是 B。就仿真结果来看,是 B。