基于FPGA的矩阵键盘驱动

  按键数量较多时,为了减少 I/O ⼝的占⽤,通常将按键排列成矩阵形式。在矩阵式键盘中,每条⽔平线和垂直线在交叉处不直接连通,⽽是通过一个按键连接。⼋根线就可以控制4*4=16个按键,⽐之直接将端⼝线⽤于键盘多出了⼀倍,⽽且线越多,区别越明显,⽐如多加⼀条线就可以构成20键的键盘。

  由此可⻅,在需要的键数⽐较多时,采⽤矩阵法来做键盘是合理的。

1、模块端口

  模块相应的端口信号如下表所示。

表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 被按下按键的编号有效指⽰信号

2、设计思路

  下图是4*4矩阵键盘原理图,4条列输入线均被上拉到高电平,所以没有按键按下时,列输入引脚就是高电平。行线KEY_R1~KEY_R4一般被FPGA驱动。4根⾏线和4根列线形成16个相交点。

基于FPGA的矩阵键盘驱动_第1张图片

图1 矩阵键盘原理图

  ⾏扫描法(逐⾏(或列)扫描查询法)如下:

  1. 判断键盘中有⽆键按下:将全部⾏线KEY_R1~KEY_R4置为低电平,同时检测列线KEY_C1~KEY_C4的状态。只要有⼀列的电平为低,则表⽰键盘中有键被按下,⽽且被按下的按键位于列信号为低电平的列。 若列线全为⾼电平,则键盘中⽆键按下。

  2. 判断被按下按键所在的位置:在确认有键按下后,即可进⼊确定具体被按下的按键所在位置的过程。依次将⾏线设置为低电平(即在置某根⾏线为低电平时,其它线为⾼电平),同时检测列线的状态,如果有列线为低电平,且列数与步骤1一致,则表示该被按下的按键就在这一行,即可计算出被按下按键的具体位置。

  对于按键和触摸屏等机械设备来说,都存在 “抖动”问题,按键从最初接通到稳定接通要经过数毫秒,其间可能发⽣多次“接通-断开”这样的⽑刺。如果不进⾏处理,会使系统识别到抖动信号⽽进⾏不必要的反应,导致模块功能不正常,为了避免这种现象的产⽣,需要对按键消抖。

基于FPGA的矩阵键盘驱动_第2张图片

图2 按键抖动

  软件消抖,检测有按键闭合后执⾏延时程序,抖动时间的⻓短由按键的机械特性决定,⼀般为5ms〜20ms。让前沿抖动消失后再⼀次检测键的状态,如果仍保持闭合状态电平,则确认按下按键操作有效。

  本文通过一个状态机实现对矩阵键盘的检测,对应的状态转化图如下图所示,包括4个状态,检测矩阵键盘列状态、检测被按下按键所在行状态、延时状态、等待按键释放状态。

基于FPGA的矩阵键盘驱动_第3张图片

图3 状态转换图

  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

3、仿真

  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全为高电平了,那么消抖计数器清零,表示是抖动,图中黄色信号为消抖计数器。

基于FPGA的矩阵键盘驱动_第4张图片

图4 抖动检测及消抖

  当消抖计数器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列,用于后文计算被按下按键位号。

基于FPGA的矩阵键盘驱动_第5张图片

图5 记录被按下按键所在列

  然后就是对行进行扫描,确定被按下按键所在行,如下图所示,每一行扫描16个时钟周期,总共扫描4行。
基于FPGA的矩阵键盘驱动_第6张图片

图6 行扫描

  有上图可知,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次。

基于FPGA的矩阵键盘驱动_第7张图片

图7 检测到被按下按键所在行

  后续仿真较为简单,就行扫描结束后,状态机跳转到延时状态,等待16个时钟周期后跳转到等待状态,直到所有列均为高电平时跳转到列检测状态,进行下次检测。

基于FPGA的矩阵键盘驱动_第8张图片

图8 状态机跳转

  TestBench写的有点复杂了,但是使用比较简单,因为我提供了一个任务。直接任务key_task()函数,输入被按下按键所在行和列,注意列和行的取值均为[0,3]闭区间。使用该函数会自动生成一些按键的抖动,更有利于仿真测试。

基于FPGA的矩阵键盘驱动_第9张图片

图9 TestBech文件更改

  代码已经贴在文中了,这种模块贴出来的原因也是方便之后自己使用,需要使用直接打开百度复制自己验证过的模块即可。由于我平时写代码都是vscode+modelsim搞定仿真,不会用quartus或者vivado创建工程,所以本文就不存在工程文件了,如果要代码文件可以在公众号后台回复“基于FPGA的矩阵键盘驱动“(不包括引号)即可。

  最后说明该模块此处没有上板测试,但是我已经在其余工程中使用过该模块,没有出现任何问题,所以放心使用,也会在后续的设计中以子模块的形式出现,到时候不会在对该模块讲解。

你可能感兴趣的:(FPGA,fpga开发)