查看原文
其他

天天当键盘侠,你知道从按键到响应的底层原理吗?

Rand 程序员小灰 2022-08-22


投稿人:Rand

公众号:Rand_cs



KEYBOARD

键盘,咱们做计算机这一行的自然不必多说,天天与它打交道。但熟归熟,清楚键盘背后的原理吗?键盘上都标有各键的名称,表明了各键所代表的意义,但是计算机是如何知道的?组合键是怎样实现的?按下一个代表字符的键,怎么变成平常使用的ASCII码的?


看完本文,相信你就能了解键盘的本质,知晓这些问题答案。


01

相关介绍


键盘编码器

键盘编码器(i8048),是键盘里的芯片,主要用来监控是否有键按下,弹起,然后向键盘控制器报告此键的相关信息。键盘编码器就像是键盘的嘴,让键盘能够说话,表达目前按键状态。Num Lock键和Caps Lock键的LED灯的开关也归它控制。


键盘扫描码

上述所说的信息就是键盘扫描码,一个键有按下就会有弹起,所以每个键会有两个状态,即每个键将会对应两个扫描码,键被按下时的编码叫做通码(makecode),弹起时的编码叫做断码(breakcode)。


大部分键的通码和断码都是 8 位 1 字节,但有些操作控制键如 ctrl、alt附加键如Insert小键盘区如 / ,方向键等是 2 字节甚至多个字节。有多个字节的扫描码都是以 0xe0 开头。只有Pause Break一个键是以 0xe1 开头


断码与通码的关系:断码 = 通码 + 0x80。0x80 二进制表示为 1000 0000,所以对于断码和通码可以这样理解,它们由8位比特组成,最高位第7位表示按键状态,1表示按下,0表示弹起。


注:上述为第一套键盘扫描码的情况,现下使用的键盘基本使用的第二套键盘扫描码,但是为了兼容,最终还是要将第二套扫描码转化为第一套扫描码,这也是键盘控制器工作的一部分。


键盘控制器

键盘控制器(i8042),不在键盘内部,被集成在南桥芯片上。它主要是接收键盘编码器发来的扫描码(第二套),解码(转成第一套)后保存到自己的寄存器中,然后通过中断控制器发送中断请求。

i8042有4个寄存器,如下所示:

其中输入缓冲区和输出缓冲区共用0x60端口,状态控制器和控制寄存器共用一个0x64端口。


共用不会冲突吗?注意读写状态的不同,CPU使用int指令从8042读数据时 0x60 代表输出缓冲区,CPU使用out指令将数据写入8042时 0x60代表输入缓冲区,状态寄存器和控制寄存器同理。


注:输入输出要视对象决定,对键盘控制器来说是输出,那么对CPU来说则是输入,使用 in 指令。


每个寄存器都是8位的,保存扫描码时最多只能保存8位1字节的扫描码,每次键盘中断服务程序也只能处理 1 字节的扫描码。也就是说键盘中断的次数不是你按键、弹起的次数,而是按键、弹起对应的通码、断码(第一套)字节数。由此可以看出平时我们敲键盘时那是发生了无数次的中断呐。


那有的按键信息不是多个字节的扫描码吗? 的确是,但硬件环境如此,不能改变,只能在软件上下功夫,这就是接下来要说的键盘的中断程序,先看看键盘中断的流程,好有个清晰的路线。


02

键盘中断流程

其实上述的相关介绍已经涉及了部分键盘中断流程,在此从头至尾具体说说,先看流程图:

1. 键盘编码器监控是否有键按下或弹起,若有键按下,向键盘控制器发送此键的通码;若有键弹起,则发送断码(基本发送第二套键盘扫描码)。


2. 键盘控制器接收来自键盘编码器发来的扫描码,解码转化成第一套扫描码,保存到自己的输出缓冲区中,然后通过中断控制器向CPU发送键盘中断信号。


3.  后面的流程基本和上文讲的中断流程一样了,在此简述:未关中断的情况下CPU响应,中断控制器再通过数据线发送中断向量号,CPU据向量号定位中断服务程序,期间检查特权级自动压栈,然后运行中断服务程序处理中断。


03

键盘中断服务程序

键盘中断在所有的可屏蔽中断中优先级仅次于时钟中断,也需要尽快的处理。在Linux 0.11里的整个键盘服务程序都是用汇编来写的,汇编语言直接操作底层的指令,没有编译器来增加额外的东西,所以运行起来比高级语言写的程序快,但也增加了编写程序的难度。


linux0.11版本的键盘中断服务程序的框架源码如下图所示:

这个框架程序主要做了以下事情:


保护现场——压栈

上文中写到压栈ss, esp,  eflags, cs, eip, error_code (若有特权级变化且中断带有错误码) 来保存现场,那只是CPU自动执行的部分,完全保存原任务的信息还是在中断处理程序中进行的。


如上图所示,键盘中断服务程序里通用寄存器只保存了4个,eax, ebx, ecx, edx,若为了省事不追求效率完全可以无脑操作pushad压栈所有的通用寄存器,但人家是Linux系统嘛,虽然只是0.11版本,但也要追求精确,效率,只压栈中断程序需要用到,可能破坏的寄存器。


读取扫描码

inb $0x60, al  从键盘控制器的输出缓存区0x60端口读取扫描码。若不从输出缓冲区读取数据的话,键盘控制器是不会继续工作的,意思是无论你怎么按键,键盘控制器不会响应键盘操作,不会存下新的扫描码发送中断信号等。当然不读取扫描码后续的键盘中断程序也没法工作没有意义,在此只是说明一下。


判断是否为 0xe0 或 0xe1

如果扫描码是 0xe0 或者 0xe1,那说明这个键的扫描码是有多个字节的,需要先保存下来等待接下来的扫描码组合成完整的扫描码。 


寻址、调用相应的键处理程序

拿到完整的扫描码之后就该去寻找相应的键处理程序了,源码中有个key_table,table, 说明它是一张表,或者说一个数组,这里面就按照扫描码大小存放了各个键的实际处理程序地址。


如何找到相应的键处理程序呢?其实跟数组用下表获取元素一样,只是汇编里面有一些听起来高大上的名词:据源码所示采用比例变址寻址的方式,即key_table(, %eax, 4),也就是说相应的键处理程序的地址是key_table + eax * 4。key_table,相当于数组首地址;eax里面存放的扫描码,扫描码可以看成数字索引号,相当于数组下标;地址32位,4字节,所以乘4。


回复现场——出栈

压栈保护现场的逆过程,在此不再赘述,需要注意执行到 iret 时的栈顶应是 eip。


04

键处理程序

键的扫描码有通码和断码,有着不同的处理,主要的键处理程序我分为了以下几类(各点开头出现的名字都是Linux0.11中实际键处理程序的函数名称):


1、 ctrl,alt,caps,shift,num等控制键处理程序,整个键盘中断程序维护了两个8位的变量mode和leds。它们的每一位(没用完)代表着一个键,1表示按下,0表示弹起。mode 代表的键有caps, alt, ctrl, shift。leds代表的键有NumLock, CapsLock, ScrollLock。所以操作控制键的键处理程序就是设置变量的相应位。


2、 do_self,处理普通键的程序,主要的功能就是将扫描码转换成ASCII码,然后放进键盘缓冲区中。键盘中断程序维护了一张扫描码到ASCII码,名为key_map的映射表,do_self依据这张表做转换。


3、 func, 处理功能键如Fxx键的程序


4、 cursor,设置光标位置,它是处理方向键,PgUp,Backsp等键的程序


5、 unctrl, unshift等,将mode和leds复位,如unctrl将mode中的ctrl位置0。


6、 none,除开特殊键的断码对应的键处理程序,什么都不做,直接返回。而特殊键的断码处理程序就是上述的5,复位就行。


由上,我们也能得知平时可能成为习惯但没具体关注的几个问题:


1. 使用组合键时需要先按下控制键。键盘的中断程序为这些控制键设置了标识(mode/leds)。先按下控制键,程序为控制键设置好按下状态,再处理后到来的键时会检查这些标识,是否有控制键按下,以便做出不同的操作。


2. 组合键按键时有顺序,但弹起无顺序要求。由上面的键处理程序可知,只有通码的键处理程序在做事,而断码的键处理程序除了特殊键需要复位之外其他键都是直接中断返回的。所以使用键盘控制输入时重要的是按键,而不是键弹起,所以只要按键对了,怎样弹起并不重要。


3. 一直按着某个键时会一直触发键盘中断,若是普通的字符键,电脑屏幕可能会出现一直打印某个字符的现象。若是一些控制键,则中断程序可能会不停地将这个键设为按下状态。当然,键盘中断程序是否记录上次按键取决于具体实现,大多是不记录的,触发一次键盘中断就处理一个扫描码。


END


关于键盘控制输入的原理就是这样,这条线应该还是很清楚的。键盘输入是以键盘中断为核心的,如果还不是很清楚,可以回头看看键盘中断的流程图。


好啦,本文就到这里,如果有什么错误还请批评指正。如果有所帮助,还请多多关注支持。


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存