基于FPGA的矩阵键盘驱动

  按键数量较多时,为了减少 I/O ⼝的占⽤,通常将按键排列成矩阵形式。在矩阵式键盘中,每条⽔平线和垂直线在交叉处不直接连通,⽽是通过一个按键连接。⼋根线就可以控制4*4=16个按键,⽐之直接将端⼝线⽤于键盘多出了⼀倍,⽽且线越多,区别越明显,⽐如多加⼀条线就可以构成20键的键盘。

  由此可⻅,在需要的键数⽐较多时,采⽤矩阵法来做键盘是合理的。

1、模块端口

  模块相应的端口信号如下表所示。

表1 模块端口信号

信号I/O位宽含义
clkI1系统⼯作时钟,默认50MHz。
rst_nI1系统复位信号,低电平有效。
key_colI4矩阵键盘列输入信号,默认⾼电平,被按下时,所在列为低电平。
key_rowO4矩阵键盘⾏输出信号,默认输出低电平,⾏扫描时当前⾏输出低电平,其余⾏输出。
key_outO4被按下按键的编号
key_vldO1被按下按键的编号有效指⽰信号

2、设计思路

  下图是4*4矩阵键盘原理图,4条列输入线均被上拉到高电平,所以没有按键按下时,列输入引脚就是高电平。行线KEY_R1~KEY_R4一般被FPGA驱动。4根⾏线和4根列线形成16个相交点。

在这里插入图片描述

图1 矩阵键盘原理图

  ⾏扫描法(逐⾏(或列)扫描查询法)如下:

  1. 判断键盘中有⽆键按下:将全部⾏线KEY_R1~KEY_R4置为低电平,同时检测列线KEY_C1~KEY_C4的状态。只要有⼀列的电平为低,则表⽰键盘中有键被按下,⽽且被按下的按键位于列信号为低电平的列。 若列线全为⾼电平,则键盘中⽆键按下。

  2. 判断被按下按键所在的位置:在确认有键按下后,即可进⼊确定具体被按下的按键所在位置的过程。依次将⾏线设置为低电平(即在置某根⾏线为低电平时,其它线为⾼电平),同时检测列线的状态,如果有列线为低电平,且列数与步骤1一致,则表示该被按下的按键就在这一行,即可计算出被按下按键的具体位置。

  对于按键和触摸屏等机械设备来说,都存在 “抖动”问题,按键从最初接通到稳定接通要经过数毫秒,其间可能发⽣多次“接通-断开”这样的⽑刺。如果不进⾏处理,会使系统识别到抖动信号⽽进⾏不必要的反应,导致模块功能不正常,为了避免这种现象的产⽣,需要对按键消抖。

在这里插入图片描述

图2 按键抖动

  软件消抖,检测有按键闭合后执⾏延时程序,抖动时间的⻓短由按键的机械特性决定,⼀般为5ms〜20ms。让前沿抖动消失后再⼀次检测键的状态,如果仍保持闭合状态电平,则确认按下按键操作有效。

  本文通过一个状态机实现对矩阵键盘的检测,对应的状态转化图如下图所示,包括4个状态,检测矩阵键盘列状态、检测被按下按键所在行状态、延时状态、等待按键释放状态。

在这里插入图片描述

图3 状态转换图

  CHK_COL状态:该状态下向矩阵键盘的所有行输出低电平,同时检测列输入信号。当检测到列输入不全为高电平时,计数器shake_cnt对时钟进行计数,当计数到20ms时,表示按键被按下,将低电平所在列寄存,表示被按下按键就在这一列,然后状态机跳转到CHK_ROW检测行。如果在中途发现列输入全为高电平,则计数器清零,表示之前检测的是按键抖动,继续检测。

  CHK_ROW状态:该状态需要检测被按下按键所在行,检测方式就是逐行扫描,所以需要两个计数器,一个计数器row_cnt用来记录一行扫描的时间,目前一行扫面时间设置为16个时钟周期。另一个计数器row_index用来记录当前扫描到第几行了,因为该键盘只有四行,所以row_index最大值为3。

  计数器row_index对应行输出低电平,其余行输出高电平,同时检测列信号,如果列信号不全为高,并且为低电平的列与CHK_COL状态下检测到低电平的列一致,表示此时计数器row_index的值就是被按下按键所在行。根据row_index的值和之前记录按键的列就可以计算出被按下按键的位号,然后输出给下游模块。

  DELAY状态:就是为了多等待几个时钟周期,让行信号全部驱动为低电平之后,在跳转到等待按键被释放状态,否则会出现错误。

  WAIT_END状态:当检测到被按下按键位号后,需要等待被按下的按键释放,之后才能开始下一次检测,不然会重复检测同一个按键。防止按键被按下1次,但是却检测出多次按下的错误。

  其余计数器,标志信号这些就不细讲了,也比较简单,直接看代码吧。参考代码如下所示:

module  key_scan #(parameter       TCLK            =   20                      ,//系统时钟周期,单位ns。parameter       TIME_20MS       =   20_000_000              //按键消抖时间,单位为ns。
)(  input                               clk                     ,//系统时钟信号,默认50MHz。input                               rst_n                   ,//系统复位,低电平有效;input           [3 : 0]             key_col                 ,//矩阵键盘的列号;output reg      [3 : 0]             key_row                 ,//矩阵键盘的行号;output reg      [3 : 0]             key_out                 ,//矩阵键盘被按下按键的数值;output reg                          key_vld                  //矩阵键盘被按下按键数据输出有效指示信号;
);  //自定义参数;localparam      CHK_COL         =   4'b0001                 ;//状态机的列扫描状态;localparam      CHK_ROW         =   4'b0010                 ;//状态机的的行扫描状态;localparam      DELAY           =   4'b0100                 ;//状态机的延时状态;localparam      WAIT_END        =   4'b1000                 ;//状态机的等待状态;localparam      TIME_20MS_NUM   =  TIME_20MS / TCLK         ;//计算出TIME_20MS对应的系统时钟个数;localparam      TIME_20MS_W     =   clogb2(TIME_20MS_NUM-1) ;//利用函数计算出TIME_20MS_NUM对应的寄存器位宽;reg   [3 : 0]                       key_col_ff0             ;reg   [3 : 0]                       key_col_ff1             ;reg   [1 : 0]                       key_col_get             ;reg   [3 : 0]                       state_c                 ;reg   [TIME_20MS_W - 1 : 0]         shake_cnt               ;reg   [3 : 0]                       state_n                 ;reg   [1 : 0]                       row_index               ;reg   [3 : 0]                       row_cnt                 ;wire                                end_shake_cnt           ;wire                                col2row_start           ;wire                                row2del_start           ;wire                                del2wait_start          ;wire                                wait2col_start          ;wire                                add_row_cnt             ;wire                                end_row_cnt             ;wire                                add_shake_cnt           ;wire                                add_row_index           ;wire                                end_row_index           ;//自动计算位宽函数;function integer clogb2(input integer depth);beginif(depth == 0)clogb2 = 1;else if(depth != 0)for(clogb2=0 ; depth>0 ; clogb2=clogb2+1)depth=depth >> 1;endendfunction//将输入的列信号打两拍,降低亚稳态出现的机率。always@(posedge clk)begin{key_col_ff1,key_col_ff0} <= {key_col_ff0,key_col};end//计数器shake_cnt,如果有按键被按下,则key_col_ff1!=4'hf,此时计数器计数。always@(posedge clk or negedge rst_n)beginif(rst_n==0)beginshake_cnt <= 0;endelse if(add_shake_cnt)beginif(end_shake_cnt)//按键被按下20ms时,计数器清零;shake_cnt <= 0;else//否则当按键被按下时,计数器进行计数;shake_cnt <= shake_cnt + 1;endelse begin//没有按键被按下时,计数器清零;shake_cnt <= 0;endendassign add_shake_cnt = (key_col_ff1!=4'hf) && (state_c == CHK_COL);assign end_shake_cnt = add_shake_cnt  && shake_cnt == TIME_20MS_NUM-1 ;//当列检查结束时,将被按下按键所在列寄存;always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//初始为0,没有按键被按下;key_col_get <= 0;endelse if(col2row_start)begin//当状态机从列检查跳转到行检查时,将按键对应列保存;if(key_col_ff1==4'b1110)//最低位为0,则表示第0列按键被按下;key_col_get <= 0;else if(key_col_ff1==4'b1101)//第1位位0,则表示第1列按键被按下;key_col_get <= 1;else if(key_col_ff1==4'b1011)//第2位位0,则表示第2列按键被按下;key_col_get <= 2;else//否则表示第3列按键被按下;key_col_get <= 3;endend//状态机的第一段;always@(posedge clk or negedge rst_n)begin  if(rst_n==1'b0)begin  state_c <= CHK_COL;  end  else begin  state_c <= state_n;  end  endalways@(*)begin  case(state_c)CHK_COL: begin//检查列触发;if(col2row_start)beginstate_n = CHK_ROW;endelse beginstate_n = CHK_COL;endendCHK_ROW: begin//检查行触发;if(row2del_start)beginstate_n = DELAY;endelse beginstate_n = CHK_ROW;endendDELAY :  begin//这个状态的存在是为了等待行扫描结束后,计算结果输出。if(del2wait_start)beginstate_n = WAIT_END;endelse beginstate_n = DELAY;endendWAIT_END: begin//此时四行全部输出低电平,如果按键被按下,没有松开,那么会持续之前的状态,就需要一致等待按键松开;if(wait2col_start)beginstate_n = CHK_COL;endelse beginstate_n = WAIT_END;endenddefault: state_n = CHK_COL;endcaseend//状态机第三段,描述assign col2row_start = (state_c==CHK_COL ) && end_shake_cnt;//检查到有对应列持续20MS被按下。assign row2del_start = (state_c==CHK_ROW ) && end_row_index;//行扫描完成;assign del2wait_start= (state_c==DELAY   ) && end_row_cnt;assign wait2col_start= (state_c==WAIT_END) && key_col_ff1==4'hf;//4'hf表示前面的按键已经被松开,状态机重新回到列检测状态。//控制行数据的输出,在检查被按下按键所在行时,进行行循环扫描。//从第一行开始一次拉低,其余行拉高,其余时刻所有行全部拉低。always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginkey_row <= 4'b0;endelse if(state_c==CHK_ROW)begin//行扫描,依次将每行的电平拉低。key_row <= ~(1'b1 << row_index);endelse beginkey_row <= 4'b0;endend//行扫描的计数器,对行进行扫描。//每行扫描持续时间为行计数器row_cnt的计数值,目前为16个时钟周期。//当4行全部扫面完毕时,计数器清零;always@(posedge clk or negedge rst_n)beginif(rst_n==0)beginrow_index <= 0;endelse if(add_row_index) beginif(end_row_index)row_index <= 0;elserow_index <= row_index + 1;endelse if(state_c!=CHK_ROW)beginrow_index <= 0;endendassign add_row_index = state_c==CHK_ROW && end_row_cnt;assign end_row_index = add_row_index  && row_index == 4-1 ;//每行扫描持续时间,初始值为0,此处设置每行扫面16个时钟周期;//状态机位于行扫描或者等待状态时进行计数,当计数到最大值16时清零。always@(posedge clk or negedge rst_n)beginif(rst_n==0)beginrow_cnt <= 0;endelse if(add_row_cnt)beginif(end_row_cnt)row_cnt <= 0;elserow_cnt <= row_cnt + 1;endendassign add_row_cnt = state_c==CHK_ROW || state_c==DELAY;assign end_row_cnt = add_row_cnt  && row_cnt == 16-1;always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginkey_out <= 0;end//计算被按下按键的数值;else if(state_c==CHK_ROW && end_row_cnt && key_col_ff1[key_col_get]==1'b0)beginkey_out <= {row_index,key_col_get};endend//按键数值有效指示信号,高电平时表示key_out输出的值是有效的。always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginkey_vld <= 1'b0;endelse begin//当没扫描一行,前面暂存的列为低电平的时候,表示这一行,这一列的按键被按下。key_vld <= (state_c==CHK_ROW && end_row_cnt && key_col_ff1[key_col_get]==1'b0);endendendmodule

3、仿真

  TestBench写起来有点小麻烦,可能存在一些小错误,本文提供的参考代码如下所示:

`timescale 1 ns/1 ns
module test();localparam 	CYCLE		=   20          ;//系统时钟周期,单位ns,默认20ns;localparam 	RST_TIME	=   10          ;//系统复位持续时间,默认10个系统时钟周期;localparam  TIME_20MS   =   2_000       ;//按键消抖时间,默认20ms,单位ns,仿真时将时间缩短;reg			                clk         ;//系统时钟,默认100MHz;reg			                rst_n       ;//系统复位,默认低电平有效;reg   [3 : 0]               key_col     ;reg   [3 : 0]               key_col_r   ;reg                         key_col_sel ;reg   [1 : 0]               now_row     ;//当前行reg   [1 : 0]               now_col     ;//当前列;wire  [3 : 0]               key_row     ;wire  [3 : 0]               key_out     ;wire                        key_vld     ;//例化需要测试的模块;key_scan #(.TCLK       ( CYCLE     ),.TIME_20MS  ( TIME_20MS ))u_key_scan (.clk        ( clk       ),.rst_n      ( rst_n     ),.key_col    ( key_col   ),.key_row    ( key_row   ),.key_out    ( key_out   ),.key_vld    ( key_vld   ));//生成周期为CYCLE数值的系统时钟;initial beginclk = 0;forever #(CYCLE/2) clk = ~clk;end//生成复位信号;initial beginrst_n = 1;key_col = 4'hf;key_col_sel=0;key_col_r = 4'hf;now_col = 0;now_row = 0;#1;rst_n = 0;//开始时复位10个时钟;#(RST_TIME*CYCLE);rst_n = 1;#(20*CYCLE);key_task(0,1);//按下第0行第1列按键;1号按键key_task(1,2);//按下第1行第2列按键;6号按键key_task(3,1);//按下第3行第1列按键;13号按键key_task(2,2);//按下第2行第2列按键;10号按键key_task(0,0);//按下第0行第0列按键,0号按键#(2000*CYCLE);$stop;//停止仿真;end//生成对应按键的信号;task key_task(input	[1 : 0]		row	,//被按下按键的行号;input	[1 : 0]	    col  //被按下按键的列号;);beginnow_col <= col;now_row <= row;key_col_r <= 4'hf;//初始时,没有按键被按下;key_col_sel <= 1'b0;@(posedge clk);key_col_r[col] <= 1'b0;//repeat(20) begin//将信号随机翻转20次,模拟按键按下时的抖动。#(({$random} % (TIME_20MS/(CYCLE+5))) * CYCLE);key_col_r[col] <= ~key_col_r[col];endkey_col_sel <= 1'b1;//列选通信号拉高;repeat(TIME_20MS/7)begin//按键按下的保持时间;@(posedge clk);endkey_col_sel <= 1'b0;//列选通信号拉低;key_col_r <= 4'hf;//按键被释放;repeat(20) begin//将信号随机翻转20次,模拟按键释放时的抖动。#(({$random} % (TIME_20MS/(CYCLE+5))) * CYCLE);key_col_r[col] <= ~key_col_r[col];endrepeat(TIME_20MS)begin//释放按键的保持时间;@(posedge clk);endendendtaskalways@(*)beginif(key_col_sel)begincase (now_col)2'd0 : key_col <= {3'd7,key_row[now_row]};2'd1 : key_col <= {2'd3,key_row[now_row],1'd1};2'd2 : key_col <= {1'd1,key_row[now_row],2'd3};2'd3 : key_col <= {key_row[now_row],3'd7};endcaseendelse beginkey_col <= key_col_r;endendendmodule

  仿真结果如下图所示,橙色信号key_row输出全为低电平,当列输入信号key_col_ff1不全为高电平时,消抖计数器shake_cnt就一直对时钟进行计数,没有计数20ms,但是检测到key_col_ff1全为高电平了,那么消抖计数器清零,表示是抖动,图中黄色信号为消抖计数器。

在这里插入图片描述

图4 抖动检测及消抖

  当消抖计数器shake_cnt计数到20ms(为了加快仿真,在TestBench中该数值设置的比较小)时,表示有按键被按下,则此时记下被按下按键所在列,且状态机跳转到CHK_ROW状态,图中紫红色信号分别为状态机的现态state_c与状态机的次态state_n。

  下图是检测到按键被按下后,因为key_col_ff1为4’hd=4’b1101,所以第1列为0,表示第1列有按键被按下。此时key_col_get赋值为1,表示被按下的按键位于第1列,用于后文计算被按下按键位号。

在这里插入图片描述

图5 记录被按下按键所在列

  然后就是对行进行扫描,确定被按下按键所在行,如下图所示,每一行扫描16个时钟周期,总共扫描4行。
在这里插入图片描述

图6 行扫描

  有上图可知,key_vld在扫描第0行时拉高,表示被按下的按键在第0行,将这部分波形放大,得到下图。

  key_row信号输出4’he,最低位为0,其余位高电平,对第0行进行扫描。当计数row_cnt计数到最大值时,检测列输入信号key_col_ff1=4‘hd。表示被按下按键就在第0行,根据4*此时row_index的值+ key_col_get = 4 * 0 + 1 = 1。所以检测到被按下的按键位号为1,key_out输出1,且key_vld拉高一个时钟周期,表示1号按键被按下1次。

在这里插入图片描述

图7 检测到被按下按键所在行

  后续仿真较为简单,就行扫描结束后,状态机跳转到延时状态,等待16个时钟周期后跳转到等待状态,直到所有列均为高电平时跳转到列检测状态,进行下次检测。

在这里插入图片描述

图8 状态机跳转

  TestBench写的有点复杂了,但是使用比较简单,因为我提供了一个任务。直接任务key_task()函数,输入被按下按键所在行和列,注意列和行的取值均为[0,3]闭区间。使用该函数会自动生成一些按键的抖动,更有利于仿真测试。

在这里插入图片描述

图9 TestBech文件更改

  代码已经贴在文中了,这种模块贴出来的原因也是方便之后自己使用,需要使用直接打开百度复制自己验证过的模块即可。由于我平时写代码都是vscode+modelsim搞定仿真,不会用quartus或者vivado创建工程,所以本文就不存在工程文件了,如果要代码文件可以在公众号后台回复“基于FPGA的矩阵键盘驱动“(不包括引号)即可。

  最后说明该模块此处没有上板测试,但是我已经在其余工程中使用过该模块,没有出现任何问题,所以放心使用,也会在后续的设计中以子模块的形式出现,到时候不会在对该模块讲解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/422037.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

应用层—HTTP详解(抓包工具、报文格式、构造http等……)

文章目录 HTTP1. 抓包工具的使用1.1 配置信息1.2 观察数据 2. 分析 https 抓包结果3. HTTP请求详解3.1 认识 URL3.1.1 URL 基本格式3.1.2 查询字符串 (query string)3.1.3 关于 URL Encode 3.2 认识 http 方法3.2.1 [经典问题] Get 和 Post 主要的区别是什么&#xff1f;&#…

HubSpot整合小红书,MessageBox助力多系统融合

在当今数字化潮流中&#xff0c;HubSpot与小红书的强大联盟成为了数字化市场的亮点。今天运营坛将深入解析它们的合作策略&#xff0c;聚焦于MessageBox在整合中的关键角色&#xff0c;以及它在推动HubSpot和小红书整合方面的关键作用。 HubSpot与小红书&#xff1a;数字化市场…

数据库设计和数据库对象

目录 序言 一、数据库设计的步骤 二、如何绘制E-R图 2.1 酒店管理系统的基本功能 2.2 绘制E-R 实体关系图&#xff08;三要素&#xff09;&#xff1a; 2.3 关系型数据库常见映射基数&#xff1a; 2.4 转化E-R图形成数据库模型图 三、使用三大范式实现数据库设计规范化…

本地部署轻量级web开发框架Flask并实现无公网ip远程访问开发界面

文章目录 1. 安装部署Flask2. 安装Cpolar内网穿透3. 配置Flask的web界面公网访问地址4. 公网远程访问Flask的web界面 本篇文章主要讲解如何在本地安装Flask&#xff0c;以及如何将其web界面发布到公网进行远程访问。 Flask是目前十分流行的web框架&#xff0c;采用Python编程语…

C++入门学习(七)整型

整型就是整数类型的数据&#xff08;-1&#xff0c;0&#xff0c;1等等&#xff09; 数据类型占用空间取值范围short(短整型)2字节 (-2^15 ~ 2^15-1) 32768~32767 int(整型)4字节(-2^31 ~ 2^31-1)long(长整形) Windows为4字节, Linux为4字节(32位), 8字节(64位) (-2^31 ~ 2^31…

flink部署模式介绍

在一些应用场景中&#xff0c;对于集群资源分配和占用的方式&#xff0c;可能会有特定的需求。Flink 为各种场景提供了不同的部署模式&#xff0c;主要有以下三种&#xff0c;它们的区别主要在于&#xff1a; 集群的生命周期以及资源的分配方式&#xff1b;应用的 main 方法到…

jmeter生成html性能结果报告

从3.0版本开始&#xff0c;jmeter引入了Dashboard Report模块&#xff0c;用于生成HTML类型的可视化图形报告。 1.生成html报告 1.1操作步骤&#xff1a; 进入JMeter的bin目录下&#xff1b;打开命令行窗口&#xff0c;输入以下命令&#xff1a; 对于Windows系统&#xff1a;…

open3d点云平移

功能简介 open3d中点云的平移函数为&#xff1a;pcd.translate((tx, ty, tz), relativeTrue)。当relative为True时&#xff0c;&#xff08;tx, ty, tz&#xff09;表示点云平移的相对尺度&#xff0c;也就是平移了多少距离。当relative为False时&#xff0c;&#xff08;tx, …

HCIA——20应用层:C/S、P2P、peer

学习目标&#xff1a; 计算机网络 1.掌握计算机网络的基本概念、基本原理和基本方法。 2.掌握计算机网络的体系结构和典型网络协议&#xff0c;了解典型网络设备的组成和特点&#xff0c;理解典型网络设备的工作原理。 3.能够运用计算机网络的基本概念、基本原理和基本方法进行…

AI绘画Midjourney绘画提示词Prompt入门到精通【宝藏级收藏】

一、AI绘画工具 SparkAi创作系统是基于ChatGPT进行开发的Ai智能问答系统和Midjourney绘画系统&#xff0c;支持OpenAI-GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美&#xff0c;那么如何搭建部署AI创作ChatGPT&#xff1f;小编这里写一个详细图文教程吧。已支…

解决github无法访问的问题(修改hosts)

1.先ping github.com看是否能ping通 不能ping通的话&#xff0c;找到github最新的ip地址&#xff0c;修改hosts文件 找最新的ip地址的办法&#xff1a; a.cmd中ping时返回的 b.点击ipaddress.com查询网站链接 修改hosts文件&#xff1a; 140.82.113.4 github.com借鉴博客&a…

deepin20.9设置root登录

首先设置root 账户密码 sudo passwd root创建 /etc/lightdm/lightdm-deepin-greeter.conf&#xff0c;输入以下内容 sudo touch /etc/lightdm/lightdm-deepin-greeter.conf sudo deepin-editor /etc/lightdm/lightdm-deepin-greeter.conf [General] loginPromptInputtrue别忘…