以前我们讨论的都是 CPU 对指令的执行。我们知道,CPU 在计算机系统中,除了能够执行指令,进行运算外,还应该对外部设备进行控制,接收它们的输入,向他们进行输出。也就是说,CPU 除了有运算能力外,还要有 I/O(Input/Output,输入/输出)能力。比如,我们按下键盘上的一个键,CPU 最终能够处理这个键。在使用文本编辑器时,按下 a 键后,我们可以看到屏幕上出现 “a”,是 CPU 将从键盘上输入的键所对应的字符送到显示器上的。

要及时处理外设的输入,需要解决两个问题:

  1. 外设的输入随时可以发生,CPU 如何得知?
  2. CPU 从何处得到外设的输入?

§ 第15章 外中断 - 图1

现在我们以键盘输入为例,讨论这个问题。

15.1 接口芯片和端口

14 章中讲过,PC 系统的接口卡和主板上,装有各种接口芯片。这些外设接口芯片内部有若干寄存器,CPU 将这些寄存器当作端口访问。

外设的输入不直接送入内存和 CPU,而是送入相关的接口芯片的端口中;CPU 向外设的输出也不是直接送入外设,而是先送入端口中,再由相关的芯片送到外设。CPU 还可以向外设输出控制命令,而这些控制命令也是先送到相关芯片的端口中,然后再由相关的芯片根据命令对外设实施控制。

CPU 通过端口和外部设备进行联系。

§ 第15章 外中断 - 图2

15.2 外中断信息

现在,我们知道了外设的输入被存放在端口中,可是外设的输入输出随时都有可能到达,CPU 如何及时地知道,并进行处理呢?更一般地讲,就是外设随时都可能发生需要 CPU 及时处理的事件,CPU 如何及时得知并进行处理?

CPU 提供中断机制来满足这种需要。前面说过,当 CPU 的内部有需要处理的事情发生的时候,将产生中断信息,引发中断过程。这种中断信息来自 CPU 的内部。

还有一种中断信息,来自 CPU 外部,当 CPU 外部有需要处理的事情发生的时候,比如说,外设的输入到达,相关芯片将向 CPU 发出相应的中断信息。CPU 在执行完当前指令后,可以检测到发送过来中断信息,引发中断过程,处理外设的输入。

在 PC 系统中,外中断源一共有以下两类。

§ 第15章 外中断 - 图3

可屏蔽中断

可屏蔽中断是 CPU 可以不响应的外中断CPU 是否响应可屏蔽中断,要看标志寄存器的 IF 位的设置。当 CPU 检测到可屏蔽中断信息时,如果 IF=1,则 CPU 在执行完当前指令后响应中断,引发中断过程;如果 IF=0,则不响应可屏蔽中断。

我们回忆一下内中断所引发的中断过程:

  1. 取中断类型码 n;
  2. 标志寄存器入栈,IF=0,TF=0;
  3. CS、IP 入栈;
  4. (IP)=(n4),(CS)=(n4+2)

由此转去执行中断处理程序。

可屏蔽中断所引发的中断过程,除在第 1 步的实现上有所不同外,基本上和内中断的中断过程相同。因为可屏蔽中断信息来自 CPU 外部,中断类型码是通过数据总线送入 CPU 的;而内中断的终端类型码是在 CPU 内部产生的。

现在,我们可以解释中断过程中将 IF 设置为 0 的原因了。将 IF 设置为 0 的原因就是,在进入中断处理程序后,禁止其他的可屏蔽中断

当然,如果在中断处理程序中需要处理可屏蔽中断,可以用指令将 IF 设置为1.8086 CPU 提供的设置 IF 的指令如下:

  • sti:设置 IF = 1;
  • cli:设置 IF = 0。

不可屏蔽中断

不可屏蔽中断是 CPU 必须响应的中断。当 CPU 检测到不可屏蔽中断信息时,则在执行完当前指令后,立即响应,引发中断过程

对于 8086CPU,不可屏蔽中断的中断类型码固定为 2,所以中断过程中,不需要取中断类型码。则不可屏蔽中断的中断过程为:

  1. 标志寄存器入栈,IF=0,TF=0;
  2. CS、IP 入栈;
  3. (IP)=(8),(CS)=(0AH)

几乎所有由外设引发的外中断,都是可屏蔽中断。当外设有需要处理的事件(比如说键盘输入)发生时,相关芯片向 CPU 发出可屏蔽中断信息。不可屏蔽中断是系统中有必须处理的紧急情况发生时用来通知 CPU 的中断信息。在我们的课程中,主要讨论可屏蔽中断。

15.3 PC机键盘的处理过程

下面我们看一下键盘输入的处理过程,以此来体会一下 PC 机处理外设输入的基本方法。

1.键盘输入

键盘上的每一个键相当于一个开关,键盘上有一个芯片对键盘上的每一个键的开关状态进行扫描。

按下一个键时,开关接通,该芯片就产生一个扫描码,扫描码说明了按下的键在键盘上的位置。扫描码被送入主板上的相关接口芯片的寄存器中,该寄存器的端口地址为 60h。

松开按下的键时,也产生一个扫描码。扫描码说明了松开的键在键盘上的位置,松开按键时产生的扫描码也被送入 60h 端口中。

一般将按下一个键时产生的扫描码被称为通码,松开一个键产生的扫描码被称为断码。扫描长度为一个字节,通码的第 7 位为 0,断码的第 7 位为1,即

§ 第15章 外中断 - 图4

比如,g 键的通码为 22h,断码为 a2h。

下表为键盘上部分键的扫描码,只列出通码。断码=通码+80h。

§ 第15章 外中断 - 图5

§ 第15章 外中断 - 图6

2.引发9号中断

键盘的输入到达 60h 端口时,相关的芯片就会向 CPU 发出中断类型码为 9 的可屏蔽中断信息。CPU 检测到该中断信息后,如果 IF=1,则响应中断,引发中断过程,转去执行 int 9中断例程。

§ 第15章 外中断 - 图7

3.执行 int9 中断例程

BIOS 提供了 int 9中断例程,用来进行基本的键盘输入处理,主要的工作如下:

§ 第15章 外中断 - 图8 读出 60h 端口中的扫描码;

§ 第15章 外中断 - 图9 对于不同的扫描码有不同的处理方式

  • 如果是字符键的扫描码,将该扫描码和它所对应的字符码(即 ASCII 码)送入内存的 BIOS 键盘缓冲区;
  • 如果是控制键(比如 Ctrl)和切换键(比如 CapsLock)的扫描码,则将其转变为状态字节(用二进制位记录控制键和切换键状态的字节)写入内存中存储状态字节的的单元

§ 第15章 外中断 - 图10 对键盘系统进行相关的控制,比如说,向相关芯片发出应答信息。

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章 外中断 - 图11

15.4 编写 int 9 中断例程

从上面的内容中,可以看出键盘输入的处理过程:

§ 第15章 外中断 - 图12

上面的过程中,第 1、2、3 步都是由硬件系统完成的。我们能够改变的只有 int 9 中断处理程序。我们可以重写编写 int 9中断例程,按照自己的意图来处理键盘的输入。但是,在课程中,我们不准备完整地编写一个键盘中断的处理程序,因为要设计一些硬件细节。

但是,我们却还要编写新的键盘中断处理程序,来进行一些特殊的工作,那么这些细节如何处理呢?这点比较简单,因为 BIOS 提供的 int 9中断例程已经对这些硬件细节进行了处理。我们只要在自己编写的中断例程中调用 BIOS 的 int 9中断例程就可以了

编程:在屏幕中间依次显示 “a” ~ “z”,可以让人看清。在显示的过程中,按下 Esc 键后,改变显示的颜色。

我们先来看一下如何依次显示 “a” ~ “z”。

  1. assume cs:code
  2. code segment
  3. start: mov ax,0b800h
  4. mov es,ax
  5. mov ah,'a'
  6. s: mov es:[160*12+40*2],ah
  7. inc ah
  8. cmp ah,'z'
  9. jna s
  10. mov ax,4c00h
  11. int 21h
  12. code ends
  13. end start

在上面的程序的执行过程中,我们无法看清屏幕上的显示。因为一个字母刚显示到屏幕上,CPU 执行几条指令后,就又变成了另一个字母,字母之间切换到太快,无法看清。

应该在每显示一个字母后,延时一段时间,让人看清后,再显示下一个字母。那么如何延时呢?我们让 CPU 执行一段时间的空循环。因为现在 CPU 的速度非常快,所以循环的次数一定要大,用两个 16 位寄存器来存放 32 位的循环次数。如下:

  1. mov dx,10h
  2. mov ax,0
  3. sub: sub ax,1
  4. sbb dx,0
  5. cmp ax,0
  6. jne s
  7. cmp dx,0
  8. jne s

上面的程序,循环 100000h 次。我们可以将循环延时的程序段写为一个子程序。

现在,我们的程序如下:

  1. assume cs:code
  2. stack segment
  3. db 128 dup (0)
  4. stack ends
  5. code segment
  6. start: mov ax,stack
  7. mov ss,ax
  8. mov sp,128
  9. mov ax,0b800h
  10. mov es,ax
  11. mov ah,'a'
  12. s: mov es:[160*12+40*2],ah
  13. call delay
  14. inc ah
  15. cmp ah,'z'
  16. jna s
  17. mov ax,4c00h
  18. int 21h
  19. delay: push ax
  20. push dx
  21. mov dx,1000h ; 循环 10000000h 次,读者可以根据自己机器的速度调整循环次数
  22. mov ax,0
  23. s1: sub ax,1
  24. sbb dx,0
  25. cmp ax,0
  26. jne s1
  27. pop dx
  28. pop ax
  29. ret
  30. code ends
  31. end start

显示 “a” ~ “z”,并可以让人看清,这个任务已经实现。那么如何实现,按下 Esc 键后,改变显示的颜色呢?

键盘输入到达 60h 端口号,就会引发 9 号中断,CPU 则转去执行 int 9中断例程。我们可以编写 int 9 号中断例程,功能如下:

  1. 从 60h 端口读入键盘的输入;
  2. 调用 BIOS 的 int 9中断例程,处理其他硬件细节;
  3. 判断是否为 Esc 的扫描码,如果是,改变显示的颜色后返回;如果不是则直接返回。

下面对这些功能一一进行分析。

下面对这些功能的实现一一分析。

§ 第15章 外中断 - 图13 从端口 60h 读出键盘的输入

  1. in al,60h

§ 第15章 外中断 - 图14 调用 BIOS 的 int 9中断例程

有一点要注意的是,我们写的中断处理程序要成为新的 int 9 中断例程,主程序必须要将中断向量表中的 **int 9** 中断例程的入口地址改为我们写的中断处理程序的入口地址。则在新的中断处理程序中调用原来的 int 9 中断例程时,中断向量表中的 int 9中断例程的入口地址却不是原来的 int 9 中断例程的地址。所以不能使用 int指令直接调用。

要在我们写的新中断例程中调用原来的中断例程,就必须在将中断向量表的中断例程的入口地址改为新地址之前,将原来的入口地址保存起来。这样,在需要调用的时候,我们才能找到原来的中断例程的入口。

§ 第15章 外中断 - 图15

对于我们现在的问题,假设将原来 int 9 中断例程的偏移地址和段地址保存在 ds:[0] 和 ds:[2] 单元中。那么,我们在需要调用原来的 int 9中断例程的时候,就可以在 ds:[0]、ds:[2]单元中找到它的入口地址。

那么,有了入口地址后,如何进行调用呢?

当然不能使用指令 int 9来调用。我们可以用别的指令来对 **int** 指令来进行一些模拟,从而实现对中断例程的调用

我们来看,int 指令在执行的时候,CPU 进行下面的工作。

  1. 取中断类型码 n;
  2. 标志寄存器入栈;
  3. IF=0、TF=0;
  4. CS、IP入栈;
  5. (IP)=(n4),(CS)=(n4+2)。

取中断类型码是为了定位中断例程的入口地址,在我们的问题中,中断例程的入口地址已经知道。所以,我们用别的指令模拟 int 指令的时候,不需要做第 1 步。在假设要调用的中断例程的入口地址在 ds:0 和 ds:2 单元中的前提下,我们将 int 过程用下面几步模拟。

§ 第15章 外中断 - 图16

可以注意到第 3、4步和 call dword ptr ds:[0]的功能一样,call dword ptr ds:[0]的功能也是

  1. CS、IP入栈;
  2. (IP)=((ds)16+0),(CS)=((ds)16+2)。

所以,int 过程的模拟过程为

§ 第15章 外中断 - 图17

对于步骤1,标志寄存器入栈可以使用 pushf 实现;

对于步骤2,IF=0, TF=0可以用下面的指令实现:

  1. ; 步骤2IF=0, TF=0
  2. pushf
  3. pop ax
  4. and ah,11111100b ; IF TF 为标志寄存器的第9位和第8
  5. push ax
  6. popf

则模拟 int 指令的调用功能,调用入口地址在 ds:0、ds:2 中的中断例程的程序为:

  1. ; 步骤1:标志寄存器入栈
  2. pushf ; 标志寄存器入栈
  3. ; 步骤2IF=0, TF=0
  4. pushf
  5. pop ax
  6. and ah,11111100b
  7. push ax
  8. popf ; IF=0, TF=0
  9. ;步骤3
  10. call dword ptr ds:[0] ; CSIP 入栈; (IP)=((ds)*16+0), (CS)=((ds)*16+2)

§ 第15章 外中断 - 图18 如果是Esc 的扫描码,改变显示的颜色后返回

如何改变显示的颜色?

显示的位置是屏幕的中间,即第 12 行 40 列,显存中的偏移地址为:16012+402。所以字符的 ASCII 码要送入段地址 b800h,偏移地址 16012+402处。而段地址 b800h,偏移地址 16012+402+1 处是字符的属性,只要改变此处的数据就可以改变在段地址 b800h,偏移地址 16012+402 处显示的字符的颜色了。

§ 第15章 外中断 - 图19

该程序的最后一个问题是,要在程序返回前,将中断向量表中的 int 9中断例程的入口地址恢复为原来的地址。否则在程序返回后,别的程序将无法使用键盘。

经过分析,完整的程序如下。

  1. assume cs:code
  2. stack segment
  3. db 128 dup (0)
  4. stack ends
  5. data segment
  6. dw 0,0
  7. data ends
  8. code segment
  9. start: mov ax,stack
  10. mov ss,ax
  11. mov sp,128
  12. mov ax,data
  13. mov ds,ax
  14. mov ax,0
  15. mov es,ax
  16. push es:[9*4]
  17. pop ds:[0]
  18. push es:[9*4+2]
  19. pop ds:[2] ; 将原来的int 9中断例程的入口地址保存在ds:[0]、ds:[2]单元中
  20. mov word ptr es:[9*4],offset int9
  21. mov es:[9*4+2],cs ; 在中断向量表中设置新的int 9中断例程的入口地址
  22. mov ax,0b800h
  23. mov es,ax
  24. mov ah,'a'
  25. s: mov es:[160*12+40*2],ah
  26. call delay
  27. inc ah
  28. cmp ah,'z'
  29. jna s
  30. mov ax,0
  31. mov es,ax
  32. push ds:[0]
  33. pop es:[9*4]
  34. push ds:[2]
  35. pop es:[9*4+2] ; 将中断向量表int 9中断例程入口恢复为原来的地址
  36. mov ax,4c00h
  37. int 21h
  38. delay: push ax
  39. push dx
  40. mov dx,1000h
  41. mov ax,0
  42. s1: sub ax,1
  43. sbb dx,0
  44. cmp ax,0
  45. jne s1
  46. cmp dx,0
  47. jne s1
  48. pop dx
  49. pop ax
  50. ret
  51. ; --------------- 以下为新的int 9中断例程 ---------------
  52. int9: push ax
  53. push bx
  54. push es
  55. in al,60h
  56. pushf
  57. pushf
  58. pop bx
  59. and bh,11111100b
  60. push bx
  61. popf
  62. call dword ptr ds:[0] ; int指令进行模拟,调用原来的int 9中断例程
  63. cmp al,1
  64. jne int9ret
  65. mov ax,0b800h
  66. mov es,ax
  67. inc byte ptr es:[160*12+40*2+1] ; 将属性值加 1,改变颜色
  68. int9ret: pop es
  69. pop bx
  70. pop ax
  71. iret
  72. code ends
  73. end start

运行结果如下所示:

§ 第15章 外中断 - 图20

注意:本章中所有关于键盘的程序,因为要直接访问真实的硬件,则必须在 DOS 实模式下运行。在 Windows2000 的 DOS 方式下运行,会出现一些和硬件工作原理不符合的现象。

检测点 15.1

§ 第15章 外中断 - 图21

15.5 安装新的 int 9中断例程

下面,我们安装一个新的 int 9 中断例程,使得原 int 9 中断例程的功能得到扩展。

  • 任务:安装一个新的 int 9 中断例程
  • 功能:在 DOS 下,按 F1 键后改变当前屏幕的显示颜色,其他的键照常处理

我们进行一下分析

§ 第15章 外中断 - 图22 改变屏幕的显示颜色

改变从 B8000H 开始的 4000 个字节中的所有奇地址单元中的内容,当前屏幕的显示颜色即发生改变。程序如下

  1. mov ax,0b800h
  2. mov es,ax
  3. mov bx,1
  4. mov cx,2000
  5. s: inc byte ptr es:[bx]
  6. add bx,2
  7. loop s

§ 第15章 外中断 - 图23 其他键照常处理

可以调用原 int 9中断处理程序,来处理其他的键盘输入。

§ 第15章 外中断 - 图24int 9 中断例程入口地址的保存

因为在编写的新 int 9 中断例程中要调用原 int 9 中断例程。所以,要保存原 int 9 中断例程的入口地址。保存在哪里?显然不能保存在安装程序中,因为安装程序返回地址将丢失。我们将地址保存在 0:200 单元处。

§ 第15章 外中断 - 图25int 9 中断例程的安装

这个问题在前面已经详细讨论过。我们可将新的 int 9 中断例程安装在 0:204 处。

完整的程序如下:

  1. assume cs:code
  2. stack segment
  3. db 128 dup (0)
  4. stack ends
  5. code segment
  6. start: mov ax,stack
  7. mov ss,ax
  8. mov sp,128
  9. push cs
  10. pop ds
  11. mov ax,0
  12. mov es,ax
  13. mov si,offset int9 ; 设置 ds:si 指向源地址
  14. mov di,204h ; 设置 es:di 指向目的地址
  15. mov cx,offset int9end-offset int9 ; 设置 cx 为传输长度
  16. cld ; 设置传输方向为正
  17. rep movsb
  18. push es:[9*4]
  19. pop es:[200h]
  20. push es:[9*4+2]
  21. pop es:[202h]
  22. cli
  23. mov word ptr es:[9*4],204h
  24. mov word ptr es:[9*4+2],0
  25. sti
  26. mov ax,4c00h
  27. int 21h
  28. int9: push ax
  29. push bx
  30. push cx
  31. push es
  32. in al,60h
  33. pushf
  34. call dword ptr cs:[200h] ; 当此中断例程执行时(CS)=0
  35. cmp al,3bh ; F1的扫描码为 3bh
  36. jne int9ret
  37. mov ax,0b800h
  38. mov es,ax
  39. mov bx,1
  40. mov cx,2000
  41. s: inc byte ptr es:[bx]
  42. add bx,2
  43. loop s
  44. int9ret: pop es
  45. pop cx
  46. pop bx
  47. pop ax
  48. iret
  49. int9end: nop
  50. code ends
  51. end start

这一章中,我们通对过键盘输入的处理,讲解了 CPU 对外设输入的通常处理方法。即:

  1. 外设的输入输出端口;
  2. 向 CPU 发送外中断(主要是可屏蔽中断)信息;
  3. CPU 检测到可屏蔽中断信息,如果 IF=1,则 CPU在执行完当前指令后响应中断,执行相应的中断例程;
  4. 可在中断例程中实现对外设输入的处理。

端口和中断机制,是 CPU 进行 I/O 的基础