除了切换显示页之外,这两个寄存器还有一个重要的功能--滚动屏幕。因为PC机从未要求这两个寄存器必须装入每个显示页的起始地址,所以我们可以设想如果将CRTC12设为0,而将CRTC13设为50H话,那么我们就应该看到显示器上的字符向上滚动了一行。是否会有想象中的结果,请看下面的实验:
C:\ASM\>DEBUG[Enter] -a100[Enter] 0BFC:0100 MOV AX,B800 ;初始化ES寄存器指向显示缓冲区 0BFC:0103 MOV ES,AX 0BFC:0105 MOV DI,0000 ;DI寄存器指向第0个显示页 0BFC:0108 MOV AX,0141 ;AX寄存器装入"蓝色"的"A" 0BFC:010B MOV CX,0800 ;写入一个显示页的数据 0BFC:010E REPZ STOSW 0BFC:0110 MOV AX,0242 ;AX寄存器装入"绿色"的"B" 0BFC:0113 MOV CX,0800 ;写入一个显示页的数据 0BFC:0116 REPZ STOSW 0BFC:0118 MOV BX,0000 ;BX寄存器清0 0BFC:011B MOV AH,00 ;等待键盘输入 0BFC:011D INT 16 0BFC:011F CMP AL,1B ;输入的是"ESC"吗? 0BFC:0121 JZ 013A ;是"ESC",转013A结束程序 0BFC:0123 ADD BX,+50 ;BX寄存器加50H(一个字符行) 0BFC:0126 MOV DX,03D4 ;DX指向索引寄存器 0BFC:0129 MOV AL,0C ;选择CRTC12寄存器 0BFC:012B OUT DX,AL ;输出索引号 0BFC:012C INC DX ;DX指向数据寄存器 0BFC:012D MOV AL,BH ;取得偏移地址的高字节 0BFC:012F OUT DX,AL ;并将其输出至CRTC12 0BFC:0130 DEC DX ;DX指向索引寄存器 0BFC:0131 MOV AL,0D ;选择CRTC13寄存器 0BFC:0133 OUT DX,AL ;输出索引号 0BFC:0134 INC DX ;DX指向数据寄存器 0BFC:0135 MOV AL,BL ;取得偏移地址的低字节 0BFC:0137 OUT DX,AL ;并将其输出至CRTC13 0BFC:0138 JMP 011B ;转011B继续 0BFC:013A MOV AX,0003 ;设置显示模式为03H 0BFC:013D INT 10 0BFC:013F INT 20 ;结束程序
这个程序先在显示缓冲区第0页中写入一屏蓝色的"A",在第1页写入一屏绿色的"B",首先出现在屏幕上的是第0页。我们可以看到每按一次键,屏幕便向上滚动一行,同时可以看到第1页的内容也有一部分出现在屏幕上。连续按键则第0页的内容逐渐滚出屏幕,而第1页的内容出现在屏幕上。直到按下Esc后程序重置显示模式并结束运行。
用这种方法滚动屏幕比使用10H中断要快速,一般情况下我们往往将这种滚屏方法称为"硬件滚屏",而将使用10H中断滚动屏幕称为"软件滚屏"。这种滚屏方法应用不多,因为它总要将下一显示页的内容滚上屏幕。但有时使用这种滚屏技术可以很简单地产生一些特殊效果,比如使一段文字从屏幕下面"徐徐升起"。
③ CRTC14-CRTC15寄存器称光标地址寄存器,用于控制光标位置。这两个寄存器的结构和显示起始地址寄存器(CRTC12-CRTC13)一样,只不过低14位表示的是光标所在的内存地址。
也就是说,CRT控制器看待光标位置的方式和我们不一样,我们习惯于用行与列坐标来给出光标位置,而CRTC则是把光标当做字符来看待,光标位置就是"光标字符"在显示缓存中的偏移地址,单位同显示起址一样使用"字"。例如我们需要将光标置于第一行第四列,则光标地址为"(1×160+4×2)/2=54H"。请看下面的实验:
C:\ASM\>DEBUG[Enter] -a100[Enter] 1028:0100 MOV BX,0000 ;光标地址清0 1028:0103 MOV AH,00 ;等待键盘输入 1028:0105 INT 16 1028:0107 CMP AH,01 ;是"ESC"键吗? 1028:010A JZ 011C ;是"ESC"则转011C结束 1028:010C INC BX ;光标地址加1 1028:010D MOV DX,03D4 ;DX指向索引寄存器 1028:0110 MOV AL,0E ;CRTC14寄存器的索引号送AL 1028:0112 MOV AH,BH ;光标地址高字节送AH 1028:0114 OUT DX,AX ;将光标地址高字节送CRTC14 1028:0115 MOV AL,0F ;CRTC15寄存器的索引号送AL 1028:0117 MOV AH,BL ;光标地址低字节送AH 1028:0119 OUT DX,AX ;将光标地址低字节送CRTC15 1028:011A JMP 103 ;转0103继续 1028:011C INT 20 ;结束程序
运行这个程序,按下任一键后光标跑到第一行第二列上,此后无论按下哪个键光标都会移动到下一个位置,直至按下Esc后程序结束。这个程序看上去很简单,但它又包含了两个新的知识点:
(1)当我们使用16H中断的0号功能取得按键后我们并未跟据AL寄存器中的ASCII码来判断按下何键,而采用了AH中的返回数据;
(2)在向端口输出数据时,我们也没有采用先由索引端口送出寄存器号,后由数据端口送出数据的标准形式,而是将寄存器号放入AL,将数据放入AH,然后向端口3D4H输出了一个字。这两个新知识点都与AH寄存器有关,我们先来讨论第二点。
还记得我们前面讨论的关于"PTR"操作符的问题吗?访问存储器时我们可以用这个操作符指定访问内存中的一个字节或一个字,其实访问端口也可以指定位宽,我们使用AL寄存器访问端口时只能输出或输入一个字节数据,如果使用AX寄存器访问端口就能输出或输入一个字的数据。例如上面程序中的指令"OUT DX,AX"就是向端口3D4H输出一个字数据。那么这一个字数据究竟送到哪儿去了?
从上面这个程序执行的结果来看我们可以猜测出AL寄存器中的数据应该送至端口3D4H,而AH中的数据应该送至端口3D5H,这才可能出现光标移动的结果。这个猜测的确是对的,输出数据的低字节送到了DX寄存器指定的端口,而高字节送到了端口DX+1处。这一点和访问内存时的情况一样,指令"MOV [BX],AX"执行时同样会将AL中的数据送至[BX]处,而将AH中的数据送至[BX+1]处。我们前面曾经说过端口也是一些存储单元,它具有内部寄存器和存储器双重的特性,学习到这里你应该对这个说法有了充分的理解。不过有一点特殊的地方我必须说明,那就是16位数据只能从偶数地址端口输出或输入,而不能从奇数地址端口输出或输入字数据,这一点是和访问内存不一样的。
说了这么多的东西,这一节的内容到此就可以结束了。一般来说我们不提倡通过端口控制显示卡的工作,因为这会使程序的可移植性变坏。这是有先例的,CCDOS2.1版就是因为这个原因无法在EGA/VGA卡上运行。所以除非特殊需要,我们应尽量使用BIOS完成对显示卡的控制,使用直接写屏的方法完成显示操作,这样就可以兼顾效率与可移植性两个方面。