实验11 矩阵键盘之BCD计数器
矩阵键盘原理
在键盘中按键数量较多时,为了减少I/O口的占用,通常将按键排列成矩阵形式。在矩阵键盘中,行线和列线不直接连通,而是通过一个按键进行连接。如下是本文所使用的FPGA开发板上板载矩阵键盘的原理图。
矩阵键盘电路原理图
从图中可以看到,键盘的列线已经都设置了上拉电阻,在没有按下按键时,键盘列线的输出为高电平。若给键盘的某一行输入低电平,并按下该行的一个按键,那么该按键连接的列线的输出就会被拉低。因此,使用FPGA驱动矩阵键盘时,一般通过输送低电平给矩阵键盘的行信号,并读取键盘列信号来判断是否有按键按下。举个例子,按照上图中的管脚名,Key_Row0~Key_Row3为矩阵键盘的行信号,Key_Col0~Key_Col3为矩阵键盘的列信号,若给Key_Row0输送低电平,读取键盘列信号Key_Col0~Key_Col3的结果为1011,则说明Key_Col2 这一列和KEY_Row0这一行交叉点处的按键KEY2被按下了。
基于FPGA的矩阵键盘驱动
本实验将使用到矩阵键盘的一行共四个按键,将实现一个多功能计数器,矩阵键盘四个按键分别对应预置、清零、向上计数和向下计数,每次按下某个按键,计数器就以对应模式进行工作。另外,由拨码开关作为预置数的输入,数码管进行计数器输出数据的显示。
根据以上描述,整个电路按照功能划分可以大致分为三个模块,分别是处理键盘信号的键盘模块,负责多功能计数的BCD计数器模块,以及最后进行结果显示的数码管显示模块。本实验大致的电路功能框图如下所示:
功能框图
代码解读
首先来看看键盘模块的顶层,该模块负责处理矩阵键盘的按键信号,并输出消抖后的按键信号作为后续计数器模块的模式控制信号。该模块由一个按键消抖模块和一个带使能的4bit寄存器构成。按键消抖后的按键信号会由寄存器模块进行寄存,这里寄存器的设置是为了在保持某种计数功能时不必一直按住按键。可以注意一下顶层代码中寄存器模块使能端口的连接方式,松开按键后寄存器使能无效,仅在有新的按键被按下时才会更新输出的按键信号。
module keyboard(clk_50M,key_col,key_row,key_out);
input clk_50M;
input [3:0] key_col;
output key_row;
output [3:0] key_out;
wire [3:0] key_deb;
key_filter key_filter(
.clk(clk_50M),
.rstn(1'b1),
.key_in(key_col),
.key_deb(key_deb)
);
assign key_row = 1'b0;
wire en = (key_deb == 4'b0000) ? 1'b0 : 1'b1;
register_4bit register_4bit(
.clk(clk_50M),
.en(en),
.data_in(key_deb),
.data_out(key_out)
);
endmodule
如下是实例一键盘模块中的按键消抖子模块的参考代码,由于本例需要用到一行按键,所以要对四个按键进行消抖,故在前文中按键消抖代码的基础上将涉及的位宽拓宽到了四位,其余部分未改动。
module key_filter(clk,rstn,key_in,key_deb);
input clk;
input rstn;
input [3:0] key_in;
output [3:0] key_deb;
//分频计数
parameter CNTMAX = 999_999;
reg [19:0] cnt = 0;
always@(posedge clk or negedge rstn) begin
if(~rstn)
cnt <= 0;
else if(cnt == CNTMAX)
cnt <= 0;
else
cnt <= cnt + 1'b1;
end
//每20ms采样一次按键电平
reg [3:0] key_reg0;
reg [3:0] key_reg1;
reg [3:0] key_reg2;
always@(posedge clk or negedge rstn) begin
if(~rstn) begin
key_reg0 <= 4'b1111;
key_reg1 <= 4'b1111;
key_reg2 <= 4'b1111;
end
else if(cnt == CNTMAX) begin
key_reg0 <= key_in;
key_reg1 <= key_reg0;
key_reg2 <= key_reg1;
end
end
assign key_deb = (~key_reg0&~key_reg1&~key_reg2)|(~key_reg0&~key_reg1&key_reg2);
endmodule
如下是实例一键盘模块中的寄存器子模块的参考代码,描述的是带使能信号的4bit寄存器。
module register_4bit(clk,en,data_in,data_out);
input clk;
input en;
input [3:0] data_in;
output reg [3:0] data_out;
always@(posedge clk)
if(en) data_out <= data_in;
endmodule
接下来介绍一下BCD码计数器模块的设计。该模块的顶层电路框图如下图所示。该模块一个时钟分频模块和两个进行级联的BCD码多功能计数器子模块构成。输入的50MHz的时钟通过时钟分频得到周期为1s的时钟,该时钟作为计数器的时钟输入,而两个BCD码多功能计数器子模块进行级联后输出可用于显示译码的BCD码计数值。
BCD码计数器模块顶层电路框图
这里的BCD码多功能计数器便是在实验九中的BCD向下计数器上进行的功能扩充。该模块的输入端口包括时钟输入、级联信号输入(对应向上计数和向下计数分别代表进位输入和借位输入)、预置数输入和模式输入。该模块的输出包括计数值输出和级联信号输出(对应向上计数和向下计数分别代表进位输出和借位输出)。
BCD码多功能计数器
BCD码功能计数器子模块的参考代码如下所示,模式输入mode为了与键盘模块的输出匹配,以独热码的形式定义了四种模式,分别对应预置、清零、向上计数和向下计数。在每个输入时钟的上升沿,该模块都会根据当前的模式设置执行不同的计数功能。
module BCD_functional_counter(
input clk, //时钟信号输入
input [3:0] mode, //计数模式信号输入
input [3:0] BCD_preset, //预置数据信号输入
output [3:0] BCD_out, //4bitBCD码输出
input bcin, //用于级联的信号输入
output reg bcout //用于级联的信号输出
);
parameter preset = 4'b0001;
parameter clear = 4'b0010;
parameter up = 4'b0100;
parameter down = 4'b1000;
reg [3:0] cnt = 0;
//BCD计数器功能描述
always @ (posedge clk)
case(mode)
preset : cnt <= BCD_preset;
clear : cnt <= 0;
up : if(bcin) begin
if(cnt == 4'd9)
cnt <= 0;
else
cnt <= cnt + 1'b1;
end
down : if(bcin) begin
if(cnt == 4'd0)
cnt <= 4'd9;
else
cnt <= cnt - 1'b1;
end
default : cnt <= cnt;
endcase
assign BCD_out = cnt; //BCD计数值输出
//级联信号(借位输出/进位输出)
always@(*) begin
if(mode == up)
bcout = bcin && (cnt == 4'd9);
else if(mode == down)
bcout = bcin && (cnt == 4'd0);
else
bcout = 1'b0;
end
endmodule
时钟分频模块相信读者已经很熟悉代码的编写方式,这里就不赘述了。接下来给出了BCD计数器顶层模块的参考代码,完成两个BCD码计数器的级联,并为计数器提供分频后的时钟信号,按照构想的功能框图正确完成各端口信号的连接即可。
module BCD_counter_TOP(clk_50M,mode,BCD_preset,BCD_out);
input clk_50M;
input [3:0] mode;
input [7:0] BCD_preset;
output [7:0] BCD_out;
//例化时钟分频模块
wire clk_1s;
clock_division #(
.DIVCLK_CNTMAX(24_999_999)
)
my_clock_0(
.clk_in(clk_50M),
.divclk(clk_1s)
);
//例化BCD功能计数器并级联
wire [3:0] BCD_units_preset = BCD_preset[3:0];
wire [3:0] BCD_units_out;
wire bcout_0;
BCD_functional_counter BCD_units(
.clk(clk_1s),
.mode(mode),
.BCD_preset(BCD_units_preset),
.BCD_out(BCD_units_out),
.bcin(1'b1),
.bcout(bcout_0)
);
wire [3:0] BCD_tens_preset = BCD_preset[7:4];
wire [3:0] BCD_tens_out;
BCD_functional_counter BCD_tens(
.clk(clk_1s),
.mode(mode),
.BCD_preset(BCD_tens_preset),
.BCD_out(BCD_tens_out),
.bcin(bcout_0),
.bcout()
);
assign BCD_out = {BCD_tens_out,BCD_units_out};
endmodule
数码管显示模块
最后是数码管显示模块,其电路功能框图如上图所示。
最后给出了实例一顶层模块的参考代码,按照介绍该实例一开始给出的构想框图,将三个大的功能模块进行正确的连接即可。
module keyboard_instance1(clk,key_col,BCD_preset,key_row,seg_sel,seg_led);
input clk;
input [3:0] key_col;
input [7:0] BCD_preset;
output key_row;
output [3:0] seg_sel;
output [7:0] seg_led;
//例化键盘模块
wire [3:0] key_out;
keyboard keyboard(
.clk_50M(clk),
.key_col(key_col),
.key_row(key_row),
.key_out(key_out)
);
//例化BCD功能计数器模块
wire [7:0] BCD_out;
BCD_counter_TOP BCD_counter(
.clk_50M(clk),
.mode(key_out),
.BCD_preset(BCD_preset),
.BCD_out(BCD_out)
);
//例化数码管显示模块
seg_disp seg_disp(
.clk_50M(clk),
.BCD_disp(BCD_out),
.seg_sel(seg_sel),
.seg_led(seg_led)
);
endmodule
上板验证
下面为该例具体的管脚约束。
实验11管脚约束
set_pin_assignment { clk } { LOCATION = R7; }
set_pin_assignment { BCD_preset[0] } { LOCATION = A9; }
set_pin_assignment { BCD_preset[1] } { LOCATION = A10; }
set_pin_assignment { BCD_preset[2] } { LOCATION = B10; }
set_pin_assignment { BCD_preset[3] } { LOCATION = A11; }
set_pin_assignment { BCD_preset[4] } { LOCATION = A12; }
set_pin_assignment { BCD_preset[5] } { LOCATION = B12; }
set_pin_assignment { BCD_preset[6] } { LOCATION = A13; }
set_pin_assignment { BCD_preset[7] } { LOCATION = A14; }
set_pin_assignment { seg_sel[0] } { LOCATION = C9; IOSTANDARD = LVCMOS33; }
set_pin_assignment { seg_sel[1] } { LOCATION = B6; IOSTANDARD = LVCMOS33; }
set_pin_assignment { seg_sel[2] } { LOCATION = A5; IOSTANDARD = LVCMOS33; }
set_pin_assignment { seg_sel[3] } { LOCATION = A3; IOSTANDARD = LVCMOS33; }
set_pin_assignment { seg_led[0] } { LOCATION = A4; IOSTANDARD = LVCMOS33; }
set_pin_assignment { seg_led[1] } { LOCATION = A6; IOSTANDARD = LVCMOS33; }
set_pin_assignment { seg_led[2] } { LOCATION = B8; IOSTANDARD = LVCMOS33; }
set_pin_assignment { seg_led[3] } { LOCATION = E8; IOSTANDARD = LVCMOS33; }
set_pin_assignment { seg_led[4] } { LOCATION = A7; IOSTANDARD = LVCMOS33; }
set_pin_assignment { seg_led[5] } { LOCATION = B5; IOSTANDARD = LVCMOS33; }
set_pin_assignment { seg_led[6] } { LOCATION = A8; IOSTANDARD = LVCMOS33; }
set_pin_assignment { seg_led[7] } { LOCATION = C8; IOSTANDARD = LVCMOS33; }
set_pin_assignment { key_row } { LOCATION = E10; IOSTANDARD = LVCMOS33; }
set_pin_assignment { key_col[0]} { LOCATION = E11; IOSTANDARD = LVCMOS33; }
set_pin_assignment { key_col[1]} { LOCATION = D11; IOSTANDARD = LVCMOS33; }
set_pin_assignment { key_col[2]} { LOCATION = C11; IOSTANDARD = LVCMOS33; }
set_pin_assignment { key_col[3]} { LOCATION = F10; IOSTANDARD = LVCMOS33; }
本例使用了开发板上的KEY0这一行按键。KEY0对应计数器的置数、KEY1对应清零、KEY2对应向上计数,KEY3对应向下计数功能,拨码开关SW0~SW7为置数模式下的BCD码输入。通过观察数码管的显示情况,可以看到设计的计数器各个模式都工作正常。
实验12 矩阵键盘之按键识别
文章来源:https://www.yuque.com/yingmuketang/01/gdt8i9
实验原理
在实验11中仅使用了矩阵键盘的一行按键,而在本实验中,将使用整个矩阵键盘的所有按键。本实验想要实现的功能非常简单,为矩阵键盘的十六个按键编号为0~15,按下哪一个按键,数码管就显示该按键对应编号的十六进制数。
根据前面介绍的矩阵键盘原理可以知道,对于列线已设置上拉的矩阵键盘,应该通过对所用的行线输送低电平信号,读取列信号的值,如果某一列的信号低电平,则说名该行列交叉处的按键被按下。由于本例中将使用所有的按键,如果对所有的行线都输送低电平信号,即使读到了某一列的电平信号为低也无法判定是哪一行的按键,这时就需要采用行扫描法。其原理也非常简单,即按照合适的扫描频率依次给各行输送低电平信号,当前行列电平信号均为低的按键就是被按下的按键。
如下给出了本例的顶层电路功能框图。
顶层电路功能框图
根据本例的需求进行功能划分,一共有四个功能模块,分别是按键扫描模块、按键消抖模块、独热码转BCD码模块和数码管显示模块。其中,按键扫描模块负责监测矩阵键盘的列信号并对矩阵键盘进行逐行扫描,并将判定后的按键信号输出;按键消抖模块负责接收按键扫描模块得到的按键信号并进行消抖处理;独热码转二进制数模块则用于将消抖后的按键信号转换为二进制数;最后由数码管段码译码模块进行译码并驱动数码管的显示,本例只使用一位数码管故不需要进行数码管的段选译码。
代码解析
下面给出了矩阵扫描模块的参考代码,通过移位操作周期性地为矩阵键盘的行信号输送1110、1101、1011、0111的电平信号,实现行扫描。可以注意到,在时序设计上,行信号的移位操作是在扫描时钟的上升沿进行的,而列信号的采集是在扫描时钟的下降沿进行的。分析代码不难得出,如果有按键按下(这里仅考虑一次只按下一个按键),该模块的按键信号输出key会是一个16bit的独热码。
module keyboard_scan(clk,col,row,key);
input clk;
input [3:0] col;
output reg [3:0] row = 4'b1110;
output reg [15:0] key;
reg [31:0] cnt = 0;
reg scan_clk = 0;
always@(posedge clk) begin
if(cnt == 2499) begin
cnt <= 0;
scan_clk <= ~scan_clk;
end
else
cnt <= cnt + 1;
end
always@(posedge scan_clk)
row <= {row[2:0],row[3]};
always@(negedge scan_clk)
case(row)
4'b1110 : key[3:0] <= col;
4'b1101 : key[7:4] <= col;
4'b1011 : key[11:8] <= col;
4'b0111 : key[15:12] <= col;
default : key <= 0;
endcase
endmodule
接下来是按键消抖模块的参考代码,本例要用到矩阵键盘的所有按键,因此需要将涉及到的寄存器位宽都拓宽到16位。
module key_filter(clk,rstn,key_in,key_deb);
input clk;
input rstn;
input [15:0] key_in;
output [15:0] key_deb;
//分频计数
parameter CNTMAX = 999_999;
reg [19:0] cnt = 0;
always@(posedge clk or negedge rstn) begin
if(~rstn)
cnt <= 0;
else if(cnt == CNTMAX)
cnt <= 0;
else
cnt <= cnt + 1'b1;
end
//每20ms采样一次按键电平
reg [15:0] key_reg0;
reg [15:0] key_reg1;
reg [15:0] key_reg2;
always@(posedge clk or negedge rstn) begin
if(~rstn) begin
key_reg0 <= 16'hffff;
key_reg1 <= 16'hffff;
key_reg2 <= 16'hffff;
end
else if(cnt == CNTMAX) begin
key_reg0 <= key_in;
key_reg1 <= key_reg0;
key_reg2 <= key_reg1;
end
end
assign key_deb = (~key_reg0&~key_reg1& ~key_reg2)|(~key_reg0&~key_reg1&key_reg2);
endmodule
下面给出了独热码转二进制模块,实现将消抖后的按键信号转化为可供现成的数码管段码译码模块进行显示译码的编码。由于位数较少,直接使用查表的方法实现。另外,该模块引入了时序电路,还含有实例一中那个带使能端的寄存器类似的功能,即松开按键后,该模块的输入为16bit的全零信号,此时按照如下代码的描述将不会更新编码输出,而是保持上次按下按键时相同的输出,故不需要一直按住按键来保持数码管的显示。
module onehot2binary(clk,onehot,binary);
input clk;
input [15:0] onehot;
output reg [3:0] binary;
always@(posedge clk)
case(onehot)
16'h0001 : binary <= 4'b0000;
16'h0002 : binary <= 4'b0001;
16'h0004 : binary <= 4'b0010;
16'h0008 : binary <= 4'b0011;
16'h0010 : binary <= 4'b0100;
16'h0020 : binary <= 4'b0101;
16'h0040 : binary <= 4'b0110;
16'h0080 : binary <= 4'b0111;
16'h0100 : binary <= 4'b1000;
16'h0200 : binary <= 4'b1001;
16'h0400 : binary <= 4'b1010;
16'h0800 : binary <= 4'b1011;
16'h1000 : binary <= 4'b1100;
16'h2000 : binary <= 4'b1101;
16'h4000 : binary <= 4'b1110;
16'h8000 : binary <= 4'b1111;
endcase
endmodule
数码管段码译码模块的代码同样可以阅读之前的文章得到,这里就不贴出来占用过多的篇幅了。最后给出顶层模块参考代码,按照一开始给出的电路功能框图正确连接各个模块。
module keyboard_instance2(clk_50M,col,row,seg_led,seg_sel);
input clk_50M;
input [3:0] col;
output [3:0] row;
output [7:0] seg_led;
output [3:0] seg_sel;
assign seg_sel = 4'b1110;
wire [15:0] key;
keyboard_scan keyboard_scan(
.clk(clk_50M),
.col(col),
.row(row),
.key(key)
);
wire [15:0] key_deb;
key_filter key_filter(
.clk(clk_50M),
.rstn(1'b1),
.key_in(key),
.key_deb(key_deb)
);
wire [3:0] data_disp;
onehot2binary onehot2binary(
.clk(clk_50M),
.onehot(key_deb),
.binary(data_disp)
);
seg_led_decoder seg_led_decoder(
.data_disp(data_disp),
.seg_led(seg_led)
);
endmodule
上板验证
下面给出实例二具体的管脚约束,如下表所示:
实例二管脚约束
set_pin_assignment { clk_50M } { LOCATION = R7; }
set_pin_assignment { seg_sel[0] } { LOCATION = C9; IOSTANDARD = LVCMOS33; }
set_pin_assignment { seg_sel[1] } { LOCATION = B6; IOSTANDARD = LVCMOS33; }
set_pin_assignment { seg_sel[2] } { LOCATION = A5; IOSTANDARD = LVCMOS33; }
set_pin_assignment { seg_sel[3] } { LOCATION = A3; IOSTANDARD = LVCMOS33; }
set_pin_assignment { seg_led[0] } { LOCATION = A4; IOSTANDARD = LVCMOS33; }
set_pin_assignment { seg_led[1] } { LOCATION = A6; IOSTANDARD = LVCMOS33; }
set_pin_assignment { seg_led[2] } { LOCATION = B8; IOSTANDARD = LVCMOS33; }
set_pin_assignment { seg_led[3] } { LOCATION = E8; IOSTANDARD = LVCMOS33; }
set_pin_assignment { seg_led[4] } { LOCATION = A7; IOSTANDARD = LVCMOS33; }
set_pin_assignment { seg_led[5] } { LOCATION = B5; IOSTANDARD = LVCMOS33; }
set_pin_assignment { seg_led[6] } { LOCATION = A8; IOSTANDARD = LVCMOS33; }
set_pin_assignment { seg_led[7] } { LOCATION = C8; IOSTANDARD = LVCMOS33; }
set_pin_assignment { row[3] } { LOCATION = D9; IOSTANDARD = LVCMOS33; }
set_pin_assignment { row[2] } { LOCATION = F9; IOSTANDARD = LVCMOS33; }
set_pin_assignment { row[1] } { LOCATION = C10; IOSTANDARD = LVCMOS33; }
set_pin_assignment { row[0] } { LOCATION = E10; IOSTANDARD = LVCMOS33; }
set_pin_assignment { col[0]} { LOCATION = E11; IOSTANDARD = LVCMOS33; }
set_pin_assignment { col[1]} { LOCATION = D11; IOSTANDARD = LVCMOS33; }
set_pin_assignment { col[2]} { LOCATION = C11; IOSTANDARD = LVCMOS33; }
set_pin_assignment { col[3]} { LOCATION = F10; IOSTANDARD = LVCMOS33; }
上板可以看到,每按下一个按键,数码管都正确显示了对应的十六进制数。
END
文章来源:https://www.yuque.com/yingmuketang/01/howmdu
推荐内容
- 【安路 EG4S20 版本】基础板卡信息及使用教程:芯片和板卡简介
- 【安路 EG4S20 版本】基础板卡信息及使用教程:第一工程
- 【安路 EG4S20 版本】基础板卡信息及使用教程:使用ChipWatcher
- 【安路 EG4S20 版本】基础板卡信息及使用教程:使用Modelsim
- 【安路 EG4S20 版本】基础实验设计与实现:实验1 流水灯
- 【安路 EG4S20 版本】基础实验设计与实现:实验2 集成逻辑门及其基本应用
- 【安路 EG4S20 版本】基础实验设计与实现:实验3 译码器 编码器
- 【安路 EG4S20 版本】基础实验设计与实现:实验4 数据选择器
- 【安路 EG4S20 版本】基础实验设计与实现:实验5 触发器
- 【安路 EG4S20 版本】基础实验设计与实现:实验6 加法计数器
- 【安路 EG4S20 版本】基础实验设计与实现:实验7 抢答器
- 【安路 EG4S20 版本】基础实验设计与实现:实验8 功能数字钟
- 【安路 EG4S20 版本】基础实验设计与实现:实验9 矩阵键盘
- 【安路 EG4S20 版本】基础实验设计与实现:实验10 二进制转BCD码
- 【安路 EG4S20 版本】综合性实验设计与实现:实验11 PWM
- 【安路 EG4S20 版本】综合性实验设计与实现:实验12 DA及DDS
- 【安路 EG4S20 版本】综合性实验设计与实现:实验13 FPGA内部AD多通道采样实验
- 【安路 EG4S20 版本】综合性实验设计与实现:实验14 UART串行通信
- 【安路 EG4S20 版本】综合性实验设计与实现:实验15 高速ADC和DAC实验
- 【安路 EG4S20 版本】HDMI_Ethernet_DAP_SDRAM_Camera使用指南
- 【安路 EG4S20 版本】组合逻辑基础实验
- 【安路 EG4S20 版本】组合逻辑模块化设计实验
- 【安路 EG4S20 版本】点亮LED实验
- 【安路 EG4S20 版本】数码管动态显示实验
- 【安路 EG4S20 版本】按键消抖实验
更多内容请关注走进FPGA专栏