你知道键盘是如何工作的吗?(xv6键盘驱动程序)
键盘驱动程序
- 公众号:Rand_cs
键盘如何工作的前文曾经说过,当时是以 Linux 0.11 为基础讲的但不系统,本文以 xv6 的键盘驱动程序为例来系统地讲述键盘是如何工作的。关于驱动程序前文磁盘那一篇说过了,它就是硬件物理接口的封装,所以了解键盘驱动程序,同样的还是先来了解键盘的一些物理接口。
与键盘相关的芯片有两个,一个是键盘编码器 i8048
,另一个是键盘控制器 i8042
,分别来看。
键盘编码器
键盘编码器位于键盘,它的作用主要是监测键的按下和弹起,然后将两种状态编码,发送给键盘控制器。
上述说的码叫做键盘扫描码,编码方式一共有三种,相应的也就有三套键盘扫描码,各套键盘扫描码具体怎么编码的就不说了,见后面的链接。现今的键盘大多数都是用的第二套键盘扫描码,但也不排除使用第一套和第三套的,所以为了兼容,键盘控制器会统统地转换为第一套扫描码。当然这是默认的情况,具体使用哪一套扫描码,控制器是否转化,还是要看硬件是否支持与具体怎么设置,有兴趣的详见文末链接。
因此第一套键盘扫描码还是得说道说道,一个键有按下就会有弹起,所以每个键会有两个状态,即每个键将会对应两个扫描码,键被按下时的编码叫做通码(
m
a
k
e
c
o
d
e
makecode
makecode),弹起时的编码叫做断码(
b
r
e
a
k
c
o
d
e
breakcode
breakcode)。
大部分键的通码和断码都是 8 位 1 字节,但有些操作控制键如 Ctrl
、Alt
,附加键如Insert
,小键盘区如/
,方向键
等是 2 字节甚至多个字节。有多个字节的扫描码通常都是以
0
x
E
0
0xE0
0xE0 开头。只有
P
a
u
s
e
B
r
e
a
k
Pause Break
PauseBreak 一个键是以
0
x
E
1
0xE1
0xE1 开头。
断码与通码的关系:
断
码
=
通
码
+
0
x
80
断码=通码+0x80
断码=通码+0x80。
0
x
80
0x80
0x80 二进制表示为
1000
0000
1000 \ 0000
1000 0000,所以对于断码和通码可以这样理解,它们由 8 位比特组成,最高位第 7 位表示按键状态,1 表示按下,0 表示弹起。
键盘控制器
键盘控制器(i8042
),不在键盘内部,被集成在南桥芯片上。主要接收键盘编码器发来的键盘扫描码,做一些处理(比如第二套扫描码转第一套),然后触发中断通知 CPU 来读取扫描码。
键盘控制器有 4 个 8 bits 寄存器,Status Register
和 Control Register
,两者共用一个端口 0x64
,读的时候是状态寄存器,写的时候是控制寄存器。Input Buffer
和 Output Buffer
,两者共用一个端口 0x60
,读的时候是输出缓冲器,写的时候是输入缓冲器。
状态寄存器:
bit0
:1 表示输出缓存器满,CPU 读取后清零。从编码器发过来的扫描码就放在这里。
bit1
:1 表示输入缓存器满,控制器读取后清零。
控制寄存器:
通过写 0x64
端口来向控制器发送命令,注意是向控制器本身发命令而不是向硬件设备键盘发命令,对于键盘的控制就是通过控制器来间接控制,所以只需要操作键盘就是了。
命令控制器就是将命令字节写入 0x64
端口,一般命令就是一字节,如果有两字节,则将第二个字节写入 0x60
端口。因为要写 0x60
端口表示的缓存区,所以要先判断该缓存区是否为空。
比如进入保护模式设置
A
20
A20
A20 时,先判断输入缓存区是否为空,空的话表示控制器已取走数据,可以继续进行,否则不空的话循环等待:
inb $0x64,%al # Wait for not busy 等待i8042缓冲区为空
testb $0x2,%al
jnz seta20.1
再向 0x64
端口写入命令
0
x
D
1
0xD1
0xD1,表示准备写 Output
端口,随后写入 0x60
端口的字节将放入 Output
端口。
inb $0x64,%al # Wait for not busy 同上
testb $0x2,%al
jnz seta20.2
movb $0xdf,%al # 0xdf -> port 0x60 向端口0x60写入0xdf,打开A20
outb %al,$0x60
同样的先判断输入缓存区是否为空,然后写入命令第二字节
0
x
D
F
0xDF
0xDF,这个字节会被送到 Output
端口,这个端口也是一个控制端口,bit2
控制着
A
20
A20
A20 的开关,所以如果是命令字节
0
x
D
D
0xDD
0xDD 表示关闭
A
20
A20
A20。
关于键盘控制器就说这么多,只讲述了与 xv6 相关的部分,其他部分同样的感兴趣的见文末的链接。
XV6
驱动程序就是硬件物理接口的封装,键盘驱动程序也是如此,它的主要功能就是将读取扫描码转换成计算机所需要的信息,比如说转换成字符,信号等等。xv6 在这方面实现的比较简单,只实现了字符转化,一些功能控制键,我们来看看。
首先在
k
b
d
.
h
kbd.h
kbd.h 头文件中定义了端口号,控制键如 Ctrl
,特殊键如 UP
,以及最重要的映射表,来看个普通情况下的映射表:
static uchar normalmap[256] =
{
NO, 0x1B, '1', '2', '3', '4', '5', '6', // 0x00
'7', '8', '9', '0', '-', '=', '\b', '\t',
'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', // 0x10
'o', 'p', '[', ']', '\n', NO, 'a', 's',
'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', // 0x20
'\'', '`', NO, '\\', 'z', 'x', 'c', 'v',
'b', 'n', 'm', ',', '.', '/', NO, '*', // 0x30
NO, ' ', NO, NO, NO, NO, NO, NO,
NO, NO, NO, NO, NO, NO, NO, '7', // 0x40
'8', '9', '-', '4', '5', '6', '+', '1',
'2', '3', '0', '.', NO, NO, NO, NO, // 0x50
[0x9C] '\n', // KP_Enter
[0xB5] '/', // KP_Div
[0xC8] KEY_UP, [0xD0] KEY_DN,
[0xC9] KEY_PGUP, [0xD1] KEY_PGDN,
[0xCB] KEY_LF, [0xCD] KEY_RT,
[0x97] KEY_HOME, [0xCF] KEY_END,
[0xD2] KEY_INS, [0xD3] KEY_DEL
};
键盘扫描码是一个键的代表,但不是我们想要的,我们想要的是这个键表示的意义,比如数字键 1 的通码是
0
x
02
0x02
0x02,
0
x
02
0x02
0x02 显然不是我们想要的,我们想要的是数字 1,所以需要一个映射关系来转换,将所有键的映射关系集合在一起就是上述的映射表。是个大数组,下标就是这个键的扫描码,内容就是所表达的意思。
上述是一般情况,那当然还有非一般的情况,比如有按下 Shift
,CapsLock
,Ctrl
等控制键,当按下这些控制键后,其他键按下之后表达的意义就不一样了,所以还需要另外的映射表,这里就不列出来了,太多了,可以直接参考代码。举个例子,当按下 Shift
键之后再按下数字键 1
,通码
0
x
02
0x02
0x02 则应该映射成 !
而不是1
。
有了这些了解之后来看
k
b
d
.
c
kbd.c
kbd.c 里面的源码:
int kbdgetc(void)
{
static uint shift; //shift用bit来记录控制键,比如shift,ctrl
static uchar *charcode[4] = {
normalmap, shiftmap, ctlmap, ctlmap
}; //映射表
uint st, data, c;
st = inb(KBSTATP);
if((st & KBS_DIB) == 0) //输出缓冲区未满,没法用指令in读取
return -1;
data = inb(KBDATAP); //从输出缓冲区读数据
if(data == 0xE0){ //通码以e0开头的键
shift |= E0ESC; //记录e0
return 0;
} else if(data & 0x80){ //断码,表键弹起
// Key released
data = (shift & E0ESC ? data : data & 0x7F);
shift &= ~(shiftcode[data] | E0ESC);
return 0;
} else if(shift & E0ESC){ //紧接着0xE0后的扫描码
// Last character was an E0 escape; or with 0x80
data |= 0x80;
shift &= ~E0ESC;
}
shift |= shiftcode[data]; //记录控制键状态,如Shift,Ctrl,Alt
shift ^= togglecode[data]; //记录控制键状态,如CapsLock,NumLock,ScrollLock
c = charcode[shift & (CTL | SHIFT)][data]; //获取映射表的内容,也就是该键表示的意义
if(shift & CAPSLOCK){
if('a' <= c && c <= 'z')
c += 'A' - 'a';
else if('A' <= c && c <= 'Z')
c += 'a' - 'A';
}
return c;
}
这个程序就可以看作极简的键盘驱动程序,也是键盘中断的服务程序的主体,完成键盘扫描码到所需信息的转化。下面就来仔细分析分析:
前面说过有多张映射表多种映射方式,那怎么知道用哪张?用哪张得看看有没有相应的控制键按下,所以得有个东西来记录控制键的按下与否,这个东西就是变量
s
h
i
f
t
shift
shift,虽然变量名是
s
h
i
f
t
shift
shift,但不代表只记录 Shift
键的状态,记录的信息有:
#define SHIFT (1<<0)
#define CTL (1<<1)
#define ALT (1<<2)
#define CAPSLOCK (1<<3)
#define NUMLOCK (1<<4)
#define SCROLLLOCK (1<<5)
#define E0ESC (1<<6) //通码断码以E0开头
从这种定义控制键的方式就可以看出使用
s
h
i
f
t
shift
shift 来记录控制键的方式应该是使用位运算。
c
h
a
r
c
o
d
e
charcode
charcode 表示一个二维数组,可以看作是映射表的集合,根据
s
h
i
f
t
shift
shift 的记录信息来选择映射表,后面用到的时候就明白了。
st = inb(KBSTATP);
if((st & KBS_DIB) == 0) //输出缓冲区为空,没法用指令in读取
return -1;
data = inb(KBDATAP); //从输出缓冲区读数据
这几句用来读取键盘扫描码,从键盘发过来的扫描码就放在输出缓冲区中。要读取扫描码首先从状态寄存器读取当前状态到
s
t
st
st,再做与运算取出第 0 位,表示输出缓冲区的状态,如果为 0 表示输出缓冲区寄存器为空,没法读取返回 -1。如果为 1 表示输出缓冲区寄存器已满有内容,可以读取,所以接着从端口 0x60
输出缓冲区读出扫描码到
d
a
t
a
data
data。
if(data == 0xE0){ //通码以e0开头的键
shift |= E0ESC; //记录e0
return 0;
}
如果这个扫描码为 0xE0,说明按下的键是特殊键,扫描码不止 8 字节,这种情况在
s
h
i
f
t
shift
shift 变量中做好标记就可以直接返回了,等待下一个数据的到来再做具体处理
else if(data & 0x80){ //断码,表键弹起
// Key released
data = (shift & E0ESC ? data : data & 0x7F);
shift &= ~(shiftcode[data] | E0ESC);
return 0;
}
d
a
t
a
&
0
x
80
data \ \& \ 0x80
data & 0x80 为 1 的话,表示第 7 位为 1,说明此数据为断码,收到断码不需要额外的做什么事,但如果这个断码是某个控制键的断码,则应该将该控制键在
s
h
i
f
t
shift
shift 里面的记录信息给清除掉。
所以得知道读出的
d
a
t
a
data
data 表示哪一个控制键,所以有了
s
h
i
f
t
c
o
d
e
shiftcode
shiftcode 映射:
static uchar shiftcode[256] ={ [0x1D] CTL, [0x2A] SHIFT, [0x36] SHIFT, [0x38] ALT, [0x9D] CTL, [0xB8] ALT,};
私以为这个定义方式很不对头啊,实在不太明白一些控制键用通码,一些用断码,这也就导致了那条使用了条件表达式的
d
a
t
a
data
data 赋值语句必须存在,因为
s
h
i
f
t
c
o
d
e
shiftcode
shiftcode 中映射 Shift
键的时候没有用断码,所以得转换成通码。私以为这么映射很混乱,导致后面
k
b
d
.
c
kbd.c
kbd.c 中有些语句意义也不太明确,要么就应该将映射关系给补全,然后可以省掉那句
d
a
t
a
data
data 赋值语句,使后面的语句书写变得更明确一点。当然这不是重点,能理解这过程意思就行,总而言之如果
d
a
t
a
data
data 是个断码,不需要干其他的事,如果是控制键的断码,将记录在
s
h
i
f
t
shift
shift 中的控制键信息给清除掉就行。
else if(shift & E0ESC){ // Last character was an E0 escape; or with 0x80 data |= 0x80; shift &= ~E0ESC;}
这种情况对应的是紧接着
0
x
E
0
0xE0
0xE0 后面的键盘扫描码,键盘扫描码有多个字节的,都是成对存在也就是
E
0
h
X
X
h
E
0
h
X
X
h
E0h\ XXh\ E0h\ XXh
E0h XXh E0h XXh 这种形式,每次收到
X
X
h
XXh
XXh,都要将
s
h
i
f
t
shift
shift 键中记录的
E
0
E0
E0 信息给清除掉。至于前面还有一句 $data\ |= 0x80 $还是与 xv6 设计的映射表有关,键盘上有着许多相同意义的键,xv6 将一些键的映射关系用断码来映射,比如除号键 /
。
shift |= shiftcode[data]; //记录控制键状态,如Shift,Ctrl,Altshift ^= togglecode[data]; //记录控制键状态,如CapsLock,NumLock,ScrollLock
这两句来记录控制键的状态,分了两种情况,两种运算方式。应该能看出它们之间的区别吧,实现组合键的时候,Shift,Ctrl,Alt
需要按住不放才能生效,弹起后不再生效。而像 CapsLock
之类的控制键,只需要按下一次即可,即便弹起之后同样生效。所以一个使用或运算,一个使用异或运算,自己模拟一下过程应该很容易明白。
c = charcode[shift & (CTL | SHIFT)][data]; //获取映射表的内容,也就是该键表示的意义if(shift & CAPSLOCK){ //如果有 CapsLock 存在 if('a' <= c && c <= 'z') //小写变大写 c += 'A' - 'a'; else if('A' <= c && c <= 'Z') //大写变小写 c += 'a' - 'A';}
根据
s
h
i
f
t
shift
shift 中记录的控制键信息,来选取映射表,根据
d
a
t
a
data
data 去获取该键盘扫描码所表示的意义。因为 CapsLock
和 Shift
键的功能有相同之处,所以如果 c 就是个普通 26 个英文字母字符的话,需要额外判断大小写。
关于 xv6 的键盘驱动程序差不多就是这么多,当然还有一些功能没说,比如 Ctrl
组合键功能,键盘的缓冲区等等,这在另一个文件里面涉及到了另外的知识,咱们放在后面再详述吧。
在此再聊聊常见的一些问题,在第一篇键盘里也说过,再来看看:
使用组合键时需要先按下控制键。键盘的中断程序为这些控制键设置了标识(
s
h
i
f
t
shift
shift)。先按下控制键,程序为控制键设置好按下状态,再处理后到来的键时会检查这些标识,是否有控制键按下,以便做出不同的操作。
组合键按键时有顺序,但弹起无顺序要求。由上面的键处理程序可知,只有通码的键处理程序在做事,而断码的键处理程序除了控制键的标识位需要复位之外其他键都是直接返回的。所以使用键盘控制输入时重要的是按键,而不是键弹起,所以只要按键对了,怎样弹起并不重要。
一直按着某个键时会一直触发键盘中断,若是普通的字符键,电脑屏幕可能会出现一直打印某个字符的现象。若是一些控制键,则驱动程序可能会不停地将这个键设为按下状态。当然,驱动程序是否记录上次按键取决于具体实现,大多是不记录的,xv6 也是如此,触发一次键盘中断就处理一个扫描码。
最后总结一番,键盘驱动程序同样的是封装键盘的物理接口使用,比如读取状态,读取扫描码等等。键盘本身使用的是键盘扫描码,每个键都有自己的键盘扫描码,一个是通码表按下,一个表断码表弹起。这个键盘扫描码只是唯一标识一个键,可以将键盘扫描码看作是一个键的物理意义,但这不是我们想要的,我们想要的是这个键代表的逻辑意义。所以物理意义和逻辑之间需要一个转化,这就是映射表存在的意义。
键盘上有各种各样的键,还能组合使用,它们所代表的意义、具有的功能很多也很杂,xv6 只实现了其中一部分,但也足以让我们明白其中的本质。不论要通过按键实现什么功能,还是就只是简单的使用一个键所代表的逻辑意义,都是要先获取键的扫描码,再通过映射表转化成所需要的信息,后续什么功能再在其上做文章。
好了本文就到这里结束,有什么错误还请批评指针,也欢迎大家来同我讨论交流一起学习进步。
参考:
https://www.win.tue.nl/~aeb/linux/kbd/scancodes-11.html
https://wiki.osdev.org/“8042”_PS/2_Controller#Command_Register
你知道键盘是如何工作的吗?(xv6键盘驱动程序)的更多相关文章
- iOS之 利用通知(NSNotificationCenter)获取键盘的高度,以及显示和隐藏键盘时修改界面的注意事项
我们在开发中会遇到这样的情况:调用键盘时需要界面有一个调整,避免键盘遮掩输入框. 但实现时你会发现,在不同的手机上键盘的高度是不同的.这里列举一下: //获取键盘的高度 /* iphone 6: 中文 ...
- hook键盘驱动中的分发函数实现键盘输入数据的拦截
我自己在看<寒江独钓>这本书的时候,书中除了给出了利用过滤的方式来拦截键盘数据之外,也提到了另外一种方法,就是hook键盘分发函数,将它替换成我们自己的,然后再自己的分发函数中获取这个数据 ...
- iOS 键盘处理(改变键盘为完成键),UITextField键盘显示隐藏,弹出,回弹
很多时候用到UITextField时,处理键盘是一个很棘手的问题. 问题一:如何隐藏键盘? 方案1.改变键盘右下角的换行(enter)键为完成键,后实现代理方法键盘自动回弹 keyBoardContr ...
- ios 最新系统bug与解决——微信公众号中弹出键盘再收起时,原虚拟键盘位点击事件无效
最近ios发布新版本系统12.1,随着部分用户的系统更新,一些问题也渐渐暴露出来... 公司用户反映微信公众号出现了点击无效的bug!!测试调查发现,只有iphonex.iphone6,ihpone7 ...
- h5 中软键盘弹出后,点击退出键盘,页面无法恢复
input 绑定blur事件,设置 window.scroll(0,0);
- VB模拟键盘输入的N种方法
VB模拟键盘输入的N种方法http://bbs.csdn.net/topics/90509805hd378发表于: 2006-12-24 14:35:39用VB模拟键盘事件的N种方法 键盘是我们使用计 ...
- 【小贴士】虚拟键盘与fixed带给移动端的痛!
前言 今天来公司的主要目的就是研究虚拟键盘与fixed的问题,期间因为同事问起闭包与事件委托(阻止冒泡)相关问题,便穿插了一篇别的: [小贴士]工作中的”闭包“与事件委托的”阻止冒泡“,有兴趣的朋友可 ...
- [译] 用 Swift 创建自定义的键盘
本文翻译自 How to make a custom keyboard in iOS 8 using Swift 我将讲解一些关于键盘扩展的基本知识,然后使用iOS 8 提供的新应用扩展API来创建一 ...
- Selenium_Selenium WebDriver 中鼠标和键盘事件分析及扩展
在使用 Selenium WebDriver 做自动化测试的时候,会经常模拟鼠标和键盘的一些行为.比如使用鼠标单击.双击.右击.拖拽等动作:或者键盘输入.快捷键使用.组合键使用等模拟键盘的操作.在 W ...
- Selenium WebDriver中一些鼠标和键盘事件的使用
转自:http://www.ithov.com/linux/133271.shtml 在使用 Selenium WebDriver 做自动化测试的时候,会经常模拟鼠标和键盘的一些行为.比如使用鼠标单击 ...
随机推荐
- 从托管到原生,MPP架构数据仓库的云原生实践
简介:本文介绍了云原生数据仓库产品AnalyticDB PostgreSQL从Cloud-Hosted到Cloud-Native的演进探索,探讨为了实现真正的资源池化和灵活售卖的底层设计和思考,涵盖 ...
- MaxCompute 挑战使用SQL进行序列数据处理
简介: MaxCompute 挑战使用SQL进行序列数据处理 --而不是用MR和函数 日常编写数据加工任务,主要的方法就是使用SQL.第一是因为自己对SQL掌握的比较好(十多年数据开发经验,就这几个关 ...
- 干掉讨厌的 CPU 限流,让容器跑得更快
简介: 让人讨厌的 CPU 限流影响容器运行,有时人们不得不牺牲容器部署密度来避免 CPU 限流出现.本文介绍的 CPU Burst 技术可以帮助您既能保证容器运行服务质量,又不降低容器部署密度.文 ...
- [FAQ] VisualStudio, Source file requires different compiler version (current compiler is 0.6.1+cxxxxxx)
当使用的 Solidity 库文件中 pragma 指定的 版本 与本地编译器的使用版本不一致时,会出现这类提示. 解决方式是菜单栏 View -> Extensions -> Exten ...
- dotnet 6 精细控制 HttpClient 网络请求超时
本文告诉大家如何在 dotnet 6 下使用 HttpClient 更加精细的控制网络请求的超时,实现 HttpWebRequest 的 ReadWriteTimeout 功能 本文将介绍如何在 Ht ...
- 2019-8-31-C#-简单读取文件
title author date CreateTime categories C# 简单读取文件 lindexi 2019-08-31 16:55:58 +0800 2018-07-19 16:48 ...
- WebSocket集群分布式改造:实现多人在线聊天室
前言 书接上文,我们开始对我们的小小聊天室进行集群化改造. 上文地址: [WebSocket入门]手把手搭建WebSocket多人在线聊天室(SpringBoot+WebSocket) 本文内容摘要: ...
- SpringBoot3.1.5对应新版本SpringCloud开发(2)-Eureka的负载均衡
Eureka的负载均衡 负载均衡原理 负载均衡流程 老版本流程介绍 当order-servic发起的请求进入Ribbon后会被LoadBalancerInterceptor负载均衡拦截器拦截,拦截器获 ...
- 【python爬虫案例】用python爬豆瓣音乐TOP250排行榜!
目录 一.爬虫对象-豆瓣音乐TOP250 二.python爬虫代码讲解 三.同步视频 四.获取完整源码 一.爬虫对象-豆瓣音乐TOP250 今天我们分享一期python爬虫案例讲解.爬取对象是,豆瓣音 ...
- Linux查看文件指定行数内容与查找文件内容
Linux查看文件指定行数内容 1.tail date.log 输出文件末尾的内容,默认10行 tail -20 date.log 输出最后20行的内容 tail -n -20 date.log 输出 ...