端口的作用
在PC系统的接口卡和主板上,装有各种接口芯片。这些外设接口芯片内部有若干寄存器,CPU将这些寄存器当作端口来访问。
外设的输入不直接送入内存和CPU,而是送入相关的接口芯片的端口中;CPU向外设的输出也不是直接送入外设,而是先送入端口中,再由相关的芯片送到外设。CPU还可以向外设输出控制命令,这些命令也是先送到相关芯片的端口中,再由相关芯片根据命令对外设实施控制。
外中断
之前所提到的中断类型都是属于内中断,也就是在CPU的内部有需要处理的事情时,产生中断信息,引发中断过程。
在此,我们主要介绍外中断,也就是说当CPU的外部有需要处理的事情时,比如:外设的输入到达,相关芯片将向CPU发出相应的中断信息。CPU在执行完当前指令后,可以检测到发送过来的中断信息,引发中断过程来处理外设的输入。
外中断源一共有两类:
(1)可屏蔽中断
这类中断是CPU可以不响应的外中断。CPU是否响应可屏蔽中断,在于标志寄存器IF位的设置。当CPU检测到可屏蔽中断信息时,如果IF=1,则CPU在执行完当前指令后响应中断,引发中断过程;如果IF=0,则不响应可屏蔽中断。
故:中断过程中的置TF=0是为了在中断处理程序中防止CPU陷入单步执行(内中断)循环;而IF=0是为了在中断处理程序中禁止其他的可屏蔽中断(默认中断不能嵌套)。
8086CPU提供的设置IF的指令如下:sti,设置IF=1;cli,设置IF=0。
(2)不可屏蔽中断
这类中断是CPU必须响应的外中断。当CPU检测到不可屏蔽中断信息时,则在执行完当前指令后,立即响应引发中断过程。 对于8086CPU,不可屏蔽中断的中断类型码固定为 2,所以在中断过程中不需要取中断类型码。
几乎所有由外设引发的外中断都属于可屏蔽中断。可屏蔽中断也是我们的重点所在。
键盘的中断处理过程
(1)键盘输入
键盘上的每一个键相当于一个开关,键盘中有一个芯片对键盘上的每一个键的开关状态进行扫描。
按下一个键时,开关接通,该芯片产生一个扫描码,称其为通码。通码说明了按下的键在键盘上的位置。通码被送入主板上的相关接口芯片的寄存器中,该寄存器的端口地址为60h。
松开按下的键时,也产生一个扫描码,称其为断码。断码说明了松开的键的位置,也被送入到60h端口中。
扫描码的长度为一个字节,通码的第 7 位为0,断码的第 7 位为1。 断码 = 通码 + 80h。比如 g 键的通码为 22h,断码为 a2h。
(2)引发 9 号中断
当键盘输入到达 60h 端口时,相关芯片会对CPU发出中断类型码为 9 的可屏蔽中断。如果此时 IF=1,则响应中断,引发中断过程,执行相应的中断处理程序。
(3)执行 int 9 中断例程
读出60h端口中的扫描码。
如果为字符键的扫描码,将它和它对应的字符码(ASCII码)送入内存中的BIOS键盘缓冲区;如果是控制键(Ctrl、Caps)的扫描码,将它转变为状态字节写入内存中存储状态字节的单元。
对键盘系统进行相关的控制,如,对相关芯片发出应答信息。
补充:BIOS键盘缓冲区是系统启动后,BIOS用于存放 int 9 中断例程所接收的键盘输入的内存区。该内存区可以存储15个键盘输入,int 9 中断例程除了接收扫描码外,还需要产生和扫描码对应的字符码,故:在缓冲区中,一个键盘输入用一个字单元存放,高位字节存放扫描码,低位字节存放字符码。
内存单元 0040:17 存储键盘状态字节,用来记录控制键和切换键的状态。比如说:按下 Ctrl 键,对应的位置1,松开置0。按下 Insert 键,对应的位置1表示处于删除态,否则置0。
编写 int 9 中断例程
编程:在屏幕中间依次显示‘a’-’z’,并可以让人看清。在显示的过程中,按下Esc键后,改变显示的颜色。(Esc键的通码是1)
思路:在键盘输入到达60h端口后,就会引发9号中断,CPU转而去执行 int 9 中断例程。我们编写 int 9 中断例程的功能如下:
(1)从60h读取键盘输入,这是为了对特定输入稍后进行处理。
(2)调用BIOS的 int 9 中断例程,处理其他细节。
调用原 int 9 的中断过程需要有4步:
a,取中断类型码9;
b,pushf;
c,置 IF、TF为0;
d,设置新的CS:IP。
其中a步骤可以忽略,并且c步骤也是可以忽略的,因为在CPU进入到我们编写的新 int 9 中断例程后,IF、TF已经置为0了,而执行指令 in al,60h 后不会改变IF、TF的值,所以c被省略。
补充:c步骤代码如下
pushf
pop bx
and bh,11111100b IF、TF在标志寄存器的第9位和第8位。
push bx
popf
(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
push es:[9*4] ;这几句是将原来int 9 例程保存在ds:0处,
pop ds:[0] ;因为需要对常规的键盘输入进行处理。
push es:[9*4+2]
pop ds:[2]
cli ;这几句是将我们编写的新的 int 9 例程作为默认的
mov word ptr es:[9*4],offset int9 ;键盘输入的中断处理程序。应该注意到:
mov es:[9*4+2],cs ;在此并没有将新的例程安装到0:200处,
sti ;因为新的 int 9例程只在本程序执行期间
;才会被使用,程序结束后不再被调用,所以无需在内存中保存。
;并且在执行过程中需要屏蔽CPU对键盘中断的响应。
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 ;在程序结束后,临时的 int 9 例程不再被使用,需要将
mov es,ax ;中断向量表原 int 9 的入口地址恢复。
cli ;但是在对中断例程的入口地址更改时,应该禁止对键盘中断的响应,
push ds:[0] ;否则CPU会跳到一个错误的中断处理程序入口地址。
pop es:[9*4]
push ds:[2]
pop es:[9*4+2]
sti
mov ax,4c00h
int 21h
delay: push ax ;cpu执行指令的循环次数为10000000h次,以此作为延迟显示的时间间隔
push dx
mov dx,1000h
mov ax,0
sl: sub ax,1
sbb dx,0
cmp ax,0
jne s1
cmp dx,0
jne s1
pop dx
pop ax
ret
int9: push ax
push bx
push es
in al,60h ;读取键盘输入
pushf ;模仿调用int 9中断例程来处理其他硬件细节
call dword ptr ds:[0]
cmp al,1 ;判断是否为Esc的扫描码,并进行我们自己特定的键盘中断处理
jne int9ret
mov ax,0b800h
mov es,ax
inc byte ptr es:[160*12+40*2+1]
int9ret:pop es
pop bx
pop ax
iret
code ends
end start
实验15
安装一个新的 int 9 中断例程,功能:在DOS下,按下“A”键后,除非不再松开,如果松开,就显示满屏幕的“A”,其他的键照常处理。(断码=通码+80h。“A”的通码为1Eh)
assume cs:code
stack segment
db 128 dup(0)
stack ends
code segment
start: mov ax,stack
mov ss,ax
mov sp,128
mov ax,cs ;将ds:si处的新int 9例程安装到内存0:204h处。
mov ds,ax
mov si,offset int9
mov ax,0
mov es,ax
mov di,204h
mov cx,offset int9end-offset int9
cld
rep movsb
push es:[9*4] ;将原来的int 9入口地址存储在内存0:200h-0:203h处。
pop es:[200h]
push es:[9*4+2]
pop es:[202h]
cli
mov word ptr es:[9*4],204h ;将新的int 9例程的入口地址放在中断向量表中。
mov word ptr es:[9*4+2],0
sti
mov ax,4c00h
int 21h
int9: push ax
push es
push di
push cx
in al,60h
pushf
call dword ptr cs:[200h]
cmp al,1eh+80h ;判断是否为“A”的断码
jne int9ret
mov ax,0b800h
mov es,ax
mov di,0
mov cx,2000
s: mov byte ptr es:[di],'A'
add di,2
loop s
int9ret: pop cx
pop di
pop es
pop ax
iret
int9end: nop
code ends
end start