接口芯片和端口
PC系统的接口卡和主板上,装有各种接口芯片。这些外设接口的内部有若干寄存器,CPU 将这些寄存器当做端口来访问。
外设的输入不直接送入内存和CPU,而是送入相关的接口芯片的端口中;CPU 向外设的输出也不是直接送入外设,而是先送入端口中,再由相关的芯片送到外设。CPU也是用这种方式向外设输出控制命令。
CPU通过端口和外部设备进行联系。
外中断信息
eg:外设的输入到达,相关的芯片将向 CPU 发出相应的中断信息。CPU 在执行完当前指令后,可以检测到发送过来的中断信息,引发中断过程,处理外设的输入。
外中断源一共有两类:
1、可屏蔽中断 可屏蔽中断是CPU可以不相应的外中断。CPU是否响应可屏蔽中断,要看标志寄存器的 IF 位的设置。当 CPU 检测到可屏蔽中断信息时,如果 IF=1,则 CPU 在执行完当前指令后相应中断,引发中断过程;如果 IF=0,则不响应可屏蔽中断。 可屏蔽中断信息来自于 CPU 外部,中断类型码是通过数据总线送入CPU的;而内中断的中断类型码是在CPU内部产生的。 ① 获取到中断类型码 n; ② 标志寄存器入栈,IF=0,TF=0; 如果在中断处理程序中需要处理可屏蔽中断,可以用指令将 IF 置1。 ③ CS、IP 入栈; ④ (IP)=(n*4),(CS)=(n*4+2) 转去执行中断处理程序 sti ,用于设置 IF=1; cli ,用于设置 IF=0; |
2、不可屏蔽中断 不可屏蔽中断是CPU必须立即响应的外中断。当CPU检测到不可屏蔽中断信息时,则在执行完当前指令后,立即响应,引发中断过程。 对于 8086CPU,不可屏蔽中断的中断类型码固定为2,所以中断过程中,不需要取中断类型码。不可屏蔽中断的中断过程: ① 标志寄存器入栈,IF=0,TF=0; ② CS、IP 入栈; ③ (IP)=(8),(CS)=(0AH)。 几乎所有由外设引发的外中断,都是可屏蔽中断。当外设有需要处理的事件(比如键盘输入)发生时,相关芯片向CPU发出可屏蔽中断信息。不可屏蔽中断是在系统中有必须处理的紧急情况发生时,用来通知CPU的中断信息。(电源中断,就是不可屏蔽中断) |
PC 机键盘的处理过程
键盘上的每一个键相当于一个开关,键盘中有一个芯片对键盘上的每一个键的开关状态进行扫描。
按下一个键时,开关接通,该芯片就产生一个扫描码,扫描码说明了按下的键在键盘上的位置。扫描码被送入主板上的相关接口芯片的寄存器中,该寄存器的端口地址为 60H。
松开按下的键时,也产生一个扫描码,扫描码说明了松开的键在键盘上的位置。松开按键时产生的扫描码也被送入 60H 端口中。
一般按下一个键时产生的扫描码称为通码,松开一个键产生的扫描码称为断码。扫描码长度为一个字节,通码的第7位为0,断码的第7位为1。即:断码=通码+80H
eg: g 键的通码为22H,断码为a2H。
// 都是通码
引发9号中断
键盘的输入到达 60H 端口时,相关的芯片就会向 CPU 发出中断类型码为 9 的可屏蔽中断信息。CPU 检测到该中断信息后,如果 IF=1 ,则响应中断,引发中断过程,转去执行 int 9 中断例程。
执行 int 9 中断例程
BIOS 提供了 int 9 中断例程,用来进行基本的键盘输入处理,主要的工作如下:
(1) 、读出 60H 端口中的扫描码;
(2)、 如果是字符键的扫描码,将该扫描码和它所对应的字符码 (ASCII)送入内存中的 BIOS 键盘缓冲区;如果是控制键(eg:Ctrl)和切换键(eg:Caps Lock)的扫描码,则将其转换为状态字节(用二进制位记录控制键和切换键状态的字节)写入内存中存储状态字节的单元。
(3)、对键盘系统进行相关的控制,eg: 向相关芯片发出应答信息。
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:Caps Lock 状态,置 1 表示输入大写字母; | 7:Insert 状态,置 1 表示处于删除状态; |
编写int 9中断例程
从键盘输入的处理过程:
① 键盘产生扫描码;
② 扫描码送入 60h 端口;
③ 引发 9 号中断;
④ CPU 执行 int 9 中断例程处理键盘输入;
1、2、3步都是由硬件系统完成,我们能够改变的只有int 9中断处理程序。
编程:在屏幕中间依次显示“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
code ends
end start
这样写由于CPU执行速度太快,看不清每个字符;可以让CPU重复执行一段代码,达到延迟效果 |
assume cs:code
data segment
db 128 dup (0) ; 自己开辟栈空间,可以不要这步操作
data ends
code segment
start: mov ax , data
mov ss , ax
mov sp , 128
mov ax , 0b800h
mov es , ax
mov ah , 'a'
s: mov es:[160*12+40*2] , ah
mov byte ptr es:[160*12+40*2+1] , 01001010b
call delay
inc ah
cmp ah , 'z'
jna s
mov ax , 4c00h
int 21h
delay: push ax
push dx
mov ax , 0
mov dx , 5h
s1: sub ax , 1 ; 首次执行(ax)=-1,计算机存的是补码ffff=65535
sbb dx , 0 ; 第一次执行dx-0-(CF),CF的值为1,减完CF=0
cmp ax , 0
jne s1
cmp dx , 0
jne s1 ; 一次call执行减法的次数就是65536*5
pop dx
pop ax
ret
code ends
end start
|
编写 int 9 中断例程
键盘输入的处理过程:1、键盘产生扫描码;2、扫描码送入60h 端口;3、引发 9 号中断;4、CPU执行 int 9 中断例程处理键盘输入。其中,1~3 是由硬件系统完成,我们能改变的只有 int 9 中断处理程序。前面实现了显示“a~z”,现在实现在显示过程中,按下 Esc 键后,改变显示的颜色。
键盘输入到达 60h 端口后,就会引发 9 号中断,CPU 则转去执行 int 9 中断例程。
// 编写 int 9 中断例程 1、从 60h 端口读出键盘输入; 2、调用 BIOS 的 int 9 中断例程,处理其他硬件细节;
|
|
int 指令在执行的时候,CPU进行下面几个工作: ① 取中断类型码 n ; \ 目的是定位中断例程的入口地址。我们自己编写中断例程,这个地址我们自己设定,已经知道 ② 标志寄存器入栈; ③ IF=0 , TF=0; ④ CS、IP 入栈; 假设:中断例程的入口地址( EA:SA )在 ds:0 和 ds:2 ,模拟 int 过程 ① 标志寄存器入栈; ② IF=0,TF=0; ③ CS、IP 入栈; ④ (IP)=((ds)*16+0),(CS)=((ds)*16+2) 注意:第 ③ ④ 步和 call dword ptr ds:[0] 的功能一样 |
|
int 过程的模拟过程: ① 标志寄存器入栈; 可以由 pushf 实现 ② IF=0 , TF=0; 可以由指令实现 ③ call dword ptr ds:[0] pushf
pop ax
and ah , 11111100b ; IF和TF为标志寄存器的第9位和第8位
push ax
popf
|
|
模拟 int 指令的调用功能,调用入口地址在 ds:0、ds:2 中的中断例程的程序为: pushf ; 标志寄存器入栈
pushf
pop ax
and ah , 11111100b
push ax
popf ; 置IF=0、TF=0
call dword ptr ds:[0] ; CS、IP入栈;(IP)=((ds)*16+0),(CS)=((ds)*16+2)
|
|
如果是 Esc 键的扫描码,改变颜色后返回: 注意:程序返回前,要将中断向量表中的 int 9 中断例程的入口地址恢复为原来的地址,否则程序返回后,别的程序无法使用键盘中断。 |
assume cs:code
stack segment
db 128 dup (0)
stack ends
data segment
dw 0 , 0 ; 用来保存原本int 9中断例程的入口地址,在新的int9中用来调用原本的中断例程处理其他硬盘细节和程序返回之前恢复9号中断例程入口地址
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] ; 中断向量表中的9号中断的中断例程入口偏移地址进栈
pop ds:[0] ; 定义的 data 段保存9号中断的原有中断例程入口偏移地址
push es:[9*4+2] ; 中断向量表中的9号中断的中断例程入口段地址进栈
pop ds:[2] ; 定义的data保存9号中断的原有中断例程入口段地址
mov word ptr es:[9*4] , offset int9
mov es:[9*4+2] , cs ; 在中断向量表中设置新的int 9中断例程的入口地址
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
push ds:[0]
pop es:[9*4]
push ds:[2]
pop es:[9*4+2] ; 将中断向量表中的int 9中断例程的入口恢复为原来的地址
mov ax , 4c00h
int 21h
delay: push ax
push dx
mov ax , 0
mov dx , 5h
s1: sub ax , 1
sbb dx , 0
cmp ax , 0
jne s1
cmp dx , 0
jne s1
pop dx
pop ax
ret
|
;--------新的中断9例程--------;
int9: push ax
push bx
push es
in al , 60h
pushf ; 标志寄存器进栈保存
pushf ; 一种新的给寄存器赋值方法,要修改的数据先进栈,然后直接出栈放到寄存器中进行处理
pop bx
and bh , 11111100b ; IF和TF是标志寄存器的第9位和第8位
push bx
popf ; 修改标志寄存器
call dword ptr ds:[0] ; 对 int 指令进行模拟,调用原来的int 9中断例程,处理键盘输入
; 这段代码的作用就是①保存当前CS和IP,②用ds:[0]~ds:[1]中的数据设置IP,ds:[2]~ds:[3]中的数据设置CS;这段内存我们事先放的是原本int 9的中断例程
; 原来的int 9要对键盘输入做一系列操作,如果改写了int 9中断例程,但是在自己新写的例程中不调用原来的
; 中断例程对输入做处理系统就要不知道发生了什么,运行卡住
; test,注释了这行代码,运行程序的时候键盘产生了回车的扫描码,
; 但是我们在程序中如果没有这行调用原来的中断例程,系统不知道怎么处理
; 只是拿扫描码来比较一下。
; 因为对于后面的键盘输入,调用不到原先的中断程序,我们的写的中断
; 也没涉及到处理键盘输入,系统崩溃;
; debug 单步调试的时候,单步到设置新的int 9中断例程入口地址就卡住
; 这是因为键盘输入-t命令的时候,入口地址更改命令要回车之后才执行
; 但是在这之前对于键盘的输入,系统就无法处理了。所以不支持单步调试
cmp al , 1 ; Esc 的扫描码是 01h
jne int9ret ; 处理完键盘输入后,再执行我们想要的操作:如果按下 Esc 就改变颜色属性
mov ax , 0b800h
mov es , ax
inc byte ptr es:[160*12+40*2+1] ; 将颜色属性加1,改变颜色
int9ret: pop es
pop bx
pop ax
iret ; 中断返回
code ends
end start
|
// 在上面的汇编程序运行的时候,如果我不按 Esc 键,等到程序显示到 'z' 之后正常退出返回到DOS,但是如果我在程序显示 'a~z' 时候按下 Esc (我按了4次)程序退出之后又显示了 4个 '\' Dos 下面按下 'Esc' 显示的就是 '\' ;猜想:BIOS 键盘缓冲区存放int 9中断接收到的键盘输入,本来要显示在屏幕上的,但是程序执行的时候按下按键产生中断,修改了 IF=0,可屏蔽中断被屏蔽了,只能等到程序运行结束恢复 IF=1 再处理这个可屏蔽中断,在屏幕上打印出 Esc 所代表的字符 '\'; 因为这个缓冲区只能存15个字符,我按下20次 Esc 最后也只能输出15个 '\' ; 第二个运行截图是我执行程序后按下 'abc\\\abc';
改进: pushf
pushf
pop bx
and bh , 11111100b
push bx
popf
call dword ptr ds:[0]
pushf call dword ptr ds:[0]
|
执行中断要先执行:
① 取中断类型码 n ;
② 标志寄存器入栈;pushf
③ IF=0,TF=0;
④ CS、IP 入栈;
在我们自己写的int9中断例程中,已经进入到中断例程,说明前4步已经做完了,所以就不必在设置IF和TF了,还要有一个 pushf 是因为程序主中我们通过call命令又执行了一次原本的9号中断例程,例程的最后都要执行iret,pushf和call中iret对应
iret==>pop IP、pop CS、popf
我们自己定义的中断例程最后的iret才是恢复到进入我们写的中断例程前cpu相关状态
|
cli
mov word ptr es:[9*4] , offset int9
mov es:[9*4+2] , cs
sti
|
防止在修改9号中断的入口地址的时候产生键盘中断 |
CPU 对外设输入的通常处理方法:
① 外设的输入送入端口。
② 向 CPU 发出外中断(可屏蔽中断)信息。
③ CPU检测到可屏蔽中断信息,如果 IF=1,CPU 在执行完当前指令后响应中断,执行相应的中断例程。
④ 可在中断例程中实现对外设输入的处理。
端口和中断机制,是 CPU 进行 I/O 的基础。