转自关于键盘冲突那点事(3键冲突/7键冲突/PS2/USB的各种原理)

最近闲得无聊,正好看到有人发帖提问,于是就来详细说说所谓键位冲突和无冲突的各种原理——基本上这也是个老生常谈的话题了,但相关的技术帖比较零乱难找,而且充斥了大量电工术语,也不是很容易看懂。这里就尽量用通俗易懂的语言来讲(我的目标是即使你只有初中文化水平也能看懂,保守地说绝对不超过高中文科生能理解的范围),帖子比较长,有兴趣的朋友请慢慢阅读。慢慢看,用心理解,包你看懂。



为了降低阅读门槛,本文难免有不严谨之处,还请工科同学高抬贵手。如果是特别荒谬的原则性错误,欢迎指正。

——————电路基本常识:输出与输入——————



我们的手指按下一个键,电脑是怎么知道的呢?在这短短几十微秒的时间里发生了什么事呢?为什么有时候同时按下几个键就没反应了呢?首先要讲讲电路的通断。



即使你没有什么计算机知识,大概也应该听过一个词:【二进制】。不管你家里的电脑外表多么五颜六色,它底层的逻辑却是非黑即白,只有【1】和【0】。任何储存在你电脑里的东西,无论游戏、音乐还是你最钟爱的小电影,都是用一长串你数不清的1和0的组合来记录和处理的。

明白了这个概念以后,再想想,电脑电脑,它的基础是什么?对,要有【电】。下一个问题很自然地:这电怎么就能变成1和0呢?说来更简单,有电就是1,没电就是0呗——这么说似乎太不专业了。严谨一点说:在电路中一个点,它当前表示的数据是1还是0,需要检测这一点的电压到底是更接近【悬空】(对于USB和PS/2接口,指+5V),还是更接近【大地】(0V)。如果高于某个界限值,称作【高电平】,也就是1;而相对地,低于某个界限值,称作【低电平】,也就是0。

接下来的问题更是小学生也会答:1×1等于多少?

你当然知道答案是1。

那么1×0呢?

对了,不管什么数字乘以0,结果都是0。

如同在游泳池里面尿尿一样,一泡尿就把干净水变成脏水。大地就是这么邪恶:无数个悬空的点,它们之间互相连接还是悬空,然而只要其中有一个点接着地,它们就全等于接地了。



(重要知识)高电平的点和低电平的点连通短接之后,两点都成为低电平。



你压住不耐烦,看到这里,心想,这他妈的和键盘有毛的关系?

别着急,我们再来看看一个典型的可编程芯片是什么样子(图片引自泡泡网的poker拆解):



 



这个黑色方方的就是芯片,它周围那一排排张牙舞爪的刺叫做【引脚】,是芯片用来和外界沟通的渠道,图中这只芯片一共有48个引脚。

其中一些引脚负责电源、时钟、控制等基础功能,但占绝对多数的是负责输入/输出数据的,称为【I/O引脚】。

通过程序设置,芯片既可以改变每个I/O引脚的电压(设置1或0的值),也可以检测引脚的电压(读取1或0的值),以下如果没有特殊说明,提到引脚一词均指数据I/O引脚。



现在请假设这样一个场景:

你是一颗芯片,你的左手和右手是两个引脚,有一大团杂乱无章的导线,露出两个线头摆在你面前,你如何判断它们是否是同一根线的两头?(即这两个线头是否连通)



仔细考虑之后,聪明的你大概可以想到:只要把左手设置为0,右手设置为1,分别握上两个线头,然后检查右手的状态,如果变成0了,说明它们刚刚做过一次相乘运算,1被拉下水变成了0,这条线是连通的。



换一个比喻:如同一根管道,在左手的洞口放一只小老鼠,右手的洞口放一块奶酪(这只小老鼠的速度无敌快)。当小老鼠从左边进去,又从右边钻出来吃掉了奶酪,说明管道中间没有被堵死。



对,这就是键盘按键接通的原理。

在按键下面的【电路板】(或者电路薄膜)上,印制有许多导线,导线经过每个按键下方的部分是断开的。按键,也就是开关,当它压下的时候,下面的导线会被接通。而导线最终两端都是连接到芯片上,芯片会不停地反复检测每条线的连通情况,从而随时判断哪个键当前是按下的。这就好像学校的保安头子坐在监控室里,切换着镜头,偷窥哪个自习室中有男女生OOXX那样。

——————主控芯片与矩阵设计——————



我们继续深入话题:一块普通的键盘,少则几十个多则上百个按键,显然无论从哪个方面看,怎么都不可能给每个键都单独连个导线到CPU芯片去——先不说成本多高,谁愿意桌面上横着手腕粗的一大捆线呢?



在上个世纪末,电脑开始走入寻常百姓家庭,当时的PC界霸主是IBM公司。为了简化接口,顺便垄断标准,IBM陆续设计了XT、AT、PS/2协议用来处理键盘这样的输入设备,大体意思是,只要在键盘内部放一块主控芯片,用来管理所有按键状态并转换为串行信号,包括电源在内总共只要4根线就可以传输所有的数据(扫描码),而相应地,主板上也会有一个称作键盘控制器的IO芯片(一般集成在南桥中),把这些扫描码翻译为ASCII码给CPU。

最后,PS/2协议作为成熟而稳定的形态,成为了二十多年来的市场规范,也就是大家熟知的那个圆形接口,里面实际用到的4根线分别负责:时钟、数据、电源、接地。



上面这一段可能有点复杂,如果你没能全看懂,也没啥大碍,只是为了说明【键盘主控芯片】的存在。



总之,整理一下到目前为止的知识,现在你应当知道键盘是遵循如下的通讯过程:

【按键】——【键盘主控芯片】——(翻译成扫描码,经过PS/2协议)——【主板IO芯片】——(翻译成ASCII码)——【CPU】



这样看起来不错,但还有个问题:主控芯片是怎么“知道”所有键的状态的?



按照前面说的,要得知一个按键是否按下,需要在引脚A输出0,引脚B输出1,再检测引脚B的值是1还是0。(如果这里看不懂就麻烦了,请向上翻翻,复习一下左右手攥电线或者小老鼠吃奶酪的例子)



现在,假设我们要做一个36键的键盘,包括10个数字和26个英文字母。



于是我们令引脚A永远=0,而且连接到所有的按键上。

然后做引脚B1、B2、B3、……、B36,分别与对应的36个按键连接。

这样总共是需要37个引脚。



接着,先令所有B引脚=1,然后从B1到B36挨个检查,谁变成0了,就说明谁对应的按键按下了。当然,为了时刻获取最新的状态,每秒钟要进行几十至上百轮这样的扫描。



但是104个键的键盘怎么办?老老实实做105个引脚吗?这也太复杂了吧!有没有办法能用更小、更简单一些的芯片实现呢?要知道这可直接关系到成本啊。



工程师们想了个办法:【矩阵】。听起来很专业,其实就是利用“组合”,来成倍地提高引脚利用率。还拿上面的例子说,我们可以把引脚数量从37缩减到12。怎么做呢?



请想象一个表格,行标题为A1、A2、A3、A4、A5、A6,列标题为B1、B2、B3、B4、B5、B6。这样就构成了一个6×6=36的矩阵。然后把按键分别放到每个格子里面去,如下图。



 



在电路中,每个按键都是负责连接它所对应的两个引脚,比如按键A连接引脚A1和B1,而按键W连接A5和B4。这样一来,引脚之间就形成了【交叉组合关系】,也就是矩阵。任意两个引脚之间只通过一个按键连接。



现在我们按下J键,芯片中的程序是怎么检测到这个行动的呢?



首先令A1=0,其他所有引脚=1,然后从B1到B6挨个检查。由于那一列的按键都没有按下,没有任何一个B引脚和A1接通,因此它们的值都是1。

接下来,令A2=0,其他所有引脚=1,重复以上工作。接着再检查A3列……

最后所有行列检查完毕后,结果发现只有在A4=0的时候,B2=0,也就是说A4和B2是接通的。于是程序便通过预先定义好的按键表格,知道按下的是J键。

同样地,这一整轮扫描每秒要重复几十上百遍,所以你在任何时候敲下或抬起按键,电脑都能很快反应出来。



现在市面上绝大多数键盘的工作原理都是基于这种矩阵的。我们很容易想到,矩阵的行数乘以列数的结果,就是它能够容纳按键的最大数量。普通的104键键盘是应用16×8的矩阵,来覆盖所有按键。只需要24个数据引脚。

——————三键冲突:矩阵的麻烦——————



如果你耐心地一行一行读到这里,我相信经过了两节的铺垫,你已经掌握了足以继续读下去的基础知识。那么废话到此为止,下面开始介绍本帖的重点问题:【键位冲突】。



在刚才的段落中,你已经知道了系统是如何判定单个键有没有按下的。但我们人类的双手上长了十个手指,谁也不能保证不会同时按下两个按键——甚至很多时候组合键是故意设计要用的。这样一来,就会有一个潜在的问题出现……



请回忆一下刚才用来举例的36格矩阵图,如果我们同时按下B、H、G键,在程序看来是什么样子呢?



像平时一样,它从(A1,B1)开始检测,现实中我们并没有按下A键,所以当A1=0,其他引脚=1的时候,B1的值应该是1,表示A键没有被按下才对。但是,请注意:



由于G键被按下,A1和B2是接通的,

由于H键被按下,B2和A2是接通的,

由于B键被按下,A2和B1也是接通的!

也就是说,现在的电路中,A1和B1其实是连在一起的!



还记得吗?不管多少个1相乘,只要中间有0,最后就会变成0。

换句话说,我们见A1和B1没有直接连通,就天真地以为B1的奶酪不会被吃掉——但有个致命的错误就在于我们根本不关注其它奶酪。瞬间,电流飞驰,经过3个按键,最终钻进地下。这只飞快的小老鼠沿着管线从A1出发,先是吃掉了B2的奶酪,然后又吃掉了A2,最后从B1钻出来大快朵颐。(注:严格来说,其实老鼠与电流方向是相反的,此处的比喻是为了更容易理解)



就这样,芯片以为A键也被按下了。

事实上,按下这4键中的任意3键,在电脑看来都是相同的,因为A1、A2、B1、B2这四点已经变成短路的状态。



任意两行两列所构成的4个交点,也即某长方形的四角所对应的4个键,同时按下3个时,都会出现这样的问题——在四通八达的管道中,剩余的那个键的状态到底是按下还是没按下,对于芯片来讲是一片茫然。怎么办呢?

扫描按键的程序是人写的,稍作改动也不是不可能。于是需要增加如下的处理方法:给它一个“小账本”,随时记录当前按下的所有按键。每当按下或抬起某个键时,就在账本中如实增加或抹除。但是,如果账本显示:某个“四角组合”其中已经有两个按键同时按下时,这个组合剩余的键就被逻辑锁定——即使你按了,程序也拒绝接受,除非之前的某个键抬起。



这样设计的理由很简单:宁可错杀一千,不能放过一个,不知道按没按的话,当成没按更保险。你能想象当你同时按下B键和G键以后,再按H键,屏幕上出现的却是A吗?太无厘头了,还不如什么反应都没有。



这也就是所谓的三键冲突的原型所在。



任何没做无冲处理的矩阵式键盘,都存在许多特定的三键组合不能同时按。举个著名的例子,黑寡妇的A、W、L。



你可能会说:“不会啊我的键盘可以七键一起按都没冲突的。”



是的,不同品牌型号的键盘走线设计可能有区别,因此它们存在冲突的键位也不一样。只要不构成四角组合关系,大部分键都是可以随便同按的,以打字为主要用途的普通键盘,即使有这样那样的冲突,也足够日常使用了。

但是四角组合数不胜数——比如上面例子中6×6的矩阵就存在多达55个四角组合,220种三键冲突,可想而知全尺寸键盘会有多少个键位冲突。虽然大部分冲突组合都是你平时不会按到的,但玩游戏的时候需要的键位总是千奇百怪各不相同,比如玩劲乐团可能需要SDF空格JKL不冲突,而BMIIDX则需要ZSXDCFV不冲突。如果你什么都玩,有很大几率会碰到那么一两个冲突键位郁闷你。即使对键盘最没要求的FPS游戏,还是有少数键盘的四角组合悲剧地包含QWA或者1WD之类经常需要一起按的键……

一个比较讨巧的办法就是把左侧常用十来个键位的走线全部串到一起,这样至少可以保证打CS情绪稳定。因为我们知道,会起冲突的按键是位于任意两行两列的4个交点中的3个,而全部处于同一列或同一行的键,不管怎么按也不会冲突。



当然,最完美的还是全键盘无冲突,也就是所谓的【NKRO】。这就要放在下一节讲了。

——————无冲突的技术本源——————



之前你已经意识到了,普通的矩阵键盘,都会存在成百上千的三键冲突组合。但是市面上却有那么几款键盘,号称全键无冲突,实际测试也是威武异常,整个手掌拍下去都能毫不犹豫地识别出来,这是为什么呢?



这里要介绍一个美妙的电气元件——【二极管】。



二极管是计算机逻辑电路最基本的元件之一(包括CPU芯片在内的各种集成电路芯片内部都有大量的二极管和三极管),大家津津乐道的LED就是二极管中能发光的一种。

一个典型的二极管会有两条腿,即阳极和阴极。它的特点就是——电流只能从它的阳极流向阴极,而反向则难以通过。

如果身为芯片的你捏着一个二极管的两端,你左手是1,右手是0时,只消一瞬间,左手的1就会变成0。但若调换成右手是1,左手是0,右手的1则不会受到影响。这二极管就相当于一个单向的小门,老鼠只可以从这边跑到那边,却不能从那边跑到这边。



那么这个特点对我们具体有什么帮助呢?



只要你回忆一下按键冲突的问题是如何产生的,就会恍然大悟了。



冲突,是为了防止当A1和B2、A2和B2、A2和B1分别连通时,程序误以为A1和B1也连通,因此当发现3个按键互相形成回路时,就屏蔽第三颗按键的设计。



现在,我们在每个按键的电路中增加一个二极管,让小老鼠只能从A端跑到B端,而不能从B跑向A。

回到之前的例子,同时按下B、H、G三个键。尽管H键接通了A2和B2,但由于二极管的限制,信息只能从A2到B2传导,而不能从B2到A2。

于是,虽然受G键按下的影响,当A1=0的时候,B2的值被修改为0,但这个0在这里就到此为止了。因为老鼠到达B2后,被门挡住,无法继续去吃A2的奶酪。既然A2不会跟着变成0,而是保持正确的1,B1的值当然也还是1。

由此,系统自然能够判断出,A键没有被按下,和事实一致。也就是说,二极管的防逆流特性,彻底消除了按键之间的干扰。



有了这些二极管做保障,自然根本不需要什么屏蔽第三颗按键的逻辑了。于是,每一颗按键可以独立自主反应,活动自如,成就了我们的无冲突键盘。



至于为什么无冲突键盘基本都是机械键盘,我想可能有两个原因:

1,机械键盘采取的电路板比较容易安装二极管。而薄膜键盘基本无解。

2,机械键盘本身的定位也比较高,相对这个售价水平来讲,增加一百颗二极管的成本并不显著。

——————USB永远的痛——————



讲了这么多,终于到最后一节了。前面已经把造成键盘冲突的原理和解决办法从头到尾介绍了一遍,但还没有讲过USB接口的键盘,即使硬件上是NKRO结构了,为什么还是只能做到6键无冲突。



这里所指的6键,是除去Ctrl、Shift、Alt、Win之外的键,同时按下任意6个都不会有冲突,但第7个键按下就没有反应——或者会直接抹掉第一个键,总之逻辑上同时只能有6个键处于按下的状态。



但是这样的键盘,使用PS2转接头连接电脑,又可以实现完美NKRO(除了部分键盘干脆不支持PS/2转接,例如poker)。

看来问题就出在USB接口上了。



事实上的确是这样,因为键盘输入设备在USB接口和PS/2接口的传输协议完全不同,也就是说,它们采取了完全不同的工作方式,也难怪效果不同。现在你能买到的大部分机械键盘,其主控芯片可以根据当前连接的端口,自动适应PS/2或USB协议。只有少量无法转接。



既然你已经坚持看到这里了,我相信你对它们的具体区别会比较感兴趣,别着急,这就慢慢道来。



(还是有些废话:如果你搞不清【字节】和【位】的概念请看本段)

位(bit,缩写为小写的b),就是二进制位,取值范围只有0和1两个值,是最小的单位。

字节(Byte,缩写为大写的B),为8个位的组合,取值范围是从0到255(2的8次方),也是常见的计算机数据量单位。

1字节=8位,所以如果你的网速标称10Mb,实际下载速度只有1.25MB。



PS/2协议下,键盘是每次发生按键/抬键动作,都会发送数据信号给主机。通常按下一个键这个动作所包含的数据(通码)为1或2个字节,抬起一个键(断码)则是2或3个字节。如果按住一个键不放,则会不停地向主机循环发送通码,直到抬起按键发送断码。根据10-20kHz的工作频率规范,每位数据的传输时间大约是40-80微秒,加上中间的保留延迟,每个字节会占用0.5-1ms的传输时间。不过在实际应用中,这个延迟完全可以接受——即使像铁拳那样以帧来计算的格斗游戏,对出招的严格度也不会低于16ms。



而USB协议下,键盘会以某个固定的回报率(每秒125-1000次),定期向主机发送当前按键的状态,每次发送8个字节,这8个字节的具体内容则是:

第一个字节:8位分别表示左右的Ctrl、Shift、Alt、Win各自是否被按下。这8个键统称为【modifer key】,因为规范已经事先定义好每一位的含义,从而得以能够只用一个字节就表示8个键的状态。

第二个字节:保留(无用)

其余6个字节:当前正按下的6个【普通按键】(如果按了7个以上,根据键盘主控芯片内置的程序,可能取最先按的6个,也可能取最后按的6个)。

即每1-8ms,可以发送最多14个按键的状态信息。



发现问题所在了吧?如果说按键是上厕所的人,传输协议是看守厕所的大叔……



PS/2大叔会一直盯着厕所门口,每次有人进去就向主机汇报,有人出来再汇报。

USB大叔呢,则是急急忙忙冲进厕所,看有哪些人在,记在小纸条上,然后跑出来一起汇报,之后再冲进去,如此循环。可惜他的小纸条地方太小,只够写下6个人的名字(另外还有8位闹肚子的熟客是事先打好招呼的,只要用暗号记载汇报就可以)。



所以说,USB协议下,包含两边的Ctrl、Shift、Alt、Win在内,单键盘最多只能同时识别14个键。如果只算普通键,则只能同时识别6个。



至于最近一年刚兴起的【USB无冲】技术,似乎是通过将一个物理键盘虚拟成多个逻辑键盘实现的,程序兼容性还有待提高,在此暂且不表。



关于键盘冲突那点事,差不多也说完了。感谢你耐心阅读本文。



关于键盘冲突那点事(3键冲突/7键冲突/PS2/USB的各种原理)的更多相关文章

  1. Atitit.android js 的键盘按键检测Back键Home键和Menu键事件

    Atitit.android js 的键盘按键检测Back键Home键和Menu键事件 1. onKeyDown @Override public boolean onKeyDown(int keyC ...

  2. Android 键盘键名和键值列表

     电话键 KEYCODE_CALL 拨号键 5 KEYCODE_ENDCALL 挂机键 6 KEYCODE_HOME 按键Home 3 KEYCODE_MENU 菜单键 82 KEYCODE_BACK ...

  3. SQL语句建表、设置主键、外键、check、default、unique约束

    · 什么是数据库? 存放数据的仓库. · 数据库和数据结构有什么区别? 数据结构要解决在内存中操作数据的问题,数据库要解决在硬盘中操作数据的问题.数据结构研究一些抽象数据模型(ADT)和以及定义在该模 ...

  4. Activity的生命周期,BACK键和HOME键生命周期

    Activity的生命周期模型在Google提供的官方文档上有比较详细的一个图示 public class HelloActivity extends Activity { public static ...

  5. 在Activity,Service,Window中监听Home键和返回键的一些思考,如何把事件传递出来的做法!

    在Activity,Service,Window中监听Home键和返回键的一些思考,如何把事件传递出来的做法! 其实像按键的监听,我相信很多人都很熟练了,我肯定也不会说这些基础的东西,所以,前期,还是 ...

  6. Android-服务中监听电源键和Home键的广播、在锁屏下仍然工作的方法

    Android-服务中监听电源键和Home键的广播  http://blog.csdn.net/u014657752/article/details/49512485 Android开发之如何监听让服 ...

  7. appium怎么按下系统按键?如按下返回键、home键等等

    ava_client3.0版本以后使用pressKeyCode方法,之前的版本使用sendKeyEvent方法 1. 返回:driver.pressKeyCode(AndroidKeyCode.BAC ...

  8. mysql数据库外键、主键详解

    一.什么是主键.外键: 关系型数据库中的一条记录中有若干个属性,若其中某一个属性组(注意是组)能唯一标识一条记录,该属性组就可以成为一个主键 比如  学生表(学号,姓名,性别,班级) 其中每个学生的学 ...

  9. android 实现返回键执行home键方法

    在公司的产品开发,中老板很是执着于,不要看到启动界面,但是又不想去掉启动界面. so 实现返回键执行home键方法. public boolean onKeyDown(int keyCode, Key ...

随机推荐

  1. Java基础知识强化之集合框架笔记70:模拟斗地主洗牌和发牌(ArrayList)

    1. 模拟斗地主洗牌和发牌 分析:     A:创建一个牌盒     B:装牌     C:洗牌     D:发牌     E:看牌 2. 代码实现: package cn.itcast_03; im ...

  2. kickstart安装

    1.生成ks.cfg 文件 安装Kickstart # yum install system-config-kickstart 8.2 在桌面环境下配置Kickstart 启动X Windows 环境 ...

  3. Mybatis特殊字符处理,Mybatis中xml文件特殊字符的处理

    Mybatis特殊字符处理,Mybatis中xml文件特殊字符的处理 >>>>>>>>>>>>>>>>& ...

  4. Oracle--常见Exception

    1.  错 误 名 称 错误代码    错 误 含 义 2.  CURSOR_ALREADY_OPEN ORA_06511   试图打开已经打开的游标 3.  INVALID_CURSOR  ORA_ ...

  5. yii中的自定义组件

    yii中的自定义组件(组件就是一些自定义的公用类) 1.在项目目录中的protected/components/Xxxx.php 2.在Xxxx.php中定义一个类,类名必须与文件名相同 3.控制器中 ...

  6. 如何在网页上显示html代码?

    a: 把代码写在文本区域 <textarea> 标签中.可以设置 disabled="disabled" 属性,禁止用户操作.b: 把要显示在html文档中标签的 &q ...

  7. asp.net 开发问题:Web 服务器上的请求筛选被配置为拒绝该请求,因为内容长度超过配置的值。

    "Web 服务器上的请求筛选被配置为拒绝该请求,因为内容长度超过配置的值." 这个问题在开发需要上传文件的时候可能会遇到,今天遇到这个问题,百度过也有挺多的修改方法. 方法1: 修 ...

  8. 那天有个小孩跟我说LINQ(三)转载

    1  LINQ TO Objects续2(代码下载)      新建项目 linq_Ch3控制台程序    1.1 操作字符串        ①查找字符串中包含的大写字母,字符串是由多个char类型组 ...

  9. windows8 8.1 安装完 ubuntu无法挂载 ntfs分区 解决方法

    windows8 8.1 安装完 ubuntu无法挂载 ntfs分区 解决方法: 最近安装完发现8.1系统后,ubuntu无法加载以前的ntfs分区了,特别是我添加到了/etc/fstab里面了 导致 ...

  10. 周末充电之WPF(三 ) .后台动态生成控件

    布局 -连连看: 代码: private void Window_Loaded_1(object sender, RoutedEventArgs e) { //动态创建行 ; i < ; i++ ...