基于EP4CE6F17C8的FPGA矩阵键盘实例(另类方法)

一、电路模块

电路模块参见“基于EP4CE6F17C8的FPGA矩阵键盘实例”部分。

二、实验代码

本例使用6个数码管依次显示按下按键的键值,每位显示的值可从0~F,对应16个矩阵按键。按键reset为复位键,代码使用Verilog编写,具体如下。

先编写数码管实现显示字形解码的程序,模块名称为seg_decode,文件名称为seg_decode.v,代码如下。

module seg_decode(
        input[4:0]         data,          //显示的字形,可显示0~F十六个字形,所以需要5位
        output reg[7:0]    seg7           //字形编码,包含小数点,共8位
);
always@(*)                                //敏感信号为所有输入量
begin
    case(data)
        5'd0:seg7 <= 8'b1100_0000;        //字形0的编码
        5'd1:seg7 <= 8'b1111_1001;        //字形1的编码
        5'd2:seg7 <= 8'b1010_0100;        //字形2的编码
        5'd3:seg7 <= 8'b1011_0000;        //字形3的编码
        5'd4:seg7 <= 8'b1001_1001;        //字形4的编码
        5'd5:seg7 <= 8'b1001_0010;        //字形5的编码
        5'd6:seg7 <= 8'b1000_0010;        //字形6的编码
        5'd7:seg7 <= 8'b1111_1000;        //字形7的编码
        5'd8:seg7 <= 8'b1000_0000;        //字形8的编码
        5'd9:seg7 <= 8'b1001_0000;        //字形9的编码
        5'ha:seg7 <= 8'b1000_1000;        //字形A的编码
        5'hb:seg7 <= 8'b1000_0011;        //字形B的编码
        5'hc:seg7 <= 8'b1100_0110;        //字形C的编码
        5'hd:seg7 <= 8'b1010_0001;        //字形D的编码
        5'he:seg7 <= 8'b1000_0110;        //字形E的编码
        5'hf:seg7 <= 8'b1000_1110;        //字形F的编码
        default:
             seg7 <= 8'b1111_1111;        //默认不显示
    endcase
end
endmodule

接下来编写矩阵键盘扫描程序,模块名称为key4x4,文件名称为key4x4.v,代码如下。

module key4x4(
        input              clk,                  //系统时钟
        input              rst_n,                //复位按键
        input[3:0]         key_in_y,             //输入矩阵键盘的列信号(KEY0~KEY3)
        output reg[3:0]    key_out_x,            //输出矩阵键盘的行信号(KEY4~KEY7)                            
        output reg[4:0]    key_val               //输出矩阵键盘按键键值
);
reg[19:0] count;                                 //定义20位扫描计数器
//20ms整体扫描矩阵键盘一次矩阵键盘
always @(posedge clk or negedge rst_n)           //敏感信号为时钟上沿或复位下沿
begin
   if(!rst_n)                                    //低电平复位
    begin
      count <= 20'd0;
      key_out_x <= 4'b1111;                      //复位时计数值清零,行输出全1  
   end        
   else
    begin
    if(count == 20'd0)                           //0ms时扫描第一行
    begin
        key_out_x <= 4'b1110;                    //第一行输出0
        count <= count + 20'b1;                  //计数器加1
    end
    else if(count == 20'd249_999)                //5ms时扫描第二行
    begin
        key_out_x <= 4'b1101;                    //第二行输出0
        count <= count + 20'b1;                  //计数器加1
    end                
    else if(count ==20'd499_999)                 //10ms时扫描第三行
    begin
        key_out_x <= 4'b1011;                    //第三行输出0
        count <= count + 20'b1;                  //计数器加1
    end    
    else if(count ==20'd749_999)                 //15ms时扫描第四行
    begin
        key_out_x <= 4'b0111;                    //第四行输出0
        count <= count + 20'b1;                  //计数器加1
    end                
    else if(count ==20'd999_999)                 //20ms时计数器清零
    begin
        count <= 0;
    end    
    else
        count <= count + 20'b1;                  //计数器加1            
    end
end
//采样列的按键信号
reg[3:0] key_h1_scan;                            //第一行按键扫描值KEY
reg[3:0] key_h1_scan_r;                          //第一行按键扫描值寄存器KEY
reg[3:0] key_h2_scan;                            //第二行按键扫描值KEY
reg[3:0] key_h2_scan_r;                          //第二行按键扫描值寄存器KEY
reg[3:0] key_h3_scan;                            //第三行按键扫描值KEY
reg[3:0] key_h3_scan_r;                          //第三行按键扫描值寄存器KEY
reg[3:0] key_h4_scan;                            //第四行按键扫描值KEY
reg[3:0] key_h4_scan_r;                          //第四行按键扫描值寄存器KEY
always @(posedge clk or negedge rst_n)           //敏感信号为时钟上沿或复位下沿
begin
    if(!rst_n)                                   //低电平复位,复位时按键扫描值全部置1
    begin
        key_h1_scan <= 4'b1111;     
        key_h2_scan <= 4'b1111;          
        key_h3_scan <= 4'b1111;          
        key_h4_scan <= 4'b1111;        
    end        
    else
    begin
    if(count == 20'd124_999)                     //2.5ms时获取第一行矩阵键盘值
        key_h1_scan <= key_in_y;
    else if(count == 20'd374_999)                //7.5ms时获取第二行矩阵键盘值
        key_h2_scan <= key_in_y;
    else if(count == 20'd624_999)                //12.5ms时获取第三行矩阵键盘值
        key_h3_scan <= key_in_y;
    else if(count == 20'd874_999)                //17.5ms时获取第四行矩阵键盘值
        key_h4_scan <= key_in_y; 
    end
end
//获取到的按键信号锁存一个时钟节拍,用于后面产生按键节拍
always @(posedge clk)
begin
        key_h1_scan_r <= key_h1_scan;       
        key_h2_scan_r <= key_h2_scan; 
        key_h3_scan_r <= key_h3_scan; 
        key_h4_scan_r <= key_h4_scan;  
end 
//以下为第一行扫描的四个按键有效值,当检测到本行按键有下降沿时,flag_h1_key有效一个时钟周期   
wire[3:0] flag_h1_key = key_h1_scan_r[3:0] & (~key_h1_scan[3:0]);
//以下为第二行扫描的四个按键有效值,当检测到本行按键有下降沿时,flag_h2_key有效一个时钟周期
wire[3:0] flag_h2_key = key_h2_scan_r[3:0] & (~key_h2_scan[3:0]);
//以下为第三行扫描的四个按键有效值,当检测到本行按键有下降沿时,flag_h3_key有效一个时钟周期 
wire[3:0] flag_h3_key = key_h3_scan_r[3:0] & (~key_h3_scan[3:0]);
//以下为第四行扫描的四个按键有效值,当检测到本行按键有下降沿时,flag_h4_key有效一个时钟周期 
wire[3:0] flag_h4_key = key_h4_scan_r[3:0] & (~key_h4_scan[3:0]);
//按键键值编码
always @ (posedge clk or negedge rst_n)          //敏感信号为时钟上沿或复位下沿
begin
    if (!rst_n)                                  //低电平复位
        key_val <= 5'b11111;                     //复位时键值全部置1
    else
    begin            
         if(flag_h1_key[0])                      //矩阵键盘key1键按下,键值为0
            key_val <= 5'd0;
         else                                    //key1未按下,键值恢复为全1
            key_val <= 5'b11111;
         if(flag_h1_key[1])                      //矩阵键盘key2键按下,键值为1
            key_val <= 5'd1;
         if(flag_h1_key[2])                      //矩阵键盘key3键按下,键值为2
            key_val <= 5'd2;
         if(flag_h1_key[3])                      //矩阵键盘key4键按下,键值为3
            key_val <= 5'd3;
         if(flag_h2_key[0])                      //矩阵键盘key5键按下,键值为4
            key_val <= 5'd4;
         if(flag_h2_key[1])                      //矩阵键盘key6键按下,键值为5
            key_val <= 5'd5;
         if(flag_h2_key[2])                      //矩阵键盘key7键按下,键值为6
            key_val <= 5'd6;
         if(flag_h2_key[3])                      //矩阵键盘key8键按下,键值为7
            key_val <= 5'd7;
         if(flag_h3_key[0])                      //矩阵键盘key9键按下,键值为8
            key_val <= 5'd8;
         if(flag_h3_key[1])                      //矩阵键盘key10键按下,键值为9
            key_val <= 5'd9;
         if(flag_h3_key[2])                      //矩阵键盘key11键按下,键值为10
            key_val <= 5'd10;
         if(flag_h3_key[3])                      //矩阵键盘key12键按下,键值为11
            key_val <= 5'd11;
         if(flag_h4_key[0])                      //矩阵键盘key13键按下,键值为12
            key_val <= 5'd12;
         if(flag_h4_key[1])                      //矩阵键盘key14键按下,键值为13
            key_val <= 5'd13;
         if(flag_h4_key[2])                      //矩阵键盘key15键按下,键值为14
            key_val <= 5'd14;
         if(flag_h4_key[3])                      //矩阵键盘key16键按下,键值为15
            key_val <= 5'd15;
    end
end
endmodule

最后编写显示模块,并设置为顶层模块,模块名称为key_show,文件名称为key_show.v,代码如下。

module key_show(
    input               clk,                            //板载50HMz系统时钟
    input               rst_n,                          //复位按键
    input[3:0]          key_in_y,                       //输入矩阵键盘的列信号(KEY0~KEY3)
    output[3:0]         key_out_x,                      //输出矩阵键盘的行信号(KEY4~KEY7)
    output reg[7:0]     seg7,                           //段码端口
    output reg[5:0]     bit                             //位选端口
);
wire[4:0]    key_val;                                   //定义键值存储变量
//下面例化矩阵键盘
key4x4 u1(.clk(clk), .rst_n(rst_n), .key_in_y(key_in_y), .key_out_x(key_out_x), .key_val(key_val));
//下面定义6个数码管的字形码存储变量
wire [7:0] seg_0,seg_1,seg_2,seg_3,seg_4,seg_5;
//下面定义6个数码管显示数值的存储变量
reg [4:0] count_data0 = 5'b11111;
reg [4:0] count_data1 = 5'b11111;
reg [4:0] count_data2 = 5'b11111;
reg [4:0] count_data3 = 5'b11111;
reg [4:0] count_data4 = 5'b11111;
reg [4:0] count_data5 = 5'b11111;
//下面例化秒的个位字形解码单元
seg_decode seg0(.data(count_data0), .seg7(seg_0));
//下面例化秒的十位字形解码单元
seg_decode seg1(.data(count_data1), .seg7(seg_1));
//下面例化分的个位字形解码单元
seg_decode seg2(.data(count_data2), .seg7(seg_2));
//下面例化分的十位字形解码单元
seg_decode seg3(.data(count_data3), .seg7(seg_3));
//下面例化时的个位字形解码单元
seg_decode seg4(.data(count_data4), .seg7(seg_4));
//下面例化时的十位字形解码单元
seg_decode seg5(.data(count_data5), .seg7(seg_5));
always @(posedge clk or negedge rst_n)                  //敏感信号为时钟上沿或复位下沿
begin
   if(!rst_n)                                           //低电平复位,复位时数值存储变量全部置1
    begin
      count_data0 <= 5'b11111;
        count_data1 <= 5'b11111;
        count_data2 <= 5'b11111;
        count_data3 <= 5'b11111;
        count_data4 <= 5'b11111;
        count_data5 <= 5'b11111;  
   end        
   else
    begin
    case(key_val)                                       //判断键值
        5'd0:                                           //0号键值,显示左移一位,最低位显示0
        begin
            count_data5 <= count_data4;
            count_data4 <= count_data3;
            count_data3 <= count_data2;
            count_data2 <= count_data1;
            count_data1 <= count_data0;
            count_data0 <= 5'd0;
        end
        5'd1:                                           //1号键值,显示左移一位,最低位显示1
        begin
            count_data5 <= count_data4;
            count_data4 <= count_data3;
            count_data3 <= count_data2;
            count_data2 <= count_data1;
            count_data1 <= count_data0;
            count_data0 <= 5'd1;
        end
        5'd2:                                           //2号键值,显示左移一位,最低位显示2
            begin
            count_data5 <= count_data4;
            count_data4 <= count_data3;
            count_data3 <= count_data2;
            count_data2 <= count_data1;
            count_data1 <= count_data0;
            count_data0 <= 5'd2;
        end
        5'd3:                                           //3号键值,显示左移一位,最低位显示3
            begin
            count_data5 <= count_data4;
            count_data4 <= count_data3;
            count_data3 <= count_data2;
            count_data2 <= count_data1;
            count_data1 <= count_data0;
            count_data0 <= 5'd3;
        end
        5'd4:                                           //4号键值,显示左移一位,最低位显示4
            begin
            count_data5 <= count_data4;
            count_data4 <= count_data3;
            count_data3 <= count_data2;
            count_data2 <= count_data1;
            count_data1 <= count_data0;
            count_data0 <= 5'd4;
        end
        5'd5:                                           //5号键值,显示左移一位,最低位显示5
            begin
            count_data5 <= count_data4;
            count_data4 <= count_data3;
            count_data3 <= count_data2;
            count_data2 <= count_data1;
            count_data1 <= count_data0;
            count_data0 <= 5'd5;
        end
        5'd6:                                           //6号键值,显示左移一位,最低位显示6
            begin
            count_data5 <= count_data4;
            count_data4 <= count_data3;
            count_data3 <= count_data2;
            count_data2 <= count_data1;
            count_data1 <= count_data0;
            count_data0 <= 5'd6;
        end
        5'd7:                                           //7号键值,显示左移一位,最低位显示7
            begin
            count_data5 <= count_data4;
            count_data4 <= count_data3;
            count_data3 <= count_data2;
            count_data2 <= count_data1;
            count_data1 <= count_data0;
            count_data0 <= 5'd7;
        end
        5'd8:                                           //8号键值,显示左移一位,最低位显示8
            begin
            count_data5 <= count_data4;
            count_data4 <= count_data3;
            count_data3 <= count_data2;
            count_data2 <= count_data1;
            count_data1 <= count_data0;
            count_data0 <= 5'd8;
        end
        5'd9:                                           //9号键值,显示左移一位,最低位显示9
            begin
            count_data5 <= count_data4;
            count_data4 <= count_data3;
            count_data3 <= count_data2;
            count_data2 <= count_data1;
            count_data1 <= count_data0;
            count_data0 <= 5'd9;
        end
        5'd10:                                          //10号键值,显示左移一位,最低位显示A
            begin
            count_data5 <= count_data4;
            count_data4 <= count_data3;
            count_data3 <= count_data2;
            count_data2 <= count_data1;
            count_data1 <= count_data0;
            count_data0 <= 5'd10;
        end
        5'd11:                                          //11号键值,显示左移一位,最低位显示b
            begin
            count_data5 <= count_data4;
            count_data4 <= count_data3;
            count_data3 <= count_data2;
            count_data2 <= count_data1;
            count_data1 <= count_data0;
            count_data0 <= 5'd11;
        end
        5'd12:                                          //12号键值,显示左移一位,最低位显示C
            begin
            count_data5 <= count_data4;
            count_data4 <= count_data3;
            count_data3 <= count_data2;
            count_data2 <= count_data1;
            count_data1 <= count_data0;
            count_data0 <= 5'd12;
        end
        5'd13:                                          //13号键值,显示左移一位,最低位显示d
            begin
            count_data5 <= count_data4;
            count_data4 <= count_data3;
            count_data3 <= count_data2;
            count_data2 <= count_data1;
            count_data1 <= count_data0;
            count_data0 <= 5'd13;
        end
        5'd14:                                          //14号键值,显示左移一位,最低位显示E
            begin
            count_data5 <= count_data4;
            count_data4 <= count_data3;
            count_data3 <= count_data2;
            count_data2 <= count_data1;
            count_data1 <= count_data0;
            count_data0 <= 5'd14;
        end
        5'd15:                                          //15号键值,显示左移一位,最低位显示F
            begin
            count_data5 <= count_data4;
            count_data4 <= count_data3;
            count_data3 <= count_data2;
            count_data2 <= count_data1;
            count_data1 <= count_data0;
            count_data0 <= 5'd15;
        end
    endcase
    end
end
reg[17:0]    time_cnt;                                  //定义20位时钟计数器
reg[3:0]     scan_sel;                                     //定义扫描位置计数器
//3.3毫秒循环计数
always@(posedge clk or negedge rst_n)                   //敏感信号为时钟上沿或复位下沿
begin
    if(rst_n == 1'b0)                                   //低电平复位时计数器全部清零
    begin
        time_cnt <= 18'd0;
        scan_sel <= 4'd0;
    end
    else if(time_cnt >= 18'd166_666)                    //时钟计数器到达3.3毫秒时
    begin
        time_cnt <= 18'd0;                              //时钟计数器清零
        if(scan_sel == 4'd5)                            //如果扫描位置计数器已经到1则恢复0
            scan_sel <= 4'd0;
        else
            scan_sel <= scan_sel + 4'd1;                //否则扫描位置计数器加1,即每3.3ms加一次
    end
    else
        begin
            time_cnt <= time_cnt + 18'd1;               //否则时钟计数器加1,即来一次时钟脉冲加一次
        end
end
//数码管扫描显示
always@(posedge clk or negedge rst_n)                   //敏感信号为时钟上沿或复位下沿
begin
    if(!rst_n)                                          //低电平复位时数码管全灭
    begin
        bit <= 6'b111111;
        seg7 <= 8'hff;
    end
    else 
        case(scan_sel)
            4'd0:                                       //数码管0显示个位
            begin
                bit <= 6'b111110;
                seg7 <= seg_0;
            end
            4'd1:                                       //数码管1显示十位
            begin
                bit <= 6'b111101;
                seg7 <= seg_1;
            end
                4'd2:                                   //数码管2显示百位
            begin
                bit <= 6'b111011;
                seg7 <= seg_2;
            end
                4'd3:                                   //数码管3显示千位
            begin
                bit <= 6'b110111;
                seg7 <= seg_3;
            end
                4'd4:                                   //数码管4显示万位
            begin
                bit <= 6'b101111;
                seg7 <= seg_4;
            end
                4'd5:                                   //数码管5显示十万位
            begin
                bit <= 6'b011111;
                seg7 <= seg_5;
            end
            default:                                    //数码管全部熄灭
            begin
                bit <= 6'b111111;
                seg7 <= 8'hff;
            end
        endcase
end
endmodule

三、代码说明

1、本例主要讨论矩阵键盘的设计方法,数码管的扫描及字形解码可参看“基于EP4CE6F17C8的FPGA数码管动态显示实例”一文。
2、本例中使用的矩阵键盘为4X4型,共16个按键,行、列各引出4个引脚,共8个引脚。程序代码先对第一行输出0,其余行输出1;经过5ms之后第二行输出0,第一行输出1;再过5ms之后,第三行输出0,第二行输出1;再过5ms之后,第四行输出0,第三行输出1;再过5ms之后重复第一行扫描。全部行扫描一次用时20ms。
3、获取按键值的时刻取5ms的一半,即2.5ms进行。在第一个2.5ms时读取四个列的数据,即在第一行输出0的5ms时间的中点取列上的数据,第二个在7.5ms时读取,第三个在12.5ms时读取,第四个在17.5ms时读取。
4、把第3步获取到的四个按键数据锁存一个时钟节拍,用于产生按键节拍,具体原理可参见“基于EP4CE6F17C8的FPGA键控灯实例”一文。
5、把第4步锁存的前后两个数据进行相关逻辑操作(如key_h1_scan_r[3:0] & (~key_h1_scan[3:0])得到下降沿, (~key_h1_scan_r[3:0])&key_h1_scan[3:0])得到上升沿,可得到本行扫描时的有效按键的值(有效时对应位为1),且存续一个时钟周期,之后恢复到全0。
6、对全部四行扫描到的16个按键有效值进行编码,键值从0编到15。在输出键值编码时,仍然采取一个时钟周期有效的方式,即健值只存在一个时钟周期。这样可以有效消除按一次键形成多次动作的缺陷。
7、在对按键值进行编码的代码中,只有第一个if语句使用了else,其原因如下。当flag_h1_key[0]有效时,在本时钟周期内,只对它进行编码(key_val <= 5'd0),若无效则编码一个无效的键值(5'b11111,因为只有16个按键编码不到该值)。当flag_h1_key[1]有效时,在本时钟周期内,只对它进行编码(key_val <= 5'd1),其余14个按键编码均如此。由于16个按键flag_hm_key[n]的有效信号只存在一个时钟周期,所以,在当前时钟周期内,都会对有效按键进行编码,但在下一个时钟周期内所有按键都无效,因此会执行第一个if的else部分,即输出无效编码。这样就保证了键值编码只会输出一个时钟周期。注意,无效赋值(key_val <= 5'b11111)只能放在第一个if语句中的else部分,理由如下。假设放在最后一个if语句的else中,则除了最后个按键外其他所有按键都得不到编码。因为前面所有的编码都会被最后一个if语句的else部分所覆盖。相反,放在第一个if语句中,当flag_h1_key[0]无效时,编码先被赋无效值5'b11111,但在其后按键有效时编码会被真实键值覆盖,所以不影响。以第二行第一列的flag_h2_key[0]为例,当其有效时(k5键被按下),其编码过程如下。由于16个按键在同一个时钟范围内,只会有一个按键有效,所以第一个if语句中的flag_h1_key[0]无效,编码被赋值为5'b11111,当执行到第5个if语句时,由于flag_h2_key[0]有效,编码被修改为5'd4。当下一个时钟来时,flag_h2_key[0]从有效变为无效,所以编码值不更新,仍然为第一个if语句的无效赋值5'b11111。这样就保证了key5按下一次,只输出一个时钟周期的键值编码5'd4。
8、本例中的矩阵键盘设计并没有采取延时消抖方式,但实际使用效果还不错。其原因是利用每行5ms的扫描时间,且获取按键在其时间中点进行,间接也取到了消抖的效果。
9、显示部分采用了向左移位的方式,即当前按下的键值只显示在最右边一位数码管上,其余的往左移一位。

四、实验步骤

FPGA开发的详细步骤请参见“基于EP4CE6F17C8的FPGA开发流程(以半加器为例)”一文,本例只对不同之处进行说明。

本例工程放在D:\EDA_FPGA\Exam_9文件夹下,工程名称为Exam_9。模块文件名称为key_show.v,并设置为顶层实体。其余步骤与“基于EP4CE6F17C8的FPGA开发流程”中的一样。

接下来看管脚约束,本例中6个数码管一共有14个引脚,矩阵键盘8个引脚,再加上时钟晶振和复位按钮,一共24个。具体的端口分配如下图所示。

对于未用到的引脚设置为三态输入方式,多用用途引脚全部做为普通I/O端口,电压设置为3.3-V LVTTL(与”基于EP4CE6F17C8的FPGA开发流程“中的一样)。需要注意,程序中的每个端口都必须为其分配管脚,如果系统中存在未分配的I/O,软件可能会进行随机分配,这将造成不可预料的后果,存在烧坏FPGA芯片的风险。

接下来对工程进行编译,编译完成后,可查看一下逻辑器件的消耗情况,如下图所示。可见消耗的逻辑器件要比“基于EP4CE6F17C8的FPGA矩阵键盘实例”中的多,因此并不推荐采用此方法,仅提供学习参考而已。

最后进行下载,并查看结果,结果可参看“基于EP4CE6F17C8的FPGA矩阵键盘实例”一文。

posted @ 2024-04-22 12:49  fxzq  阅读(129)  评论(0编辑  收藏  举报