汇编语言实验 12:9 号中断

277 阅读8分钟

1. 预备知识

  1. 除前面介绍的除法错误、单步执行、int 指令等来自 CPU 内部的中断,外部中断共分为两类:可屏蔽中断,CPU 根据标志位 IF 的值来决定是否响应该中断,如果 IF=1 则响应该中断,否则不响应;不可屏蔽中断,CPU 必须响应该中断,中断类型码固定为 2。

  2. 键盘上的每个按键相当于一个开关,键盘中的芯片对按键的开关状态进行扫描。按键按下和松开将产生对应的扫描码,扫描码描述了键在键盘中的位置。

  3. 一般将键按下时产生的码称为通码,松开时产生的码称为断码,产生的通码和断码均送入 60h 端口中。扫描码的长度为 1 个字节,且满足断码=通码+80h。键盘上部分键的通码如下图:

  1. 以键盘输入为例,PC 机处理外部中断的流程包括:(1)键盘按键按下和松开时产生的扫描码被送入 60h 端口;(2)相关芯片向 CPU 发出中断类型码为 9 的可屏蔽中断信息;(3)执行 9 号中断例程。

2. 实验任务 1:延时显示

在屏幕上依次显示 'a'~'z',并可以让人看清。

2.1 实验分析

在屏幕上依次显示 'a'~'z':

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
code ends
end start 

程序运行结果如下:

由结果可知,我们只能在屏幕看到字母 z 的显示。这是由于在一个字母显示完成后,CPU 继而切换至下一个字母的显示程序,切换时间太短,导致无法看清中间字母的显示。

我们让当前字母显示完成后,延时一段时间,看清后再显示下一个字母。解决办法是,在显示完当前字母后让 CPU 执行一段时间的空循环。为保证足够长的延时,用两个 16 位寄存器 AX 和 DX 来存放 32 位的循环次数。实现延时功能的子程序为:

delay:		;共执行循环10h*1000h次
    push ax 
    push dx 	;保护现场
    mov dx,10h
    mov ax,0
help:
    sub ax,1	;(AX)=0FFFFh
    sbb dx,0	;无符号运算(AX)=(AX)-1使得CF=1,本句相当于(DX)=(DX)-0-CF(1)=0Fh
    cmp ax,0	
    jne help 	;如果(AX)不等于零则转移至s1,循环10000h次
    cmp dx,0
    jne help 	;如果(DX)不等于零则转移至s1,0Fh不等于零而(AX)等于零使得sub ax,1又等于0FFFFh
    pop dx 
    pop ax 	;恢复现场
    ret 

在上述程序中,sub 减法得到的结果为 0FFFFh,第一个 jne 固定执行 10000h 次循环。sbb 为带借位的减法指令,由于 sub 减法产生了借位,sbb 实际执行 (DX)=(DX)-1。执行第二个 jne 时 AX 的值为 0,所以第二个 jne 跳转至 help 后,AX 又变为 0FFFFh。以此,使用两个 16 位寄存器 AX 和 DX 共执行 10000h*(DX) 次循环。整体代码为:

assume cs:code
code segment
start:
    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,4c00h
    int 21h
delay:		;共执行循环10h*10000h次
    push ax 
    push dx 	;保护现场
    mov dx,10h
    mov ax,0
help:
    sub ax,1	;(AX)=0FFFFh
    sbb dx,0	;无符号运算(AX)=(AX)-1使得CF=1,本句相当于(DX)=(DX)-0-CF(1)=0Fh
    cmp ax,0	
    jne help 	;如果(AX)不等于零则转移至s1,循环10000h次
    cmp dx,0
    jne help 	;0Fh不等于零而(AX)等于零使得sub ax,1又等于0FFFFh
    pop dx 
    pop ax 	;恢复现场
    ret 
code ends
end start 

2.2 实验结果

3. 实验任务 2:键盘控制字符属性

在上一任务的基础上,在字母显示过程中,按下 Esc 键改变字母的颜色。

3.1 实验分析

根据预备知识的第四条,重写 int 9 中断例程,满足以下功能:

  • 从 60h 端口读取键盘输入

  • 调用 int 9 中断例程

  • 基于扫描码判断当前是否按下 Esc 键,如果按下则改变字符颜色并返回;否则直接返回

(1)基于 in 指令从 60h 端口读取键盘输入:

in al,60h

(2)调用 int 9 中断例程。我们编写完新的 int 9 中断处理程序后,需设置中断向量表的对应项。由于在新中断处理程序中需处理键盘输入,我们仍需调用旧中断处理程序。此时,无法使用 int 指令调用,因为对应中断向量已修改为新中断处理程序的入口地址。

所以,在写入新中断处理程序的地址前,首先保存旧中断向量的入口地址,将其保存在内存的 ds:0 和 ds:2 单元。然后,在调用旧中断向量时模拟 int 指令的执行状态:

  • 取中断类型码

  • 标志寄存器入栈

  • IF=0、TF=0

  • CS 和 IP 入栈

  • (IP)=n*4,(CS)=(n*4+2)

上述第一步不用模拟,后四步可使用以下代码代替:

pushf                  ;标志寄存器入栈
pushf
pop ax
and ah,11111100b       ;TF和IF为第9位和第8位
push ax
popf                   ;TF=0、IF=0
call dword ptr ds:[0]  ;CS和IP入栈,且目的段地址由内存单元的高地址给出、偏移地址由低地址给出

(3)如果按下 Esc 键,则改变字母的显示颜色。字母的显示单元为 b800:160*12+40*2,则其属性单元是 b800:160*12+40*2+1。

mov ax,0b800h
mov es,ax
inc byte ptr es:[160*12+40*2+1]

(4)在程序返回前,将中断向量表的 9 号中断向量恢复为原中断处理程序的入口地址。否则程序返回后,别的程序无法使用 int 指令调用。

mov ax,0
mov es,ax
push ds:[0]
pop es:[9*4]
push ds:[2]
pop es:[9*4+2]

(5)如果在设置中断例程的段地址和偏移地址之间发生了键盘中断,则 CPU 将转去一个错误的地址执行指令。而键盘中断为可屏蔽中断,可通过 IF 位控制 CPU 是否响应该中断。所以,在设置地址前将 IF 设置为 0 表示不响应中断(cli 指令)、设置完成后设置 IF=1(sti 指令)以保证正确设置地址。

cli
mov ax,0
mov es,ax
push ds:[0]
pop es:[9*4]
push ds:[2]
pop es:[9*4+2]
sti

综上,整体代码为:

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]
    pop ds:[0]
    push es:[9*4+2]
    pop ds:[2]		;将原来int 9中断例程的入口地址保存在ds:0和ds:2单元
    cli
    mov word ptr es:[9*4],offset int9
    ;在中断向量表中设置新的int 9中断例程的偏移地址
    mov es:[9*4+2],cs
    ;在中断向量表中设置新的int 9中断例程的段地址
    sti
    mov ax,0b800h
    mov es,ax
    mov ah,'a'
s:
    mov es:[160*12+40*2],ah
    call delay		;调用子程序,让CPU休眠
    inc ah 
    cmp ah,'z'
    jna s
    cli
    mov ax,0
    mov es,ax
    push ds:[0]
    pop es:[9*4]
    push ds:[2]
    pop es:[9*4+2]	;恢复原int 9中断例程的地址
    sti
    mov ax,4c00h
    int 21h
delay:		;共执行循环10h*10000h次
    push ax 
    push dx 	;保护现场
    mov dx,10h
    mov ax,0
help:
    sub ax,1	;(AX)=0FFFFh
    sbb dx,0	;无符号运算(AX)=(AX)-1使得CF=1,本句相当于(DX)=(DX)-0-CF(1)=0Fh
    cmp ax,0	
    jne help 	;如果(AX)不等于零则转移至s1,循环10000h次
    cmp dx,0
    jne help 	;如果(DX)不等于零则转移至s1,0Fh不等于零而(AX)等于零使得sub ax,1又等于0FFFFh
    pop dx 
    pop ax 	;恢复现场
    ret 
int9:
    push ax 
    push bx 
    push es	;保护现场
    in al,60h	
    pushf	;标志寄存器入栈
    ;由于在上述使用60h端口响应中断时,TF和IF的值已经设置为0,下面代码可以去除
    ;pushf
    ;pop bx
    ;and bh,11111100b   ;TF和IF为第9位和第8位
    ;push bx 
    ;popf 	;TF=0、IF=0
    call dword ptr ds:[0]
    ;CS和IP入栈,且目的CS由内存单元高地址给出、IP由低地址给出
    cmp al,1	;esc的通码为1
    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 

3.2 实验结果

4. 总结

  1. 本文以键盘输入为例介绍了外部中断的处理流程,键盘通过将按键扫描码送入 60h 端口进而触发 9 号中断完成相应的功能

  2. 在编写 9 号中断处理程序时,应保存原中断处理程序的地址以在新中断处理程序中调用,且在程序返回前恢复中断向量表的对应项

  3. 参考:汇编语言/王爽著.——北京:清华大学出版社,2003