一、电路模块
1、数码管
开发板板载了6个数码管,全部为共阳型,原理图如下图所示,段码端引脚为DIG[0]~DIG[7]共8位(包含小数点),位选端引脚为SEL[0]~SEL[5]共6位。端口均为低电平有效。
其实物图如下所示。
数码管引脚分配见下表。
2、时钟晶振
开发板板载了一个50MHz的有源晶振,为系统提供时钟。
其实物图如下所示。
时钟输出引脚分配见下表。
3、按键
开发板板载了4个独立按键,其中有3个用户按键(KEY1~KEY3),1个功能按键(RESET)。按键按下为低电平(0),释放为高电平(1),4个按键的原理图如下图所示。
其实物图如下所示。
按键的引脚分配见下表。
4、矩阵键盘
本例使用了开发板配套的矩阵键盘模块,共有16个独立按键,原理图如下图所示。
其实物图如下所示。
矩阵按键的引脚分配见下表。
二、实验代码
本例使用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, //板载50HMz系统时钟
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芯片的风险。
接下来对工程进行编译,编译完成后,可查看一下逻辑器件的消耗情况,如下图所示。
另外,还可以点击菜单Tools->Netlist Viewers->RTL Viewer,查看一下生成的RTL电路图。
最后进行程序下载,并查看结果。下图为初始状态,即没有键按下时的情况。
下图为按下矩阵键盘上的key1键,显示键值0。
下图为按下矩阵键盘上的key2键,显示键值1。
下图为按下矩阵键盘上的key3键,显示键值2。
下图为按下矩阵键盘上的key10键,显示键值9。
下图为按下矩阵键盘上的key13键,显示键值C。
下图为按下矩阵键盘上的key15键,显示键值E。
下图为按下开发板上的reset键,显示全部熄灭。