原文地址
http://chuanwang66.iteye.com/blog/1069889
很多PC都配置了Intel 8042 微控制器芯片。这个控制器芯片要么嵌入到一个集成电路(IC )里,要么直接安装到主板上。它一般连在南桥上。这个控制器通过一个连接到你的键盘的芯片和键盘上的控制器芯片交流。
当你在键盘上按下一个键时, 就按下了键盘下面的一个橡胶块,橡胶块下面可以导电,当按下去的时候就联通了键盘上的电路,电流可以在上面流过,每个键对应一对电线,当信号改变时(取决于键是否按下),会产生一个编码(通过一系列电线),这个编码设置了键盘里的控制器,也就通过连接到计算机的硬件端口设置了计算机里面的那个控制器,这是通过一系列高低电脉冲实现的,根据时钟周期,每个脉冲信号被转换为位模式中的一位。
在主板上,这些通过南桥上的电信号到达8042 控制器,控制器将上面的编码解码得到扫描码,并保存在内部寄存器里,就是我们说的缓冲区,这个内部的寄存器是EEPROM芯片,可以通过电擦洗,重写入我们需要的如何数据。
键盘里的控制器 <----->
Intel 8042微控制芯片(IC中或主板上) <----->寄存器 EEPROM芯片(可电擦洗,通常分配到0x60端口地址)
引导的时候,BIOS POST为每个设备(通过I/O控制器) 分配端口地址。这是通过查询设备完成的。一般的,BIOS POST会将这地个内部寄存器分配到端口址 0x60. 这表示, 当我们访问端口0x60 时, 我们实际上是从这个内部寄存器读数据。
你知道关于端口映射和IN/ OUT 指令的其他内容,我们从这个寄存器读数据试试:
in al, 0x60 ; 从8042 微控制器输入寄存器读数据
你可能会猜到, 8042微控制器是键盘控制器. 通过与这个芯片的一系列寄存器交流,我们可以读到键盘的输入,映射扫描码,以及其他的一系列操作:比如开启 A20.
你可能会好奇为什么需要和键盘控制器交流,来开启A20。在后面你会看到。
A20门 - 理论
最后我们讨论如何开启A20 ,我知道,上面的内容和A20没有太多直接的关系,但是我还是想要在开启A20前,包含直接为硬件编程的基本内容,因为开启A20需要,其他控制器编程也需要。
开启A20需要为键盘控制器编程,因此,我们只是讨论为键盘控制器编程而不是为键盘编程。
一点历史
在IBM设计 IBM PC AT 时,他们使用了新的Intel 80286处理器, 而这个处理器与前面的x86处理器在实模式下不兼容。什么问题呢?旧的x86 处理器不使用A20到A31 的地址线(32位地址线的高12位),也没有这么多条地址线。所有的超过1 MB的地址被回卷了。而在80286 的地址空间里,需要32条地址线,也就是说全部的32 条地址线都可以被访问,我们就有了回卷问题。
为了解决这个问题Intel在处理器和系统总线之间加了一个逻辑门以控制第20 条地址线,这个逻辑门就叫做Gate A20。对于旧的程序可以关闭A20,对于新的则打开A20。
在引导的时候,BIOS会在计算和测试内存时打开A20,而在将控制权交给操作系统之前再关上(为了向旧的计算机兼容),因此默认的对于我们的操作系统A20是关上的。
有很多种方法来开启A20. 当A20 打开之后,我们就可以访问全部的32条地址总线, 这样通过使用32位的地址, 最大到0xFFFFFFFF - 4 GB内存.
Gate A20是一个逻辑或门,原来连在8042 控制器(键盘控制器) 的P21 线上。这个逻辑门对应数据输出端口的第1位 ,我们可以发送命令来接收数据或是修改它。通过检查这一位,并向线上输出些数据,我们就能控制这个逻辑门,以开启/ 关闭A20。我们可以直接或是间接完成这个工作。下一节里会详细介绍。
有多种方法来重新打开A20,这要看主板的配置。我会涉及多种开启A20的常见方法。
下面是详细内容o(∩_∩)o…
A20门 – 开启
有很多方法开启A20 。如果你只是写一个只有你用的系统,你要做的只是找出一种你能用的就成,如果要求可移植性的话,就要多用几种方法了。
方法 1: 系统控制端口
这个很快,但是可移植不好。
Sone 系统包括 MCA 和EISA使用系统控制端口I/O 0x92控制A20 。制造商对于端口0x92有非常好的实现。下面是常用的几位:
Bit 0 – 设成1来快速重设 (用于返回实模式)
Bit 1 - 0: 关闭 A20; 1:开启 A20
Bit 2 –制造商定义
Bit 3 –电源口令(CMOS bytes 0x38-0x3f或0x36-0x3f). 0: 可访问 , 1:不可访问
Bits 4-5 -制造商定义
Bits 6-7 - 00: HDD 活动LED 灭;其他 "亮"
这是用这种方法启动A20 的例子:
- mov al, 2 ; 设置第2位(开启a20)
- out 0x92, al
用这个端口也可以做其他是一些事情:
- mov al, 1 ; 设置第1位(快速重设)
- out 0x92, al
注意:
这个方法在 Bochs中可以使用。虽然这是一个简单的方法。我发现这种方法与一些硬件会发生冲突。可能会导致系统停机,如果你想用方法,注意一些。
其它端口
一些系统允许使用其他的I/O端口来启动A20.
最常见的是I/O端口 0xEE。如果I/O 端口 0xEE ("快速 A20门") 在这个系统有效,从这个端口读数据会开启A20,写数据会关闭A20。相似的端口 0xEF ("快速CPU重设") 会重置系统。
其他系统会使用不同的端口(i.e. AT&T 6300+需要写0x90 到I/O 端口 0x3f20 来开启A20, 还有写0关闭A20). 也有些系统使用I/O 端口0x65的第2位或是I/O端口0x1f8的第0位开启或关闭 A20 (0: 关, 1:开).
如你所见,A20的操作多数与硬件相关,所以你要先确定你的主板制造商。
方法 2: Bios
很多Bios中断可以开启或是关闭A20.
Bochs 支持
一些版本的Bochs 可以使用这些方法而其他的版本并不支持. 这里略
方法 3: 键盘控制器
这可能是最常见的开启A20 的方法,简单,但要求键盘控制器编程的知识。这也最有可移植性的方法。因为有键盘控制器编程的要求,我先介绍一下。
这也是我要先介绍硬件编程的原因。
8043 键盘控制器 - 端口映射
为了与控制器交流, 我们得知道这个控制器的I/O 端口映射。
端口映射 | ||
端口 | 读/ 写 | 描述 |
0x60 | 读 | 读输入缓存 |
0x60 | 写 | 写输出缓存 |
0x64 | 读 | 读状态寄存器 |
0x64 | 写 | 给控制器发命令 |
要给这个控制器发送命令,将命令写到端口0x64。 如果这个命令要接收一个参数,参数写到端口0x60。命令的所有返回值都可以从端口0x60读回。
要注意键盘控制器很慢。因为我们的代码执行要比键盘控制器快得多, 我们需要一种方法来等待控制器完成它的工作,再继续我们的代码。
这经常使用检测控制器状态的方法实现。很困惑?别担心,后面会解释清楚的。
8043 键盘控制器状态寄存器
好,我们怎么得到控制器的状态呢?看看上面的表,我们知道从0x64读数据就行了。从这个寄存器读出的值有 8位,格式如下:
Bit 0: 输出缓冲区状态
0: 输出缓冲区空,不能读
1: 输出缓冲区满,请读
Bit 1: 输入缓冲区状态
0: 输入缓冲区空,可写
1: 输入缓冲区慢,不能写
Bit 2: 系统标志
0: 系统重设后置为0
1: 完成键盘控制器自检后设为1
Bit 3: 命令/数据
0: 最后写到输入缓冲区的是数据 (通过端口0x60)
1: 最后写到输入缓冲区的是命令 (通过端口0x64)
Bit 4: 键盘锁
0: 锁定的
1: 未锁定的
Bit 5: 辅助输出缓冲区满
PS/2 系统:
0: 决定是否可以从端口0x60读,0=键盘数据
1:鼠标数据,只能从端口 0x60读
AT 系统s:
0: OK 标志
1:超时. 键盘可能不存在
Bit 6: 超时
0: OK 标志
1: 超时
PS/2:
一般超时
AT:
超时,可能有错 (Bit 6 和7 同时置1)
Bit 7: 奇偶错误
0: OK 标志, 没错
1: 奇偶错误
如你所见,有很多东西,其中重要的都在上面加粗显示了,我们要检测控制器的输出、输入缓冲区是满的还是空的。
这是一个例子。我们向控制器发命令时,命令写到控制器的输入缓冲区。因此当输入缓冲区满的时候就不能执行,就像这样:
wait_input:
in al,0x64 ;读状态寄存器
test al,2 ; 测试第2位 (输入缓冲区状态)
jnz wait_input ; 如果非0(不空)跳转,继续等
在输入输出的时候都要这么做。
现在我们知道怎么等控制器了, 我们必须要告诉控制器我们要做什么。我们看看“命令字”。
8042 键盘控制器命令寄存器
再看看I/O 端口映射表,我们可以通过写命令到 I/O端口0x64来发送命令。键盘控制器有很多命令,因为这不是键盘编程的教材, 我没有吧它们全列出来,但下面是最重要的:
键盘控制器的命令字(键盘命令/描述)
0x20 读键盘控制器命令字
0x60 写键盘控制器命令字
0xAA 自检
0xAB 接口检测
0xAD 使键盘无效
0xAE 使键盘有效
0xC0 读输入端口
0xD0 读输出端口
0xD1 写输出端口
0xDD开启A20 地址线
0xDF 关闭 A20地址线
0xE0 读测试输入
0xFE 系统重设
Mouse 命令
0xA7 使鼠标端口失效
0xA8 使鼠标端口有效
0xA9 测试鼠标端口
0xD4 写鼠标
方法 3.1: 通过键盘控制器开启A20
注意到命令0xDD和 0xDF用于开启/关闭 A20:
; 方法 3.1: 通过键盘控制器开启A20
mov al, 0xdd ; 命令 0xdd: 开启a20
out 0x64, al ; 发送命令到控制器
不是所有的键盘控制器都支持这个方法,如果有效的话,它确实很简单
方法 3.2: 通过输出端口开启A20
也有使用键盘控制器的输出端口开启A20的,这样我们需要使用命令D0和D1来读、写输出端口 (参考键盘控制器的命令字) 。
这种方法比起其他的要复杂的多,但也不太差。基本的想法是,停止键盘,并且读键盘控制器的输出端口。8042 有3个端口:一个输入,其他的是输出,第3个用于检测,这些"端口"实际上就是控制器上的硬件线路。
简单起见(因为这不是键盘编程教材), 我们只看输出端口。
好的,从输出端口读数据,简单的发送一个读输出端口的命令(0xD0)到控制器就行了: (参考键盘控制器的命令字)
; 读输出端口到al
mov al,0xD0
out 0x64,al
现在我们得到了输出端口的数据,但他不是太有用,实际上输出端口的格式如下,一个特定的位模式。
Bit 0: 系统重设
0: 重设计算机
1: 一般操作
Bit 1: A20
0: 关
1: 开
Bit 2-3: 未定义
Bit 4: 输入缓冲区满
Bit 5: 输出缓冲区空
Bit 6: 键盘时钟
0: High-Z
1: Pull Clock Low
Bit 6: 键盘数据
0: High-Z
1: Pull Data Low
其中的大多数位我们不希望改变,将第0为置0 会重启计算机;第1位置1会开启A20 ,你可以通过将这个值与某一位为1的掩码做或操作设置特定的位,设好后,写回去就行了( 命令字0xD1).
写数据到输出端口,从输入、输出缓冲区取得控制器的数据。
这表示,如果我们要读输出端口, 数据要从控制器的输入缓冲区寄存器读。看看I/O 端口映射表,我们知道要从端口0x60读数据。
看个例子,在读写操作时,我们要等待控制器完成操作。wait_input 用于等待输入缓冲区变空,而wait_output等待输出缓冲区变空。
- ; 发送读输出端口命令
- mov al,0xD0
- out 0x64,al
- call wait_output
- ; 读输入缓存区,并保存在栈中,数据从输出端口读入
- in al,0x60
- push eax
- call wait_input
- ; 发送写输出端口命令
- mov al,0xD1
- out 0x64,al
- call wait_input
- ; 从栈中弹出输出端口的数据,并设置第1位 (A20)
- pop eax
- or al,2 // 2 = 10(二进制)
- out 0x60,al // 写数据到输出端口. 这通过输出缓冲区完成
- wait_input:
- in al,0x64 ;读状态寄存器
- test al,2 ; 测试第2位 (输入缓冲区状态)
- jnz wait_input ; 如果非0(不空)跳转,继续等
以上是这个方法的全部:) 这个方法比其他的要复杂但有更好的可移植性。