本文主体转自http://shanzy.bokee.com/834368.html,因为我需要对龙芯1A上的PS/2控制器移植驱动,参考该文件时,发现其对一些地方说的不够全面且没有对i8042的兼容芯片或控制器做一些说明,所以我又对其进行了一些补充说明,补充说明的一些地方描述可能不够准确,欢迎批评指正。
ps/2 键盘硬件概述
对于驱动来说,和键盘相关的最重要的硬件是两个芯片。一个是 intel 8042 芯片,位于主板上,CPU 通过 IO 端口直接和这个芯片通信,获得按键的扫描码或者发送各种键盘命令。另一个是 intel 8048 芯片或者其兼容芯片,位于键盘中,这个芯片主要作用是从键盘的硬件中得到被按的键所产生的扫描码,与 i8042 通信,控制键盘本身。
当键盘上有键被按下时,i8048 直接获得键盘硬件产生的扫描码。i8048 也负责键盘本身的控制,比如点亮 LED 指示灯,熄灭 LED 指示灯。i8048 通过 ps/2 口和 i8042 通信,把得到的扫描码传送给 i8042。CPU 通过读写端口,可以直接把 i8042 中的数据读入到 CPU 的寄存器中,或者把 CPU 寄存器中的数据写入 i8042 中。ps/2 口一共有 6 个引脚,可以拔下 ps/2 插头看一下,这 6 个引脚分别为,时钟,数据,电源地,电源+5V,还有2个引脚没有被用到。由于只有一个引脚传输数据,所以 ps/2 口上的数据传输是串行的。
下面几幅图是一个键盘的内部,可以看到用来产生扫描码的按键矩阵( Key Martix ),可以看到键盘中的芯片(这里不是i8048,是一个兼容的其他型号的芯片)。
详细的图见:http://jiurl.nease.net/document/KbdDriver/JiurlKbd1.htm
i8042 键盘控制器
键盘驱动直接读写 i8042 芯片,通过 i8042 间接的向键盘中的 i8048 发命令。所以对于驱动来说,直接发生联系的只有 i8042 ,因此我们只介绍 i8042 ,不介绍 i8048。
(PS:因为最早的时候没有鼠标,所以当时i8042只是一个键盘控制器,后来才又添加了对鼠标的控制,后来i8042寄存器各位的定义稍有变化,且后来出现的一些PS/2键盘鼠标控制器一般也都采用了这样的兼容模式,下边会详细说明。)
象 i8042,i8048 这样的芯片,本身就是一个小的处理器,它的内部有自己的处理器,有自己的 Ram,有自己的寄存器,等等。
i8042 有 4 个 8 bits 的寄存器,他们是 Status Register(状态寄存器),Output Buffer(输出缓冲器),Input Buffer(输入缓冲器),Control Register(控制寄存器)。使用两个 IO 端口,60h 和 64h。(端口是X86 CPU上独有的一种说法,其他的嵌入式CPU上没有端口这一说,都是实现成了内存读写,在其他的一些非Intel 公司实现的i8042兼容的PS/2控制器上,这两个端口号被实现成了两个地址,用IO内存读写即可实现与读写0x60,0x64端口相同的功能)
Status Register(状态寄存器)
状态寄存器是一个8位只读寄存器,任何时刻均可被cpu读取。其各位定义如下
Bit7: PARITY-EVEN(P_E): 从键盘获得的数据奇偶校验错误
Bit6: RCV-TMOUT(R_T): 接收超时,置1
(PS:Bit6: 该位兼容芯片改为 0 无超时错误, 1 出现超时错误)
Bit5: TRANS_TMOUT(T_T): 发送超时,置1
(PS: Bit5:该位兼容芯片改为 0 鼠标输出缓冲区空, 1 鼠标缓冲区满, linux内核自带的i8042控制器驱动采用该兼容模式的定义编写)
Bit4: KYBD_INH(K_I): 为1,键盘没有被禁止。为0,键盘被禁止。
(PS: Bit4: 该位兼容芯片定义可能为 1 禁止与设备通信,为 0 使能与设备通信,一般不影响程序的编写)
Bit3: CMD_DATA(C_D): 为1,输入缓冲器中的内容为命令,为0,输入缓冲器中的内容为数据。
Bit2: SYS_FLAG(S_F): 系统标志,加电启动置0,自检通过后置1
Bit1: INPUT_BUF_FULL(I_B_F): 输入缓冲器满置1,i8042 取走后置0
Bit0: OUT_BUF_FULL(O_B_F): 输出缓冲器满置1,CPU读取后置0
(PS: Bit0: 如果使能中断的话,当数据来时会触发中断, CPU读取后把该位置0,同时i8042自动清除中断)
Output Buffer(输出缓冲器)
输出缓冲器是一个8位只读寄存器。驱动从这个寄存器中读取数据。这些数据包括,扫描码,发往 i8042 命令的响应,间接的发往 i8048 命令的响应。
Input Buffer(输入缓冲器)
输入缓冲器是一个8位只写寄存器。缓冲驱动发来的内容。这些内容包括,发往 i8042 的命令,通过 i8042 间接发往 i8048 的命令,以及作为命令参数的数据。
Control Register(控制寄存器)
也被称作 Controller Command Byte (控制器命令字节)。其各位定义如下
(PS:下文所述的Command Byte 即指该控制寄存器)
Bit7: 保留,应该为0
Bit6: 将第二套扫描码翻译为第一套
Bit5: 置1,禁止鼠标
Bit4: 置1,禁止键盘
Bit3: 置1,忽略状态寄存器中的 Bit4
(PS:Bit3 该位,i8042兼容的芯片可能保留,linux内核自带的i8042芯片驱动没有涉及该位)
Bit2: 设置状态寄存器中的 Bit2
Bit1: 置1,enable 鼠标中断
BitO: 置1,enable 键盘中断
2个端口 0x60,0x64
驱动中把 0x60 叫数据端口
驱动中把 0x64 叫命令端口
(PS: 在兼容芯片或控制器中这两个端口可能会用两个内存地址来表示,用IO内存的读写方式即可读写)
1.5 命令
驱动可以直接给 i8042 发命令,可以通过 i8042 间接给 i8048 发命令。命令这部分内容直接来自 < 参考资料 [1] >。
(PS: 通过访问PS2控制器的接口寄存器来实现命令控制,不外乎是三类操作:
发命令给键盘,发命令给鼠标,发命令给控制器自身。
在软件所统当怎样实现这三类功能呢?下面给出实现流程的描述
1. 发送命令给键盘。
通过直接将一个Byte写到输入寄存器,就可以实现向键盘发送读定命令
的功能。
2. 发送命令给鼠标。
发命令给鼠标要特殊一些,首先要向命令寄存器写入一个控制命令
(D4H),接着再向输入寄存器写入一个命令,这个命令就会被发送给鼠标。
3. 发送命令给控制器自身。
直接向0x64端口(如果i8042兼容的控制器没有实现成端口的话向其提供的与0x64端口对应的地址)写入命令即可。如果该命令需要参数的话,再接着向
输入寄存器所写入参数,如果该命令对应有返回值的话,返回值会放在
输出寄存器所。)
1.5.1 发给i8042的命令
驱动对键盘控制器发送命令是通过写端口64h实现的,共有12条命令,分别为
20h
准备读取8042芯片的Command Byte;其行为是将当前8042 Command Byte的内容放置于Output Register中,下一个从60H端口的读操作将会将其读取出来。
60h
准备写入8042芯片的Command Byte;下一个通过60h写入的字节将会被放入Command Byte。
(PS:下边与密码有关的命令一般不用,i8042兼容控制器可能会没有实现)
A4h
测试一下键盘密码是否被设置;测试结果放置在Output Register,然后可以通过60h读取出来。测试结果可以有两种值:FAh=密码被设置;F1h=没有密码。
A5h
设置键盘密码。其结果被按照顺序通过60h端口一个一个被放置在Input Register中。密码的最后是一个空字节(内容为0)。
A6h
让密码生效。在发布这个命令之前,必须首先使用A5h命令设置密码。
AAh
自检。诊断结果放置在Output Register中,可以通过60h读取。55h=OK。
ADh
禁止键盘接口。Command Byte的bit-4被设置。当此命令被发布后,Keyboard将被禁止发送数据到Output Register。
AEh
打开键盘接口。Command Byte的bit-4被清除。当此命令被发布后,Keyboard将被允许发送数据到Output Register。
C0h
准备读取Input Port。Input Port的内容被放置于Output Register中,随后可以通过60h端口读取。
D0h
准备读取Outport端口。结果被放在Output Register中,随后通过60h端口读取出来。
D1h
准备写Output端口。随后通过60h端口写入的字节,会被放置在Output Port中。
D2h
准备写数据到Output Register中。随后通过60h写入到Input Register的字节会被放入到Output Register中,此功能被用来模拟来自于Keyboard发送的数据。如果中断被允许,则会触发一个中断。
(PS:以下是兼容控制器的一些添加的命令:
0xD3: 写鼠标缓冲区命令。把紧随该命令的参数写到输出缓冲区就
像是从鼠标接收到的一样。
0xD4: 写鼠标设备命令。把紧随该命令的参数发给鼠标。)
1.5.2 发给8048(即发送给键盘)的命令
共有10条命令,分别为
EDh
设置LED。Keyboard收到此命令后,一个LED设置会话开始。Keyboard首先回复一个ACK(FAh),然后等待从60h端口写入的LED设置字节,如果等到一个,则再次回复一个ACK,然后根据此字节设置LED。然后接着等待。。。直到等到一个非LED设置字节(高位被设置),此时LED设置会话结束。
EEh
诊断Echo。此命令纯粹为了检测Keyboard是否正常,如果正常,当Keyboard收到此命令后,将会回复一个EEh字节。
F0h
选择Scan code set。Keyboard系统共可能有3个Scan code set。当Keyboard收到此命令后,将回复一个ACK,然后等待一个来自于60h端口的Scan code set代码。系统必须在此命令之后发送给Keyboard一个Scan code set代码。当Keyboard收到此代码后,将再次回复一个ACK,然后将Scan code set设置为收到的Scan code set代码所要求的。
F2h
读取Keyboard ID。由于8042芯片后不仅仅能够接Keyboard。此命令是为了读取8042后所接的设备ID。设备ID为2个字节,Keyboard ID为83ABh。(PS:现在的一些PS/2键盘可能回复的ID不止两个字节,Intel做的带PS/2键盘鼠标控制器的芯片如83627等在这里可能直接就屏蔽了无用的不符合兼容性的数据,对上次软件屏蔽了下层的差异,但如果你用的一些兼容I8042的PS/2控制器没这个功能的话就需要你在linux内核里自己修改了,一般在读取ID这一步增加判断是否是键盘,如果是,就把ID写成上边说的Keyboard ID 就可以了,如果是鼠标,则继续读取ID即可。毕竟现在所用的键盘一般都是标准键盘,否则的话,如果驱动收到的ID不正常的话,驱动可能会报错,退出对当前键盘的控制)当键盘收到此命令后,会首先回复一个ACK,然后,将2字节的Keyboard ID一个一个回复回去。
F3h
设置Typematic Rate/Delay。当Keyboard收到此命令后,将回复一个ACK。然后等待来自于60h的设置字节。一旦收到,将回复一个ACK,然后将Keyboard Rate/Delay设置为相应的值。
F4h
清理键盘的Output Buffer。一旦Keyboard收到此命令,将会将Output buffer清空,然后回复一个ACK。然后继续接受Keyboard的击键。
F5h
设置默认状态(w/Disable)。一旦Keyboard收到此命令,将会将Keyboard完全初始化成默认状态。之前所有对它的设置都将失效——Output buffer被清空,Typematic Rate/Delay被设置成默认值。然后回复一个ACK,接着等待下一个命令。需要注意的是,这个命令被执行后,键盘的击键接受是禁止的。如果想让键盘接受击键输入,必须Enable Keyboard。
F6h
设置默认状态。和F5命令唯一不同的是,当此命令被执行之后,键盘的击键接收是允许的。
FEh
Resend。如果Keyboard收到此命令,则必须将刚才发送到8042 Output Register中的数据重新发送一遍。当系统检测到一个来自于Keyboard的错误之后,可以使用自命令让Keyboard重新发送刚才发送的字节。
FFh
Reset Keyboard。如果Keyboard收到此命令,则首先回复一个ACK,然后启动自身的Reset程序,并进行自身基本正确性检测(BAT-Basic Assurance Test)。等这一切结束之后,将返回给系统一个单字节的结束码(AAh=Success, FCh=Failed),并将键盘的Scan code set设置为2。
(PS: 发送到鼠标的命令列表
0xFF(Reset):复位鼠标命令。
0xF6(Set Defaults):设置鼠标的默认工作模式。
0xF5(Disable Data Reporting):禁止鼠标的数据报告功能并复位它的
位移算数器。
0xF4(Enable Data Reporting): 使能鼠标的数据报告功能并复位它的
位移算数器这条命令只对Stream 模式下的数据报告科效。
0xF3(Set Sample Rate): 设置鼠标采样率。鼠标用0xFA 回统,然后从
主机指入一个或更多字节作为新的采样速率。在收到采样速率后鼠标再次用
统答0xFA 回统并复位它的位移算数器。有效的采样速率是10, 20, 40,60, 80,
100和200采样点/秒。
0xF2(Get Device ID): 指取鼠标的设备ID。
0xF0(Set Remote Mode): 设置鼠标进入Remote模式。
0xE7设置缩放比例
0xE8 设置分辨率
0xEE(Set Wrap Mode): 设置鼠标进入Wrap模式。
0xEC(Reset Wrap Mode):重设鼠标的工作模式为进入Wrap模式之前的模
式。
0xEB(Read Data): 指取鼠标采样到的位移数据包。
0xEA(Set Stream Mode): 设置鼠标的工作模式为Stream 模式。)
1.5.3 读到的数据
00h/FFh
当击键或释放键时检测到错误时,则在Output Bufer后放入此字节,如果Output Buffer已满,则会将Output Buffer的最后一个字节替代为此字节。使用Scan code set 1时使用00h,Scan code 2和Scan Code 3使用FFh。
AAh
BAT完成代码。如果键盘检测成功,则会将此字节发送到8042 Output Register中。
EEh
Echo响应。Keyboard使用EEh响应从60h发来的Echo请求。
F0h
在Scan code set 2和Scan code set 3中,被用作Break Code的前缀。
FAh
ACK。当Keyboard任何时候收到一个来自于60h端口的合法命令或合法数据之后,都回复一个FAh。
FCh
BAT失败代码。如果键盘检测失败,则会将此字节发送到8042 Output Register中。
FEh
Resend。当Keyboard任何时候收到一个来自于60h端口的非法命令或非法数据之后,或者数据的奇偶交验错误,都回复一个FEh,要求系统重新发送相关命令或数据。
83ABh
当键盘收到一个来自于60h的F2h命令之后,会依次回复83h,ABh。83AB是键盘的ID。
(PS:从linux 2.6.27驱动代码看的话,默认标准键盘收到一个来自于60h的F2h命令之后会先回应一个0xfa 即ACK,然后开始回应两个字节的ID,第一个回应的应该是0xab(意为Regular keyboards;若为 0xac: NCD Sun keyboard; 0x2b: Trust keyboard,translated; 0x5d: Trust keyboard ; 0x60: NMB SGI keyboard,translated; 0x47: NMB SGI keyboard),第二个回应的数驱动不太关心)
Scan code
除了上述那些特殊字节以外,剩下的都是Scan code。
1.6 端口操作
首先介绍一下端口的读写操作,驱动中使用函数 READ_PORT_UCHAR 进行读操作,READ_PORT_UCHAR 中使用CPU读端口指令,inb(PS:当不是端口,而是IO内存地址时用 ioread8)。驱动中使用函数 WRITE_PORT_UCHAR 进行写操作,WRITE_PORT_UCHAR 中使用CPU写端口指令,outb(iowrite8)。
1.6.1 读取状态寄存器
读取状态寄存器的方法,对64h端口进行读操作。
1.6.2 读数据
需要读取的数据有,i8042从i8048得到的按键的扫描码,i8042命令的ACK,i8042从i8048得到的i8048命令的ACK,需要命令重发的RESEND,一些需要返回结果的命令得到的结果。
当有数据需要被驱动读走的时候,数据被放入输出缓冲器,同时将状态寄存器的bit0(OUTPUT_BUFFER_FULL)置1,引发键盘中断(键盘中断的IRQ为1)。由于键盘中断,引起由键盘驱动提供的键盘中断服务例程被执行。在键盘中断服务例程中,驱动会从i8042读走数据。一旦数据读取完成,状态寄存器的bit0会被清0。
读数据的方法,首先,读取状态寄存器,判断bit0,状态寄存器bit0为1,说明输出缓冲器中有数据。保证状态寄存器bit0为1,然后对60h端口进行读操作,读取数据。
这里我们要谈一点很有用的题外话,前面提到的 IRQ,是 Interrupt Request line,中断请求线,是一个硬件线,它和中断向量是不同的。中断向量是用来在中断描述符表(IDT)中查找中断服务例程的那个序号。键盘的 IRQ 是1,键盘中断服务例程对应的中断向量可不是1。这点要弄清楚。
1.6.3 向i8042发命令
当命令被发往i8042的时候,命令被放入输入缓冲器,同时引起状态寄存器的 Bit1 置1,表示输入缓冲器满,同时引起状态寄存器的 Bit2 置1,表示写入输入缓冲器的是一个命令。
向i8042发命令的方法,首先,读取状态寄存器,判断bit1,状态寄存器bit1为0,说明输入缓冲器为空,可以写入。保证状态寄存器bit1为0,然后对64h端口进行写操作,写入命令。
1.6.4 间接向i8048发命令
向i8042发这些命令,i8042会转发i8048,命令被放入输入缓冲器,同时引起状态寄存器的Bit1 置1,表示输入缓冲器满,同时引起状态寄存器的 Bit2 置1,表示写入输入缓冲器的是一个命令。这里我们要注意,向i8048发命令,是通过写60h端口,而后面发命令的参数,也是写60h端口。i8042如何判断输入缓冲器中的内容是命令还是参数呢,我们在后面发命令的参数中一起讨论。
向i8048发命令的方法,首先,读取状态寄存器,判断bit1,状态寄存器bit1为0,说明输入缓冲器为空,可以写入。保证状态寄存器bit1为0,然后对60h端口进行写操作,写入命令。
1.6.5 发命令的参数
某些命令需要参数,我们在发送命令之后,发送它的参数,参数被放入输入缓冲器,同时引起状态寄存器的Bit1 置1,表示输入缓冲器满。这里我们要注意,向i8048发命令,是通过写60h端口,发命令的参数,也是写60h端口。i8042如何判断输入缓冲器中的内容是命令还是参数呢。i8042是这样判断的,如果当前状态寄存器的Bit3 为1,表示之前已经写入了一个命令,那么现在通过写60h端口放入输入缓冲器中的内容,就被当做之前命令的参数,并且引起状态寄存器的 Bit3 置0。如果当前状态寄存器的 Bit3 为0,表示之前没有写入命令,那么现在通过写60h端口放入输入缓冲器中的内容,就被当做一个间接发往i8048的命令,并且引起状态寄存器的 Bit3 置1。
向i8048发参数的方法,首先,读取状态寄存器,判断bit1,状态寄存器bit1为0,说明输入缓冲器为空,可以写入。保证状态寄存器bit1为0,然后对60h端口进行写操作,写入参数。
"1 ps/2 键盘的硬件" 主要参考下面的资料,关于 ps/2 键盘硬件更多的内容也请参考下面的资料
[1] http://pagoda-ooos.51.net/os_book/driver/driver-keyboard_2.htm (中文)
[2] http://panda.cs.ndsu.nodak.edu/~achapwes/PICmicro/PS2.pdf (中文)
[3] http://panda.cs.ndsu.nodak.edu/~achapwes/PICmicro/ (英文)
“2 ps/2 键盘的驱动“
i8042 最底层的操作代码在linux内核中的位置为: driver/input/serio/i8042.c,该键盘驱动的层次架构如下:
在 i8042.c中申请了一个platform设备和驱动,直接操作i8042,并对其进行自检和初始化操作,成功后,申请了一个serio总线设备,被赋予的type 为SERIO_8042,该与设备匹配的驱动为 drivers/input/keyboard/atkbd.c ,然后在该驱动中,又把i8042申请成了一个input设备,该设备对应的驱动为:dirvers/char/keyboard.c,该驱动为虚拟键盘驱动。PS/2鼠标驱动的层次结构可以参照键盘的驱动结构。
该键盘控制器驱动的详细参考资料如下:
[1] http://blog.csdn.net/easyblue99/article/details/7206090 (中文)