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

参考教程:通俗易懂的汇编语言(王爽老师的书)_哔哩哔哩_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) = 出错代码。

课程介绍 第1章 预备知识  1.1 汇编语言的由来及其特点   1 机器语言   2 汇编语言   3 汇编程序   4 汇编语言的主要特点   5 汇编语言的使用领域  1.2 数据的表示和类型   1 数值数据的表示   2 非数值数据的表示   3 基本的数据类型  1.3 习题 第2章 CPU资源和存储器  2.1 寄存器组   1 寄存器组   2 通用寄存器的作用   3 专用寄存器的作用  2.2 存储器的管理模式   1 16位微机的内存管理模式   2 32位微机的内存管理模式  2.3 习题 第3章 操作数的寻址方式  3.1 立即寻址方式  3.2 寄存器寻址方式  3.3 直接寻址方式  3.4 寄存器间接寻址方式  3.5 寄存器相对寻址方式  3.6 基址加变址寻址方式  3.7 相对基址加变址寻址方式  3.8 32位地址的寻址方式  3.9 操作数寻址方式的小结  3.10 习题 第4章 标识符和表达式  4.1 标识符  4.2 简单内存变量的定义   1 内存变量定义的一般形式   2 字节变量   3 字变量   4 双字变量   5 六字节变量   6 八字节变量   7 十字节变量  4.3 调整偏移量伪指令   1 偶对齐伪指令   2 对齐伪指令   3 调整偏移量伪指令   4 偏移量计数器的值  4.4 复合内存变量的定义   1 重复说明符   2 结构类型的定义   3 联合类型的定义   4 记录类型的定义   5 数据类型的自定义  4.5 标号  4.6 内存变量和标号的属性   1 段属性操作符   2 偏移量属性操作符   3 类型属性操作符   4 长度属性操作符   5 容量属性操作符   6 强制属性操作符   7 存储单元别名操作符  4.7 表达式   1 进制伪指令   2 数值表达式   3 地址表达式  4.8 符号定义语句   1 等价语句   2 等号语句   3 符号名定义语句  4.9 习题 第5章 微机CPU的指令系统  5.1 汇编语言指令格式   1 指令格式   2 了解指令的几个方面  5.2 指令系统   1 数据传送指令   2 标志位操作指令   3 算术运算指令   4 逻辑运算指令   5 移位操作指令   6 位操作指令   7 比较运算指令   8 循环指令   9 转移指令   10 条件设置字节指令   11 字符串操作指令   12 ASCII-BCD码调整指令   13 处理器指令  5.3 习题 第6章 程序的基本结构  6.1 程序的基本组成   1 段的定义   2 段寄存器的说明语句   3 堆栈段的说明   4 源程序的结构  6.2 程序的基本结构   1 顺序结构   2 分支结构   3 循环结构  6.3 段的基本属性   1 对齐类型   2 组合类型   3 类别   4 段组  6.4 简化的段定义   1 存储模型说明伪指令   2 简化段定义伪指令   3 简化段段名的引用  6.5 源程序的辅助说明伪指令   1 模块名定义伪指令   2 页面定义伪指令   3 标题定义伪指令   4 子标题定义伪指令  6.6 习题 第7章 子程序和库  7.1 子程序的定义  7.2 子程序的调用和返回指令   1 调用指令   2 返回指令  7.3 子程序的参数传递   1 寄存器传递参数   2 存储单元传递参数   3 堆栈传递参数  7.4 寄存器的保护与恢复  7.5 子程序的完全定义   1 子程序完全定义格式   2 子程序的位距   3 子程序的语言类型   4 子程序的可见性   5 子程序的起始和结束操作   6 寄存器的保护和恢复   7 子程序的参数传递   8 子程序的原型说明   9 子程序的调用伪指令   10 局部变量的定义  7.6 子程序库   1 建立库文件命令   2 建立库文件举例   3 库文件的应用   4 库文件的好处  7.7 习题 第8章 输入输出和中断  8.1 输入输出的基本概念   1 I/O端口地址   2 I/O指令  8.2 中断   1 中断的基本概念   2 中断指令   3 中断返回指令   4 中断和子程序  8.3 中断的分类   1 键盘输入的中断功能   2 屏幕显示的中断功能   3 打印输出的中断功能   4 串行通信口的中断功能   5 鼠标的中断功能   6 目录和文件的中断功能   7 内存管理的中断功能   8 读取和设置中断向量  8.4 习题 第9章 宏  9.1 宏的定义和引用   1 宏的定义   2 宏的引用   3 宏的参数传递方式   4 宏的嵌套定义   5 宏与子程序的区别  9.2 宏参数的特殊运算符   1 连接运算符   2 字符串整体传递运算符   3 字符转义运算符   4 计算表达式运算符  9.3 与宏有关的伪指令   1 局部标号伪指令   2 取消宏定义伪指令   3 中止宏扩展伪指令  9.4 重复汇编伪指令   1 伪指令REPT   2 伪指令IRP   3 伪指令IRPC  9.5 条件汇编伪指令   1 条件汇编伪指令的功能   2 条件汇编伪指令的举例  9.6 宏的扩充   1 宏定义形式   2 重复伪指令REPEAT   3 循环伪指令WHILE   4 循环伪指令FOR   5 循环伪指令FORC   6 转移伪指令GOTO   7 宏扩充的举例   8 系统定义的宏  9.7 习题 第10章 应用程序的设计  10.1 字符串的处理程序  10.2 数据的分类统计程序  10.3 数据转换程序  10.4 文件操作程序  10.5 动态数据的编程  10.6 COM文件的编程  10.7 驻留程序  10.8 程序段前缀及其应用   1 程序段前缀的字段含义   2 程序段前缀的应用  10.9 习题 第11章 数值运算协处理器  11.1 协处理器的数据格式   1 有符号整数   2 BCD码数据   3 浮点数  11.2 协处理器的结构  11.3 协处理器的指令系统   1 操作符的命名规则   2 数据传送指令   3 数学运算指令   4 比较运算指令   5 超越函数运算指令   6 常数操作指令   7 协处理器控制指令  11.4 协处理器的编程举例  11.5 习题 第12章 汇编语言和C语言  12.1 汇编语言的嵌入  12.2 C语言程序的汇编输出  12.3 一个具体的例子  12.4 习题 附录
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Zevalin爱灰灰

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

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

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

打赏作者

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

抵扣说明:

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

余额充值