第15章 外中断
以前我们讨论的都是CPU对指令的执行。我们知道,CPU在计算机系统中,除了能够执行指令,进行运算以外,还应该能够对外部设备进行控制,接受它们的输入,向它们进行输出,也就是说,CPU除了有运算能力外,还要有I/O能力。
要及时处理外设的输入,显然需要解决两个问题:1.外设的输入随时可能方式,CPU如何得知?2.CPU从何处的到外设的输入?
15.1 接口芯片和端口
第14章我们讲过,PC系统的接口卡和主板上,装有各种接口芯片。这些外设接口芯片的内部有若干个寄存器,CPU将这些寄存器当作端口来对待。
外设的输入不直接送入内存和CPU,而是先送入端口中;CPU向外设的输出也不是直接输入外设,而是先送入端口中,再由相关的芯片送到外设,CPU还可以向外设输出控制信息,而这些控制命令也是先送到相关芯片的端口中,然后再由相关的芯片根据命令对外设实施控制。
可见,CPU通过端口和外部设备进行联系。
15.2 外中断信息
现在,我们知道了外设的输入被存放在端口中,可是外设的输入随时都有可能到达,CPU如何及时地知道,并进行处理呢?
CPU提供中断机制来满足这种需要。前面讲过,当CPU的内部有需要处理的事情发生的时候,将产生中断信息,引发冲断过程。这种中断信息来自CPU内部。
还有一种中断信息,来自于CPU外部,当CPU外部有需要处理的事情发生的时候,比如,外设的输入到达,相关芯片将向CPU发出相应的中断信息。CPU在执行完当前指令后,可以检测到发送过来的中断信息,引发中断过程,处理外设的输入。
在PC系统中,外中断源一共有以下两类:
1.可屏蔽中断
可屏蔽中断是CPUCPU可以不相应的外中断。CPU是否相应可屏蔽中断,要看标志寄存器的IF位的设置。当CPU检测到可屏蔽中断信息时,如果IF=1,则CPU执行完当前指令后相应中断,引发中断过程。如果IF=0,则不响应可屏蔽中断。
我们回忆一下引发内中断的一个步骤,IF,TF=0。这样,在引发中断进入中断处理后,不会再相应可以屏蔽的中断,这种设置其实是非常合理的。
当然,我们可以通过指令来手动设置IF位。
sti 设置IF=1,可相应可屏蔽中断
cti 设置IF=0,不可相应可屏蔽中断。
2.不可屏蔽中断
不可屏蔽中断是CPU必须相应的外中断。当CPU检测到不可屏蔽中断信息时,则在执行完当前指令后,立即相应,引发中断过程。
对于8086CPU,不可屏蔽中断的中断类型码固定为2,所以,在中断过程中,不需要取中断类型码,过程如下:
1.标志寄存器入栈,IF=0,TF=0
2.CS,IP入栈
3.IP=8,CS=0AH
几乎所有由外设引发的外中断,都是可屏蔽中断,当外设有需要处理的时间,相关芯片向CPU发出可屏蔽中断信息。不可屏蔽中断信息是在系统有必须要处理的紧急情况发生时用来通知CPU的中断信息。在我们现在,讨论的都是可屏蔽中断
15.3 PC机键盘的处理过程
1.键盘输入
键盘上的每一个键都相当于一个开关,键盘中有一个芯片对键盘上的每一个键的开关状态进行扫描。
按下一个键,开关接通,芯片就产生了一个扫描码,扫描码说明了按下的键在键盘上的位置。扫描码被送入主板上的相关接口芯片的寄存器中,该寄存器的端口地址为60h
注意,端口地址可以直接使用纯数字表示,它不是内存,当用in out指令时计算机会自动识别相关数字为端口,这点不需要你费心,这种简单的道理是需要你明确的。
松开按下的键时,也产生一个扫描码,扫描码说明了松开键的位置。也被送入60h。
两种码分别被称为通码和断码。扫描码长度为1个字节, 通码的第7位为0,断码的第7位为1 ,即
断码=通码+80h
比如,g键的通码为22h,断码为a2h。
2.引发9号中断
键盘的输入到达60h端口时,相关的芯片就会向CPU发出中断类型码为9的可屏蔽中断信息。CPU检测到该信息后,如果IF=1,则响应中断,否则就会屏蔽中断。
3.执行 int 9 中断例程
BIOS提供了int 9中断例程,用来进行基本的键盘输入处理,主要的工作如下:
1. 读入60h端口中的扫描码;
2. 如果是字符键的扫描码,将该扫描码和它所对应的字符码(即ASCII码)送入内存中的BIOS键盘缓冲区;如果是控制键(比如ctrl)和切换键(比如Capslock)的扫描码,则将其转变为状态字节(用二进制位记录控制键合切换状态的字节)写入内存中存储状态字节的单元;
3. 对键盘系统进行相关的控制,比如说,向相关芯片发出应答信息。
BIOS键盘缓冲区 它是系统启动后,BIOS用于存放int 9中断历程所接收的键盘输入的内存区。该内存区可以存储15个键盘输入,因为int 9中断例程除了接收扫描码外,还要产生对应的ASCII码,所以,在缓冲区中,一个键盘输入用一个字单元存放,高位扫描码,低位字符码。
0040:17 单元存储键盘状态字节,该字节记录了控制键合切换键的状态,信息如下:
0:右shift状态,置1表示按下右shift键;
1:左shift状态,置1表示按下左shift键;
·······
7:Insert状态,置1表示处于删除态。
15.4 编写int 9 中断例程
从上面的内容,我们看出键盘输入的处理过程:
1. 键盘产生扫描码;
2. 扫描码送入60h端口;
3. 引发9号中断;
4. CPU执行int9中断例程处理键盘输入。
编程 在屏幕中间依次显示‘a’~‘z’,并让人看清。在显示过程中,按下ESC键后,改变显示的颜色。
先看看显示字符,这应该轻车熟路了。
assume cs:code
code segment
start: mov ax,0b800h
mov es,ax
mov ah,'a'
s: mov es:[160*12+40*2],ah
inc ah
cmp ah,'z'
jna s
mov ax,4c00h
int 21h
但上面的程序执行过程中,我们无法看清屏幕上的显示,需要再加入一些延时程序。
mov dx,10h
mov ax,0
s: sub ax,1
sbb dx,1
cmp ax,0
jne s
cmp dx,0
jne a
你应该明确该程序的内涵,一共大小为100000h次循环,这是双判断条件,必须ax,和ax同时为0,即循环完成,才可以进行下去,即执行完结束,否则,会跳过循环,就是这个样子,很简单。
现在,我们完整的写下程序代码:
assmue cs:code
stack segment
db 128 dup(0)
stack ends
code segment
start: mov ax,stack
mov ss,ax
mov sp,128
mov ax,0b800h
mov es,ax
mov ah,'a'
s: mov es:[160*12+40*2],ah
call delay
inc ah
cmp ah,'z'
jna
mov ax,4c00h
int 21h
delay: push ax
push dx
mov dx,1000h
mov ax,0
s1: sub ax,1
sbb dx,0
jne s1
cmp dx,0
jne s1
pop dx
pop ax
ret
code ends
end start
这样显示的‘a’~‘z’就可以让人看清,现在,我们来进行下一步。按下ESC键后,如何改变颜色呢?
我们需要考虑下面的过程:
- 从60h端口读出键盘的输入;
- 调用BIOS的int 9中断例程,处理其他硬件细节;
- 判断是否为Esc扫描码,如果是,改变显示的颜色后返回;如果不是则直接返回,下面我们对这些内容来一一进行分析。
1.从端口60h读出键盘的输入
in al,60h
2.调用BIOS的int 9中断例程
有一点需要注意的是,我们写的中断处理程序要称为新的int 9中断例程,主程序必须要将中断向量表中的
int 9中断例程的入口地址改为我们写的中断处理程序的入口地址。则在新的中断处理程序中调用原来的int 9 中断例程时,中断向量表中的int 9中断例程的入口地址却不是原来的int 9中断例程的程序。所有不能使用int 指令直接调用。
要能在我们写的新中断例程中调用原来的中断例程,就必须在将中断向量表中的中断例程的入口地址改为新地址之前,将原来的入口地址保存起来。这样,在需要调用的时候,我们才能找到原来的中断例程的入口。
对于我们现在的问题,假设将原来int 9 中断例程的偏移地址和段地址保存在ds:[0]和ds:[2]单元中。那么我们在需要调用原来的int 9中断例程的时候,就可以在ds:[0],ds:[2]单元中找到它的入口程序。
那么,有了入口程序后,如何进行调用呢?
当然不能使用指令 int 9来调用。我们可以用别的指令来对int指令进行一些模拟,从而实现对中断例程的调用。
模拟以下几步:
- 标志寄存器入栈;
- IF=0,TF=0;
- CS,IP入栈
- (IP)=(DS)*16+0;(CS)=(DS)*16+2
第3.4步可以合并一个双字操作,这样就三步了,则用程序表示的代码如下:
pushf
pop ax
and ax,11111100b
push ax
popf
call dword ptr ds:[0]
当然这个代码可以精简为:
pushf
call dword ptr ds:[0]
因为我们实在中断操作时使用这种程序的,不管是何种中断,CPU都会按照中断程序的步骤处理一遍。所以,IF,TF的值一定为0,所以,我们不必再调用了。
但注意,在这个程序后还有个iret,所以,必须再多加一步 pushf,这样才能一一对应起来,看一遍源码,你就会明白了。
3.如果是Esc的扫描码,改变显示的颜色后返回
如何改变相关颜色呢?
我们知道改变显示缓冲区,一个字的高字节,这样就能改变响应的颜色了,这个道理很好理解,是你需要明白的。
完整程序代码
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
mov ax,0
mov es,ax
;将原来的int 9 中断例程的入口地址保存在ds:0,ds:2 单元中。
push es:[9*4]
pop ds:[0]
push es:[9*4+2]
pop ds:[2]
;在中断向量表中设置新的int 9中断例程的入口地址
mov word ptr es:[9*4],offset int9
mov es:[9*4+2],cs
mov ax,0b800h
mov es,ax
mov ah,'a'
s: mov es:[160*12+40*2],ah
call delay
inc ah
cmp ah,'z'
jna s
mov ax,0
mov es,ax
;将中断向量表中的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 dx,1000h
mov ax,0
s1: sub ax,1
sbb dx,0
cmp ax,0
jne s1
cmp bx,0
jne s1
pop dx
pop ax
ret
-----------新的int 9中断处理程序-----------------------------
int9: push ax
push bx
push es
in al,60h
pushf
call dowrd ptr ds:[0]
;对比键盘的扫描码,注意,在这程序运行时,除了esc外其他按键均没有反应,这种操作应该明确。
cmp al,1
jne int 9ret
mov ax,0b800h
mov es,ax
inc byte ptr es:[160*12+40*2+1]
int9ret: pop es
pop bc
pop ax
iret
code ends
end start
注意,这本质还是一个中断处理程序,所以,CPU在执行完该程序后仍然会按照中断处理程序再来一遍iret,这个我们是无法操纵的,而我们这里的iret中的popf,对应的是上面的pushf,将标志寄存器入栈,否则出栈的值可能不正确的。
15.5 安装新的int 9中断例程
这个和前面的中断指令类似,都是复制在 0:200的内存区中,然后再修改中断向量表中的地址,就是这个操作,知道这个原理就好,之后再多回来看看,就是这个样子。