下冰雹 · 2022年03月09日

【安路 EG4S20 版本】矩阵键盘实验

实验11 矩阵键盘之BCD计数器

矩阵键盘原理

在键盘中按键数量较多时,为了减少I/O口的占用,通常将按键排列成矩阵形式。在矩阵键盘中,行线和列线不直接连通,而是通过一个按键进行连接。如下是本文所使用的FPGA开发板上板载矩阵键盘的原理图。
image.png
矩阵键盘电路原理图
从图中可以看到,键盘的列线已经都设置了上拉电阻,在没有按下按键时,键盘列线的输出为高电平。若给键盘的某一行输入低电平,并按下该行的一个按键,那么该按键连接的列线的输出就会被拉低。因此,使用FPGA驱动矩阵键盘时,一般通过输送低电平给矩阵键盘的行信号,并读取键盘列信号来判断是否有按键按下。举个例子,按照上图中的管脚名,Key_Row0~Key_Row3为矩阵键盘的行信号,Key_Col0~Key_Col3为矩阵键盘的列信号,若给Key_Row0输送低电平,读取键盘列信号Key_Col0~Key_Col3的结果为1011,则说明Key_Col2 这一列和KEY_Row0这一行交叉点处的按键KEY2被按下了。
image.png
基于FPGA的矩阵键盘驱动
本实验将使用到矩阵键盘的一行共四个按键,将实现一个多功能计数器,矩阵键盘四个按键分别对应预置、清零、向上计数和向下计数,每次按下某个按键,计数器就以对应模式进行工作。另外,由拨码开关作为预置数的输入,数码管进行计数器输出数据的显示。
根据以上描述,整个电路按照功能划分可以大致分为三个模块,分别是处理键盘信号的键盘模块,负责多功能计数的BCD计数器模块,以及最后进行结果显示的数码管显示模块。本实验大致的电路功能框图如下所示:
image.png
功能框图

代码解读

首先来看看键盘模块的顶层,该模块负责处理矩阵键盘的按键信号,并输出消抖后的按键信号作为后续计数器模块的模式控制信号。该模块由一个按键消抖模块和一个带使能的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码计数值。
image.png
BCD码计数器模块顶层电路框图
这里的BCD码多功能计数器便是在实验九中的BCD向下计数器上进行的功能扩充。该模块的输入端口包括时钟输入、级联信号输入(对应向上计数和向下计数分别代表进位输入和借位输入)、预置数输入和模式输入。该模块的输出包括计数值输出和级联信号输出(对应向上计数和向下计数分别代表进位输出和借位输出)。
image.png
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

image.png
数码管显示模块
最后是数码管显示模块,其电路功能框图如上图所示。
最后给出了实例一顶层模块的参考代码,按照介绍该实例一开始给出的构想框图,将三个大的功能模块进行正确的连接即可。

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,按下哪一个按键,数码管就显示该按键对应编号的十六进制数。
根据前面介绍的矩阵键盘原理可以知道,对于列线已设置上拉的矩阵键盘,应该通过对所用的行线输送低电平信号,读取列信号的值,如果某一列的信号低电平,则说名该行列交叉处的按键被按下。由于本例中将使用所有的按键,如果对所有的行线都输送低电平信号,即使读到了某一列的电平信号为低也无法判定是哪一行的按键,这时就需要采用行扫描法。其原理也非常简单,即按照合适的扫描频率依次给各行输送低电平信号,当前行列电平信号均为低的按键就是被按下的按键。
如下给出了本例的顶层电路功能框图。
image.png
顶层电路功能框图
根据本例的需求进行功能划分,一共有四个功能模块,分别是按键扫描模块、按键消抖模块、独热码转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

推荐内容

更多内容请关注走进FPGA专栏
推荐阅读
关注数
1615
内容数
27
本专栏将以【安路EG4S开发板】为例,从基础板卡信息及使用教程,基础实验设计与实现及综合性实验设计与实现带大家学习FPGA。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息