我们知道cpu在计算机系统中除了能够执行指令,进行运算以外,还应该能对外部设备进行控制,接收它们的输入,向它们进行输出,也就是说cpu除了具有运算能力外,还要有I/O(Input/Output,输入输出)能力。通过以前的学习我们知道,PC系统的接口卡和主板上装有各种接口芯片。这些外设接口芯片的内部有若干寄存器,cpu将这些寄存器当作端口来访问。
外设的输入不直接送入内存和cpu,而是送入相关的接口芯片的端口中,cpu向外设的输出也不是直接送入外设,而是先送入端口中,再由相关的芯片送到外设。cpu还可以向外设输出控制命令,而这些控制命令也是先送到相关芯片的端口中,然后再由相关的芯片根据命令对外设实施控制。可见cpu通过端口和外部设备进行联系。
至此,我们知道了外设的输入被存放在端口中,可是外设的输入随时都有可能到达,cpu如何及时的知道并进行处理呢?这就用到了我们今天的主角-中断机制。cpu提供中断机制来满足这种需要,当cpu的内部有需要处理的事情发生时,将产生中断信息,引发中断过程,这种中断信息来自cpu内部,还有一种中断信息,来自于cpu外部,当cpu外部有需要处理的事情发生的时候,比如说,外设的输入到达,相关芯片将向cpu发出相应的中断信息。cpu在执行完当前指令后,可以检测到发送过来的中断信息,引发中断过程,处理外设的输入。
PC机处理键盘输入的过程如下:
键盘上的每一个按键相当于一个开关,键盘中有一个芯片对键盘上的每一个键的开关状态进行扫描,按下一个键时,开关接通,
该芯片就产生一个扫描码,扫描码说明了按下的键在键盘上的位置。扫描码被送入主板上的相关芯片的寄存器中,该寄存器的
端口地址为60h。松开按下的键时,也产生一个扫描码, 扫描码说明了松开的键在键盘上的位置,松开按键时产生的扫描码也
被送入60端口中。一般我们将按下一个键时产生的扫描码称为通码,松开一个按键产生的扫描码称为断码。扫描码长度为一个
字节,通码的第七位为0,断码的第七位为1,即断码 = 通码 + 80h。
键盘的输入到达60端口时,相关的芯片就会向cpu发出中断类型码为9的可屏蔽中断信息。cpu检测到改中断信息后,如果IF = 1
,则响应中断,引发中断过程,转去执行int 9中断例程。
BIOS提供了int 9中断例程,用来进行基本的键盘输入处理,主要的动作如下:
(1)读出60端口中的扫描码;
(2)如果是字符键的扫描码,将该扫描码和它所对应的字符码即ASCII码送入内存中的BIOS键盘缓冲区;如果是控制键(如ctrl)
和切换键(如CapsLock)的扫描码,则将其转变为状态字节(用二进制位记录控制键和切换键状态的字节)写入内存中存储 状态字节的单元;
(3)对键盘系统进行相关的控制,比如说,向相关芯片发出应答信息。
BIOS键盘缓冲区是系统启动后,BIOS用于存放int 9号中断例程所接受的键盘输入的内存区。该内存区可以存储15个键盘输入,因为int 9中断例程除了接受扫描码外,还要产生和扫描码对应的字符码,所以在BIOS键盘缓冲区中,一个键盘输入用一个字单元存放,高位字节存放扫描码,低位字节存放字符码。
0040:17单元存储键盘状态字节,该字节记录了控制键和切换键的状态。键盘状态字节各位记录的信息如下:
由以上内容我们知道,键盘输入的处理过程:
1、键盘上的芯片产生扫描码,2、扫描码被送入60端口,3、引导9号中断;4、cpu执行int9中断例程处理键盘输入
在上面的过程中,前3步都是由硬件系统完成的。我们能改变的只有int 9中断处理程序。我们可以重新编写int9中断例程。按照
自己的意图来处理键盘的输入,因为一个完整的键盘输入的处理要涉及一些硬件细节,针对这种情况,我们可以在自己编写的中
断例程中调用BIOS的int9中断例程帮我们处理。
编程:在屏幕中间依次显示"a"~“z”,并可以让人看清。在显示的过程中,按下Esc键后,改变显示的颜色。
依次显示"a"~"z"的代码如下:
assume cs:code
code segment
start:mov ax, 0b800h
mov es, ax
mov ah, 'a'
s1: mov es:[160*12+40*2], ah
inc ah
cmp ah, 'z'
jna s1
mov ax, 4c00h
int 21h
code ends
end start
在上面代码的执行过程中,我们无法看清屏幕上字符的变化,因为cpu执行太快导致字符切换过快,因此我们想办法在每显示一个
字符后延时一段时间,让人看清后再显示下一个字符,这里我们实现一个延时的子程序供主程序调用,代码如下:
delay: push ax
push dx
mov ax, 0
mov dx, 1000h ;//cpu循环执行10000 000h次
s2:sub ax, 1
sbb dx, 0
cmp ax, 0
jne s2
cmp dx, 0
jne s2
pop dx
pop ax
ret
那如何改变颜色呢?
键盘输入到达端口后,就会引发9号中断,cpu转去执行int9中断例程。我们可以编写int9中断例程,功能如下:
(1)从60端口中读出键盘的输入;
(2)调用BIOS的int9中断例程,处理其他硬件细节;
(3)判断按键是否为Esc的扫描码,如果是,则改变显示的颜色后返回;如果不是则直接返回。
下面我们分步实现以上步骤
in al, 60h
有一点我们要注意,在我们编写好int9中断例程后,主程序必须将中断向量表中的int9中断例程的入口地址改为我们新写的中断
处理程序的入口地址,但是在新的中断处理程序中调用原来的int9中断例程时,中断向量表中的int9号中断例程的入口地址已不
是原来的int9中断例程的地址,所以我们不能使用int指令直接调用。
要能在我们新写的中断例程中调用原来的中断例程,就必须在将中断向量表中的中断例程的入口地址改为新地址之前,将原来的
入口地址保存起来,这样在需要调用的时候才能找到。
保存了入口地址后,该如何调用呢?
当然不能使用指令int9来调用,我们可以使用别的指令来对int指令进行模拟,从而实现对中断例程的调用。
我们知道,int指令在执行的时候,cpu进行下面的工作。
(1)取中断类型码n;
(2)标志寄存器入栈
(3)IF=0,TF=0;
(4)CS、IP入栈
(5)(IP)=(n4), (CS=n4+2)
取中断类型码是为了定位中断例程的入口地址,在我们自己写的中断例程中已知道地址,因此不需要做第一步,假设要调用的中断
例程的入口地址被保存在ds:0和ds:2单元,我们将用以下步骤来模拟int过程
(1)标志寄存器入栈
(2)IF=0,TF=0;
(3)CS、IP入栈
(4)(IP)=(ds16+0), (CS=ds16+2)
注意到第三步和第四步的功能和call dword ptr ds:[0]的功能一样,call dword ptr ds:[0]的功能也是
push cs, push ip, (IP)=(ds16+0), (CS=ds16+2),所以int的模拟过程变为:
(1)标志寄存器入栈
(2)IF=0,TF=0
(3)call dword ptr ds:[0]
对于上面步骤一可用pushf实现。对于步骤二可用下面的指令来实现
pushf
pop bx
and bh, 11111100B
push bx
popf ;IF=0, TF=0
;CS, IP入栈,IP = n*4, CS = n*4+2
call dword ptr ds:[0] ;对int指令进行模拟,调用原来的int 9中断例程
如何改变现实的颜色呢?
显示的位置是在屏幕的中间,即第12行40列,显存中的偏移地址为:16012+402。所以字符的ASCII码要送入段地址b800h,偏移
地址16012+402处。而段地址b800h偏移地址16012+402+1处时字符的属性,我们只要改变此处的数据就可以改变显示字符的颜色
该程序的最后一个问题是,要在程序返回前,将中断向量表中的int9号中断例程的入口地址恢复为原来的地址。否则程序返回后,
别的程序将无法使用键盘。
经以上分析,完整源码如下:
assume cs:code
stack segment
db 128 dup (0)
stack ends
data segment
dw 0, 0
data ends
code segment
start:mov ax, stack
mov ss, ax
mov sp, 128
mov ax, data
mov ds, ax
;保存原有int 9号中断处理程序的入口地址
mov ax, 0
mov es, ax
push es:[9*4]
pop ds:[0]
push es:[9*4+2]
pop ds:[2]
mov word ptr es:[9*4], offset int9
mov es:[9*4+2], cs
mov ax, 0b800h
mov es, ax
mov ah, 'a'
s1: mov es:[160*12+40*2], ah
call delay
inc ah
cmp ah, 'z'
jna s1
;为了不影响后续的键盘输入处理,需要将int 9号中断处理程序的入口地址恢复为原来的地址
push ds:[0]
pop es:[9*4]
push ds:[2]
pop es:[9*4+2]
mov ax, 4c00h
int 21h
delay:push ax
push dx
mov ax, 0
mov dx, 1000h ;//cpu循环执行10000 000h次
s2:sub ax, 1
sbb dx, 0
cmp ax, 0
jne s2
cmp dx, 0
jne s2
pop dx
pop ax
ret
;-----------新的int 9中断例程-------------
int9: push ax
push bx
push es
in al, 60h ;从端口60h读出键盘的输入
pushf ;标志寄存器入栈
;设置IF = 0, TF = 0
pushf
pop bx
and bh, 11111100B
push bx
popf
;CS, IP入栈,IP = n*4, CS = n*4+2
call dword ptr ds:[0] ;对int指令进行模拟,调用原来的int 9中断例程
cmp al, 1 ;esc键的扫描码为1
jne int9ret
mov ax, 0b800h
mov es, ax
inc byte ptr es:[160*12+40*2 + 1] ;属性值加1改变颜色
int9ret:pop es
pop bx
pop ax
iret
code ends
end start
程序运行图示如下:
注意,本文中所有关于键盘的程序,因为要直接访问真实的硬件,则必须在DOS实模式下运行。在Windows2000的DOS方式下运行,
会出现一些和硬件工作原理不符合的现象。