小物件,大学问——按键


Pixabay:电话按键


Unsplash:电脑键盘

说起按键,这个大家应该不会陌生,因为我们每天都接触的啊,至少我们每天都用手机,我们发短信,打电话都会通过按键输入信息,虽然有的手机按键是“坚硬的”按键,有的按键是“软软的”按键,但这些统统称为“物理按键”,就如图中座机的按键和键盘的按键亦是如此;但还有一种“虚拟的”按键,就如手机中触摸屏的按键,这是一种软件虚拟出来的按键,但它的功能和真实的物理按键并没有什么两样。

但话说回来,按键究竟有什么玄机呢?按键是我们根据自己的意愿向机器设备发送指令的地方,它其实涉及到了物理信号到电信号的转变,如下图所示,物理信号指的是我们用手指接触按键并给它压力,电信号则是按键受到压力后变低使电路中的两个点连通了,我们所有的电子设备都是基于电路而工作的。当电路检测到A和B两点接通了,就认为按键按下了。
img_0.4703703795142323.jpg

Proteus ISIS软件:按键

但是如果我们使用16个按键呢?就得有16根线与按键相连,或者说有16个通路。但是这样就有些耗费资源,如果实际上我们没有足够的通路,那又该怎么办呢?这时采用的方法就是矩阵按键。
img_0.7108697981499275.jpg
Proteus ISIS:16路独立按键


Proteus ISIS:4✖4矩阵按键

可以看出同样是16个按键,但这时只连接了8根线,比16路独立按键节省了一半的线。为什么矩阵按键8根线也能控制16个键呢?这是因为它采用了行列分别控制,对于4×4的矩阵按键来说,就是4行和4列,每一行控制4个,每一列也控制4个,而对于其中任何一个按键来说,由它所在的行和列分别控制。
img_0.31573027269900034.jpg
Proteus ISIS:矩阵按键原理图

但对我们来说,有一个好消息,还有一个坏消息。好消息是我们的连线变少了,坏消息是,我们检测按键的过程变复杂了。通常,我们只要知道某个按键两边的点连通了,我们就认为按键按下了,对于独立按键,16个按键对应独立的16对的点,我们只要一一检测就好了,比如右端全给0,左端检测到0就可以了,如果有多个按键同时按下也能检测出来。但是对于矩阵按键,如果我们列全给0的话,假设某行为0,我们并不能判断出是这一行的那个列被按下了,所以只能一列一列的排查,就是只能一列为0,其余列的输入不能改变行的状态,比如1。当然,虽然稍微麻烦了一点,我们还是能克服的,毕竟它的优势是显而易见的。

一般情况下,我们检测按键是通过CPU读取相应连线的值来实现的。我们的手机里也有CPU,电脑里也有CPU,而我们所熟知的安卓系统,苹果系统,Windows系统等都是运行在CPU里的,这些“系统”都是CPU里运行的程序。当然,CPU也可以通过变成来读取按键连线上的值。

那么,问题来了。CPU是怎么读取按键连线上的值的呢?当然是人在教它通过一定的方法步骤来最终识别按键连线上的值的。 但每个人的做事方式不一样,CPU的最终识别方式也就不一样,换句话说,就是CPU上的程序运行方式不一样。但这只是说CPU做事的步骤不一样,就好如做面条,有的人先加水,再加葱花,再加油,有的人先加油,再加葱花,再加水,但最终出来都是香喷喷的面条。所以,所有的CPU都是按照一定的步骤来做事的。

现在,就以矩阵按键来说,CPU要想控制它,就需要一个流程,我们就需要根据这个流程告诉CPU遇到什么事情该怎么做,就如救援机器人一样,检测到生命体就实施救援,没有检测到就返回原地并发送消息。而矩阵按键,我们也可以根据当前的情况来进行相应的操作,比如下面这个流程图。
img_0.862472762425835.jpg

Quartus II: FSM

根据这个流程,CPU就可以根据步骤处理按键的事了。这个步骤可以说是很详细的。

首先,我们使用的手机,电脑,电视机遥控开关其实都有一个默认状态,那就是大多数时候,我们并不操作它,它处于一个空闲状态。这个例子也是,我们假设按键默认处于空闲状态,也就是没有被按下的状态。同时,我们假设行1、2、3、4也是默认状态,均为1。同时,CPU默认给列A、B、C、D一个0,那么如果有按键被按下,被接通的两点就会出现一根线一端为1,而另一端为0的情况,这时为1的一端也会变成0(这个规律可以总结为:一根线两端同时为0,则两端同时为0;两端同时为1,则两端同时为1;一端为0,一端为1,则两端仍然均为0)。那么CPU就会去检测了,如果行1、2、3、4不是默认的全为1,必然是有按键按下了。但这时它并不能判断出,究竟按下的是哪一个按键。

有按键按下了,就好说了。先让CPU把这个按键的值保存一下,保存很重要。这里又考虑了另外一个细节,就是防抖动。举个例子,你用遥控给电视机选台,如果你手很冷,手一抖,连按了好几下,那么本来你想选的台就直接被跳过了。我们设计的时候得防止这件事,那就是CPU一旦检测到按下按键了,就会延时一段时间,在这一段时间内无论手都还是什么的都会被屏蔽,专业术语就叫“按键滤波”。不过术语什么的,并不重要。而保存按键的值就会避免后续的抖动而导致最初的值丢失。

CPU中有一个称为计数器的东西,其实也就是定时器来实现延时。你可以用一个电信号来启动定时器,定时器走到规定的时间后也会输出一个电信号,这样一旦我们发现按键被按下了,就启动定时器;定时器走到规定的时间就关掉定时器。如果延时一段时间后,行1、2、3、4仍不是默认的全为1,那CPU相信你是真正的想按这个按键,而不是不小心碰了一下什么的。

CPU知道你已经按下了按键,那么接下来它要弄清楚你按的是那个按键了。如果它检测到某一行为0,那么它并不知道哪一列被按下了,因为任一列被按下都会是这一行变为0,怎么办?一列一列的试。它首先只将第一列变为0,然后检测行有没有按键被按下,有的话,就说明这一列有按键被按下了。然后它再将第二列变为0,检测按键有没有按下,如果有,那说明这一列按键被按下了。如果这一列被按下,就将这一列的标志标记为1(与这一列为0并不冲突,只是标志)。以此类推,每一列都要试上一遍。

试完了,下一步就是输出结果。如果应用比如计算器,一般都是一次只能按一个键,不能同时按两个键。所以这个例子也采用了这样的机制,如果同时按两个键则无效。那么结果就可以表示成xxxx_xxxx的形式。其中前面的4位表示4行,后面的4位,表示4列,如1110_0001则表示第“0行0列”被按下了。当然这只是一种标记,你也可以写成“1110_1110”表示第“0行0列”被按下了。我们已前一种方式为例,因为我们说过,只能一次按一个键才生效,所以同时按两个或多个键都无效。这就要求我们检测是不是只按了一个键,如果只按了一个键,那么行xxxx不可能出现多于1个的0,所以xxxx各位的和应等于3,而列呢,不可能出现多余1个的1,也就是列的和只能等于1。满足这个条件,我们就说我们检测到有效的按键被按下,就可以输出一个检测成功的标志并输出结果。如果不满足这个条件,说明一次可能按下了多个键,这不符合我们的预设,我们可以忽略它,那么结果就是不产生检测成功标志,以及按键的检测结果仍保留原先的值,而不会更新。

或许有的应用需要使用组合键,如PC的键盘,这时就可以将需要组合的按键定义进去。其实也可以增加组合键的机制,不过流程上可能就稍微复杂一些了。一般是应用需要什么,我们就增加什么,这种思想叫做“面向应用”。那如果将所有的按键组合都囊括进去,估计得耗费大量的资源,CPU估计会吃不消,且流程会相当复杂,很少人会那么干。

得到结果了,一般你就不会再按着键盘不放了,你要松开手了。CPU也会随时检测你有没有松开手,如过检测到行xxxx1111,那表示你确实松开手了,但CPU还会延时一段时间,考察你是不是真的松开手,会不会有再按回去的可能,这种过程术语叫做“释放滤波”。还有一种可能就是你按着不放,CPU检测到了这种行为就会一直延时,如果计数器达到时间后,你仍不松手,CPU会再一次输出检测成功标志,如果一开始就检测成功了,CPU的输出结果会一直保持直到按下一个按键,如果一开始是多个按键的组合,CPU的输出结果会继续维持上一次的输出结果。

最终CPU延时了一段时间后,发现你确实释放了按键。CPU就会继续检测行的值,如果没有按键按下,就会跳到“空闲状态”,如果有按键按下就会跳到上一状态。

整个过程,总结下来就是:

  • 按键空闲,时刻检测行状态,如果检测到了按键被按下,那么
  • 延时,直到延时结束,然后
  • 检测按键是否按下,否,跳到步骤1,是,检测是否是第0列按键按下引起的
  • 如果检测到是第0列按下引起的,将第0列标志置1,否则,标志仍为0
  • 检测是否是第1列引起的,如果是,将第1列标志置1,否则标志仍未0
  • 检测是否是第2列引起的,如果是,将第2列标志置1,否则标志仍未0
  • 检测是否是第3列引起的,如果是,将第3列标志置1,否则标志仍未0
  • 判断是否只有一个按键按下,如果是,输出检测成功标志,并输出结果,否则
    不输出检测成功标志,结果维持不变
  • 检测按键是否被释放,是,延时一段时间(释放延时),否,等待(等待延
    时,也可只等待就行,不必加计数器设定延时)
  • 检测到确实被释放了,并且延时完成,于是
  • 继续检测是否有按键按下,有,说明按键被释放时有抖动,需跳到上一步继续
    延时,否则说明按键进入空闲状态,跳到步骤1

这就是整个流程了,这里设定了等待释放的过程中加入等待计数器,达到指定延时后,不论是否按了一个键或是两个键都会输出检测成功标志,这一点也可以不加入这样的计数器,指定延时后,无需输出检测成功标志。不过,只要按照当初的需求就好。

然后,根据CPU的控制流程设计好控制程序后,我们还要对它验证。验证的方法就是给施加相应的信号,看看CPU的反应。但是我们加信号要符合实际的应用场景,比如一上电,系统的时钟clk就会自动运行(所谓系统时钟,就好比人类人类生活的时间,人类做任务规划,也是基于时间,而电子系统亦是如此,它这一步做什么,下一步做什么都是基于一个时钟的)。然后系统的“复位”信号rst_n一开始为低(复位信号其实就类似于人类的出生,刚进入世间,大脑完全归零,不懂任何事物,而电子系统也是如此,刚进入工作时,会将内部所有的控制逻辑归零,或者叫清空,为下一步运行做准备)。以上两个信号为电子设计领域的常识了,可以说每个CPU必有此信号。

img_0.8601874454875442.jpg

Quartus II:Waveform

而根据我们的设计要求,键盘的行信号都是默认为1(如果行未选通,由行寄存器,也就是内部数据寄存器提供此信号,同时受行选通信号控制,所以如果行如果被选通时,由列信号提供行信号),所谓行选通row_sel,其实也类似于一个开关,行的信号通过开关选择。由图中的信号图可知,row_sel为0,未选通。

img_0.08255204788635953.jpg

Proteus ISIS: row_sel选通原理

列信号col与按键成功标志key_flag,按键默认值key_value均为CPU输出,在这种情况下,它们的默认输出均为0,符合我们的设计要求。

等了50ms后,我按下了第0行0列的按键,可以看出计数器1开始启动,计数器开始计数,状态也由原来的00000000001变化到00000000010。但是由于按键有抖动,所以按键的值总是在01之间跳变。我们需要等待一会儿,等到按键的值变稳定,至于等多长时间,则由计数器说的算,等到它计数完成,按键的值也就稳定了。

img_0.9586655565373791.jpg

Quartus II:Waveform

我们可以看到计数器cnt1记满数后会重新变为0,而记满标志cnt_done1会变为1,并持续1个时钟周期。得知记满标志变为1后,计数器启动信号en_cnt1会变为0,状态也由00000000010变为00000000100,进而开始逐列扫描,看看到底是那一列按键被按下,可以看出只有第0列被按下会使行为1110,其余列按下行始终为1111。且此时状态变化到了00001000000

接下来状态变到00010000000,开始输出结果了,同时变化到状态00100000000。此时内部寄存器key_flag_r被拉高,同时启动连按计数器en_cnt2,表示此时按键还被按着,而基于此输出信号key_flag也被拉高(基于key_flag_r,所以延迟一拍)。因为按键并未释放,所以CPU就一直等着按键释放。

img_0.5427370582789481.jpg

Quartus II:Waveform

然后检测到row变为1,说明按键被释放了,而这里我的检测标志key_flag应该清零了(图中未清零)。然后状态转到01000000000,同时释放延迟计数en_cnt1启动,长按延迟计数en_cnt2终止。释放延时计数开始从0计数。

img_0.7062456080463159.jpg

Quartus II:Waveform

等到计数完成,我们会看到计数值归0。然后en_cnt1归0,计数终止。状态变为10000000000,这个状态读取row的值,若无按键按下,那么状态重新回到初始状态00000000001

img_0.4767405793151688.jpg

Quartus II:Waveform

如此,则验证了这个程序的正确性。


不过我却多了深一层的思考:你看,电子世界靠着时钟一步一步照着人类给予的指令做着自己的事情,而观看我们人类自己,是依照着谁的指令一步步做事的呢?

注:除本文前两幅图片源于Pixabay和Unsplash,其余均为原创。
参考源码:FPGA矩阵键盘设计 提取码:1234

H2
H3
H4
3 columns
2 columns
1 column
3 Comments
Ecency