从入门到精通汇编语言 第六章(中断及外部设备操作)

参考教程:通俗易懂的汇编语言(王爽老师的书)_哔哩哔哩_bilibili

一、移位指令

1、8个移位指令

(1)逻辑左移指令SHL:SHL OPR, CNT。

①OPR为操作数,CNT为左移位数,该指令将OPR视作二进制无符号数,向左移位相应的位数,低位补0,最后一个被移出的高位写入CF中

②当CNT大于1时,必须将其存入寄存器CL中,以寄存器名字CL的形式给出。

(2)逻辑右移指令SHR:SHR OPR, CNT。

①OPR为操作数,CNT为右移位数,该指令将OPR视作二进制无符号数,向右移位相应的位数,高位补0,最后一个被移出的低位写入CF中

②当CNT大于1时,必须将其存入寄存器CL中,以寄存器名字CL的形式给出。

(3)循环左移指令ROL:ROL OPR, CNT。

①OPR为操作数,CNT为左移位数,该指令将OPR视作二进制数,向左移位相应的位数,被移出的高位会从低位移入,最后一个被移出的高位写入CF中

②当CNT大于1时,必须将其存入寄存器CL中,以寄存器名字CL的形式给出。

(4)循环右移指令ROR:ROR OPR, CNT。

①OPR为操作数,CNT为右移位数,该指令将OPR视作二进制数,向右移位相应的位数,被移出的低位会从高位移入,最后一个被移出的低位写入CF中

②当CNT大于1时,必须将其存入寄存器CL中,以寄存器名字CL的形式给出。

(5)算数左移指令SAL:SAL OPR, CNT。

①OPR为操作数,CNT为左移位数,该指令将OPR视作二进制有符号数,向左移位相应的位数,低位补0,最后一个被移出的高位写入CF中

②当CNT大于1时,必须将其存入寄存器CL中,以寄存器名字CL的形式给出。

(6)算数右移指令SAR:SAR OPR, CNT。

①OPR为操作数,CNT为右移位数,该指令将OPR视作二进制有符号数,向右移位相应的位数,每移一位时高位补0或1取决于次高位是0或1(与次高位相同),最后一个被移出的低位写入CF中

②当CNT大于1时,必须将其存入寄存器CL中,以寄存器名字CL的形式给出。

(7)带进位循环左移RCL:RCL OPR, CNT。

①OPR为操作数,CNT为左移位数,该指令将OPR视作二进制数,向左移位相应的位数,每移一位时,原高位写入CF,原CF的内容从低位移入

②当CNT大于1时,必须将其存入寄存器CL中,以寄存器名字CL的形式给出。

(8)带进位循环右移RCR:RCR OPR, CNT。

①OPR为操作数,CNT为右移位数,该指令将OPR视作二进制数,向右移位相应的位数,每移一位时,原低位写入CF,原CF的内容从高位移入

②当CNT大于1时,必须将其存入寄存器CL中,以寄存器名字CL的形式给出。

2、移位指令使用示例

(1)以逻辑移位指令进行示例:将X逻辑左移一位,相当于执行X = X * 2;将X逻辑右移一位,相当于执行X = X / 2。

(2)汇编程序:

assume cs:code
code segment
main:	mov al, 00000001b 			;执行后(al)=00000001b=1
		shl al, 1 					;执行后(al)=00000010b=2
        shl al, 1 					;执行后(al)=00000100b=4
        shl al, 1 					;执行后(al)=00001000b=8

        mov cl, 3
        shl al, cl 					;执行后(al)=01000000b=64

        mov cl, 2
        shr al, cl 					;执行后(al)=00010000b=16

        mov ax, 4c00h
        int 21h

code ends
end main

二、操作显存数据

1、显示的原理

(1)8086的内存空间中有这么一块显存地址空间,屏幕上的显示内容和显存地址空间中的数据一一对应。

(2)通过往显示缓冲区中写入数据,可以实现在屏幕上显示特定属性字符的效果。

2、显示缓冲区的结构

(1)显示缓冲区总共25行80列(单位为字),每个字由两个字节组成,其中低位字节存放要显示符号的ASCII码,高位字节存放要显示字符的属性。

(2)字符的显示属性由8位组成,其中0-2位为前景的RGB参数(三色参数均仅有0或1可选),3位决定是否高亮,4-6位为背景的RGB参数(三色参数均仅有0或1可选),7为决定是否闪烁。

3、举例

(1)目的:编写汇编程序,在屏幕的中间,属性为白底蓝字,显示‘Welcome to masm!’。

(2)汇编程序:

assume cs:code, ds:data
data segment
            db ‘Welcome to masm!’
data ends

code segment
    main:	mov ax, data 				;获取数据段地址
		    mov ds, ax 				;将数据段地址送入DS中
            mov ax, 0b800h			;获取显示缓冲区首地址
            mov es, ax				;将显示缓冲区首地址送入ES中
            mov si, 0
            mov di, 160*12+80-16

            mov cx, 16
    w:		mov al, [si]
            mov es:[di], al				;将字符ASCII码载入缓冲区
            inc di					;操作下一个字节
            mov al, 71h
            mov es:[di], al				;将字符属性载入缓冲区
            inc si					;指向数据区字符串的下一个字符
            inc di					;操作下一个字节
            loop w

            mov ax, 4c00h
            int 21h

code ends
end main

三、描述内存单元的标号

1、数据标号

(1)代码段中的标号可以用来标记指令、段的起始地址,也可以用来标记数据所在的位置。如下汇编程序,其作用是将a标号处的8个字节数据累加,结果存储到b标号处的字中。

assume cs:code
code segment
    a:		    db 1, 2, 3, 4, 5, 6, 7, 8
    b:		    dw 0

    start:		mov si,offset a			;获取标号a处“数据堆”的首地址
                mov bx,offset b		;获取标号b处“数据堆”的首地址

                mov cx,8
    s:		    mov al,cs:[si]
                mov ah,0
                add cs:[bx],ax
                inc si
                loop s

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

(2)数据标号可以把冒号去掉,此时数据标号不同于仅仅表示地址的地址标号,它同时描述内存地址和单元长度。如下汇编程序,其作用是将a标号处的8个字节数据累加,结果存储到b标号处的字中。

assume cs:code
code segment
    a		    db 1, 2, 3, 4, 5, 6, 7, 8		;标号a以后的内存单元最小单位都是字节
    b		    dw 0						    ;标号b以后的内存单元最小单位都是字

    start:		mov si,0			

                mov cx,8
    s:		    mov al,a[si]				;(al) = (cs * 16 + a + si)
                mov ah,0
                add b,ax					;(cs * 16 + b) = (ax)
                inc si
                loop s

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

2、数据的直接定址表

(1)数据标号除了可用于标识代码段中的数据以外,还可以用于标识数据段中的数据。如下汇编程序,其作用是将a标号处的8个字节数据累加,结果存储到b标号处的字中。

assume cs:code, ds:data
data segment
    a 		    db 1, 2, 3, 4, 5, 6, 7, 8		;标号a以后的内存单元最小单位都是字节
    b 		    dw 0					    	;标号b以后的内存单元最小单位都是字
data ends
code segment
    start:		mov ax, data
                mov ds, ax
                mov si,0			

                mov cx,8
    s:		    mov al,a[si]				;(al) = (ds * 16 + a + si)
                mov ah,0
                add b,ax					;(ds * 16 + b) = (ax)
                inc si
                loop s

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

(2)标号可以当作数据定义,如下所示。

assume cs:code, ds:data
data segment
    a 		    db 1, 2, 3, 4, 5, 6, 7, 8
    b 		    dw 0
    c 		    dw offset a, seg a, offset b, seg b
data ends
code segment
    start:		mov ax, data
                mov ds, ax
                mov si,0			

                mov cx,8
    s:	    	mov al,a[si]				;(al) = (ds * 16 + a + si)
                mov ah,0
                add b,ax					;(ds * 16 + b) = (ax)
                inc si
                loop s

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

(3)鉴于标号可以当作数据定义,不妨尝试给若干组数据用标号标识,把这些标号全部搁一起,当作一组数据定义,这样就能得到一个数据直接定址表,换句话说,利用数据直接定址表可在两个数据集合之间建立一种映射关系,用查表的方法根据给出的数据得到其在另一集合中的对应数据

(4)举例:编写程序,计算sin(x),x∈{0°,30°,60°,90°,120°,150°,180°},并在屏幕中间显示计算结果。

①解决方案:空间换时间,将所要计算的sin(x) 的结果都存储到一张表中,然后用角度值来查表,找到对应的sin(x)的值,并显示在屏幕上。

②汇编程序:

assume cs:code
code segment
    start:		mov al,60					;用ax向子程序传递角度值
                call showsin
                mov ax,4c00h
                int 21h

    showsin:	jmp short show			    ;转移至子函数下一条代码处
                table dw ag0, ag30, ag60, ag90, ag120, ag150, ag180
                ag0 db '0' ,0 				;sin(0)对应的字符串'0'
                ag30 db '0.5' ,0 			;sin(30)对应的字符串'0.5'
                ag60 db '0.866', 0 			;sin(60)对应的字符串'0.866'
                ag90 db '1' ,0 				;sin(90)对应的字符串'1'
                ag120 db '0.866' ,0 		;sin(120)对应的字符串'0.866'
                ag150 db '0.5', 0 			;sin(150)对应的字符串'0.5'
                ag180 db '0', 0 			;sin(180)对应的字符串'0'
    show:	    push bx
                push es
                push si
                mov bx, 0b800h
                mov es, bx

                mov ah, 0
                mov bl, 30
                div bl			    ;用角度值/30作为相对于table的偏移量
                mov bl, al			
                mov bh,0
                add bx, bx		    ;注意table与其它标号描述的内存单元大小
                mov bx, table[bx]	;取得对应的字符串的偏移地址,放在bx中

                mov si, 160*12+40*2
    shows:     	mov ah, cs:[bx]
                cmp ah, 0
                je showret
                mov es:[si],ah
                inc bx
                add si,2
                jmp shows
    showret:	pop si
                pop es
                pop bx
                ret
code ends
end start

3、代码的直接定址表

(1)除了数据有直接定址表以外,代码也可以有直接定址表,其实现思路是将若干个功能写成相应的若干个子程序,将这些功能子程序的入口地址存储在一个表中,它们在表中的位置和功能号相对应,对应关系为“功能号 * 2 = 对应的功能子程序在地址表中的偏移”

(2)举例:

①目标:实现一个子程序setscreen,为显示输出提供如下功能。

[1]清屏。

[2]设置前景色。

[3]设置背景色。

[4]向上滚动一行

②子程序入口参数说明:

[1]用AH寄存器传递功能号,0表示清屏,1表示设置前景色,2表示设置背景色,3表示向上滚动一行。

[2]对2、3号功能,用AL传送颜色值,(al)∈{0, 1, 2, 3, 4, 5, 6, 7 }。

③各个功能的子程序实现:

[1]清屏:将显存中当前屏幕中的字符设为空格符。

sub1:
    push bx
    push cx
    push es

    mov bx, 0b800h
    mov es, bx
    mov bx, 0
    mov cx, 2000
sub1s:
    mov byte ptr es:[bx], ' '
    add bx, 2
    loop sub1s

    pop es
    pop cx
    pop bx
    ret 						;sub1结束

[2]设置前景色:设置显存中奇地址的属性字节的第0、1、2位。

sub2:
    push bx
    push cx
    push es

    mov bx, 0b800h
    mov es, bx
    mov bx, 1
    mov cx, 2000
sub2s:
    and byte ptr es:[bx], 11111000b
    or es:[bx], al
    add bx, 2
    loop sub2s

    pop es
    pop cx
    pop bx
    ret 									;sub2结束

[3]设置背景色:设置显存中奇地址的属性字节的第4、5、6位。

sub3:
    push bx
    push cx
    push es

    mov cl, 4
    shl al, cl
    mov bx, 0b800h
    mov es, bx
    mov bx, 1
    mov cx, 2000
sub3s:
    and byte ptr es:[bx],10001111b
    or es:[bx], al
    add bx, 2
    loop sub3s

    pop es
    pop cx
    pop bx
    ret 									; sub3结束

[4]向上滚动一行:依次将第n+1行的内容复制到第n行处,并清空最后一行。

sub4:
    push cx
    push si
    push di
    push es
    push ds

    mov si, 0b800h
    mov es, si
    mov ds, si
    mov si,160 			;ds:si指向第n+1行
    mov di, 0 			;es:di指向第n行
    cld
    mov cx, 24			;共复制24行

sub4s:
    push cx
    mov cx, 160
    rep movsb	    	;复制1行
    pop cx
    loop sub4s

    mov cx,80
    mov si,0
sub4s1:
    mov byte ptr es:[160*24+si], ' '	;清空最后一行
    add si,2
    loop sub4s1

    pop ds
    pop es
    pop di
    pop si
    pop cx
    ret 					        	;sub4结束

④主程序与setscreen子程序:

assume cs:code
code segment
start:
        mov ah, 2
        mov al, 5
        call setscreen
        mov ax, 4c00h
        int 21h

setscreen:		;要在其中再加入新功能,只需要在地址表中加入它的入口地址即可
        jmp short set
        table dw sub1,sub2,sub3,sub4			;地址表
set:
        push bx
        cmp ah,3
        ja sret
        mov bl,ah
        mov bh,0
        add bx,bx
        call word ptr table[bx]	;根据bx中的功能号索引相应的标号,执行其子程序
sret:
        pop bx
        ret
        ;4个功能的子程序放在此处
code ends
end start

四、中断及其处理

1、中断的概念与分类

(1)中断是指CPU不再接着(刚执行完的指令)向下执行,而是转去处理中断信息

(2)中断的分类:

①内中断:由CPU内部发生的事件而引起的中断。

②外中断:由外部设备发生的事件引起的中断。

2、8086的内中断

(1)CPU内部产生的中断信息:

①除法错误,比如执行DIV指令时产生除法溢出(除0错误)。

②单步执行中断。

③INTO命令。

④INT命令。

(2)8086的中断类型码:

①除法错误:0。

②单步执行中断:1。

③INTO命令:4。

④INT <立即数n>命令:立即数n。

3、中断处理程序

(1)CPU处理中断信息,本质上就是执行中断处理程序

(2)中断向量表:由中断类型码可查表得到中断处理程序的入口地址(低字节存放IP-偏移地址,高字节存放CS-代码段地址),从而定位中断处理程序。((IP) = (N*4),(CS) = (N*4+2),N为中断类型码)

(3)举例:触发系统的0号中断,CPU会根据中断类型码在中断向量表中找到中断处理程序的入口地址,并根据入口地址设置CS寄存器与IP寄存器,将转至中断服务程序执行。

五、编制中断处理程序

1、中断处理程序及其结构

(1)CPU随时都可能检测到中断信息,所以中断处理程序必须常驻内存(一直存储在内存某段空间之中),中断处理程序的入口地址,也即中断向量,必须存储在对应的中断向量表表项中(0000H:0000H-0000H:03FFH)

(2)触发并进入中断处理程序的过程

①取得中断类型码N。

②pushf —— 标志寄存器内容入栈(保存标志寄存器)。

③TF = 0,IF = 0 —— 防止非预期的中断嵌套触发。

④push CS —— 保存原程序断点。

⑤push IP —— 保存原程序断点。

⑥(IP) = (N*4)、(CS) = (N*4+2) —— 转移至N号中断的中断服务程序。

2、编制中断处理程序——以除法错误中断为例

(1)预期效果:编写一个0号中断处理程序do0,它的功能是在屏幕中间显示“overflow!”后,返回到操作系统。

(2)准备工作:

①do0子程序应该存放在内存的确定位置,并且要重新找个地方,不破坏系统,可利用中断向量表中的空闲单元来存放我们的程序。经过估计,do0的长度不可能超过256个字节,就选用从0000:0200至0000:02FF的256个字节的空间。

②0号中断处理程序要有新的入口地址(需说明,实际应用中不要随便自己改写中断处理程序)。

(3)程序框架梳理:

①编写可以显示“overflow!”的中断处理程序do0。

②将do0送入内存0000H:0200H处(安装程序)。

③将do0中断处理程序的入口地址0000H:0200H存储在中断向量表0号表项中。

(4)汇编程序:

assume cs:code
code segment
start:
        ;安装程序do0
        mov ax, cs
        mov ds, ax						    ;do0的段地址送入DS中
        mov si, offset do0					;获取do0的偏移地址
        mov ax, 0
        mov es, ax
        mov di, 200h						;ES:DI指向0000H:0200H
        mov cx, offset do0end - offset do0	;获取do0程序所占用字节数
        cld
        rep movsb						    ;将do0下的内容送入内存0000H:0200H处

        ;设置中断向量表
        mov ax, 0
        mov es, ax
        mov word ptr es:[0*4], 200h
        mov word ptr es:[0*4+2], 0

        mov ax,4c00h
        int 21h
do0:
        jmp short do0start
        db ‘overflow!’
do0start:
        mov ax, cs
        mov ds, ax
        mov si, 202h
        mov ax, 0b800h
        mov es, ax
        mov di, 12*160+36*2
        mov cx, 9
s: 
        mov al, [si]
        mov es:[di], al
        inc si
        add di, 2
        loop s
        mov ax, 4c00h
        int 21h
do0end:	nop
code ends
end start

(5)测试:运行上面的程序,改变中断向量,然后执行DIV指令,除数为0,触发除0错误,观察屏幕现象。

六、单步中断

1、Debug的T命令回顾

(1)Debug利用了CPU提供的单步中断的功能,使用T命令时,Debug会将TF标志设为1,使CPU工作在单步中断方式下。

(2)每使用一次T命令,Debug就会执行一条指令,并显示寄存器中的内容和下一条需要执行的指令(CS:IP指向该条指令)。

2、单步中断处理过程

(1)两个和中断相关的寄存器标志位:

①TF-陷阱标志(Trap flag):当TF=1时,每条指令执行完后产生陷阱,由系统控制计算机;当TF=0时,CPU正常工作,不产生陷阱。(用于调试时的单步方式操作)

②IF-中断标志(Interrupt flag):当IF=1时,允许CPU响应可屏蔽中断请求;当IF=0时,关闭中断。

(2)CPU在执行完一条指令之后,如果检测到标志寄存器的TF位为1,则产生单步中断(中断类型码为1),引发中断过程,执行中断处理程序

(3)进入中断处理程序时需要将TF置为0,这是因为中断处理程序也由一条条指令组成的,如果在执行中断处理程序之前TF=1,则CPU在执行完中断处理程序的第一条指令后又要产生单步中断,转去执行单步中断的中断处理程序的第一条指令,以此往复,将陷入一个永远不能结束的循环,CPU永远执行单步中断处理程序的第一条指令,所以在进入中断处理程序之前,需要设置TF=0。

(4)一般情况下,CPU在执行完当前指令后,如果检测到中断信息就响应中断,引发中断过程。不过在有些情况下,CPU 在执行完当前指令后,即便是发生中断,也不会响应,如在执行完向SS寄存器传送数据的指令后,即便是发生中断,CPU也不会响应,这是因为SS:SP联合指向栈顶,而对它们的设置应该连续完成(实际上如果不连续设置SS和SP,编译阶段也不会报错,但编程时应养成良好的习惯),以此保证对栈的正确操作。

七、由INT指令引发的中断

1、INT指令介绍

(1)格式:INT <立即数n>。(n为中断类型码)

(2)INT指令可无条件引发任何中断过程,CPU执行“int n”指令,相当于引发一个n号中断的中断过程,执行过程如下

①取得中断类型码N。

②pushf —— 标志寄存器内容入栈(保存标志寄存器)。

③TF = 0,IF = 0 —— 防止非预期的中断嵌套触发。

④push CS —— 保存原程序断点。

⑤push IP —— 保存原程序断点。

⑥(IP) = (N*4)、(CS) = (N*4+2) —— 转移至N号中断的中断服务程序。

(3)一般情况下,系统将一些具有一定功能的子程序,以中断处理程序的方式提供给应用程序调用。

2、编写供应用程序调用的中断例程

(1)编程时,可以用INT指令调用子程序,此子程序即中断处理程序,简称为中断例程(与一般的子程序一样,需注意保存现场和恢复现场)。可以自定义中断例程,实现特定功能。

(2)举例:写7ch号中断的中断例程,完成特定任务。

①目标:求一个word型数据的平方,用AX进行参数传递,DX、AX中分别存放结果的高16位、低16位。

②任务分解:

[1]编程实现求平方功能的程序。

[2]安装程序,将其安装在0000H:0200H处。

[3]设置中断向量表,将程序的入口地址保存在7ch表项中,使其成为中断7ch的中断例程。

③知识补充:IRET指令常用于中断处理函数结尾处,它相当于指令“pop ip”、“pop cs”、“popf”(标志寄存器内容出栈)。

④安装中断例程的汇编程序:

assume cs:code
code segment
start:		mov ax, cs
            mov ds, ax
            mov si, offset sqr
            mov ax ,0
            mov es, ax
            mov di, 200h
            mov cx, offset sqrend - offset sqr
            cld
            rep movsb

            mov ax, 0
            mov es, ax
            mov word ptr es:[7ch*4], 200h
            mov word ptr es:[7ch*4+2], 0

            mov ax,4c00h
            int 21h

sqr: 		mul ax
            iret
sqrend:	    nop
code ends
end start

⑤测试使用的汇编程序:

assume cs:code
code segment
start: 	mov ax,3456
        int 7ch 			;引发7ch号中断,计算(ax)^2
        add ax,ax
        adc dx, dx

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

八、BIOS和DOS中断处理

1、BIOS——基本输入输出系统

(1)BIOS是在系统板的ROM中存放着的一套程序,容量为8KB,从FE000H开始。

(2)BIOS中的主要内容:

①硬件系统的检测和初始化程序。

②外部中断和内部中断的中断例程。

③用于对硬件设备进行I/O操作的中断例程。

④其它和硬件系统相关的中断例程。

(3)使用BIOS功能调用,程序员不用了解硬件操作细节,直接使用指令设置参数,并中断调用BIOS例程,即可完成相关工作

(4)BIOS具体有哪些功能可查找BIOS中断手册,里面有详细的介绍,这里不再赘述。

2、DOS中断

(1)通过执行指令“int 21”,可引发DOS中断类,和硬件设备相关的DOS中断例程中,一般都调用BIOS的中断例程。

(2)BIOS和DOS在所提供的中断例程中包含了许多子程序,这些子程序实现了程序员在编程的时常用到的功能。

3、BIOS和DOS中断例程的安装过程

(1)CPU一上电,初始化(CS)=0FFFFH,(IP)=0,自动从FFFFH:0000H单元开始执行程序。FFFFH:0000H处有一条转跳指令,CPU执行该指令后,转去执行BIOS中的硬件系统检测和初始化程序。

(2)初始化程序将建立BIOS 所支持的中断向量,即将BIOS提供的中断例程的入口地址登记在中断向量表中。

(3)硬件系统检测和初始化完成后,调用“int 19h”进行操作系统的引导,从此将计算机交由操作系统控制。

(4)DOS启动后,除完成其它工作外,还将它所提供的中断例程装入内存,并建立相应的中断向量。

九、端口的读写

1、IN指令与OUT指令

(1)CPU可以直接读写3个地方的数据——CPU内部的寄存器、内存单元、端口,从CPU角度,可以将各寄存器当作端口并统一编址,CPU用统一的方法与各种设备通信

(2)读写端口需要用专门的指令IN和OUT,IN指令用于CPU从端口读取数据,OUT用于CPU往端口写入数据。

(3)“IN <寄存器> <端口地址>”执行的操作是将端口地址(可以其它形式给出,如存储在寄存器中)中的数据读入CPU相应的寄存器中,执行该指令时总线有如下相关操作:

①CPU通过地址线将端口地址信息发出。

②CPU通过控制线发出端口读命令,选中端口所在的芯片,并通知要从中读取数据。

③端口所在的芯片将端口中的数据通过数据总线送入CPU。

(4)“OUT <端口地址> <寄存器>”执行的操作是将CPU相应的寄存器中的数据写入端口地址(可以其它形式给出,如存储在寄存器中)对应的空间中,执行该指令时总线有如下相关操作:

①CPU通过地址线将端口地址信息发出。

②CPU通过控制线发出端口写命令,选中端口所在的芯片,并通知要往里面写入数据。

③CPU通过数据总线将数据送入端口所在的芯片的端口中。

2、8086的I/O端口分配

3、用端口访问外设举例

(1)61h端口地址的设备控制寄存器功能如下所示:

(2)汇编程序:

assume cs:codeseg
codeseg segment
start:		mov al, 08h			    ;设置声音的频率
            out 42h, al
            out 42h, al
            in al, 61h 				;读设备控制器端口原值
            mov ah, al 			    ;保存原值
            or al, 3 				;打开扬声器和定时器
            out 61h, al 			;接通扬声器,发声

            mov cx, 60000 		    ;延时
delay:	    nop
            loop delay

            mov al, ah 
            out 61h, al			    ;恢复端口原值
            mov ax, 4c00h
            int 21h
codeseg ends
end start

十、操作CMOS RAM芯片

1、CMOS RAM芯片介绍

(1)包含一个实时钟和一个有128个存储单元的RAM存储器。

(2)128个字节的RAM中存储:内部实时钟、系统配置信息、相关的程序(用于开机时配置系统信息)。

(3)CMOS RAM 芯片靠电池供电,关机后其内部的实时钟仍可正常工作,RAM中的信息不丢失。

(4)该芯片内部有两个端口,端口地址为70h和71h,CPU通过这两个端口可以读写CMOS RAM。

①70h地址端口存放要访问的CMOS RAM单元的地址。

②71h数据端口存放从选定的单元中读取的数据,或要写入到其中的数据。

2、举例——提取CMOS RAM中存储的月份信息

(1)背景知识:CMOS RAM中以BCD码的形式存储时间信息,其中月份信息存储在8号单元中,具体内容分布如下所示。

(2)任务分解:

①从CMOS RAM的8号单元读出当前月份的BCD码。

②将用BCD码表示的月份以十进制的形式显示到屏幕上。

(3)汇编程序:

assume cs:code
code segment
start:		mov al, 8
            out 70h, al		;存放要访问的CMOS RAM单元的地址(8号单元)
            in al, 71h			;将其中的月份信息读入AL

            mov ah, al
            mov cl, 4
            shr ah, cl
            and al, 00001111b

            add ah, 30h
            add al, 30h

            mov bx, 0b800h
            mov es, bx
            mov byte ptr es:[160*12+40*2], ah
            mov byte ptr es:[160*12+40*2+2], al

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

十一、外设连接与中断

1、由外部设备发生的事件引起的中断(外中断)

(1)可屏蔽中断与不可屏蔽中断:

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

不可屏蔽中断是CPU必须响应的外中断,当CPU检测到不可屏蔽中断信息时,则在执行完当前指令后立即响应,引发中断过程。对于8086CPU,不可屏蔽中断的中断类型码固定为2。

(2)几乎所有由外设引发的外中断都是可屏蔽中断,比如键盘输入、打印机请求;不可屏蔽中断在系统中有必须处理的紧急情况发生时用来通知CPU的中断信息。

(3)CPU在执行指令过程中,可以检测到外设发送过来的中断信息,引发中断过程,处理外设的输入

2、中断的处理过程

(1)可屏蔽中断所引发的中断过程:

①取中断类型码n(可屏蔽中断信息来自于CPU外部,中断类型码通过数据总线送入CPU)。

②pushf —— 标志寄存器内容入栈(保存标志寄存器)。

③TF = 0,IF = 0 —— 防止非预期的中断嵌套触发,并禁止其它可屏蔽中断(如果在中断处理程序中需要处理可屏蔽中断,可以用指令将IF置1)。

④push CS —— 保存原程序断点。

⑤push IP —— 保存原程序断点。

⑥(IP) = (N*4)、(CS) = (N*4+2) —— 转移至N号中断的中断服务程序。

(2)不可屏蔽中断的中断过程:

①中断值固定为2,不必取中断码。

②pushf —— 标志寄存器内容入栈(保存标志寄存器)。

③TF = 0,IF = 0 —— 防止非预期的中断嵌套触发,并禁止其它可屏蔽中断(如果在中断处理程序中需要处理可屏蔽中断,可以用指令将IF置1)。

④push CS —— 保存原程序断点。

⑤push IP —— 保存原程序断点。

⑥(IP)=(8)、(CS)=(0AH)。

3、STI和CLI指令

(1)STI指令无操作数,它用于设置IF=1。

(2)CLI指令无操作数,它用于设置IF=0。

十二、PC机键盘的处理过程

1、第一步——键盘输入

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

(2)按下一个键时的操作:

①开关接通,该芯片产生一个扫描码,扫描码说明了按下的键在键盘上的位置。

②扫描码被送入主板上的相关接口芯片的寄存器中,该寄存器的端口地址为60H。

(3)松开按下的键时的操作:

①产生一个扫描码,扫描码说明了松开的键在键盘上的位置。

②松开按键时产生的扫描码也被送入60H端口中。

(4)扫描码——长度为一个字节的编码:

①按下一个键时产生的扫描码——通码,通码的第7位为0。

②松开一个键时产生的扫描码——断码,断码的第7位为1。

③通码 + 80H = 断码。

2、第二步——引发9号中断

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

(2)BIOS键盘缓冲区是系统启动后,BIOS用于存放int 9中断例程所接收的键盘输入的内存区,可以存储15 个键盘输入,一个键盘输入用一个字单元存放,高位字节存放扫描码,低位字节存放字符码。

(3)若输入了控制键或切换键,将会修改键盘状态字节,其地址为0040H:0017H,具体定义如下。

3、第三步——执行int 9中断例程

(1)读出60H端口中的扫描码。

(2)根据扫描码分情况对待:

①如果是字符键的扫描码,将该扫描码和它所对应的字符码(即ASCII码)送入内存中的BIOS键盘缓冲区。

②如果是控制键(比如Ctrl)和切换键(比如CapsLock)的扫描码,则将其转变为状态字节(用二进制位记录控制键和切换键状态的字节)写入内存中存储状态字节的单元。

(3)对键盘系统进行相关的控制,如向相关芯片发出应答信息。

4、按照开发需求定制键盘输入处理举例

(1)需求分解:

①在屏幕中间依次显示字母'a'~'z',并可以让人眼看清。

②在显示的过程中,按下Esc键后,改变显示的颜色。

(2)策略说明:

①尽可能忽略硬件处理细节,充分利用BIOS提供的int 9中断例程对这些硬件细节进行处理,由此可以自编int 9中断,自编的中断处理程序在实现需求之余还需调用原来的int 9中断例程,并且要将中断向量表中的int 9中断例程的入口地址改为自编的中断处理程序的入口地址,在新中断处理程序中调用原来的int 9中断例程,还需要是原来的int 9中断例程的地址,这样,按下按键引发int 9中断就能执行定制的中断例程,并且不需要程序员考虑硬件处理细节。

②这个开发需求可能并不是全生命周期都需要的,所以,相关功能处理完以后需要将中断向量表还原为原来的内容,后续按下按键时CPU还是调用原来的int 9中断。

(3)汇编程序分步实现:

①依次显示'a'~'z',并可以让人眼看清,这就需要在字母切换的过程中加塞一堆“无用指令”。

assume cs:code
stack segment
        db 128 dup (0)
stack ends
code segment
start: 	mov ax, stack
        mov ss, ax
        mov sp, 128

        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,4c00h
        int 21h

delay: 	push ax						    ;延时程序没有什么实质操作
        push dx						    ;纯浪费CPU算力
        mov dx, 10h
        mov ax, 0
s1: 	sub ax, 1
        sbb dx ,0
        cmp ax, 0
        jne s1
        cmp dx, 0
        jne s1
        pop dx
        pop ax
        ret
code ends
end start

②实现按下Esc键后改变显示的颜色。

assume cs:code
stack segment
        db 128 dup (0)
stack ends
data segment
        dw 0,0
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]						    ;保存旧中断例程入口
        pop ds:[0]
        push es:[9*4+2]
        pop ds:[2]
        mov word ptr es:[9*4], offset int9		;设置新中断例程入口
        mov es:[9*4+2], cs

        ;显示字母
        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]

        mov ax,4c00h
        int 21h

        ;定义延时程序
delay: 	push ax
        push dx
        mov dx, 10h
        mov ax, 0
s1: 	sub ax, 1
        sbb dx ,0
        cmp ax, 0
        jne s1
        cmp dx, 0
        jne s1
        pop dx
        pop ax
        ret

        ;定义中断例程
int9:	push ax
        push bx
        push es
        in al, 60h				;从60h端口读出键盘的输入
        pushf
        pushf
        pop bx
        and bh, 11111100b
        push bx
        popf
        call dword ptr ds:[0]	;调用原int 9指令功能
        cmp al, 1 				;判断是否为Esc的扫描码,是则改变显示的颜色
        jne int9ret
        mov ax, 0b800h
        mov es, ax
        inc byte ptr es:[160*12+40*2+1]
int9ret:pop es
        pop bx
        pop ax
        iret
code ends
end start

十三、用中断响应外设(以键盘为例)

1、键盘相关的中断

(1)硬件中断int 9h:

由键盘上按下或松开一个键时,如果中断是允许的,就会产生int 9h中断,并转到BIOS的键盘中断处理程序。

(2)BIOS中断int 16h:

        BIOS中断提供基本的键盘操作,引发该中断时,执行何种功能取决于此时AH中的内容,也即功能号(AH):

        00H、10H —从键盘读入字符

        01H、11H —读取键盘状态

        02H、12H —读取键盘标志

        03H —设置重复率

        04H —设置键盘点击

        05H —字符及其扫描码进栈

(3)DOS中断int 21h:

        DOS中断提供丰富、便捷的功能调用,执行何种功能取决于此时AH中的内容,也即功能号(AH):

        01H —从键盘输入一个字符并回显

        06H —读键盘字符

        07H —从键盘输入一个字符不回显

        08H —从键盘输入一个字符,不回显,检测CTRL-Break

        0AH — 输入字符到指定地址的缓冲区

        0BH — 读键盘状态

        0CH — 清除键盘缓冲区,并调用一种键盘功能

2、对键盘输入的处理的int 9h中断和int 16h中断

(1)int 9h中断:

①BIOS提供了int 9中断例程,键盘输入将引发9号中断,9号中断会将键盘的输入存入缓冲区或改变状态字。

②int 9中断例程从60h端口读出扫描码,并将其转化为相应的ASCII码或状态信息,存储在内存的指定空间(键盘缓冲区或状态字节)中。

③键盘缓冲区中有16个字单元,可以存储15个按键的扫描码和对应的入ASCII码。

(2)int 16h中断:

①BIOS提供了int 16h中断例程供程序员调用,以完成键盘的各种操作。

②举例:当(AH) = 0时,CPU检测键盘缓冲区中是否有数据,无则重复检测,有则从键盘缓冲区中读取一个键盘输入,并且将其从缓冲区中删除,将读取的扫描码送入AH,ASCII码送入AL。

(3)B1OS 的int 9 中断例程和int 16h中断例程是一对相互配合的程序,int 9中断例程向键盘缓冲区中写入,int 16h中断例程从缓冲区中读出。它们写入和读出的时机不同,int 9中断例程在有键按下的时候向键盘缓冲区中写入数据,而int 16h中断例程是在应用程序对其进行调用的时候,将数据从键盘缓冲区中读出。

3、调用int 16h从键盘缓冲区中读取键盘的输入举例

(1)例1:

①目标:接收用户的键盘输入,输入“r”将屏幕上的字符设置为红色,输入“g”将屏幕上的字符设置为绿色,输入“b”将屏幕上的字符设置为蓝色。

②汇编程序:

assume cs:code
stack segment
            db 128 dup (0)
stack ends
code segment
    start: 	mov ah,0    
            int 16h			                ;调用中断,等待输入

            ;识别按键并进行相应跳转
            mov ah, 1
            cmp al, 'r'
            je red
            cmp al, 'g'
            je green
            cmp al, 'b'
            je blue
            jmp short sret

            ;设置屏幕颜色
    red: 	shl ah, 1
    green: 	shl ah, 1
    blue: 	mov bx, 0b800h
            mov es, bx
            mov bx, 1
            mov cx, 2000					;需要修改整个显示缓冲区的字符属性
    s: 		and byte ptr es:[bx], 11111000b
            or es:[bx], ah					;设置属性字节前3位
            add bx, 2
            loop s

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

(2)例2:

①设计一个最基本的字符串输入程序,需要具备下面的功能:

[1]在输入的同时需要显示这个字符串。

[2]一般在输入回车符后,字符串输入结束。

[3]能够用退格键删除已经输入的字符。

②逻辑抽象为计算机语言:

[1]用栈的方式来管理字符串的存储空间,DS:DI指向字符串的存储空间,字符串以’\0’为结尾符。

[2]输入回车符后 ,在字符串中加入’\0’,表示字符串结束。

[3]每次有新的字符输入和删除一个字符的时候,都应该重新显示字符串,即从字符栈的栈底到栈顶,显示所有的字符((dh)、(dl) = 字符串在屏幕上显示的行、列位置)。

③汇编程序:

[1]字符输入后的判断及处理:

assume cs:code, ds:data
data segment
            db 32 dup (?)				;字符串的“栈”空间
data ends
code segment
    start: 	mov ax, data
            mov ds, ax
            mov si, 0
            mov dh, 12
            mov dl, 20
            call getstr
    return: mov ax, 4c00h
            int 21h

    getstr:	push ax
    getstrs:mov ah, 0
            int 16h				    	;调用int 16h从键盘缓冲区中读取1个字符
            cmp al, 20h
            jb nochar 					;ASCII码小于20h的为非字符,转去处理
            mov ah, 0					;AH存放charstack程序功能号
            call charstack				;AL中的字符入栈,显示栈中的字符
            jmp getstrs

    nochar: 							;处理非字符
            cmp ah, 0eh 				;退格键的扫描码
            je backspace
            cmp ah, 1ch 				;回车键的扫描码
            je enter
            jmp getstrs
    backspace: 			    			;退格
            mov ah, 1					;AH存放charstack程序功能号
            call charstack				;字符出栈,显示栈中的字符
            jmp getstrs
    enter: 				    			;回车
            mov al, 0
            mov ah, 0					;AH存放charstack程序功能号
            call charstack				;’\0’字符入栈,显示栈中的字符

            pop ax
            ret
code ends
end start

[2]字符栈的入栈、出栈和显示功能子程序:

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:										;功能号0,AL中的字符入栈
        mov bx, top
        mov [si][bx], al
        inc top					    			;栈顶指针自增
        jmp sret

charpop:										;功能号1,字符出栈到AL中
        cmp top, 0
        je sret
        dec top					    			;栈顶指针自减
        mov bx, top
        mov al, [si][bx]
        jmp sret

charshow:									    ;功能号2,显示字符串
        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

十四、读写磁盘

1、BIOS提供的磁盘直接服务——int 13h

2、用BIOS int 13h对磁盘进行读操作

(1)入口参数:

①(ah) = 2(2表示读扇区)。

②(al) = 读取的扇区数。

③(ch) = 磁道号,(cl) = 扇区号。

④(dh) = 磁头号(对于软盘即面号,一个面用一个磁头来读写)。

⑤(dl) = 驱动器号。软驱从0开始,0——软驱A,1——软驱B;硬盘从80h开始,80h——硬盘C,81h——硬盘D。

⑥ES:BX指向接收从扇区读入数据的内存区。

(2)返回参数:

①操作成功:(ah) = 0,(al) = 读入的扇区数。

②操作失败:(ah) = 出错代码。

3、用BIOS int 13h对磁盘进行写操作

(1)入口参数:

①(ah) = 3(3表示读扇区)。

②(al) = 写入的扇区数。

③(ch) = 磁道号,(cl) = 扇区号。

④(dh) = 磁头号(对于软盘即面号,一个面用一个磁头来读写)。

⑤(dl) = 驱动器号。软驱从0开始,0——软驱A,1——软驱B;硬盘从80h开始,80h——硬盘C,81h——硬盘D。

⑥ES:BX指向将写入磁盘的数据。

(2)返回参数:

①操作成功:(ah) = 0,(al) = 写入的扇区数。

②操作失败:(ah) = 出错代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zevalin爱灰灰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值