文章修改记录
修改日期 | 修改内容 |
---|---|
2018-2-4 | 修改了一处错别字;增加了表格的使用方法 |
今天我们讨论如何编程以在屏幕上显示出彩色的文字。
为了显示文字,通常需要两种硬件——显示器和显卡。
显卡的作用是为显示器提供要显示的内容,并且控制显示器的模式和状态。
显示器的作用是把那些内容以人们可见的方式呈现在屏幕上。
1.显存
每个显卡都有自己的存储器,因为它位于显卡上,所以称为显示存储器,简称“显存”。和其他存储器一样,显存并没有什么特殊的地方,也是一个按字节访问的存储器件。
2.显卡的两种工作模式
显卡最基本的两种工作模式是文字(也称为文本)模式和图形模式。在不同的模式下,显卡对显存内容的解释是不同的。要想设置显卡的显示模式,可以用指令访问显卡,也可以直接调用BIOSint 10h
中断。
3.BIOS调用之设置显示模式
功能号:AH = 00H
用 途:设置显示模式
参 数:AL = 显示模式号
调 用:INT 10H
返 回:无
AL的取值说明:
AL | 文字/图形 | 分辨率 | 颜色 |
---|---|---|---|
00H | 文字 | 40*25 | 2 |
01H | 文字 | 40*25 | 16 |
02H | 文字 | 80*25 | 2 |
03H | 文字 | 80*25 | 16 |
04H | 图形 | 320*200 | 2 |
05H | 图形 | 320*200 | 4 |
06H | 图形 | 640*200 | 2 |
需要说明的是:计算机在加电自检后会自动初始化到AL=03H
的文字模式。在这种模式下,一屏幕可以显示25行,每行80个字符,总共是80*25=2000个字符。
4.文本模式下,显存到内存的映射
0xB8000
到0xBFFFF
这段物理地址被映射到显存。也就是说,写这些物理地址,就可以控制显示内容。
显存和每个字符(假入从0开始数,那就是0~1999)的对应关系,如下图所示。
5.关于属性
bit | [7] | [6:4] | [3:0] |
---|---|---|---|
含义 | 1:字闪烁;0:字不闪烁 | 背景色 | 前景色 |
5.1 背景色
因为[6:4]决定背景色,所以取值是0~7。根据我判识色彩的能力,总结如下。
[6:4]取值 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
颜色 | 黑 | 深蓝 | 绿 | 青 | 红 | 粉红 | 棕 | 灰白 |
5.2 前景色
因为[3:0]决定前景色,所以取值是0x0~0xF。根据我判识色彩的能力,总结如下。
[3:0]取值 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
颜色 | 黑 | 深蓝 | 绿 | 青 | 红 | 粉红 | 棕 | 灰白 | 灰 | 亮蓝 | 亮绿 | 亮青 | 亮红 | 亮粉红 | 黄 | 亮白 |
6.编程实践——遍历所有颜色
关于颜色,眼见为实。也许我眼中的青色,在你眼中就是蓝色。不妨编程看看,显示在屏幕上的到底是什么颜色。
思路:在不考虑闪烁的情况下,前景色搭配背景色,共有16*8=128种可能,我们的目的是提供一个表格,每一行表示前景色的不同取值,每一列表示背景色的不同取值。
关于要显示的内容,可以选择一个字符串(考虑到一行最多显示80个字符,80/16=5,所以字符串长度不宜超过5个),一共显示128次。
6.1 通过调用BIOS中断实现
jmp near start
message db 'KARL ' ;字符串任意,但是不要超过5个字符
;取KARL是因为KARL是我徒弟的英文名
;int 10h (video service)
;AH=13h, 在teletype模式下显示字符串
;入口参数:
; AL[1:0]=显示方式
; [0]: 0表示不移动光标,1表示移动光标
; [1]: 0表示字符串中仅包含字符,不包含属性,属性在BL中;1表示字符串中包含属性
; BH=页码
; BL=属性
; CX=字符串长度
; DH=行
; DL=列
; ES:BP=指向字符串
;
;出口参数:无
start:
mov ax,0x7c0 ;设置ES段的段地址
mov es,ax
mov bp,message ;ES:BP指向字符串
mov ah,0x13 ;在teletype模式下显示字符串
mov al,1 ;显示方式,表示字符串中仅包含字符,不包含属性,属性在BL中,移动光标
mov bl,0 ;属性初始值
mov bh,0 ;页码
mov dh,0 ;从0行开始
mov cx,8 ;循环8次,从0行到7行
put_0_8: ;------------------------------------外层循环
push cx ;因为内层循环也要用CX控制循环次数,所以压栈保护
mov dl,0 ;从0列开始
mov cx,16 ;循环16次,从0列到15列
put_0_F: ;------------内层循环
push cx ;因为循环体中要用到CX,所以压栈保护
mov cx,5 ;设置字符串长度
int 0x10 ;BIOS中断调用
inc bl ;改变属性,属性值增加1
add dl,5 ;改变列,列值增加5
pop cx
loop put_0_F ;------------内层循环
pop cx
inc dh ;改变行,行增加1
loop put_0_8 ;----------------------------外层循环
jmp near $ ;使陷入死循环
times 510-($-$$) db 0
db 0x55,0xaa
运行结果如下图
6.2 通过自己写过程实现
jmp near start
message db 'KARL '
db 0 ;本程序的过程规定以0结尾
start:
mov ax,0x7c0 ;设置数据段的段基地址
mov ds,ax
mov bx,message ;使DS:BX指向字符串
mov al,0 ;属性初始值
mov dh,0 ;从0行开始
mov cx,8 ;循环8次,从0行到7行
put_0_8: ;----------------------------外层循环
push cx
mov dl,0 ;列的初始值
mov cx,16 ;循环16次,从0列到15列
put_0_F: ;--------内层循环
call put_string
inc al ;改变属性,属性值增加1
add dl,5 ;改变列,列值增加5
loop put_0_F ;--------内层循环
pop cx
inc dh ;改变行,行增加1
loop put_0_8 ;----------------------------外层循环
jmp near $
;-------------------------------------
;功能:在某位置显示字符串
;入口参数:
; AL=属性
; DH=行
; DL=列
; DX:BX=指向字符串,字符串必须以0结尾
;出口参数:无
put_string:
push ax
push bx
push cx
push dx
push di
push es
push ax ;AX中是属性,因为下面要用AX,所以先进栈保护起来
mov ax,0xb800
mov es,ax
; x行y列,换算成偏移是:(x*80+y)*2
mov al,80
mul dh ;ax=al*dh (计算出x*80,结果在ax中)
xor dh,dh ;dh清零
add ax,dx ;计算出(x*80+y),结果在ax中
shl ax,1 ;计算出(x*80+y)*2,结果在ax中
mov di,ax ;用di保存偏移
pop ax ;得到属性
put_char:
mov cl,[bx] ;取要显示的字符到cl中
cmp cl,0 ;和0比较
jz end ;等于0则跳转
mov [es:di],cl ;写字符的ASCII码到显存
inc di
mov [es:di],al ;写字符的属性到显存
inc di ;di指向显存中的下一个位置
inc bx ;bx指向下一个字符
jmp put_char
end:
pop es
pop di
pop dx
pop cx
pop bx
pop ax
ret ;返回
;-------------------------------------------
times 510-($-$$) db 0
db 0x55,0xaa
6.3 将实验结果制作成表格
可以看到,当前景色和背景色取值相同时,就看不到字了。所以,属性组合不是128种,而是128-8=120种。
查询表格的时候,最左边一列的数字表示背景色,最上面一行的数字表示前景色(即字的颜色)。
- 举例1:0x02——黑底绿字
- 举例2:0x04——黑底红字
- 举例3:0x24——绿底红字
7.使用LOOP
要注意什么
上面的代码使用了LOOP
实现循环,对于初学者,用LOOP
时需要注意的是:
- 循环之前的初始化,比如循环次数(CX)、变量的初始值等
- 标号的位置
- 循环体内变量的自增/自减
- 对于嵌套的LOOP,尤其要注意CX的压栈出栈和其他寄存器(如果需要)的压栈、出栈
【完】