在 8086系统中,有软件中断、硬件中断等多个中断源,当多个中断源同时向CPU请求中断时,CPU应如何处理呢?办法是我们给各种中断源事先安排一个中断 优先级次序,当多个中断源同时申请中断时,CPU先比较他们的优先级(Priority),然后从优先级高到优先级低的次序来依次处理各个中断源的中断请 求。
在8.3节中我们已经提到8259A的中断命令寄存器(图8.4)的6位和7位能控制各中断请求端的优先次序。在正常的优先级方式下,优先级次序是:
IR0,IR1,IR2,IR3,IR4,IR5,IR6,IR7
在发出一个EOI命令时,7位(R)和6位(SL)有四种组合,其含义如下:
R SL
0 0 正常优先级方式
0 1 清除由 L2 - L0指定的中断请求
1 0 各中断优先级依次左循环一个位置
1 1 各中断优先级依次循环到由L2 - L0指定的中断请求到达最低优先级位置上。
硬件中断的优先级次序一般在正常优先级方式下(R=0,SL=0),但在必要的情况下,设置中断命令寄存器能改变IR0-IR7的优先级次序,例 如,IR0-IR7原为正常的优先级次序,现在要使IR4成为最低级的中断请求,则给端口20H送命令码:11100100,即 R=1,SL=1,EOI=1,L2L1L0=100,这样,各中断优先级就依次循环到IR4为最低优先级的位置上:
IR5,IR6,IR7,IR0,IR1,IR2,IR3,IR4,IR5
如果再送一个命令码:10100000,则优先级次序再向左循环一个位置,成为:
IR6,IR7,IR0,IR1,IR2,IR3,IR4,IR5
在下面的描述中,如无特别说明,中断优先级是指正常方式下的中断优先级。
2.中断嵌套
中断嵌套是指正在运行的中断处理程序,又被其它中断源中断的情况。
一个正在执行的中断处理程序,在开中断(IF=1)的情况下,能被优先级高于它的中断源中断,但如果要被同级或低级的中断源中断,则必须发出EOI命令,清除正在执行的中断请求,才能响应同级或低级的中断。
80X86没有规定中断嵌套的深度(中断程序又被中断的层次),但在实际使用时,多重的中断嵌套要受到堆栈容量的限制,所以在编写中断程序时,一定要考虑有足够的堆栈单元来保存多次中断的断点及各寄存器的内容。
首先,CPU响应优先级高的IR2,转去处理IR2的中断处理程序。进入IR2处理程序后,IF被置为1。当IR1的中断请求到达后,因IR1的优先 级高于IR2,CPU就立即中断IR2的程序,转去执行IR1的处理程序。在IR1处理程序中,由指令发出了EOI命令,结束了IR1的中断请求。返回 IR2处理程序后,同样由于发出EOI命令清除了IR2的中断请求,所以在较低级的中断请求IR4到达后,即转向处理IR4的中断请求。在IR4处理程序 的执行过程中,IR3的中断请求到达,当判断IF已被置为1,则又中断了IR4的程序,转去执行IR3的程序。在IR3程序中,也发出了开中断指令 (STI)和中断结束命令(EOI),最后IRET指令使其返回到IR4程序,IR4在返回IR2之前也发出了EOI命令,结束了IR4的中断请求。
IR2中断请求在前面已被清除,所以IR4执行完后,IR2继续执行直到返回主程序。
中断嵌套举例:
该动画的例子是在正常优先级方式下,优先级中断和中断嵌套发生时的处理过程。该例子假定在主程序的执行过程中,IR2和IR4的中断请求同时发生, 而后IR1的中断请求又到达,最后IR3的中断请求也到达。
多级8259A 中断系统:
硬件中断的扩充使用多级的8259A系统。在多级中断系统中,从属的8259A连接到主8259A的哪一端上,它就具有那一端的中断优先级别,如图8.6所示。
在图8.6的连接方式下,各中断请求端的优先级排列顺序如下:
对于主8259A和从属8259A的中断屏蔽寄存器,禁止相应端中断请求的原理和单级8259A是一样的。
8.3.5 中断处理程序
通过前面几节对中断的介绍,我们对如何编写中断程序已经有一些了解了,现在把它们归纳在一起就更清楚了。
左面是主程序为响应中断所作的准备工作以及硬件(包括CPU和外设接口)自动完成的动作:
中断处理程序的编写方法和标准子程序很类似,下面是编写中断处理子程序的步骤,请注意与子程序编写的一些不同之处。
至此,CS和IP寄存器取得了中断处理程序的段地址和偏移地址,CPU就把控制转给中断处理程序。
这里要注意的是设备发到CPU的中断请求信号在时间上是随机的,只要未被屏蔽的设备本身的状态是准备好或空闲的,它就会向CPU请求中断,如果此时 CPU正在执行一条指令,那么就要等这条指令执行完后,才响应中断。对加封锁的指令(如LOCK MOV AX, BX)应看作为一条指令处理;对加重复前缀的串指令(如REP MOVSB),也要作为一个整体来处理,但不是把串操作全部重复执行完,而是执行一次重复和串指令即可响应中断。对MOV指令和POP指令,如果处理对象 是段寄存器时,那么本条指令执行完后,接着再执行一条指令才响应中断。对开中断指令STI和中断返回指令IRET,也是要在STI或IRET指令执行完 后,再执行一条指令才响应中断。以上是几种特殊情况,对一般指令,只要一条指令的执行周期结束即可响应中断。
(1) 保存寄存器内容
(2) 如允许中断嵌套,则开中断(STI)
(3) 处理中断
(4) 关中断 (CLI)
(5) 送中断结束命令(EOI)给中断命令寄存器
(6) 恢复寄存器内容
(7) 返回被中断的程序(IRET)
进入中断处理程序时,IF和TF已经被清除,这样在执行中断处理程序的过程中,将不再响应其它外设的中断请求,如果这个中断处理程序允许其它设备中 断,则需用STI指令把IF位置1。中断结束命令(EOI)在程序的什么地方发出,这要看程序员是否要求在其处理过程中允许同级或低级中断。一般设备希望 一次中断的处理过程最好是完整的,所以只在中断处理结束之前发出EOI命令。
处理中断部分是中断处理程序的主体部分,它要完成的任务是各种各样的,这与实际应用有关。如果它的任务是处理某种错误的,一般要求显示输出一系列出错 信息。如果它是对一个I/O设备进行服务的,就按其端口地址接收或发送一个单位(字节或字)的数据。要注意的是CPU产生一次中断,I/O设备只完成一个 字节(或字)的输入/输出,所以中断处理程序所用的指针变量或数据变量一般应设置存储单元来保存。
8.3.6 中断程序举例
例8.5 编写一个中断处理程序,要求在主程序运行过程中,每隔10秒响铃一次,同时在屏幕上显示出信息“The bell is ring!”
在系统定时器(中断类型为8)的中断处理程序中,有一条中断指令 INT 1CH,时钟中断每发生一次(约每秒中断18.2次)都要嵌套调用一次中断类型1CH的处理程序。在ROM BIOS例程中,1CH的处理程序只有一条IRET指令,实际上它并没有做任何工作,只是为用户提供了一个中断类型号。如果用户有某种定时周期性的工作需 要完成,就可以利用系统定时器的中断间隔,用自己设计的处理程序来代替原有的1CH中断程序。
1CH作为用户使用的中断类型,可能已被其它功能的程序所引用,所以在编写新的中断程序时,应作下述工作:
(1) 在主程序的初始化部分,先保存当前1CH的中断向量,再置新的中断向量。
(2) 在主程序的结束部分恢复保存的1CH中断向量。
Purpose: ring and display a message every 10 seconds.
;* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
;eg8-5.asm
;Purpose: ring and display a message every 10 seconds.
;* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
.model small
;-------------------------------------------------------------------------
.stack
;-------------------------------------------------------------------------
.data
count dw 1
msg db 'The bell is ringing!', 0dh, 0ah, '$'
;-------------------------------------------------------------------------
.code
; Main program
main proc far
start:
mov ax, @data ;allot data segment
mov ds, ax
; save old interrupt vector
mov al, 1ch ;al<=vector number
mov ah, 35h ;to get interrupt vector
int 21h ;call DOS
push es ;save registers for restore
push bx
push ds
;set new interrupt vector
mov dx, offset ring ;dx<=offset of procedure ring
mov ax, seg ring ;ax<=segment of procedure ring
mov ds, ax ;ds<=ax
mov al, 1ch ;al<=vector#
mov ah, 25h ;to set interrupt vector
int 21h ;call DOS
pop ds ;restore ds
in al, 21h ;set interrupt mask bits
and al, 11111110b
out 21h, al
sti
mov di, 20000
delay: mov si, 30000
delay1:dec si
jnz delay1
dec di
jnz delay
;restore old interrupt vector
pop dx ;restore registers
pop ds
mov al, 1ch ;al<=vector#
mov ah, 25h ;to restore interrupt
int 21h ;call DOS
mov ax, 4c00h ;exit
int 21h
main endp
ring proc near
push ds ;save the working registers
push ax
push cx
push dx
mov ax, @data ;allot data segment
mov ds, ax
sti
;siren if it is time for ring
dec count ;count for ring interval
jnz exit ;exit if not for ring time
mov dx, offset msg ;dx<=offset of msg
mov ah, 09h ;to display msg
int 21h ;call DOS
mov dx, 100 ;dx<=turn on/off times(100)
in al, 61h ;get port 61h
and al, 0fch ;mask bits 0,1
sound:
xor al, 02 ;toggle bit 1
out 61h, al ;output to port 61h
mov cx, 1400h ;value of wait
wait1:
loop wait1
dec dx ;control turn on/off 10 times
jne sound
mov count, 182 ;control ring interval delay(10s)
exit:
cli
mov al,20h ;set EOI
mov 20h,al
pop dx ;restore the reg.
pop cx
pop ax
pop ds
iret ;interrupt return
ring endp
;-------------------------------------------------------------------------
end start ;end assemble
例8.6 在配置了键盘输入(中断类型09)和打印机输出(中断类型0FH)两种外部设备的8086中断系统中,要求从键盘上接收字符,同时对32字节的输入缓冲区进行测试,如果缓冲区已满,则键盘挂起(禁止键盘中断输入),由打印机输出一个提示信息。
键盘和打印机分别由中断屏蔽寄存器(21H)的1位和7位控制。键盘的输入寄存器端口地址为60H,控制寄存器的端口地址为61H。打印机输出寄存器的端口地址为378H,打印机控制寄存器的端口地址为37AH。
在这种特定情况下,只要求打印机在键盘输入缓冲区满了后,打印出提示信息,因此它可以在屏蔽键盘中断的同时,设置打印机的中断屏蔽位。另外,在中断处 理程序中用到的一些指针及计数值要保存在指定的存储单元中,每次进入中断,取出指针及计数值,退出中断时,再把修改后的指针及计数值保存起来。
这个中断程序包括以下几部分:
MAIN 初始化部分,保存09和0FH的原中断向量,设置新的中断向量。主程序用有限循环来模拟。主程序结束时,恢复原中断向量。
KBDINT 键盘中断处理程序。接收按键的扫描码并保存在缓冲区中,如果输入的字符数超过32,则屏蔽键盘中断,允许打印机中断,并调用INIT-PRT子程序初始化打印机。
INIT-PRT初始化打印机,启动适配器,发出选通信号。
PRTINT 打印机中断处理程序。按照指针取出打印机字符送到输出寄存器,发出选通信号。
DISPLAY-HEX 用十六进制显示AL中的代码。
Purpose: accept keyboard input and print messages on the printer
;* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
;eg8-6.asm
;Purpose: accept keyboard input and print messages on the printer
;* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
.model tiny
;-------------------------------------------------------------------------
.stack
;-------------------------------------------------------------------------
.data
old_ip09 dw ?
old_cs09 dw ?
old_ip0f dw ?
old_cs0f dw ?
count dw ?
buffer db 20h dup(' ')
buf_p dw ?
start_msg db 0ah, 0dh, 'RUN', 0ah, 0dh, '$'
end_msg db 0ah, 0dh, 'END', 0ah, 0dh, '$'
full_msg db 0ah, 0dh, 'Buffer full!$'
;-------------------------------------------------------------------------
.code
; Main program
main proc far
start:
mov ax, @data ;ds<=data segment
mov ds, ax
; initialize
lea ax, buffer ;buf_p<=buffer address
mov buf_p, ax
mov count, 0 ;count<=0
; save old interrupt 09h
mov al, 09h ;al<=vector#
mov ah, 35h ;to get interrupt vector
int 21h ;call DOS
mov old_cs09, es ;save registers for restore
mov old_ip09, bx
push ds ;save ds
;set new interrupt 09h
lea dx, kbdint ;dx<=offset of procedure kbdint
mov ax, seg kbdint ;ax<=segment of procedure kbdint
mov ds, ax ;ds<=ax
mov al, 09h ;al<=intrrupt number
mov ah, 25h ;to set interrupt vector
int 21h ;call DOS
pop ds ;restore ds
;set keyboard interrupt mask bits
in al, 21h
and al, 0fdh
out 21h, al
;save old interrupt 0fh
mov al, 0fh ;al<=vector#
mov ah, 35h ;to get interrupt vector
int 21h ;call DOS
mov old_cs0f, es ;save registers for restore
mov old_ip0f, bx
push ds ;save ds
;set new interrupt 0fh
lea dx, prtint ;dx<=offset of procedure prtint
mov ax, seg prtint ;ax<=segment of procedure prtint
mov ds, ax ;ds<=ax
mov al, 0fh ;al<=vector#
mov ah, 25h ;to set interrupt vector
int 21h ;call DOS
pop ds ;restore ds
mov ah, 09h ;print start message
lea dx, start_msg
int 21h
sti
mov di,20000 ;main process
mainp: mov si,30000
mainp1:dec si
jnz mainp1
dec di
jnz mainp
mov ah, 09h ;print end msg of main process
lea dx, end_msg
int 21h
cli
;restore old interrupt 09h
push ds ;save ds
mov dx, old_ip09 ;ds:dx<=old handler address
mov ax, old_cs09
mov ds, ax
mov al, 09h ;al<=vector#
mov ah, 25h ;to restore interrupt vector
int 21h ;call DOS
pop ds ;restore ds
; restore old interrupt 0fh
push ds ;save ds
mov dx, old_ip0f ;ds:dx<=old address
mov ax, old_cs0f
mov ds, ax
mov al, 0fh ;al<=vector#
mov ah, 25h ;to restore interrupt
int 21h ;call DOS
pop ds ;restore ds
; enable keyboard interrupt
in al, 21h
and al, 0fdh
; enable keyboard interrupt
in al, 21h
and al, 0fdh
out 21h, al
sti
mov ax, 4c00h
int 21h
main endp
;-----------------------------------------------------
; Interrupt Handler kbd
; Purpose: fill buffer until full when substituted for interrupt 09h
kbdint proc near
push ax ; save registers
push bx
cld ;direction: forward
in al, 60h ;read a character
push ax ;save it
in al, 61h ;get the control port
mov ah, al ;save the value in ah
or al, 80h ;reset bits for kbd
out 61h, al ;send out
xchg ah, al ;restore control value
out 61h, al ;kdb has been reset
pop ax ;restore scan code
test al, 80h ;press or release?
jnz return1 ;ignore when release
mov bx, buf_p ;bx<=buffer pointer
mov [bx], al ;store in buffer
call display_hex ;display in hex
inc bx ;move pointer
inc count ;count characters
mov buf_p, bx ;save the pointer
check:
cmp count, 20h ;judge whether full
jb return1
in al, 21h
or al, 02 ;mask kdb bits
and al, 7fh ;enable prt bits
out 21h, al
call init_prt ;initiate printer
return1:
cli
mov al, 20h ;end of interrupt
out 20h, al
;restore registers
pop bx
pop ax
iret ;interrupt return
kbdint endp
;------------------------------------------------------------------------
; Interrupt Handler prtint
; Purpose: print characters when substituted for interrupt 0fh
prtint proc near
push ax ;save registers
push bx
push dx
mov bx, buf_p ;bx<=buffer pointer
mov al, [bx] ;get from buffer
mov dx, 378h ;printer data port
out dx, al ;output a character
push ax ;save ax
mov dx, 37ah ;printer control port
mov al, 1dh ;al<=control code
out dx, al ;send out
jmp $+2 ;slight delay
mov al, 1ch ;al<=control code
out dx, al ;send out
pop ax ;restore ax
inc bx ;move pointer
mov buf_p, bx ;save the pointer
cmp al, 0ah ;end of message?
jnz return2
in al, 21h ;disable printer
or al, 80h ; interrupt
out 21h, al
return2:
mov al, 20h ;end of interrupt
out 20h, al
pop dx ;restore registers
pop bx
pop ax
iret ;interrupt return
prtint endp
;------------------------------------------------------------------------
; Procedure init_prt
init_prt proc near
push ax ;save registers
push bx
push dx
cli
lea bx, full_msg ;bx<=offset of full_msg
mov buf_p, bx ;save full_msg address
mov dx, 378h ;printer data port
mov al, 0dh ;CR
out dx, al ;output a character
mov dx, 37ah ;printer control port
mov al, 1dh ;al<=control code
out dx, al ;send out
jmp $+2 ;slight delay
mov al, 1ch ;al<=control code
out dx, al ;send out
pop dx ;restore registers
pop bx
pop ax
ret
init_prt endp
;------------------------------------------------------------------------
display_hex proc near ;display char with hex
push ax
push cx
push dx
mov ch, 2 ;number of digits
mov cl, 4
nextb:
rol al, cl ;highest of bits to lowest
push ax
mov dl, al
and dl, 0fh ;mask off left digit
or dl, 30h ;convert to ASCII
cmp dl, 3ah
jl dispit
add dl,7h ;digit is A to F
dispit:
mov ah, 2 ;display character
int 21h
pop ax
dec ch ;done 2 digits?
jnz nextb ;not yet
mov ah, 2
mov dl,',' ;display ','
int 21h
pop dx
pop cx
pop ax
ret ;return from display_hex
display_hex endp
;------------------------------------------------------------------------
end start
例8.7 除数为0时的软件中断(类型0)处理程序
此程序分成两个主要部分:初始化部分和中断处理部分。
初始化部分(Init)设置新的0型中断向量,显示一条信息,然后完成终止和驻留后退出程序。这种特殊的退出是用INT 21H的功能31H,它将保留程序所占的内存,从而使这些内存单元不被以后的应用程序破坏。
中断处理程序(Zdiv)在发生一个被零除中断时接收控制。中断处理程序先保存有关寄存器的值,然后打印出信息询问用户是退出程序(Quit)还是继 续(Continue)。若键入"C"要求继续执行程序,则处理程序恢复所有寄存器并执行IRET返回主程序(显示一个标记符#),当然此时除法的操作结 果应是无效的。若键入"Q"要求退出,则从处理程序直接返回DOS(无标记符显示)。这里返回DOS,是用INT 21H的功能4CH,该功能是唯一不依赖于任何段寄存器内容的终止功能,例如,CS寄存器不必指向PSP所在的段。该功能的另一个优点是能在AL中返回一 个表明程序是否正常终止的出口代码。系统出口代码的含义为:00 - 正常终止;01 - 用Ctrl-C终止;02 - 严重设备错误引起终止;03 - 用功能调用31H终止。左侧是处理除数为0错误的中断处理程序清单。
Purpose: zero_division handler
;* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
;eg8-7.asm
;Purpose: zero_division handler
;* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
.model small
;-------------------------------------------------------------------------
.stack
;-------------------------------------------------------------------------
.code
; Main program
main proc near
;reset interrupt vector 0
lea dx, zdiv ;set interrupt vector
mov ax, seg zdiv
mov ds, ax
mov al, 0 ;interrupt number
mov ah, 25h ;to reset interrupt vector
int 21h ;call DOS
;print introduction message
mov ax, @code ;ds<=code segment
mov ds, ax
mov dx, ok_msg ;print introduction
mov ah, 9
int 21h
;simulate zero_division condition
mov ax, 1
mov dl, 0
div dl
; display '# 'after return from interrupt handler zdiv
mov ah, 2 ;to display a character
mov dl, '#' ;dl<='#'
int 21h ;call DOS
;exit and reside in memory
mov ah, 31h ;to exit and reside
mov al, 0 ;return code: 0
mov dx, ((prog_len+15)/16)+16 ;dx<=memory paragraph
; for residence
int 21h ;call DOS
main endp
;-------------------------------------------------------------------------
; Interrupt Handler zdiv
zdiv proc far
push ax ;save registers
push bx
push cx
push dx
push si
push di
push bp
push ds
push es
sti
prt_warn:
mov ax, @code
mov ds, ax
mov dx, offset warn_msg ;print warning
mov ah, 9
int 21h
input:
mov ah, 1 ;to accept keyboard input
int 21h ;call DOS
cmp al, 'c' ;judge whether 'c'
je continue
cmp al, 'q' ;judege whether 'q'
je exit
mov dx, offset beep ;beep when illegal input
mov ah, 09
int 21h
jmp prt_warn
exit:
mov ax, 4cffh
int 21h
continue:
mov dx, offset crlf ;print CR & LF
mov ah, 09
int 21h
cli
pop es ;restore registers
pop ds
pop bp
pop di
pop si
pop dx
pop cx
pop bx
pop ax
iret ;interrupt return
zdiv endp
;------------------------------------------------------------------------
; Data area
ok_msg db 0dh, 0ah, 'Zero-division Handler installed!'
db 0dh, 0ah, '$'
warn_msg db 'Zero-division detected, ', 07h
db 'Continue or Quit(c/q)?$'
beep db 07h, '$'
crlf db 0dh, 0ah, '$'
prog_len equ $-main
;------------------------------------------------------------------------
end main
【本章小结】
1. 程序直接控制I/O的方式:
这是一种使用I/O指令直接在端口级上进行数据传送的编程方式,这种方式有时需要查询外设的状态,如果外设处于准备好或空闲状态,则CPU通过接口中 的数据寄存器进行输入或输出,如果外设没有准备好或是忙状态,CPU就查询等待,不再作有效的工作。
2. 中断程序的设计方法:
对于要求以中断方式工作的I/O设备,它们的中断类型已由硬件连线确定(如图8.3)。主程序为中断所做的准备工作如下:
(1) 保存原中断向量(INT 21H的35H功能),设置新的中断向量(INT 21H的25H功能);
(2) 设置设备的中断屏蔽位(仅对可屏蔽中断);
(3) 设置CPU的中断允许位(开中断);
(4) 在主程序结束前,恢复原中断向量。
主程序完成了上述准备工作后,I/O设备即以完全随机的方式产生中断。当CPU响应了中断请求,中断系统将自动完成以下工作:
(1) CPU接收外设的中断类型号;
(2) 当前的FLAGS、CS、IP的内容入栈保存;
(3) 清除IF、TF;
(4) 根据中断类型号取出中断向量送CS和IP;
(5) 转中断处理子程序。
中断处理子程序的编写方法:
(1)保存工作寄存器内容;
(2)如允许中断嵌套,则开中断(STI);
(3)处理中断任务;
(4)关中断 (CLI);
(5)送中断结束命令(EOI)给中断命令寄存器(仅对硬件中断);
(6)恢复工作寄存器内容;
(7)返回被中断的程序(IRET)。