CPU在计算机系统中,除了能够执行指令, 进行运算以外,还应该能够对外部设备进行控制,接收它们的输入,向它们进行输出。也就是说CPU除了有运算能力外,还要有I/O能力。
-
要及时处理外设的输入,显然需要解决两个问题:
-
(1)
外设的输入随时可能发生,CPU如何得知?
-
(2)CPU从何处得到外设的输入?
15.1 接口芯片和端口
-
CPU通过端口和外部设备进行联系:
-
(1)外设的输入不直接送入内存和CPU,而是
送入相关的接口芯片的端口中
; -
(2)CPU向外设的输出也不是直接送入外设,而是
先送入端口中,再由相关的芯片送到外设
; -
(3)
CPU还可以向外设输出控制命令,也是先送到相关芯片的端口中
,然后再由相关的芯片根据命令对外设实施控制。
15.2 外中断信息
-
在PC系统中,外中断源一共有以下两类:
可屏蔽中断和不可屏蔽中断
-
(1)可屏蔽中断是CPU可以不响应的外中断。
CPU是否响应可屏蔽中断,要看标志寄存器的IF位的设置
。 -
(2)不可屏蔽中断是CPU必须响应的外中断。
不受IF位影响
。当CPU检测到不可屏蔽中断信息时,在执行完当前指令后,立即响应。
可屏蔽中断所引发的中断过程,除在第1步上的实现有所不同外,基本上和内中断的中断过程相同。因为可屏蔽中断信息来自于CPU外部,中断类型码是通过数据线送入CPU的
;而内中断的中断类型码是在CPU内部产生的。
-
8086CPU提供的设置IF的指令如下:
-
(1)sti,设置IF=1
-
(2)cli,设置IF=0
对于8086CPU,不可屏蔽中断的中断类型码固定为2
,所以中断过程中,不需要取中断类型码。
几乎所有由外设引发的外中断,都是可屏蔽中断
。当外设有需要处理的事件发生时,相关芯片向CPU发出可屏蔽中断信息
。不可屏蔽中断是在系统中有必须处理的紧急情况发生时用来通知CPU的中断信息。
我需要修正自己的一个观点,以前在看《计算机组成原理》这本书中介绍I/O和中断的章节时,形成了一个观点,“外设发出中断请求”,看了这章之后,感觉更准确的说法是,“与外设对应的接口芯片,发出中断请求”。
记录一个模糊的观点,留着以后完善:“输入和输出的不同,输出是CPU通过指令执行主动发出
的,输入是CPU被动的检测
有没有外设的中断信息,比如下一小节介绍的键盘输入”
15.3 PC机键盘的处理过程
-
键盘输入:
-
(1)
按下一个键时,开关接通,该芯片就产生一个扫描码,扫描码说明了按下的键在键盘上的位置。
扫描码被送入主板上相关接口芯片的寄存器中,该寄存器的端口地址为60h
。 -
(2)松开按下的键时,也产生一个扫描码。
松开按键时产生的扫描码也被送入60h端口中。
-
(3)一般按下一个键时产生的扫描码称为
通码
,松开一个键产生的扫描码为断码
。 断码=通码+80h
BIOS提供了int 9中断例程,用来进行基本的键盘输入处理,主要工作如下:
- (1)读出60h端口中的扫描码
-
(2)如果是字符键的扫描码,
将该扫描码和它对应的字符码送入内存中的BIOS键盘缓冲区;如果是控制键和切换键的扫描码,则
将其转变为状态字节写入内存中存储状态字节的单元
。 - (3)对键盘系统进行相关控制。比如说,向相关芯片发出应答信息。
BIOS键盘缓冲区 是系统启动后,BIOS用于存放int 9中断例程所接收的键盘输入的内存区
。该内存区可以存储15个 键盘输入,因为int 9 中断例程除了接收扫描码外,还要产生和扫描码对应的字符码,所以在BIOS键盘缓冲区中,一个键盘输入用一个字单元存放,高位字节存放扫描码,低位字节存放字符码
。
-
0040:17单元
存储键盘状态字节,该字节记录了控制键和切换键的状态。键盘状态字节记录的各位信息如下:
- 0:右Shift状态,置1表示按下右shift键
- 1:左Shift状态,置1表示按下左shift键
- 2:Ctrl状态,置1表示按下Ctrl键
- 3:Alt状态,置1表示按下Alt键
- 4:ScrollLock状态,置1表示Scroll指示灯亮
- 5:NumLock状态,置1表示小键盘输入的是数字
- 6:CapsLock状态,置1表示输入大写字母
- 7:Insert状态,置1表示处于删除态
该小节键盘输入的细节,对我有些启发。
15.4 编写int 9中断例程
我一般都是看看例题的需求,不看书中源码,自己尝试写一下,写完之后再对比。本小节中用其它指令模拟int 9指令的中断过程要着重看一下理解透,还有后面的检测点,搞清楚为什么可以简化前面的模拟过程
水平有限,个人代码仅供参考:
(书中有源码,更简洁,我的沿用了前面的方法安装到0000:0200处)
assume cs:code,ss:stack,ds:data
data segment
dw 2 dup (0)
data ends
stack segment
dw 16 dup(0)
stack ends
code segment
;把int 9中断例程的入口地址保存到中断例程oldInter处
start:
mov ax,0
mov ds,ax
mov si,9*4 ;ds:si指向原int 9中断例程的入口地址在向量表中的地址
mov ax,cs
mov es,ax
mov di,offset oldInter ;es:di指向原int 9中断例程的入口地址要保存的位置
cld
mov cx,2
rep movsw ;将原int 9中断例程备份到新位置
;将自己写的中断例程安装到0000:0200处
mov ax,cs
mov ds,ax
mov si,offset newInter ;ds:di指向要安装的中断例程内存中的地址
mov ax,0000h
mov es,ax
mov di,0200h ;es:si指向0000:0200处
cld
mov cx,offset newInterE-offset newInter
rep movsb ;通过串传送指令进行安装
;修改中断类型码9在中断向量表对应的入口地址
cli ;防止修改int 9中断例程入口地址时,被中断导致错误
mov ax,0
mov es,ax
mov word ptr es:[9*4],0200h
mov word ptr es:[9*4+2],0
sti
;主程序,实现自动显示a~z,点击esc键变色
mov ax,stack ;初始化栈段
mov ss,ax
mov sp,32
mov ax,0b800h
mov es,ax
mov bp,160*12+40*2 ;es:bp指向显存中显示字符的位置
mov ah,'a'
c: mov es:[bp],ah
call delay ;调用延迟子程序,方便我们看清
inc ah
cmp ah,'z'
jna c
mov ax,cs
mov ds,ax
mov si,offset oldInter
mov ax,0
mov es,ax
mov di,9*4
mov cx,2
cld
rep movsw
mov ax,4c00h
int 21h
;功能:延迟一段时间。通过双字数10000000H减1实现
;参数:无
;返回:无
delay: push dx
push ax
mov dx,10h
mov ax,0000h
d: sub ax,1
sbb dx,0000h
cmp ax,0
jne d
cmp dx,0
jne d
pop ax
pop dx
ret
;中断例程:自己写的一个代替原int 9的中断例程,此中断例程需要通过call指令调用原中断例程
;功能:点击键盘esc键后,字符变色
;参数:无
;返回:无
newInter: jmp short chaCol
oldInter: db 4 dup(0) ;保存原int 9中断例程入口地址的地方
chaCol: push ax
push es
push bx
push ds
in al,60h ;获得键盘输入的扫描码
pushf ;调用原来的int 9的中断例程
mov bx,cs
mov ds,bx
call dword ptr ds:[202h] ;这里没搞清楚段地址偏移地址的关系,浪费半天时间,本来昨天就应该顺利验证通过
cmp al,01h
jne n
mov ax,0b800h
mov es,ax
inc byte ptr es:[160*12+40*2+1] ;es:bx指向显存中字符的属性
n: pop ds
pop bx
pop es
pop ax
iret
newInterE: nop
code ends
end start
-
我自己写的代码中,比较满意和需要注意是:
- (1)对offset这个指令的理解更好了。
- (2)注意中断例程中要访问的数据保存的地方,这个地方在安装后中断例程要能访问到
- (3)例程安装好后访问属于例程的数据和我写程序时访问数据的细节有些不同。 不同的原因就是段地址的改变,安装的地方和安装程序所在的地方,地址是不同的。
我在写这个代码时就是没注意例程安装好后的访问细节浪费了半天时间,一度自我怀疑,该不该不看源码自己写、该不该自学编程。
15.5 安装新的int 9中断例程
-
我自己根据需求写了中断例程,大体结构都差不多,对比书中的中断例程,给我的启发:
-
(1)我忽略了push和pop指令可以直接对内存单元操作,
我一直误以为push和pop指令后面只能跟寄存器
-
(2)对自己写的程序的细节要注意,写这个程序时忘记了在修改中断向量表中表项时要关中断。就是
cli和sti两个指令的运用
- (3)安装好的中断例程,安装程序结束不在内存中,中断例程还是能够触发的。
水平有限,个人代码仅供参考:
(书中有完整代码,我与书中代码的区别,这是在保存原int 9中断例程的入口地址方法不同)
assume cs:code
code segment
;将原int9中断例程的入口地址传送到新int9中断例程中oldAdr处
start: mov ax,0
mov ds,ax
mov si,9*4 ;ds:si指向中断向量表中原int9中断例程入口地址
mov ax,cs
mov es,ax
mov di,offset oldAdr ;es:di指向保存原中断例程入口地址处
cld
mov cx,4
rep movsb
;安装int9中断例程到0000:0200处
mov ax,cs
mov ds,ax
mov si,offset int9 ;ds:si指向新的int9中断例程
mov ax,0
mov es,ax
mov di,0200h ;es:di指向0000:0200处
cld
mov cx,offset int9E-offset int9
rep movsb
;更新中断向量表中int9对应的入口地址
cli
mov ax,0
mov ds,ax
mov word ptr ds:[9*4],0200h
mov word ptr ds:[9*4+2],0
sti
mov ax,4c00h
int 21h
;扩展后int9对应的中断例程
;功能:点击F1键,屏幕变色,其他键不变
;参数:无
;返回:无
int9: jmp short i
oldAdr: dw 2 dup (0)
i: push ax
push es
push si
push cx
push ds
in al,60h ;获取键盘输入的扫描码
cmp al,3bh
jne oldIt9 ;如果按的不是F1键,调用原int9中断例程
mov ax,0b800h
mov es,ax
mov si,1 ;es:si指向显存第一个属性地址
mov cx,2000
chaScr: inc byte ptr es:[si]
add si,2
loop chaScr
oldIt9: mov ax,cs
mov ds,ax
pushf
call dword ptr ds:[202h] ;原int9中断例程的入口地址保存在0000:0202处
pop ds
pop cx
pop si
pop es
pop ax
iret
int9E: nop
code ends
end start
实验 15 安装新的int 9中断例程
水平有限,个人代码仅供参考:
assume cs:code
code segment
;原中断例程的入口地址保存到新中断例程中oldAdr处
start: mov ax,0
mov ds,ax
mov si,9*4 ;ds:si指向源地址,即原int 9 中断例程的入口地址
mov ax,cs
mov es,ax
mov di,offset oldAdr ;es:di指向目的地址,即新int 9中断例程的oldAdr处
mov cx,4
cld
rep movsb ;串传送
;安装新中断例程到0000:0200处
mov ax,cs
mov ds,ax
mov si,offset int9 ;ds:si指向源地址,即新int9中断例程
mov ax,0
mov es,ax
mov di,0200h ;es:di指向目的地址,即0000:0200处
mov cx,offset int9E-offset int9
cld
rep movsb ;串传送指令
;修改中断向量表中原int9对应的入口地址
cli ;防止int9中断的入口地址没设置号,响应中断导致错误
mov ax,0
mov es,ax
mov word ptr es:[9*4],0200h
mov word ptr es:[9*4+2],0
sti
mov ax,4c00h ;结束程序
int 21h
;新int9中断例程
int9: jmp short showA
oldAdr: dw 2 dup (0)
showA: push ax
push es
push di
push cx
push ds
in al,60h
cmp al,9eh ;用A键的断码来判断
jne oldIt9 ;其它情况调用原int 9中断例程
mov ax,0b800h ;准备输出全屏A
mov es,ax
mov di,0
mov cx,2000
cycA: mov byte ptr es:[di],'A' ;循环输出全屏A
add di,2
loop cycA
oldIt9: mov ax,cs
mov ds,ax
pushf
call dword ptr ds:[202h]
pop ds
pop cx
pop di
pop es
pop ax
iret
int9E: nop
code ends
end start
参考前面的例子很简单,前面的例子根据键的通码判断,这里的实验改成根据键的断码判断而已