基于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矩阵键盘实例”一文。