前记
师夷长技以自强
1.基本原理
1.1键盘模块以及与FPGA的连接
矩阵键盘常用在IO端口少而需要的按键又多的场合,学习过单片机的读者应该都知道矩阵键盘是采用列(行)扫描的方式读取输入的按键的。矩阵键盘模块中每一行(列)按键都有一根导线连起来,在开发板上采用的是列扫描的方式,也就是列线作为FPGA的输出端,行线作为FPGA的输入端,行线接上拉电阻默认情况是高电平。电路如下所示
1.2状态机设计
为了便于描述,先定义几个变量
变量 | 含义 |
[3:0]Row_Data | 对应矩阵键盘的行线 |
[3:0]Col_Data | 对应矩阵键盘的列线 |
Key_Flag | 当确有按键按下时,输出一个时钟的高电平 |
[3:0]Key_Value | 按键的值,对应于16个按键 |
Clk | 系统时钟输入 |
Rst_n | 复位信号 |
[3:0]Row_Data_R | 对Row_Data信号的寄存 |
[3:0]Col_Data_R | 对Col_Data信号的寄存 |
模块的输入输出脚应如下所示,
如果不考虑按键的消抖,那么可以把状态分成如下几个
状态 | 解释 |
IDLE | 空闲状态,当Row_Data!=4'b1111时应该跳转到SC_C0状态 |
SC_C0 | 扫描第0列的状态,获取第0列按键的按下情况。若此时Row_Data!=4'b1111,说明有按键按下,应更新Col_Data_R对应位为1。 |
SC_C1 | 扫描第1列的状态,获取第1列按键的按下情况。若此时Row_Data!=4'b1111,说明有按键按下,应更新Col_Data_R对应位为1。 |
SC_C2 | 扫描第2列的状态,获取第2列按键的按下情况。若此时Row_Data!=4'b1111,说明有按键按下,应更新Col_Data_R对应位为1。 |
SC_C3 | 扫描第3列的状态,获取第3列按键的按下情况。若此时Row_Data!=4'b1111,说明有按键按下,应更新Col_Data_R对应位为1。 |
GET_RESULT | 得到按键按下结果的状态。此时应确保仅有一行一列的按键被按下。 |
状态转移图如下所示:
但是机械按键难免会有抖动,并且如果连续输入一个按键的值时,用户往往会按定某个按键一段时间。因此目前还需要键入按下消抖检测,松手检测,连按检测。因次需添加下面的几个状态:
状态 | 解释 |
P_FILTER | 按下消抖状态。通过计数器计数20ms,如果计时到了就转到RED_ROW_P状态。 |
READ_ROW_P | 读按下时行数据状态。如果Row_Data!=4'b1111,则把此时的行数据寄存下来,否则为抖动,应跳转至IDEL。 |
WAIT_R | 等待按键释放状态。如果Row_Data == 4'b1111,则跳转到R_FILTER状态。此状态应该启动间隔计数器Cnt_EN2 = 1,用于处理用户连输输入某一个按键的情况。 |
R_FILTER | 松手消抖状态。通过计数器计数20ms,如果计时到了就转到RED_ROW_R状态。 |
READ_ROW_R | 读松手时行数据状态。如果Row_Data==4'b1111,则跳转到IDLE状态,否则为抖动,应跳转至R_FILTER。 |
需要增加两个计数器,一个是关于抖动的检测,计时值为20ms,而系统时钟周期为20ns,计数值为0-999_999,需要一个[19:0]Counter_F;另一个是关于连按间隔的检测,计时值为200ms,计数值应该是0-9_999_999,需要一个[23:0]Counter_W。
最终得到的状态转移图如下所示
2.编写代码
整个模块的代码可以分为四部分:
(1)用于消抖计时的计数器和用于连按计时的计数器。
(2)状态机的编写。
(3)信号的处理。
(4)译码部分。
module MAT_KEY(
Clk,
Rst_n,
Row_Data,
Col_Data,
Key_Flag, //按键值是否有效
Key_Value //按键值
);
input Clk;
input Rst_n;
input [3:0]Row_Data;
output reg [3:0]Col_Data;
output reg Key_Flag;
output reg [3:0]Key_Value;
reg [3:0]Row_Data_R; //数据缓存
reg [3:0]Col_Data_R;
reg [19:0]Counter_F; //定时器1
reg Cnt_F_En;
reg Cnt_F_Done;
reg [24:0]Counter_W; //定时器2,连按
reg Cnt_W_En;
reg Cnt_W_Done;
/*1.两个定时器的实现*/
always@(posedge Clk,negedge Rst_n)
if(!Rst_n)
Counter_F <= 20'd0;
else if(Cnt_F_En)
if(Counter_F == 20'd999_999)
Counter_F <= 20'd0;
else
Counter_F <= Counter_F + 20'd1;
else
Counter_F <= 20'd0;
always@(posedge Clk,negedge Rst_n)
if(!Rst_n)
Cnt_F_Done <= 0;
else if(Counter_F == 20'd999_999)
Cnt_F_Done <= 1;
else
Cnt_F_Done <= 0;always@(posedge Clk,negedge Rst_n)
if(!Rst_n)
Counter_W <= 25'd0;
else if(Cnt_W_En)
if(Counter_W == 25'd24_999_999)
Counter_W <= 25'd0;
else
Counter_W <= Counter_W + 25'd1;
else
Counter_W <= 25'd0;
always@(posedge Clk,negedge Rst_n)
if(!Rst_n)
Cnt_W_Done <= 0;
else if(Counter_W == 25'd24_999_999)
Cnt_W_Done <= 1;
else
Cnt_W_Done <= 0;/*2.状态机*/
localparam IDLE = 11'b00000000001,
P_FILTER = 11'b00000000010,
READ_ROW_P = 11'b00000000100,
SC_C0 = 11'b00000001000,
SC_C1 = 11'b00000010000,
SC_C2 = 11'b00000100000,
SC_C3 = 11'b00001000000,
GET_RESULT = 11'b00010000000,
WAIT_R = 11'b00100000000,
R_FILTER = 11'b01000000000,
READ_ROW_R = 11'b10000000000;
reg [10:0]State;
always@(posedge Clk,negedge Rst_n)
if(!Rst_n)
State <= IDLE;
else
case(State)
IDLE:
if(Row_Data != 4'b1111)
State <= P_FILTER;
else
State <= State;
P_FILTER:
if(Cnt_F_Done == 1)
State <= READ_ROW_P;
else
State <= State;
READ_ROW_P:
if(Row_Data == 4'b1111)
State <= IDLE;
else
State <= SC_C0;
SC_C0:
State <= SC_C1;
SC_C1:
State <= SC_C2;
SC_C2:
State <= SC_C3;
SC_C3:
State <= GET_RESULT;
GET_RESULT:
State <= WAIT_R;
WAIT_R:
if(Row_Data == 4'b1111)
State <= R_FILTER;
else
State <= State;
R_FILTER:
if(Cnt_F_Done)
State <= READ_ROW_R;
else
State <= State;
READ_ROW_R:
if(Row_Data != 4'b1111)
State <= WAIT_R;
else
State <= IDLE;
default:
State <= IDLE;
endcase
/*3.信号的处理*/
reg [7:0]Key_Value_R;
reg Key_Flag_R;
always@(posedge Clk,negedge Rst_n)
if(!Rst_n)begin
Cnt_F_En <= 0;
Cnt_W_En <= 0;
Col_Data <= 4'b0000;
Key_Flag_R <= 0;
end
else
case(State)
IDLE:
begin
Cnt_F_En <= 0; //关闭消抖定时器
Cnt_W_En <= 0; //关闭连按定时器
Col_Data <= 4'b0000;//列输出全低,检测所有列的按键按下情况
Key_Flag_R <= 0;
end
P_FILTER:
begin
Cnt_F_En <= 1; //开启消抖定时器
end
READ_ROW_P:
begin
Cnt_F_En <= 0; //关闭消抖定时器
Row_Data_R <= Row_Data;//寄存行输入数据
if(Row_Data != 4'b1111)begin
Col_Data <= 4'b1110;
Col_Data_R <= 4'b0000;
end
end
SC_C0:
begin
Col_Data <= 4'b1101;
if(Row_Data != 4'b1111)
Col_Data_R <= Col_Data_R | 4'b0001;
end
SC_C1:
begin
Col_Data <= 4'b1011;
if(Row_Data != 4'b1111)
Col_Data_R <= Col_Data_R | 4'b0010;
end
SC_C2:
begin
Col_Data <= 4'b0111;
if(Row_Data != 4'b1111)
Col_Data_R <= Col_Data_R | 4'b0100;
end
SC_C3:
begin
if(Row_Data != 4'b1111)
Col_Data_R <= Col_Data_R | 4'b1000;
end
GET_RESULT:
begin
if((Row_Data_R[0]+Row_Data_R[1]+Row_Data_R[2]+Row_Data_R[3])==4'd3 //只有一行按下
&&(Col_Data_R[0]+Col_Data_R[1]+Col_Data_R[2]+Col_Data_R[3])==4'd1)//只有一列按下
begin
Key_Value_R <= {Row_Data_R,Col_Data_R};
Key_Flag_R <= 1;
end
end
WAIT_R:
begin
Key_Flag_R <= 0;
if(Row_Data != 4'b1111)
Cnt_W_En <= 1; //开启连按计时器
else
Cnt_W_En <= 0;
end
R_FILTER:
begin
Cnt_F_En <= 1; //开启消抖定时器
end
READ_ROW_R:;
default:;
endcase
/*4.译码电路*/
always@(posedge Clk,negedge Rst_n)
if(!Rst_n)begin
Key_Flag <= 1'd0;
Key_Value <= 4'd0;
end
else begin
Key_Flag <= Key_Flag_R | Cnt_W_Done;
case(Key_Value_R)
8'b1110_0001: Key_Value <= 4'd0; //(0,0)
8'b1110_0010: Key_Value <= 4'd1; //(0,1)
8'b1110_0100: Key_Value <= 4'd2; //(0,2)
8'b1110_1000: Key_Value <= 4'd3; //(0,3)
8'b1101_0001: Key_Value <= 4'd4; //(1,0)
8'b1101_0010: Key_Value <= 4'd5; //(1,1)
8'b1101_0100: Key_Value <= 4'd6; //(1,2)
8'b1101_1000: Key_Value <= 4'd7; //(1,3)
8'b1011_0001: Key_Value <= 4'd8; //(2,0)
8'b1011_0010: Key_Value <= 4'd9; //(2,1)
8'b1011_0100: Key_Value <= 4'd10;//(2,2)
8'b1011_1000: Key_Value <= 4'd11;//(2,3)
8'b0111_0001: Key_Value <= 4'd12;//(3,0)
8'b0111_0010: Key_Value <= 4'd13;//(3,1)
8'b0111_0100: Key_Value <= 4'd14;//(3,2)
8'b0111_1000: Key_Value <= 4'd15;//(3,3)
default:Key_Value <= Key_Value;
endcase
end
endmodule
为了在modelsim下仿真时序,需要设计一个仿真矩阵键盘按下的模型。这里仿真的过程为按行一次按下16个按键。使用$random模拟矩阵键盘按下时的抖动,使用task编写一次按下所发出的时序。矩阵键盘的状态大体上可以分为真正有键按下和空闲的状态,如果有按下则按下的行输出的数据为按下列的数据,可高可低。当矩阵键盘处于空闲状态(包括抖动状态)时,输出的数据应该为4’b1111或者所模拟的键盘抖动的数据。具体的代码如下,
`timescale 1ns/1ns
module MAT_KEY_MODEL(
Col_Data,
Row_Data
);
input [3:0]Col_Data;
output reg [3:0]Row_Data;
reg [15:0]myrand;
reg [3:0]Row_Data_r; //行寄存器
reg Row_Sel; //真正被按下
reg [1:0]Now_Row; //当前按下的行
reg [1:0]Now_Col; //当前按下的列
initial begin
Now_Row = 0;
Now_Col = 0;
Row_Sel = 0;
myrand = 0;
end
initial begin
Row_Data_r <= 4'b1111;
#50000000;
press(0,0);
press(0,1);
press(0,2);
press(0,3);
press(1,0);
press(1,1);
press(1,2);
press(1,3);
press(2,0);
press(2,1);
press(2,2);
press(2,3);
press(3,0);
press(3,1);
press(3,2);
press(3,3);
$stop;
end
task press;
input [1:0]row,col;
begin
Row_Sel = 0;
Row_Data_r = 4'b1111;
Row_Data_r[row] = 0;
Now_Row = row;
repeat(20)begin
myrand = {$random}%65536;
#myrand;
Row_Data_r[row] = ~Row_Data_r[row];
end
Row_Sel = 1;
Now_Col = col;
#22000000;
Row_Sel = 0;
Row_Data_r = 4'b1111;
repeat(20)begin
myrand = {$random}%65536;
#myrand;
Row_Data_r[row] = ~Row_Data_r[row];
end
Row_Data_r = 4'b1111;
#22000000;
end
endtask
always@(*)
if(Row_Sel)
case(Now_Row)
2'd0:Row_Data ={1'b1,1'b1,1'b1,Col_Data[Now_Col]};
2'd1:Row_Data ={1'b1,1'b1,Col_Data[Now_Col],1'b1};
2'd2:Row_Data ={1'b1,Col_Data[Now_Col],1'b1,1'b1};
2'd3:Row_Data ={Col_Data[Now_Col],1'b1,1'b1,1'b1};
endcase
else
Row_Data = Row_Data_r;
endmodule
最后的测试顶层文件是
`timescale 1ns/1ns
module MAT_KEY_tb;
reg Clk;
reg Rst_n;
wire [3:0]Row_Data;
wire [3:0]Col_Data;
wire Key_Flag;
wire [3:0]Key_Value;
MAT_KEY MAT_KEY(
.Clk(Clk),
.Rst_n(Rst_n),
.Row_Data(Row_Data),
.Col_Data(Col_Data),
.Key_Flag(Key_Flag),
.Key_Value(Key_Value)
);
MAT_KEY_MODEL MAT_KEY_MODEL(
.Col_Data(Col_Data),
.Row_Data(Row_Data)
);
initial Clk = 1;
always #10 Clk = ~Clk;
initial begin
Rst_n = 0;
#200;
Rst_n = 1;
end
endmodule
最终的测试结果如下