大多数有用的程序都需要处理用户的输入,键盘输入是最基本的输入。程序和数据通常需要长期存储,磁盘是最常用的存储设备。BIOS 为这两种外设的 I/O提供了最基本的中断例程,在本章中,我们对它们的应用和相关的问题进行讨论。
17.1 int 9中断例程对键盘输入的处理
我们讲过,键盘输入将引发 9 号中断,BIOS 提供了 int 9
中断例程。CPU 在 9 号中断发生后,执行 int 9
中断例程,从 60h 端口读出扫描码,并将其转化为相应的 ASCII 码或状态信息,存储在内存的指定空间(键盘缓冲区或状态字节)中。
一般的键盘输入,在 CPU 执行完 int 9
中断例程后,都放到了键盘缓冲区中。键盘缓冲区中有 16 个字单元,可以存储 15 个按键的扫描码和对应的 ASCII 码。
下面我们按照键盘缓冲区的逻辑结构,来看一下键盘输入的扫描码和对应的 ASCII 码是如何写入键盘缓冲区的。
注意:在我们的课程中,仅在逻辑结构的基础上,讨论 BIOS 键盘缓冲区的读写问题。其实键盘缓冲区是用于环形队列结构管理的内存区,但我们不对队列和环形队列的实现进行讨论。
下面,我们通过下面几个键:
A、B、C、D、E、Shift_A、A
的输入过程,简要地看一下 int 9
中断例程对键盘输入的处理方法。
初始状态下,没有键盘输入,键盘缓冲区为空,此时没有任何元素。
按下 A 键,引发键盘中断;CPU 执行 int 9
中断例程,从 60h 端口读出 A 键的通码;然后检测状态字节,看看是否有 Shift、Ctrl 等切换键按下;发现没有切换键按下,则将 A 键的扫描码 1eh 和对应的 ASCII 码,即字母 “a” 的 ASCII 码 61h,写入键盘缓冲区。缓冲区的字单元中,高位字节存储扫描码,低位字节存储 ASCII 码。此时缓冲区中的内容如下。
按下 B 键,引发键盘中断;CPU 执行 int 9
中断例程,从 60h 端口读出 B 键的通码;然后检测状态字节,看看是否有切换键按下;发现没有切换键按下,将 B 键的扫描码 30h 和对应的 ASCII 码,即字母 “b” 的ASCII 码62h,写入键盘缓冲区,此时缓冲区内容如下。
按下 C、D、E 键后,缓冲区中的内容如下。
按下左 Shift 键,引发键盘中断;int 9
中断例程接收左 Shift 键的通码,设置 0040:17 处的状态字节的第 1 位为 1,表示左 Shift 键按下。
按下 A 键,引发键盘中断;CPU 执行 int 9
中断例程,从 60h 端口读出 A 键的通码;检测状态字节,看看是否有切换键按下;发现左 Shift 键被按下,则将 A 键的扫描码 1Eh 和 Shift_A 对应的 ASCII 码,即字母 “A” 的 ASCII 码 41h,写入键盘缓冲区。此时,缓冲区中的内容如下。
松开左 Shift 键,引发键盘中断;int 9
中断例程接收左 Shift 键的断码,设置 0040:17 处的状态字节的第 1 位为 0,表示左 Shift 键松开。
按下 A 键,引发键盘中断;CPU 执行 int 9
中断例程,从 60h 端口读出 A 键的通码;然后检测状态字节,看看是否有切换键按下;发现没有切换键按下,则将 A 键的扫描码 1Eh 和 A 对应的 ASCII 码,即字母 “a” 的 ASCII 码 61h,写入键盘缓冲区。此时,缓冲区中的内容如下。
17.2 使用int 16h中断例程读取键盘缓冲区
BIOS 提供了 int 16h
中断例程供程序员调用。int 16h
中断例程中包含的一个最重要的功能是从键盘缓冲区中读取一个键盘输入,该功能的编号为 0。下面的指令从键盘缓冲区中读取一个键盘输入,并且将其从缓冲区中国删除。
mov ah,0
int 16j
; 结果为:(ah)=扫描码, (al)=ASCII码
下面我们接着上一节中的键盘输入过程,看一下 int 16h
如何读取键盘缓冲区。
执行
mov ah,0
int 16h
后,缓冲区的内容如下。
执行
mov ah,0
int 16h
后,缓冲区中的内容如下。
反复执行,知道缓冲区为空
执行
mov ah,0
int 16h
int 16h
中断例程检测键盘缓冲区,发现缓冲区为空,则循环等待,知道缓冲区中有数据。
这个时候再次按下 A 键后,缓冲区的内容如下。
循环等待的 int 16h
中断例程检测到键盘缓冲区中有数据,将其读出,缓冲区为空。
从上面我们可以看出,int 16h
中断例程的 0 号功能,进行如下的工作。
- 检测磁盘缓冲区中是否有数据;
- 没有则继续做第 1 步;
- 读取缓冲区第一个字单元中的键盘输入;
- 将读取的扫描码送入 ah,ASCII 码送入 al;
- 将已读取的键盘输入从缓冲区中删除。
可见,BIOS 的 **int 9**
中断例程和 **int 16h**
中断例程是一堆相互配合的程序,**int 9**
中断例程向键盘缓冲区中写入,**int 16h**
中断例程从缓冲区中读出。它们写入和读出的时机不同,int 9
中断例程是在有按键按下的时候向键盘缓冲区中写入数据;而 int 16h
中断例程是在应用程序对其进行调用的时候,将数据从键盘缓冲区中读出。
我们在编写一般的处理键盘输入的程序的时候,可以调用 int 16h
从键盘缓冲区中读取键盘的输入。
✏️ 编程,接收用户的键盘输入,输入 “r”,将屏幕上的字符设置为红色;输入 “g”,将屏幕上的字符设置为绿色;输入 “b”,将屏幕上的字符设置为蓝色。
程序如下,画线处的程序比较技巧,请读者自行分析。
assume cs:code
code segment
start: mov ah,0
int 16h
mov ah,1 ; 技巧1
cmp al,'r'
je red
cmp al,'g'
je green
cmp al,'b'
je blue
jmp short sret
red: shl ah,1 ; 技巧2
green: shl ah,1 ; 技巧3
blue: mov bx,0b800h
mov es,bx
mov bx,1
mov cx,2000
s: and byte ptr es:[bx],11111000b
or es:[bx],ah
add bx,2
loop s
sret: mov ax,4c00h
int 21h
code ends
end start
17.3 字符串的输入
用户通过键盘输入的通常不仅仅是单个字符而是字符串。下面我们讨论字符串输入中的问题和简单的解决办法。
最基本的字符串输入程序,需要具备以下的功能
- 在输入的同时需要显示这个字符串;
- 一般在输入回车符后,字符串输入结束;
- 能够删除已经输入的字符
编写一个接收字符串输入的子程序,实现上面 3 个基本功能。因为在输入的过程中需要显示,子程序的参数如下:
- (dh)、(dl)=字符串在屏幕上显示的行、列位置;
- ds:si 指向字符串的存储空间,字符串以 0 为结尾符。
下面我们进行分析。
字符的输入和删除
每个新输入的字符都存储在前一个输入的字符之后,而删除是从最后面的字符进行的,我们看下面的过程。
空字符串:
输入"a" : a
输入"ab" : ab
输入"c" : abc
输入"d" : abcd
删除一个字符 : abc
删除一个字符 : ab
删除一个字符 : a
删除一个字符 :
可以看出在字符串输入的过程中,字符的输入和输出是按照栈的访问规则进行的,即后进先出。这样,我们就可以用栈的方式来管理字符串的存储空间,也就是说,字符串的存储空间实际上是一个字符栈。字符栈中的所有字符,从栈底到栈顶,组成一个字符串。
在输入回车符后,字符串输入结束。
输入回车符后,可以在字符串中加入 0,表示字符串结束。
在输入的同时需要显示这个字符串。
每次有新的字符输入和删除一个字符的时候,都应该重新显示字符串,即从字符栈的栈底到栈顶,显示所有的字符。
程序的处理过程
现在我们可以简单地确定处理程序的处理过程如下。
- 调用
int 16h
读取键盘输入; - 如果是字符,进入字符栈,显示字符栈中的所有字符;继续执行 1;
- 如果是退格键,从字符栈中弹出一个字符,显示字符栈中的所有字符;继续执行 1;
- 如果是 Enter 键,向字符栈中压入 0,返回。
从程序的处理过程中可以看出,字符栈的入栈、出栈和显示栈中的内容,是需要在多处使用的功能,我们应该将它们写为子程序。
子程序:字符栈的入栈、出栈和显示。
参数说明:
- (ah)=功能号,0 表示入栈,1 表示出栈,2 表示显示;
- ds:si 指向字符栈空间;
- 对于 0 号功能:(al)=入栈字符;
- 对于 1 号功能:(al)=返回的字符;
- 对于 2 号功能:(dh)、(al)=字符串在屏幕上显示的行、列位置。
charstack: jmp short charstart
table dw charpush,charpop,charshow
top dw 0 ; 栈顶
charstart: push bx
push dx
push di
push es
cmp ah,2
ja sret
mov bl,ah
mov bh,0
add bx,bx
jmp word ptr table [bx]
charpush: mov bx,top
mov [si][bx],al
inc top
jmp sret
charpop: cmp top,0
je sret
dec top
mov bx,top
mov al,[si][bx]
jmp sret
charshow: mov bx,0b800h
mov es,bx
mov al,160
mov ah,0
mul dh
mov di,ax
add dl,dl
mov dh,0
add di,dx
mov bx,0
charshows: cmp bx,top
jne noempty
mov byte ptr es:[di],''
jmp sret
noempty: mov al,[si][bx]
mov es:[di],al
mov byte ptr es:[di+2],''
inc bx
add di,2
jmp charshows
sret: pop es
pop di
pop dx
pop bx
ret
上面的子程序中,字符栈的访问规则如下所示。
另外一个要注意的问题是,显示栈中字符的时候,要注意清除屏幕上上一次显示的内容。
我们现在写出完整的接收字符串输入的子程序,如下所示。
getstr: push ax
getstrs: mov ah,0
int 16h
cmp al,20h
jb nochar ; ASCII码小于20h,说明不是字符
mov ah,0
call charstack ; 字符入栈
mov ah,2
call charstack ; 显示栈中的字符
jmp getstrs
nochar: cmp ah,0eh ; 退格键的扫描码
je backspace
cmp ah,1ch ; Enter键的扫描码
je enter
jmp getstrs
backspace: mov ah,1
call charstack ; 字符出栈
mov ah,2
call charstack ; 显示栈中的字符
jmp getstrs
enter: mov al,0
mov ah,0
call charstack ; 0入栈
mov ah,2
call charstack ; 显示栈中的字符
pop ax
ret
17.4 应用int 13h中断例程对磁盘进行读写
以 3.5 英寸软盘为例,进行详解。
3.5 英寸软盘分为上下两面,每面有 80 个磁道,每个磁道又分为 18 个扇区,每个扇区的大小为 512 个字节。则:
磁盘的实际访问由磁盘控制器进行,我们可以通过磁盘控制器来访问磁盘。只能以扇区为单位对磁盘进行读写。在读写扇区的时候,要给出面号、磁道号和扇区号。面号和磁道号从 0 开始,而扇区号从 1 开始。
如果我们通过直接控制磁盘控制器来访问磁盘,则需要设计许多硬件细节。BIOS 提供了对扇区进行读写的中断例程,这些中断例程完成了许多复杂的和硬件相关的工作。我们可以通过调用 BIOS 中断例程来访问磁盘。
BIOS 提供的访问磁盘的中断例程为 int 13h
。读取 0 面 0 道 1 扇区的内容到 0:200 的程序如下所示。
mov ax,0
mov es,ax
mov bx,200h
mov al,1
mov ch,0
mov cl,1
mov dl,0
mov dh,0
mov ah,2
int 13h
入口参数:
- (ah)=int 13h 的功能号(2 表示读扇区)
- (al)=读取的扇区数
- (ch)=磁道号
- (cl)=扇区号
- (dh)=磁头号(对于软盘即面号,因为一个面用一个磁头来读写)
- (dl)=驱动器号
- 软驱从 0 开始,
0:软驱A
,1:软驱B
; - 硬盘从 80h 开始,
80h:硬盘 C
,81h:硬盘D
。
- 软驱从 0 开始,
es:bx 指向接收从扇区读入数据的内存区,返回参数:
- 操作成功:(ah)=0,(al)=读入的扇区数
- 操作失败:(ah)=出错代码
将 0:200 中的内容写入 0 面 0 道 1 扇区。
mov ax,0
mov es,ax
mov bx,200h
mov al,1
mov ch,0
mov cl,1
mov dl,0
mov dh,0
mov ah,3
int 13h
入口参数:
- (ah)=int 13h 的功能号(3 表示读扇区)
- (al)=写入的扇区数
- (ch)=磁道号
- (cl)=扇区号
- (dh)=磁头号(对于软盘即面号,因为一个面用一个磁头来读写)
- (dl)=驱动器号
- 软驱从 0 开始,
0:软驱A
,1:软驱B
; - 硬盘从 80h 开始,
80h:硬盘 C
,81h:硬盘D
。
- 软驱从 0 开始,
es:bx 指向接收从扇区读入数据的内存区,返回参数:
- 操作成功:(ah)=0,(al)=读入的扇区数
- 操作失败:(ah)=出错代码
注意:下面我们要使用 int 13h
中断例程对软盘进行读写。直接向磁盘区写入数据是很危险的,很可能覆盖掉重要的数据。如果向软盘的 0 面 0 道 1 扇区中写入了数据,要使软盘在现有的操作系统下可以使用,必须要重新格式化。
在编写相关的程序之前,必须要找一张空闲的软盘。在使用 int 13h
中断例程时一定要注意驱动器号是否正确,千万不要随便对硬盘中的扇区进行写入。
编程:将当前屏幕的内容保存在磁盘上
分析:1 屏的内容占 4000 个字节,需要 8 个扇区,用 0 面 0 道的 1~8 扇区存储显存中的内容。程序如下:
assume cs:code
code segment
start: mov ax,0b800h
mov es,ax
mov bx,0
mov al,8
mov ch,0
mov cl,1
mov dl,0
mov dh,0
mov ah,3
int 13h
mov ax,4c00h
int 21h
code ends
end start