??按键数量较多时,为了减少 I/O ?的占?,通常将按键排列成矩阵形式。在矩阵式键盘中,每条?平线和垂直线在交叉处不直接连通,?是通过一个按键连接。?根线就可以控制4*4=16个按键,?之直接将端?线?于键盘多出了?倍,?且线越多,区别越明显,?如多加?条线就可以构成20键的键盘。
??由此可?,在需要的键数?较多时,采?矩阵法来做键盘是合理的。
??模块相应的端口信号如下表所示。
表1 模块端口信号
信号 | I/O | 位宽 | 含义 |
---|---|---|---|
clk | I | 1 | 系统?作时钟,默认50MHz。 |
rst_n | I | 1 | 系统复位信号,低电平有效。 |
key_col | I | 4 | 矩阵键盘列输入信号,默认?电平,被按下时,所在列为低电平。 |
key_row | O | 4 | 矩阵键盘?输出信号,默认输出低电平,?扫描时当前?输出低电平,其余?输出。 |
key_out | O | 4 | 被按下按键的编号 |
key_vld | O | 1 | 被按下按键的编号有效指?信号 |
??下图是4*4矩阵键盘原理图,4条列输入线均被上拉到高电平,所以没有按键按下时,列输入引脚就是高电平。行线KEY_R1~KEY_R4一般被FPGA驱动。4根?线和4根列线形成16个相交点。
???扫描法(逐?(或列)扫描查询法)如下:
??1. 判断键盘中有?键按下:将全部?线KEY_R1~KEY_R4置为低电平,同时检测列线KEY_C1~KEY_C4的状态。只要有?列的电平为低,则表?键盘中有键被按下,?且被按下的按键位于列信号为低电平的列。 若列线全为?电平,则键盘中?键按下。
??2. 判断被按下按键所在的位置:在确认有键按下后,即可进?确定具体被按下的按键所在位置的过程。依次将?线设置为低电平(即在置某根?线为低电平时,其它线为?电平),同时检测列线的状态,如果有列线为低电平,且列数与步骤1一致,则表示该被按下的按键就在这一行,即可计算出被按下按键的具体位置。
??对于按键和触摸屏等机械设备来说,都存在 “抖动”问题,按键从最初接通到稳定接通要经过数毫秒,其间可能发?多次“接通-断开”这样的?刺。如果不进?处理,会使系统识别到抖动信号?进?不必要的反应,导致模块功能不正常,为了避免这种现象的产?,需要对按键消抖。
??软件消抖,检测有按键闭合后执?延时程序,抖动时间的?短由按键的机械特性决定,?般为5ms?20ms。让前沿抖动消失后再?次检测键的状态,如果仍保持闭合状态电平,则确认按下按键操作有效。
??本文通过一个状态机实现对矩阵键盘的检测,对应的状态转化图如下图所示,包括4个状态,检测矩阵键盘列状态、检测被按下按键所在行状态、延时状态、等待按键释放状态。
??CHK_COL状态:该状态下向矩阵键盘的所有行输出低电平,同时检测列输入信号。当检测到列输入不全为高电平时,计数器shake_cnt对时钟进行计数,当计数到20ms时,表示按键被按下,将低电平所在列寄存,表示被按下按键就在这一列,然后状态机跳转到CHK_ROW检测行。如果在中途发现列输入全为高电平,则计数器清零,表示之前检测的是按键抖动,继续检测。
??CHK_ROW状态:该状态需要检测被按下按键所在行,检测方式就是逐行扫描,所以需要两个计数器,一个计数器row_cnt用来记录一行扫描的时间,目前一行扫面时间设置为16个时钟周期。另一个计数器row_index用来记录当前扫描到第几行了,因为该键盘只有四行,所以row_index最大值为3。
??计数器row_index对应行输出低电平,其余行输出高电平,同时检测列信号,如果列信号不全为高,并且为低电平的列与CHK_COL状态下检测到低电平的列一致,表示此时计数器row_index的值就是被按下按键所在行。根据row_index的值和之前记录按键的列就可以计算出被按下按键的位号,然后输出给下游模块。
??DELAY状态:就是为了多等待几个时钟周期,让行信号全部驱动为低电平之后,在跳转到等待按键被释放状态,否则会出现错误。
??WAIT_END状态:当检测到被按下按键位号后,需要等待被按下的按键释放,之后才能开始下一次检测,不然会重复检测同一个按键。防止按键被按下1次,但是却检测出多次按下的错误。
??其余计数器,标志信号这些就不细讲了,也比较简单,直接看代码吧。参考代码如下所示:
module key_scan #(
parameter TCLK = 20 ,//系统时钟周期,单位ns。
parameter TIME_20MS = 20_000_000 //按键消抖时间,单位为ns。
)(
input clk ,//系统时钟信号,默认50MHz。
input rst_n ,//系统复位,低电平有效;
input [3 : 0] key_col ,//矩阵键盘的列号;
output reg [3 : 0] key_row ,//矩阵键盘的行号;
output reg [3 : 0] key_out ,//矩阵键盘被按下按键的数值;
output reg key_vld //矩阵键盘被按下按键数据输出有效指示信号;
);
//自定义参数;
localparam CHK_COL = 4'b0001 ;//状态机的列扫描状态;
localparam CHK_ROW = 4'b0010 ;//状态机的的行扫描状态;
localparam DELAY = 4'b0100 ;//状态机的延时状态;
localparam WAIT_END = 4'b1000 ;//状态机的等待状态;
localparam TIME_20MS_NUM = TIME_20MS / TCLK ;//计算出TIME_20MS对应的系统时钟个数;
localparam TIME_20MS_W = clogb2(TIME_20MS_NUM-1) ;//利用函数计算出TIME_20MS_NUM对应的寄存器位宽;
reg [3 : 0] key_col_ff0 ;
reg [3 : 0] key_col_ff1 ;
reg [1 : 0] key_col_get ;
reg [3 : 0] state_c ;
reg [TIME_20MS_W - 1 : 0] shake_cnt ;
reg [3 : 0] state_n ;
reg [1 : 0] row_index ;
reg [3 : 0] row_cnt ;
wire end_shake_cnt ;
wire col2row_start ;
wire row2del_start ;
wire del2wait_start ;
wire wait2col_start ;
wire add_row_cnt ;
wire end_row_cnt ;
wire add_shake_cnt ;
wire add_row_index ;
wire end_row_index ;
//自动计算位宽函数;
function integer clogb2(input integer depth);begin
if(depth == 0)
clogb2 = 1;
else if(depth != 0)
for(clogb2=0 ; depth>0 ; clogb2=clogb2+1)
depth=depth >> 1;
end
endfunction
//将输入的列信号打两拍,降低亚稳态出现的机率。
always@(posedge clk)begin
{key_col_ff1,key_col_ff0} <= {key_col_ff0,key_col};
end
//计数器shake_cnt,如果有按键被按下,则key_col_ff1!=4'hf,此时计数器计数。
always@(posedge clk or negedge rst_n)begin
if(rst_n==0)begin
shake_cnt <= 0;
end
else if(add_shake_cnt)begin
if(end_shake_cnt)//按键被按下20ms时,计数器清零;
shake_cnt <= 0;
else//否则当按键被按下时,计数器进行计数;
shake_cnt <= shake_cnt + 1;
end
else begin//没有按键被按下时,计数器清零;
shake_cnt <= 0;
end
end
assign add_shake_cnt = (key_col_ff1!=4'hf) && (state_c == CHK_COL);
assign end_shake_cnt = add_shake_cnt && shake_cnt == TIME_20MS_NUM-1 ;
//当列检查结束时,将被按下按键所在列寄存;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始为0,没有按键被按下;
key_col_get <= 0;
end
else if(col2row_start)begin//当状态机从列检查跳转到行检查时,将按键对应列保存;
if(key_col_ff1==4'b1110)//最低位为0,则表示第0列按键被按下;
key_col_get <= 0;
else if(key_col_ff1==4'b1101)//第1位位0,则表示第1列按键被按下;
key_col_get <= 1;
else if(key_col_ff1==4'b1011)//第2位位0,则表示第2列按键被按下;
key_col_get <= 2;
else//否则表示第3列按键被按下;
key_col_get <= 3;
end
end
//状态机的第一段;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
state_c <= CHK_COL;
end
else begin
state_c <= state_n;
end
end
always@(*)begin
case(state_c)
CHK_COL: begin//检查列触发;
if(col2row_start)begin
state_n = CHK_ROW;
end
else begin
state_n = CHK_COL;
end
end
CHK_ROW: begin//检查行触发;
if(row2del_start)begin
state_n = DELAY;
end
else begin
state_n = CHK_ROW;
end
end
DELAY : begin//这个状态的存在是为了等待行扫描结束后,计算结果输出。
if(del2wait_start)begin
state_n = WAIT_END;
end
else begin
state_n = DELAY;
end
end
WAIT_END: begin//此时四行全部输出低电平,如果按键被按下,没有松开,那么会持续之前的状态,就需要一致等待按键松开;
if(wait2col_start)begin
state_n = CHK_COL;
end
else begin
state_n = WAIT_END;
end
end
default: state_n = CHK_COL;
endcase
end
//状态机第三段,描述
assign col2row_start = (state_c==CHK_COL ) && end_shake_cnt;//检查到有对应列持续20MS被按下。
assign row2del_start = (state_c==CHK_ROW ) && end_row_index;//行扫描完成;
assign del2wait_start= (state_c==DELAY ) && end_row_cnt;
assign wait2col_start= (state_c==WAIT_END) && key_col_ff1==4'hf;//4'hf表示前面的按键已经被松开,状态机重新回到列检测状态。
//控制行数据的输出,在检查被按下按键所在行时,进行行循环扫描。
//从第一行开始一次拉低,其余行拉高,其余时刻所有行全部拉低。
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
key_row <= 4'b0;
end
else if(state_c==CHK_ROW)begin//行扫描,依次将每行的电平拉低。
key_row <= ~(1'b1 << row_index);
end
else begin
key_row <= 4'b0;
end
end
//行扫描的计数器,对行进行扫描。
//每行扫描持续时间为行计数器row_cnt的计数值,目前为16个时钟周期。
//当4行全部扫面完毕时,计数器清零;
always@(posedge clk or negedge rst_n)begin
if(rst_n==0)begin
row_index <= 0;
end
else if(add_row_index) begin
if(end_row_index)
row_index <= 0;
else
row_index <= row_index + 1;
end
else if(state_c!=CHK_ROW)begin
row_index <= 0;
end
end
assign add_row_index = state_c==CHK_ROW && end_row_cnt;
assign end_row_index = add_row_index && row_index == 4-1 ;
//每行扫描持续时间,初始值为0,此处设置每行扫面16个时钟周期;
//状态机位于行扫描或者等待状态时进行计数,当计数到最大值16时清零。
always@(posedge clk or negedge rst_n)begin
if(rst_n==0)begin
row_cnt <= 0;
end
else if(add_row_cnt)begin
if(end_row_cnt)
row_cnt <= 0;
else
row_cnt <= row_cnt + 1;
end
end
assign add_row_cnt = state_c==CHK_ROW || state_c==DELAY;
assign end_row_cnt = add_row_cnt && row_cnt == 16-1;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
key_out <= 0;
end//计算被按下按键的数值;
else if(state_c==CHK_ROW && end_row_cnt && key_col_ff1[key_col_get]==1'b0)begin
key_out <= {row_index,key_col_get};
end
end
//按键数值有效指示信号,高电平时表示key_out输出的值是有效的。
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
key_vld <= 1'b0;
end
else begin//当没扫描一行,前面暂存的列为低电平的时候,表示这一行,这一列的按键被按下。
key_vld <= (state_c==CHK_ROW && end_row_cnt && key_col_ff1[key_col_get]==1'b0);
end
end
endmodule
??TestBench写起来有点小麻烦,可能存在一些小错误,本文提供的参考代码如下所示:
`timescale 1 ns/1 ns
module test();
localparam CYCLE = 20 ;//系统时钟周期,单位ns,默认20ns;
localparam RST_TIME = 10 ;//系统复位持续时间,默认10个系统时钟周期;
localparam TIME_20MS = 2_000 ;//按键消抖时间,默认20ms,单位ns,仿真时将时间缩短;
reg clk ;//系统时钟,默认100MHz;
reg rst_n ;//系统复位,默认低电平有效;
reg [3 : 0] key_col ;
reg [3 : 0] key_col_r ;
reg key_col_sel ;
reg [1 : 0] now_row ;//当前行
reg [1 : 0] now_col ;//当前列;
wire [3 : 0] key_row ;
wire [3 : 0] key_out ;
wire key_vld ;
//例化需要测试的模块;
key_scan #(
.TCLK ( CYCLE ),
.TIME_20MS ( TIME_20MS )
)
u_key_scan (
.clk ( clk ),
.rst_n ( rst_n ),
.key_col ( key_col ),
.key_row ( key_row ),
.key_out ( key_out ),
.key_vld ( key_vld )
);
//生成周期为CYCLE数值的系统时钟;
initial begin
clk = 0;
forever #(CYCLE/2) clk = ~clk;
end
//生成复位信号;
initial begin
rst_n = 1;key_col = 4'hf;key_col_sel=0;
key_col_r = 4'hf;now_col = 0;now_row = 0;
#1;
rst_n = 0;//开始时复位10个时钟;
#(RST_TIME*CYCLE);
rst_n = 1;
#(20*CYCLE);
key_task(0,1);//按下第0行第1列按键;1号按键
key_task(1,2);//按下第1行第2列按键;6号按键
key_task(3,1);//按下第3行第1列按键;13号按键
key_task(2,2);//按下第2行第2列按键;10号按键
key_task(0,0);//按下第0行第0列按键,0号按键
#(2000*CYCLE);
$stop;//停止仿真;
end
//生成对应按键的信号;
task key_task(
input [1 : 0] row ,//被按下按键的行号;
input [1 : 0] col //被按下按键的列号;
);
begin
now_col <= col;
now_row <= row;
key_col_r <= 4'hf;//初始时,没有按键被按下;
key_col_sel <= 1'b0;
@(posedge clk);
key_col_r[col] <= 1'b0;//
repeat(20) begin//将信号随机翻转20次,模拟按键按下时的抖动。
#(({$random} % (TIME_20MS/(CYCLE+5))) * CYCLE);
key_col_r[col] <= ~key_col_r[col];
end
key_col_sel <= 1'b1;//列选通信号拉高;
repeat(TIME_20MS/7)begin//按键按下的保持时间;
@(posedge clk);
end
key_col_sel <= 1'b0;//列选通信号拉低;
key_col_r <= 4'hf;//按键被释放;
repeat(20) begin//将信号随机翻转20次,模拟按键释放时的抖动。
#(({$random} % (TIME_20MS/(CYCLE+5))) * CYCLE);
key_col_r[col] <= ~key_col_r[col];
end
repeat(TIME_20MS)begin//释放按键的保持时间;
@(posedge clk);
end
end
endtask
always@(*)begin
if(key_col_sel)begin
case (now_col)
2'd0 : key_col <= {3'd7,key_row[now_row]};
2'd1 : key_col <= {2'd3,key_row[now_row],1'd1};
2'd2 : key_col <= {1'd1,key_row[now_row],2'd3};
2'd3 : key_col <= {key_row[now_row],3'd7};
endcase
end
else begin
key_col <= key_col_r;
end
end
endmodule
??仿真结果如下图所示,橙色信号key_row输出全为低电平,当列输入信号key_col_ff1不全为高电平时,消抖计数器shake_cnt就一直对时钟进行计数,没有计数20ms,但是检测到key_col_ff1全为高电平了,那么消抖计数器清零,表示是抖动,图中黄色信号为消抖计数器。
??当消抖计数器shake_cnt计数到20ms(为了加快仿真,在TestBench中该数值设置的比较小)时,表示有按键被按下,则此时记下被按下按键所在列,且状态机跳转到CHK_ROW状态,图中紫红色信号分别为状态机的现态state_c与状态机的次态state_n。
??下图是检测到按键被按下后,因为key_col_ff1为4’hd=4’b1101,所以第1列为0,表示第1列有按键被按下。此时key_col_get赋值为1,表示被按下的按键位于第1列,用于后文计算被按下按键位号。
??然后就是对行进行扫描,确定被按下按键所在行,如下图所示,每一行扫描16个时钟周期,总共扫描4行。
??有上图可知,key_vld在扫描第0行时拉高,表示被按下的按键在第0行,将这部分波形放大,得到下图。
??key_row信号输出4’he,最低位为0,其余位高电平,对第0行进行扫描。当计数row_cnt计数到最大值时,检测列输入信号key_col_ff1=4‘hd。表示被按下按键就在第0行,根据4*此时row_index的值+ key_col_get = 4 * 0 + 1 = 1。所以检测到被按下的按键位号为1,key_out输出1,且key_vld拉高一个时钟周期,表示1号按键被按下1次。
??后续仿真较为简单,就行扫描结束后,状态机跳转到延时状态,等待16个时钟周期后跳转到等待状态,直到所有列均为高电平时跳转到列检测状态,进行下次检测。
??TestBench写的有点复杂了,但是使用比较简单,因为我提供了一个任务。直接任务key_task()函数,输入被按下按键所在行和列,注意列和行的取值均为[0,3]闭区间。使用该函数会自动生成一些按键的抖动,更有利于仿真测试。
??代码已经贴在文中了,这种模块贴出来的原因也是方便之后自己使用,需要使用直接打开百度复制自己验证过的模块即可。由于我平时写代码都是vscode+modelsim搞定仿真,不会用quartus或者vivado创建工程,所以本文就不存在工程文件了,如果要代码文件可以在公众号后台回复“基于FPGA的矩阵键盘驱动“(不包括引号)即可。
??最后说明该模块此处没有上板测试,但是我已经在其余工程中使用过该模块,没有出现任何问题,所以放心使用,也会在后续的设计中以子模块的形式出现,到时候不会在对该模块讲解。