8086汇编-输入输出与中断
9unk Lv5

输入输出的基本概念

各种输入输出设备(如:打印机、键盘、鼠标等)都要通过一个硬件接口或控制器和 CPU 相连。例如,打印机通过打印接口与系统相连;显示器通过显示控制器和系统相连。从程序设计的角度看,接口由一组寄存器组成,是完成输入输出的桥梁。程序利用 I/O 指令,存取接口上的寄存器,获得外部设备的状态信息,控制外部设备的动作,从而实现输入输出。

I/O端口地址和I/O指令

I/O端口地址

为了存取接口上的寄存器,系统给这些寄存器分配专门的存取地址,这样的地址称为I/O端口地址。
在某些微型机上,I/O端口地址和存储单元地址统一编址。这相当于把 I/O 接口(设备)视为一个或几个存储单元,利用存取内存单元的指令就可存取接口上的寄存器。但这会减少原本就有限的一部分存储空间,同时由于访问内存的指令一般超过2字节,从而延长了外部设备与处理器进行数据交换的时间。
在以 Intel 的 8086 系列的处理器为 CPU 的系统中,I/O端口地址和存储单元的地址是各自独立的,分别占两个不同的地址空间。8086/8088 提供的I/O端口地址空间达 64K 个 8 位端口(或32K个16位端口)。但实际上PC机一般只使用 0 到 3FFH 之间的 I/O端口地址。

I/O 指令

由于 8086/8088 的 I/O端口地址和内存单元地址是独立的,所以要用专门的I/O指令来存取端口上的寄存器,也就是说要用专门的I/O指令进行输入输出。

I/O指令属于数据传送指令组
(1)输入指令
输入指令一般格式如下:

IN 累加器,端口地址

  1. 输入指令从一个输入端口读取一个字节或一个字,传送至AL或AX中。
  2. 端口地址可采用直接方式表示,也可采用间接方式表示。
  3. 当采用直接方式表示端口地址时,端口地址仅为8位,即0~255;当采用间接方式表示端口地址时,端口地址存放在DX寄存器中,端口地址可为16位。
  4. 输入指令有如下四种方式:

直接端口寻址
IN AL,PORT ;PORT 是一个8位的立即数
IN AX,PORT

间接端口寻址
IN AL,DX ;PORT 是一个字时,相当于同时从n和n+1分别读取一个字节。
IN AX,DX

当端口地址超过255时,只能采用dx间接端口寻址

(2)输出指令
OUT 端口地址,累加器

输出指令将 AL 中的一个字节,或在AX中的一个字,输出到指定端口。与 IN 指令一样,端口地址可采用直接方式和间接方式。

直接方式
OUT PORT,AL
OUT PORT,AX

间接方式
OUT DX,AL ;PORT 是一个字时,相当于同时从n和n+1分别输出一个字节。
OUT DX,AX

数据传送方式

CPU与外设之间交换的信息

CPU与外设之间交换的信息包括数据、控制和状态信息。尽管这三种信息具有不同性质,但他们都通过 IN 和 OUT 指令在数据总线上进行传送,所以通常采用分配不同端口的方法将它们加以区别。

数据是CPU和外设真正要交换的信息。数据通常为8位或16位。可分为各种不同类型。不同的外设要传送的数据类型也是不同的。
控制信息输出到I/O接口,告诉接口和设备要做什么工作。
从接口输入的状态信息,表示 I/O 设备当前状态。在输入数据前,通常要先取得表示设备是否已准备好的状态信息;在输出数据前,往往要先取得表示设备是否在忙的状态信息。

数据传送方式

系统中数据传送的方式主要有:
(1)无条件传送方式
在不需要查询外设装填,即已知外设已经准备好或不忙时,可以直接使用 IN 或 OUT 指令实现数据传送。这种方式软件实现简单,只要在指令中指明端口地址,就可选通过指定外设进行输入输出。
无条件传送方式是方便的,但要求外设工作速度能与CPU同步,否则就可能出错。例如,在外设还没有准备好的情况下,就用 IN 指令得到数据就可能是不正确的数据。

(2)查询方式
查询传送方式适用于 CPU 与外设不同步的情况。输入之前,查询外设数据是否已准备好,弱数据已准备好,则输入;否则继续查询,直到数据准备好。输出之前,查询外设是否 “忙”,若不”忙”,则输出;否则继续查询,直到不 “忙”。也就是说,要等待到外设准备好时才能输入或输出数据,而通常外设速度要远远慢于 CPU 速度,于是查询时间就将花费大量时间。

(3)中断方式
为了提供CPU的效率,可采用中断方式。当外设准备好时,外设向CPU 发出中断请求,CPU转入中断处理程序,完成输入输出工作。

(4)直接存储器传送(DMA)方式
由于告诉 I/O 设别(如磁盘机等)准备数据的时间短,要爱u传送速度快等特点,所以一般采用直接存储器传送方式,即告诉设备与内存储器直接交换数据。这种方式传送数据是成组进行的。其过程是:先把数据在告诉外设中存放的起始位置、数据在内存储器中存放的起始地址、传送数据长度等参数输出到连接高速外设的接口(控制器),然后启动高速外设,设备住呢比开始直接传送数据。当高速外设直接传送准备好后,向处理机发送一个直接传送的请求信号,处理机以最短时间批准进行直接传送,并让出总线控制权,高速外设在其控制器控制下交换数据。数据交换完毕后,由高速外设发出 “完成中断请求”,并交回总线控制权。处理机响应上述中断,由对应的中断处理程序对高速外设进行控制或对已经传送的数据进行处理,中断返回后,原程继续运行。

存取 RT/CMOS RAM

关于 RT/CMOS RAM

  1. RT/CMOS RAM 是一种低耗电存储器,其主要作用是用来存放系统配置信息,以及系统日期。
  2. RT/CMOS RAM 作为一个 I/O 接口芯片,系统分配的 I/O 端口地址为 70H 至 7FH,通过 IN 和 OUT 指令对其进行存取。它共提供64个字节RAM单元,前14个字节用于实时钟,剩下的50个字节用于系统配置。
    1

存取 RT/CMOS RAM

在存取 RT/CMOS RAM 芯片内部的 64 个字节内容时,往往要分两步进行。即先把要存取单元的地址传入端口70H,然后再存取端口 71H。单元地址指的是上图中的位移。

(1)读操作代码片段

1
2
3
4
MOV AL,n        ;n 是要访问的单元地址
OUT 70H,AL ;把要访问的单元地址送入端口
JMP $+2 ;延时
IN AL,71H ;从数据端口取访问单元的内容

案例:读取 CMOS 并显示当前是几月份。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
;程序名:RCMOS.asm
;功能:演示如何读取 CMOS RAM,并显示当前是几月份。
assume cs:code

code segment
start:
mov al,8
out 70h,al
jmp $+2
in al,71h
;
mov dl,al
add dl,30h
mov ah,2
int 21h
;
mov ax,4c00h
int 21h
code ends
end start

2

(2)写操作代码片段

1
2
3
4
5
MOV AL,n        ;n 是要访问的单元地址
OUT 70H,AL ;把要访问单元的地址传送至端口
JMP $+2 ;延时
MOV AL,m ;m 是要输出的数据
OUT 71H,AL ;把数据从数据端口输出

案例:修改并重新读取 CMOS 报警秒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
;程序名:WCMOS.asm
;功能:演示如何修改并重新读取 CMOS RAM,并显示当前报警秒。
assume cs:code

code segment
start:
;修改 CMOS 报警秒
mov al,1
out 70h,al
jmp $+2
mov al,5
out 71h,al
;读取CMOS月份
mov al,1
out 70h,al
jmp $+2
in al,71h
;输出当前月份信息
mov dl,al
add dl,30h
mov ah,2
int 21h
;
mov ax,4c00h
int 21h
code ends
end start

3

CMOS RAM累加和检查和查询方式传送数据,就不写了。这些都是和硬件相关的知识,暂时用不到。

中断

中断是一种使 CPU 挂起正在执行的程序而转去处理特殊事件的操作。特殊事件是来自外设的输入输出请求,例如键盘引起的键盘中断,由串行口引起的串行口中断等;也可能是计算机的一些异常事件或其他内部原因,例如:除数为0

中断的传送方式

中断传送方式的具体过程是:当 CPU 需要输入或输出数据时,先作一些必要的准备工作(有时包括启动外部设备),然后继续执行程序;当外设完成一个数据的输入或输出后,则向 CPU 发出中断请求,CPU 就挂起正在执行的程序,转去执行输入或输出擦欧总,在完成输入或输出操作后,返回原程序继续执行。
中断传送方式是CPU和外部设备进行输入输出的有效方式,一直被大多数计算机所采用,它可以避免因反复查询外部设备的状态而浪费时间,从而提高CPU的效率。不过,每中断依次,只传送依次数据,数据的传送效率并不高,所以,中断传送方式一般用于低速外设。另外,与查询方式相比,中断方式实现比较复杂,对硬件的条件也较多。

中断向量表

IBM PC 系列及其兼容机共能支持256种类型的中断,系统给每一种中断都安排一个中断类型号(简称中断号),中断类型号依次为 0~FFH。例如,属于外部中断的定时器中断类型号为 08 和键盘中断类型号 09,属于内部中断的除法出错中断类型号为 0 等等。

每种类型的中断都由响应的中断处理程序来处理,为了使系统在响应中断后,CPU 能快速地转入对应地中断处理程序,系统用一张表来保存这些中断处理程序的入口地址,该表就称为中断向量表。中断向量表的每一项保存一个中断处理程序的入口地址,它相当于一个指向中断处理程序的指针,所以称为中断向量。中断向量也依次编号为 0~FFH,n号中断向量就是保存处理中断类型为 n 的中断处理程序的入口地址。所以,一般不再取分中断类型号和中断向量号。

中断向量表被存放在内存最低端的 1K 字节空间种。其中每个中断向量占用四个字节,前两个字节保存中断处理程序入口地址的偏移,后两个字节保存中断处理程序的入口地址的段值,所以含有256个中断向量的中断向量表需要占用1K字内存空间。

中断向量所在的单元地址=中断向量号4,如 21h号中断向量在中断向量表的 214=84=54h=0000:0054h 处。

在 IBM PC系列及其兼容机上,除保留给用户使用的 60H68H 和 F1HFFH 中断向量号外,可以认为其他中断向量号已被分配。
4
5

中断向量不一定非要指向中断处理程序,也可以作为指向一组数据的指针。如,1DH号中断向量就指向显示器参数,1EH号中断向量指向软盘基数。当然,如果中断向量m没有指向中断处理程序,那么就不应该发生类型为m的中断。

设置和获取中断向量

在系统程序或应用程序由于某种需要而提供新的中断处理程序时,就要设置对应的中断向量,使其指向新的中断处理程序。
下面的程序片段直接设置 n 号中断向量,假设对应中断处理程序的入口标号是

1
2
3
4
5
6
7
8
9
10
INTHAND:
....
MOV AX,0
MOV DS,AX
MOV BX,n*4 ;准备设置n号中断向量
CLI ;关中断
MOV WORD PTR [BX],OFFSET INTHAND ;置偏移
MOV WORD PTR [BX+2],SEG INTHAND ;置段值
STI
....

在上面的程序片段中使用了关中断指令 CLI,关中断就是关闭中断指令,此时中断指令是无法使用的。这样做目的是保证用于设置中断向量的两条传送指令能够连续执行,避免中间执行其他中断指令导致中断程序出错。如果能确定当前是关中断状态,当然就不再需要使用关中断指令,也不需要随后地开中断指令。另外,如果能够肯定在设置 n 号中断向量过程中不发生类型为n地中断,那么可不考虑是否设为关中断状态。

实际上,通常是利用 DOS 提供地25h号系统功能调用设置中断向量,这样可以避免考虑许多细节。
25号系统功能调用时设置中断向量,其入口参数如下:

1
2
3
AL=中断向量(类型)号
DS=中断处理程序入口地址的段值
DX=中断处理程序入口地址的偏移

下面的程序片段设置 n 号中断向量,假设对应中断处理程序入口标号是

1
2
3
4
5
6
7
8
9
INTHAND:
....
MOV AX,SEG INTHAND
MOV DS,AX
MOV DX,OFFSET INTHAND
MOV AH,25H
MOV AL,n
INT 21H
...

有时需要取得中断向量。例如:在应用程序要用自己的中断处理程序代替系统原有的某个中断处理程序时,先要保存原中断向量,待应用程序结束时再恢复原中断向量。
下面的程序片段直接从中断向量中取得 n 号中断向量,并保存到双字变量 OLDVECTOR 中:

1
2
3
4
5
6
7
8
....
XOR AX,AX
MOV ES,AX
MOV AX,ES:[n*4]
MOV WORD PTR OLDVECTOR,AX
MOV AX,ES:[n*4+2]
MOV WORD PTR OLDVECTOR+2,AX
....

与利用 DOS 功能调用设置中断向量一样,实际上一般都利用ODS提供的35H号系统功能调用取得中断向量。35H号系统调用的功能是获取中断向量,其入口参数如下:
入口参数:

AL=中断向量(类型)号

出口参数:

1
2
ES=中断处理程序入口地址的段值
BX=中断处理程序入口地址的偏移

下面是程序片段取得n号中断向量,并将其保存到双字变量 OLDVECTOR 中:

1
2
3
4
5
6
7
....
MOV AH,25H
MOV AL,N
INT 21H
MOV WORD PTR OLDVECTOR,ES
MOV WORD PTR OLDVECTOR,BX
....

中断响应过程

中断响应过程

通常 CPU 在执行完每一条指令后均要检测是否有中断请求,在有中断请求且满足一定条件时就响应中断,这个过程如下图所示:
6

  1. 取得中断类型号
  2. 把标志寄存器的内容压入堆栈中
  3. 禁止为外部中断和单步中断(使IF和TF标志位为0)
  4. 把下一条要执行的指令的地址压入堆栈(CS和IP)
  5. 根据中断类型号从中断向量表中取中断处理程序入口地址
  6. 转入中断处理程序

在CPU响应中断转入中断程序时,中断处理程序在最后从堆栈中弹出返回地址和原标志寄存器的值结束中断,返回被中断的程序。

中断返回指令

中断处理程序利用中断返回指令从堆栈中弹出返回地址和原标志值。中断返回指令格式如下:

1
IRET

该指令的功能从中断返回。具体操作如下所示:

1
2
3
4
5
IP <= [SP]
SP <= SP+2
CS <= [SP]
SP <= SP+2
FLAGS <= [SP]

在执行中断返回指令 IRET 时的堆栈变化如下
7

外部中断

由于发生在 CPU 外部的某个事件引起的中断称为外部中断。外部中断以完全随机的方式中断正在运行的程序。
外部中断有两条外部中断请求线:INTR(可屏蔽中断请求),NMI(非屏蔽中断请求)
8

可屏蔽外部中断

键盘和硬盘等外设的中断请求都通过中断控制器 8259A 传给可屏蔽中断请求INTR。中断控制器 8259A 共能接收 8 个独立的中断请求信号 IRQ0IRQ7。在AT机上,有两种中断控制器 8259A,一主一从。从 8259A 连接到主 8259A 的 IRQ2 上,这样 AT 系统就可以接收15个独立的中断请求信号。
中断控制器 8259A 在控制外设中断方面起着重要的作用。如果接收到一个中断请求信号,并且满足一定的条件,那么它就把中断请求信号传到 CPU 的可屏蔽中断请求线 INTR,使CPU感知到有外部中断请求;同时也把相应的中断请求类型号送给 CPU,使 CPU 在响应中断时可根据中断类型号取得中断向量,转相应的中断处理程序。
中断控制器 8259A 是可编程的,也就是说可由程序设置它如何控制中断。在及其系统加电初始化期间,已对 8259A 进行过初始化。在初始化时规定了在传出中断请求 IRQ0
IRQ7时,送出的对应中断类型号分别是 08H~0FH 如下图所示
1

例如,设传出中断请求 IRQ1(键盘中断),那么送出的中断类型号为9,所以键盘中断类型号为9,键盘中断处理程序入口地址存放在 9 号中断向量中。
从普通汇编语言设计角度看,中断控制器 8259A 包含了两个寄存器:中断屏蔽寄存器和中断命令寄存器,它们决定了传出一个中断请求信号的条件。中断屏蔽寄存器的 I/O 端口地址是 21H,它的8位对应控制8个外部设备,通过设置这个寄存器的某个位为 0 或 1 来允许或禁止相应外部设备中断。当第 i 位为 0 时,表示允许传出来自 IRQi 的中断请求信号,当第 i 位为 1 时,表示禁止传出来自 IRQi 的中断请求信号。中断屏蔽寄存器的内容称为中断屏蔽字。在 PC 系列及其兼容机上,中断屏蔽寄存器各位与对应的外设的关系如下图所示:
9
例如:为了使中断控制器 8259A 只传出来自键盘的中断请求信号,可设置中断屏蔽字 11111101B,程序片段如下

1
2
MOV AL,11111101B
OUT 21H,AL

例如:下面的程序片段使中断屏蔽寄存器的位 4 为 0,从而允许传出来自串行通信口 1 的中断请求信号:

1
2
3
IN AL,21H
AND AL,11101111B
OUT 21H,AL

尽管中断控制器把外设的中断请求信号由 INTR 传给 CPU,但 CPU 是否响应还取决于中断允许标志位 IF。如果 IF 为 0,则 CPU 仍不响应由 INTR 传入的中断请求。所以,由 INTR 传入的外部中断请求称为可屏蔽外部中断请求,由此引起的中断称为可屏蔽中断。由于外设的中断请求均由 INTR 传给 CPU,CPU 响应外设中断请求称为开中断(IF=1),反之称为关中断(IF=0)。CPU 在响应中断时会自动关中断,从而避免在中断过程中再响应其他外设中断。程序员在程序中可使用关中断指令 CLI和开中断指令 STI。

非屏蔽外部中断

当收到从 NMI 传来的中断请求信号时,不论是否处于开中断状态 CPU 总会响应。所以,由 NMI 传入的外部中断请求称为非屏蔽外部中断请求,由此引发的中断称为非屏蔽中断。不可屏蔽中断请求由电源掉电、存储器出错或者总线奇偶校验出错等紧急故障产生,要求 CPU 及时处理。

内部中断

由发生在 CPU 内部的某个事件引起的中断称为内部中断。由于内部中断是 CPU 在执行某些指令时产生,所以也称为软件中断。其特征是:不需要外部硬件的支持;不受标志 IF 的控制。

中断指令 INT 引起的中断

中断指令一般格式如下:

INT n

其中,n是一个 0~0FFH的立即数。CPU 在执行中断指令后,便产生一个类型号为 n 的中断,从而转入对应的中断程序。
例如,为了调用DOS系统功能,就在程序中安排如下的中断指令:

INT 21H

当CPU 执行该指令后,就产生一个类型为 21H 的中断,从而转入对应的中断处理程序,也即转入 DOS 系统功能服务程序。值得指出的是,程序员根据需要在程序中安排中断指令,所以它不会真正随机产生,而完全受程序控制。

CPU 遇到特殊情况引起的中断

  1. 除法错误中断
    在执行除法指令时,如果 CPU 发现除数为 0 或者商超过规定的范围,那么就产生一个除法错误中断,中断类型号为0。
    例如,在执行下面的程序片段时,会产生一个0号类型的中断:
    1
    2
    3
    MOV AX,1234
    MOV CL,3
    DIV CL ;商超过 255(AL容纳不下)

为了避免产生0号类型的中断,可改写上述程序片段如下:

1
2
3
4
5
MOV AX,1234
MOV CL,3
XOR DX,DX
XOR CH,CH
DIV CX
  1. 溢出中断
    8086/8088提供一条专门检测运算溢出的指令,该指令的格式如下:
    1
    INTO
    在溢出标志位 OF 置 1 时,如果执行该指令,则产生溢出中断。溢出中断的类型号规定为 4。如果溢出标志 OF 为 0,则执行该指令后并不断产生溢出中断。

用于程序调试的中断

  1. 单步中断
    如单步中断 TF 为 1,则在每条指令执行后产生一个单步中断,中断类型号规定为1。产生但不中断后,CPU就执行单步中断处理程序。由于CPU在响应中断时,已把 TF 置为0,所以,不会以单步方式执行单步中断程序。通常,由调试工具把 TF 置 1,在执行完一条被调试程序的指令后,就转入单步中断处理程序,一般情况下,但不中断处理程序报告各寄存器的当前内容,程序员可据此调试程序。

  2. 断点中断
    8086/8088 提供一条特殊的中断指令 “INT 3”,调试工具可用它替换断点处的代码,当 CPU 执行这条中断指令后,就产生类型号为3的中断。这种中断称为断点中断。通常情况下,断点中断处理程序恢复被替换的代码,并报告各寄存器的当前内容,程序员可据此调试程序。所以说中断指令 “INT 3”特殊是因为它只有一个字节长,其他的中断指令长 2 字节。

中断优先级和中断嵌套

  1. 中断优先级
    系统中有多个中断源,当多个中断源同时向 CPU 请求中断时,CPU 按系统设计时规定的优先级响应中断请求。在 IBM PC 系列及其兼容机系统中,规定的优先级如下:
    10

  2. 中断嵌套
    CPU 在执行中断处理程序时,又发生中断,这种情况称为中断嵌套。
    在中断处理过程中,发生内部中断,引起中断嵌套是经常的事。例如:CPU在执行中断处理程序时,遇到软中断指令,就会引起中断嵌套。在中断处理过程中,发生非屏蔽中断,也会引起中断嵌套。
    由于 CPU 在响应中断的过程中,已自动关中断,所以 CPU 也就不再自动响应可屏蔽中断。如果需要在中断处理过程的某些时候响应可屏蔽中断,那么可在中断处理程序中安排开中断指令,CPU在执行中断指令后,就处于开中断状态,也就可以响应可屏蔽中断了,直到再关中断。所以,如果再中断处理程序中使用了开中断指令,也就可能会发生可屏蔽中断引起的中断嵌套。
    8086/8088没有限制中断嵌套的深度(层次),但客观生受到堆栈容量限制。

中断处理程序的设计

CPU 在响应中断后,自动根据中断类型,取中断向量,并转入中断处理程序,所以,具体的处理工作由中断处理程序完成。不同的中断处理,由不同的中断处理程序完成。对应外设中断的外设中断处理程序和对应指令中断的软中断处理程序有些区别,下面对它们的设计分别作些介绍。

  1. 外设中断处理程序
    在开中断的情况下,外设中断的发生时随机的,在设计外设中断处理程序时必须充分注意到这一点。外设中断处理程序的主要步骤如下:
    (1)必须保护现场。这里的现场可理解为中断发生时CPU各内部寄存器的内容。CPU 在响应中断时,已把各标志和返回地址压入堆栈,所以要保护的现场主要是指通用寄存器的内容和除代码段寄存器外的其他三个段寄存器的内容。因为中断的发生是随机的,所以凡是中断处理程序中要重新赋值的各个寄存器的原有内容必须预先保护。保护的一般方法是把它们压入堆栈。
    (2)尽快完成中断处理。外设中断处理必须尽快完成,所以外设中断处理必须追求速度上的高效率。因为在进行外设中断处理时,往往不再响应其他外设的中断请求,因此必须快,以免影响其他外设的中断请求。
    (3)恢复现场。在中断处理完成后,依次恢复被保护寄存器的原有内容。
    (4)通知中断控制器中断已结束。如果应用需要,也可提早通知中断控制器中断结束,这样做必须考虑到外设中断的嵌套。
    (5)利用 IRET 指令实现中断返回。
    此外,应及时开中断。除非必要,中断处理程序应尽早开中断,以便CPU响应具有更高优先级的中断请求。

  2. 软中断处理程序
    由中断指令引起的软件中断尽管是不可屏蔽的,但它不会随机发生,只有在CPU执行了中断指令后,才会发生。所以,中断指令类似于子程序调用指令相应的软中断处理程序在很大程度上类似于子程序,但并不等同于子程序。软中断的主要步骤如下:
    (1)考虑切换堆栈,由于软中断处理程序往往在开中断状态下执行,并且可能较复杂(要占用大量的堆栈空间),所以因该考虑切换堆栈。切换堆栈对实现中断嵌套等均较为有利。
    (2)及时开中断。开中断后,CPU就可响应可屏蔽的外设中断请求,或者说使外设中断请求可及时得到处理。但要注意,如如果软中断程序要被外设中断处理程序 “调用”,则是否要开中断或者核实开中断应另外考虑。
    (3)应保护现场。应该保护中断处理程序要重新赋值的寄存器原有内容,这样使用软中断指令时,可不必考虑有关寄存器内容的保护问题。
    (4)完成中断处理。但不必过分追求速度上的高效率,除非它是被外设中断处理程序 “调用” 的。
    (5)恢复现场。依次恢复被保护寄存器的原内容。
    (6)堆栈切换。如果在开始时切换了堆栈,那么也要再重新切换回原堆栈。
    (7)一般利用 IRET 指令实现中断返回。

基本输入输出系统 BIOS

介绍 BIOS 基本概念的基础上,介绍键盘输入、显示和打印输出。

基本输入输出系统 BIOS 概念

固化在 ROM 中的基本输入输出系统 BIOS 包含了主要I/O设备的处理程序和许多常用例行程序,它们一般以中断处理程序的形式存在。例如:负责显示输出的 显示I/O程序作为 10H 号中断处理程序存在,负责打印输出的打印I/O程序作为17H号中断处理程序存在,而负责键盘输入的键盘 I/O 程序作为 16H号中断处理程序存在。再如,获取内存容量的例行程序作为 12H号中断处理程序存在。BIOS是直接建立再硬件基础上。
磁盘操作系统DOS建立在BIOS的基础上,通过BIOS操纵控制硬件。例如,DOS调用BIOS显示I/O程序完成显示输出,调用打印I/O程序完成打印输出,调用键盘 I/O程序完成键盘输入。尽管 DOS和BIOS都提供某些相同的功能,但它们之间的层次关系是明显的。
应用程序DOS、BIOS和外设接口之间的关系如下图所示:
11
通常应用程序应该调用 DOS 提供的系统功能完成输入输出或其他操作。这样做不仅实现容易,而且对硬件的依赖性最少。但有时 DOS不提供某种服务,例如,取打印机状态信息,那么就不能调用DOS实现了。
应用程序可以通过BIOS进行输入输出或完成其他功能。在下列三种场合可考虑BIOS:一是需要利用BIOS提供而DOS不提供的功能场合;二是不能利用 DOS 功能调用的场合;三是出于某种原因需要绕过DOS的场合。由于 BIOS 提供的设备处理程序和常用例行程序都以中断处理程序的形式存在,所以应用程序调用BIOS较为方便。但 BIOS 毕竟比 DOS 更靠近硬件。
应用程序也可以直接操纵外设接口来控制外设,从而获得速度上最高的效率,但这样的应用程序不仅复杂而且与硬件关系十分密切,此外,还需要程序员对硬件性能比较了解熟悉。所以,应用程序一般不能直接与硬件发生关系。
值得指出的是,有时应用程序需要扩充或替换 ROM BIOS 中的某些处理程序或例行程序,那么这些新的 BIOS程序原则上不能调用 DOS 提供的功能。

键盘输入

键盘中断处理程序

当用户按键盘时,键盘接口会得到一个代表被按键的键盘扫描码,同时产生一个中断请求。如果键盘中断是允许的,并且CPU处于开中断状态,那么 CPU 通常会响应中断请求。由于键盘中断的中断类型号为 9,所以 CPU 响应键盘中断,就是转入 9 号中断处理程序。我们把 9 号中断处理程序称为键盘中断处理程序。它属于外设中断处理程序这一类。
键盘中断处理程序首先从键盘接口取得代表被按键的扫描码,然后根据扫描码判定用户所按的键并作相应的处理,最后通知中断控制器中断结束并实现中断返回。键盘上面的键可简单地分成五种类型:字符键(字母、数字和符号等),功能键(如F1等),控制键(Ctrl、Alt和左右Shift),双态字(如Num Lock和Caps Lock等),特殊请求键(如Print screen等)。键盘中断处理程序对五种键地基本处理如下:
如果用户按的是双态键,那么就设置有关标志,在 AT 以上档次的系统上还要改变 LED 指示器状态。如果用户按的是控制键,那么就设置有关标志。如果用户按的是功能键,那么就根据键盘扫描码和是否按下某些控制键(如Alt)确定系统扫描码,把系统扫描码和一个全0字节一起存入键盘缓冲区。如果用户是字符键,那么就根据键盘扫描码和是否按下某些控制键(如Ctrl)确定系统扫描码,并且得出对应的ASCII码,把系统扫描码和ASCII码一起存入键盘缓冲区。如果用户按的是特殊请求键,那么就产生一个相应的动作,例如用户按 Print screen 键,那么就调用 5H 号中断处理程序打印屏幕。

键盘缓冲区

键盘缓冲区是一个先进先出的环形队列,结构和占用的内存区域如下:

1
2
3
BUFF_HEAD  DW ?         ;0040:001AH
BUFF_TAIL DW ? ;0040:001CH
KB_BUFFER DW 16 DUP(?) ;0040:001EH~003DH

BUFF_HEAD 和 BUFF_TAIL 是缓冲区的头指针和尾指针,当这两个指针相等时,表示缓冲区为空。由于缓冲区本身16个字,而存放一个键的扫描码和对应的 ASCII 码需要占用一个字,所以键盘缓冲区可实际存放 15 个键的扫描码和ASCII码。键盘中断处理程序把所按字符键或功能键的扫描码和对应的ASCII码依次存入键盘缓冲区。如缓冲区已满,则不再存入,而是发出 “嘟” 的一声。

键盘中断处理程序根据控制键和双态键建立的标志在内存单元 0040:0017H 字单元中。

案例:验证键盘缓冲区是否在 0040:001EH~003DH 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
;程序名:keyboard.asm
;功能:输出 0040:001EH 键盘缓冲区的内容,再用 int 16H 的 0 号功能接收键盘输入,最后再打印键盘缓冲区内容。
assume cs:code

code segment
start:
call MEM_ECHO
call NEWLINE
;
mov cx,10
L1:
mov ah,0
int 16h
loop L1
;
call NEWLINE
call MEM_ECHO
L2:
;
mov ax,4c00h
int 21h

MEM_ECHO PROC
push cx
push ax
push es
push di
push bx
push dx
;
mov cx,2eh
mov ax,0040h
mov es,ax
mov di,001Eh
mov bx,cx
MEM_ECHO1:
sub bx,0fh
MEM_ECHO2:
mov dl,20h
mov ah,2
int 21h
;
call BYTE_ECHO
inc di
cmp cx,bx
jz MEM_ECHO3
loop MEM_ECHO2
jmp MEM_ECHO4
MEM_ECHO3:
call NEWLINE
jmp MEM_ECHO1
;
MEM_ECHO4:
pop dx
pop bx
pop di
pop es
pop ax
pop cx
ret
MEM_ECHO ENDP

BYTE_ECHO PROC
push ax
push es
push di
push cx
push dx
mov al,byte ptr es:[di]
call AHTOASC
mov cx,2
mov dx,ax
BYTE_ECHO1:
xchg dh,dl
mov ah,2
int 21h
loop BYTE_ECHO1
pop dx
pop cx
pop di
pop es
pop ax
ret
BYTE_ECHO ENDP
;-----------------------------------------------------
AHTOASC PROC
;
; 功能:把8位二进制数转换为2位 ASCII 码
; 传参方式:寄存器传参
; 入口参数:AL=要转换的值
; 出口参数:AH = 十六进制数高位的 ASCII 码
; AL = 十六进制数低位的 ASCII 码
;其他说明:(1)近过程
; (2)除 AX 寄存器外,不影响其他寄存器
; (3)调用HTOASC 实现十六进制数到ASCII码的转换
;-----------------------------------------------------
mov ah,al
shr al,1
shr al,1
shr al,1
shr al,1
call HTOASC
xchg ah,al
call HTOASC
ret
AHTOASC endp

;-----------------------------------------------------
HTOASC PROC NEAR
;
; 功能:把一个十六进制数转换为对应的 ASCII 码
; 传参方式:寄存器传参
; 入口参数:al
; 出口参数:al
;-----------------------------------------------------
pushf
and al,0fh
cmp al,0ah
jb num_add
add al,37h
jmp pop_stack
num_add:
add al,30h
pop_stack:
popf
ret
HTOASC endp

;--------------------------------------------------------
NEWLINE PROC
; 功能:形成回车和换行(光标移到写一行首)
; 入口参数:无
; 出口参数:无
; 说 明:通过显示回车符形成回车,通过显示换行符形成换行
;---------------------------------------------------------
push ax
push dx
mov dl,0dh
mov ah,2
int 21h
mov dl,0ah
mov ah,2
int 21h
pop dx
pop ax
ret
NEWLINE endp
code ends
end start

15

我这边输入的是键盘字母键的第一行 q~p,键盘缓冲区中也存入了相应的字符.

键盘I/O程序

尽管系统程序和应用程序可从键盘缓冲区中取得用户所按键的代码,但除非特殊情况,一般不宜直接存取键盘缓冲区,而应调用BIOS提供的I/O程序。
键盘 I/O 程序以 16H 号中断处理程序的形式存在,它属于软中断程序这一类。它的主要功能是进行键盘输入。一般情况下,系统程序和应用程序的键盘输入最后都是调用它完成的。简单的键盘 I/O 程序从键盘缓冲区中取得所有键的ASCII码和扫描码返回给调用者。键盘中断程序、键盘缓冲区和键盘I/O程序之间的关系如下图所示:
12

键盘 I/O 程序的功能和调用方法

键盘 I/O 程序提供的每一个功能都有一个编号。在调用键盘 I/O 程序时,把功能编号置入 AH 寄存器,然后发出中断指令 “INT 16H”。调用返回后,从有关寄存器中取得出口参数。除保存出口参数的寄存器外,其他寄存器内容保持不变。
我们把控制键和双态键统称为变换键,调用键盘 I/O 程序的 2 号功能可获得各变换键的状态。变换键状态字节各位的定义如下图所示:
13
其中高4位记录双态键的变换情况,每按一下双态键,则对应的位值取反;低4位反应控制键是否正被按下,按着某个控制键时,对应的位为1。

16H 号基本功能

14
如果键盘缓冲区中有字符,那么中断处理就会很快结束,读到的字符是调用发出之前用户按下的字符。如果键盘缓冲区空,那么就等待用户按键后调用才会返回,读到的字符是调用发出之后按下的字符。如果程序员处于某种原因,要从键盘取得在调用发出之后用户按下的字符,那么就要先清除键盘缓冲区。下面的程序片段先清除键盘缓冲区,然后再从键盘读取一个字符:

1
2
3
4
5
6
7
8
9
10
11
AGAIN:
MOV AH,1
INT 16H ;判断缓冲区是否为空
JZ NEXT ;为空,跳转
MOV AH,0
INT 16H ;从键盘中取走一个字符
JMP AGAIN ;继续
NEXT:
MOV AH,0
INT 16H ;等待键盘输入
....

案例:查验键盘缓冲区指针变化(相关子程序查看上面的案例)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
;程序名:keyboard_point.asm
;功能:查验键盘缓冲区指针变化
assume cs:code

code segment
start:
call MEM_ECHO
call NEWLINE
;
mov cx,2
;
mov ah,0
int 16h

call MEM_ECHO
call NEWLINE
L1:
mov ah,0
int 16h
loop L1
;
call NEWLINE
call MEM_ECHO

mov ax,4c00h
int 21h
code ends
end start

16

从运行结果来看,我们键盘输入一个字符后,触发9号中断,将数据存储到键盘缓冲区,尾指针+2。随后用 int 16每取一个字母,键盘缓冲区头指针就会+2。通过这个案例应该更容易理解上面清除键盘缓冲区的代码片段。

案例:通过修改缓冲区头指针,使键盘缓冲区不为空,并打印出键盘缓冲区的数据(相关子程序查看上面的案例)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
assume cs:code,ds:data

data segment
message db 'Keyboard buffer is empty','$'
data ends

code segment
start:
mov ax,data
mov ds,ax
call MEM_ECHO
call NEWLINE
;
mov ax,0040h
mov es,ax
mov si,001Ah
mov bx,es:[si] ;备份键盘缓冲区头指针

mov cx,5 ;输入5个字符
L1:
mov ah,0
int 16h
loop L1
mov es:[si],bx ;还原键盘缓冲区头指针
call NEWLINE ;打印键盘缓冲区
call MEM_ECHO
call NEWLINE
L2:
;判断键盘缓冲区是否为空
mov ah,1
int 16h
jnz L3
call NEWLINE
mov ah,9
lea dx,message
int 21h
jmp L4
L3:
;键盘缓冲区不为空就打印结果
mov ah,0
int 16h
mov dl,al
mov ah,2
int 21h
jmp L2
;
L4:
call NEWLINE
call MEM_ECHO
mov ax,4c00h
int 21h

17

int 16H 的 1 号功能只做判断,不会改变头指针地址。通过该程序,加深键盘缓冲区的了解。

例1:写一个程序完成如下功能:读键盘,并把所按键盘显示出来,在检测到按下 SHIFT 键后,就结束运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
;程序名:T5-1.asm
;功能:读键盘,并把按键显示出来,在检测到按下shift键后,就结束运行
;步骤1:调用键盘I/O程序(int 16中断)的2号功能取得变换键状态字节
;步骤2:判断是否按下SHIFT键
;步骤3:在调用 0 号功能读键盘之前,先调用1号功能判断键盘是否有键可读,否则会导致不能及时检测到用户按下的SHIFT
;步骤4:0号功能读键盘
;步骤5:DOS 6号功能调用显示

assume cs:code,ds:data

data segment
l_shift = 00000010b
r_shift = 00000001b
data ends

code segment
start:
mov ah,2
int 16h
test al,l_shift+r_shift
jnz stop
mov ah,1
int 16h
jz start
mov ah,0
int 16h
mov dl,al
mov ah,6
int 21h
jmp start
stop:
mov ax,4c00h
int 21h
code ends
end start

键盘 I/O 程序的 2 号功能取得变换键状态字节,其中最低的两位有一个为1 表示按下 shift 键。在调用 0 号功能读键盘之前,要先调用 1 号功能判断键盘缓冲区是否为空,不然读错了会导致不能及时检测到用户按下的 shift 键。

显示输出

显示器通过显示适配卡与系统相连,显示适配卡是显示输出的接口。母千常用的显示适配卡是 VGA 和 TVGA 等。它们都支持两种显示方法:文本显示方式和图形显示方式,每一类显示方式还含有多种显示模式。

文本显示方式

所谓文本显示方式是指以字符为单位显示的方式。字符通常是指字母、数字、普通符号(如运算符号)和一些特殊符号(如棱形块和矩形块)。
通常 0~3 号显示模式为文本显示方式,它们之间的区别是每屏可显示的字符和可使用的颜色数目不同。这里以最常用的 3 号显示模式作为代表介绍。
在 3 号文本显示模式下,显示器的屏幕被划分成 80 列 25 行,所以每一屏最多可显示2000(80x25)个字符。我们用行号和位号组成的坐标来定位屏幕上的每个可显示位置,左上角的坐标规定为(0,0),向右增加列号,向下增加行号。

显示属性

屏幕上显示的字符取决于字符代码和字符属性。这里的属性是指显示属性,它规定字符显示时的特性。在单色显示时,属性定义了前景色和背景色。下图给出了彩色显示时字节各位的定义。
18

在属性字节中,RGB分别表示红、绿、蓝,I表示亮度,BL表示闪烁。位03组合 16 中前景色,位46组合8种背景色,亮度和闪烁只能用于前景色。下图给出了彩色文本模式下 IRGB 组合成的通常颜色。

19
20

显示存储区

显示适配卡带有显示存储器,用于存放屏幕上显示文本的代码及属性或图形信息。显示存储器作为系统存储器的一部分,可用访问普通内存的方法访问显示存储器。通常,为显示存储器安排的存储地址空间的段值是 B800H 或 B000H,对应的内存区域称为显示存储区。
3号文本模式下,屏幕上每一个显示位置依次对应显示存储区的两个字节单元,这种对应关系如下图所示:
21

低字节是 ASCII 码,高字节是属性

为了屏幕上某个位置显示字符,只需要把显示字符的代码及其属性填到显示存储区种的对应存储单元即可。

案例:写一个程序在屏幕的左上角以黑底白字显示字符 “A”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
;程序名:screen.asm
;功能:在屏幕的左上角以黑底白字显示字符 "A"
;这里意思是:把 黑底白字的字符'A' 写到 B800H:0000H 处。显存种存储字符需要两个字节,高字节存储字符属性,低字节存储 ASCII 码。
assume cs:code

code segment
start:
mov ax,0B800h
mov ds,ax
mov bx,0
;高字节存储字符属性,低字节存储 ASCII 码
mov al,'A'
mov ah,07h
mov [bx],ax
mov ax,4c00h
int 21h
code ends
end start

22

为了了解屏幕上某个显示位置所显示的字符是什么,或显示的颜色是什么,那么只要从显示存储区种的对应存储单元中取出字符的代码和属性即可。

案例:写一个程序片段在屏幕右下角显示黑底蓝字的字母’A’,再复制该字符到屏幕左上角。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
;程序名:screen1.asm
;功能:写一个程序片段在屏幕右下角显示黑底蓝字的字母'A',再复制该字符到屏幕左上角。
assume cs:code

code segment
start:
mov ax,0B800h
mov ds,ax
mov bx,(80*2*25)-2
;在右下角输出黑底蓝字字符'A'
mov ah,1
mov al,'A'
mov [bx],ax
;复制字符到屏幕左上角
mov ax,[bx]
xor bx,bx
mov [bx],ax
mov ax,4c00h
int 21h
code ends
end start

例2:采用直接写屏法在屏幕上用多种属性显示字符串 “HELLO”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
;程序名:T5-2.asm
;功能:采用直接写屏的方法在屏幕上显示"HELLO",然后用户按"-"键后后,再换一种属性显示,如果按ESC键结束程序运行。
assume cs:code,ds:data

;将坐标定义为常量,常量替换为相应的值,变量替换为内存地址,同时编译器无法将变量带入计算。
line = 5
column = 10

data segment
string db 'HELLO',0
color db 01h,04h,07h,0Eh,87h
data ends

code segment
start:
mov ax,data
mov ds,ax
mov ax,0b800h
mov es,ax
reset:
;初始化bx
xor bx,bx
reset1:
;如果颜色值是最后一个,就重新初始化bx、si、cx、di(定位)
cmp bx,5
jz reset
;初始化si、cx、di
;定位:这里直接用编译器计算结果
mov di,(80*2*(line-1))+column*2
xor si,si
mov cx,5
print:
mov al,string[si]
mov ah,color[bx]
mov es:[di],ax
add di,2
inc si
loop print
user_input:
;等待键盘输入
mov ah,0
int 16h
;如果是ESC就结束程序
cmp al,01bh
jz stop
;如果不是就换color
inc bx
;如果是 '-',就重新打印
cmp al,02dh
jz reset1
jmp reset
stop:
mov ax,4c00h
int 21h
code ends
end start

这个例子中,坐标是用编译器来算的,而书中的例子用 mul 指令来算的(这会降低程序运行的速度)。

显示 I/O 程序的功能和调用方法

利用直接写屏的方法,程序可以快速显示,但为了实现直接写屏,必须了解显示存储器用存储空间的具体细节和显示存储区与屏幕显示位置的对应关系,并且最终程序也与显示适配卡相关。所以,除非追求显示速度,一般不采用直接写屏的方法,而实调用BIOS提供的显示I/O程序。
BIOS 提供的显示 I/O 程序作为 10H 号中断处理程序。显示 I/O 程序的主要功能如下图所示:
24
25

页号:为了支持屏幕上显示 2000 多个字符,需要显示存储器容量约为4KB。如果显示存储器的内容为 32KB ,那么显示存储器可存放 8 屏显示内容。为此,把显示存储器再分成若干段,称为显示页。

例3:写一个程序完成如下功能:再屏幕中间不为开出一个窗口,随后接收用户按键,并把按键字符显示字窗口的最底行,当窗口运行满时,窗口内容向上滚动一行;用户按 CTRL+C键时,结束运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
;程序名:T5-3.asm
;功能:在屏幕中间位置开出一个窗口,然后接收用户按键,并把按键字符显示在窗口的最底行,
;当窗口底行内容满时,窗口内容就向上滚动一行,用户按 Ctrl+C 结束程序运行
assume cs:code,ds:data
data segment
pagen db 0
color db 74h
L_line db 6
L_column = 20
R_line = 18
R_column = 59
width1 = (R_column - L_column)+1
data ends

code segment
start:
mov ax,data
mov ds,ax
;
mov ah,5
mov al,pagen
int 10h
;开一个窗口
mov ah,6
mov al,0ch
mov bh,color
mov ch,L_line
mov cl,L_column
mov dh,R_line
mov dl,R_column
int 10h
;
reset:
mov dl,L_column
next:
mov ah,2
mov bh,pagen
mov dh,R_line
int 10h
;接收键盘输入并输出
mov ah,0
int 16h
cmp al,03h
jz stop
;使用int10输出字符
mov ah,0ah
mov bh,pagen
mov cx,1
int 10h
;
inc dl
cmp dl,R_column+1
jnz next
;向上滚动一行
mov ah,6
mov al,1
mov bh,color
mov ch,L_line
mov cl,L_column
mov dh,R_line
mov dl,R_column
int 10h
jmp reset
stop:
mov ax,4c00h
int 21h
code ends
end start

例4:调用显示 I/O 程序实现 T5-2.asm 显示字符串的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
;程序名:T5-4.asm
;功能:采用直接写屏的方法在屏幕上显示"HELLO",然后用户按"-"键后后,再换一种属性显示,如果按ESC键结束程序运行。
;方法:调用显示 I/O 程序实现 T5-2.asm 显示字符串的代码。
assume cs:code,ds:data

;将坐标定义为常量,常量替换为相应的值,变量替换为内存地址,同时编译器无法将变量带入计算。
line = 5
column = 10

data segment
string db 'HELLO',0
color db 01h,04h,07h,0Eh,87h
data ends

code segment
start:
mov ax,data
mov ds,ax
mov es,ax
reset:
;初始化bx
xor bx,bx
reset1:
;如果颜色值是最后一个,就重新初始化bx
cmp bx,5
jz reset
xor bh,bh ;bh置0
push bx ;程序运行时 bx 会被改动,备份bx
;
mov dh,line-1
mov dl,column-1
mov bp,offset string
print:
mov al,0
mov bh,0
mov bl,color[bx]
mov cx,5
mov ah,13h
int 10h
user_input:
;等待键盘输入
mov ah,0
int 16h
;如果是ESC就结束程序
cmp al,01bh
jz stop
;如果不是就换color
pop bx ;恢复bx
inc bx
;如果是 '-',就重新打印
cmp al,02dh
jz reset1
jmp reset
stop:
mov ax,4c00h
int 21h
code ends
end start

打印输出

打印 I/O 程序的功能和调用方法

BIOS提供的打印 IO 程序为 17H 号中断处理程序。
26

例5:写一个在0号打印机上打印屏幕内容的程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
;程序名:T5-5.asm
;功能:写一个在0号打印机上打印屏幕内容的程序
;打印流程图
;利用显示I/O程序8号功能获取屏幕内容
;以行为单位从左到右打印屏幕内容,从顶行开始到底行结束,二重循环结构
;把一行内容输出到打印机后,追加输出一个回车字符和一个换行字符,使用打印机实施打印动作。
;------------------------------------------------------------------------------------

assume cs:code
;定义常量
time_out=00000001b
io_error=00001000b
out_of_p=00100000b
flag=time_out+io_error+out_of_p
;代码段
code segment
start:
mov ah,0fh ;取当前显示页号和最大列数
int 10h
mov cl,ah ;最大列数保存
mov ch,25 ;满屏行数送CH
push cx
mov ah,3 ;取当前光标位置
int 10h
pop cx
push dx ;保存当前光标
;
xor dx,dx ;准备从左上角开始
pri1:
mov ah,2 ;置光标
int 10h
mov ah,8 ;取当前光标处字符
int 10h
or al,al ;字符有效?
jnz pri2 ;是,转
mov al,' ' ;作为空格处理
pri2:
push dx
xor dx,dx ;0号打印机
xor ah,ah ;所有取字符送打印机
int 17h
pop dx

test ah,flag ;打印机ok?
jnz err1 ;否,跳转
inc di ;是,光标+1
cmp cl,dl ;到右边界?
jnz pri1 ;否,继续下一列
push dx
xor dx,dx ;是,发出回车和换行控制符
mov ax,0dh
int 17h
mov ax,0ah
int 17h
pop dx

xor dl,dl ;光标转杯到下一行左边
inc dh ;为此,行号+1
cmp ch,dh ;最后一行完?
jnz pri1 ;否,继续下一行
pop dx ;恢复保存的光标位置
mov ah,2
int 10h
jmp short exit

err1:
pop dx ;恢复保存的光标位置
mov ah,2
int 10h ;把光标置回原处
err2:
mov al,7
mov ah,0eh
int 10h ;打印机故障,发出三声"嘟""
int 10h
int 10h
exit:
mov ax,4c00h
int 21h
code ends
end start

没有打印机,直接抄代码,加深点映像。后续,右打印机再进行测试。

软中断程序举例

时钟显示程序

在系统加电初始化期间,把系统定时器初始化位每隔约 55 毫秒发出一次中断请求。CPU 在响应定时中断请求后转入 8H 号中断处理程序。BIOS 提供的 8H 号中断程序中有一条中断指令 “INT 1CH”,所以每秒要调用到约 18.2 次 1CH 号中断处理程序。而 BIOS 的 1CH 号中断处理程序实际上并没有做任何工作,只有一条中断返回指令。这样安排的目的是为应用程序留下一个软接口,应用程序质押奥提供新的 1CH 号中断处理程序,就可能实现某些周期性的工作。

例6:利用该软接口,实现时钟显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
;程序名:T5-6.asm
;功能:在程序的右上角输出时间,按 ctrl+c 结束程序运行。
;知识点:在中断向量表中,有一个 1Ch 号中断,系统每隔55毫秒执行一次 1C 号中断。
;但是 1C 号中断没有任何功能(不做任何工作),只有一条 RETI(中断返回)指令,
;这样安排的目的是为应用程序留下一个软接口,应用程序只要提供一个新的 1Ch 号中断处理程序,
;就能实现某些周期性的工作。
;思路:保护原中断、写入新中断、恢复原中断(修改偏移来实现)
;新中断功能:先计数执行18次后获取时间,再把时间输出到屏幕上
assume cs:code,ds:data

data segment
OLDVECTOR dd 0
n = 1ch
count_val = 18
count db 0
dpage = 0
column = 72
line = 0
cursor dw 0
data ends

code segment
;显示时间
system_time proc far
;判断是否开始输出时间
cmp ds:count,0
jz next
dec ds:count
IRET
next:
mov count,count_val
sti ;开中断
;再真正执行程序之前保护寄存器
push ds
push es
push ax
push bx
push cx
push dx
push si
push bp
push ds
pushf
;取原光标位置
mov ah,3
mov bh,dpage
int 10h
mov cursor,dx
;定位
mov ah,2
mov bh,dpage
mov dh,line
mov dl,column
int 10h
;输出时间
call far ptr GET_T
;还原光标位置
mov ah,2
mov dx,cursor
int 10h
;
popf
pop ds
pop bp
pop si
pop dx
pop cx
pop bx
pop ax
pop es
pop ds
;结束程序
iret
system_time endp

GET_T proc far
mov ah,2
int 1ah
;输出小时
mov al,ch
call far ptr TTASC
call far ptr TECHO
mov ah,2
mov dl,3ah
int 21h
;输出分钟
mov al,cl
call far ptr TTASC
call far ptr TECHO
mov ah,2
mov dl,3ah
int 21h
;输出秒
mov al,dh
call far ptr TTASC
call far ptr TECHO
ret
GET_T endp

;将时间的压缩bcd码转换为ascii
;输入参数al
;输出参数ax
TTASC proc far
pushf
;清空标志位
pushf
pop bx
and bx,0
push bx
popf
mov ah,al
and al,0fh
shr ah,1
shr ah,1
shr ah,1
shr ah,1
add ax,3030h
popf
ret
TTASC endp

;输出时间
;入口参数 ax
TECHO proc far
push cx
push dx
;
mov cx,2
mov dx,ax
T_ECHO:
xchg dh,dl
mov ah,2
int 21h
loop T_ECHO
;
pop dx
pop cx
ret
TECHO endp

start:
mov ax,data
mov ds,ax
;保存1Ch号中断向量
xor ax,ax
mov es,ax
mov ax,es:[n*4]
mov word ptr OLDVECTOR,ax
mov ax,es:[n*4+2]
mov word ptr OLDVECTOR+2,ax
;设置新的1Ch号中断
mov es:[n*4+2],cs
mov ax,offset system_time
mov es:[n*4],ax

call far ptr system_time
;等待输入ctrl_c
i_ret:
mov ah,0
int 16h
cmp al,03h
jnz i_ret
;恢复原1CH号中断向量
mov ax,word ptr OLDVECTOR
mov es:[n*4],ax
mov ax,word ptr OLDVECTOR+2
mov es:[N*4+2],ax
;
mov ax,4c00h
int 21h

code ends
end start

打印软中断程序,因为没有打印机,暂时先忽略。

  • 本文标题:8086汇编-输入输出与中断
  • 本文作者:9unk
  • 创建时间:2022-09-27 17:15:00
  • 本文链接:https://9unkk.github.io/2022/09/27/8086-hui-bian-shu-ru-shu-chu-yu-zhong-duan/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!