[VBE图形界面开发介绍]做一个支持图形界面的操作系统(上)
内容目录
前言
图形界面(GUI)几乎被现在所有的主流操作系统及应用程序使用,这是因为它提供了极好的人机交互接口,微软大名鼎鼎的Windows就是一个非常成功而明显的例子,据说微软的理念有一条就是“让电脑变得越来越傻瓜,任何人都可以操作它”,很显然,要实现这个梦想,GUI界面是必须的。
如果有操作系统的支持,编写具有图形界面的程序是一件相对来说比较容易的事情,因为操作系统为你管理显卡,为你提供了各种各样诸如画点、画线、画矩形、填充等各种图形函数,你只需要将你所希望显示的东西,通过直接调用这样一些函数生成就行了,操作系统会为你完成余下的一切。
然而,如果你打算自己从头编写一个操作系统,而没办法使用已有的操作系统所提供的便利功能的时候,又应当怎样完成这样一个图形界面呢?
本文打算继续以pyos系统为例,简单描述一下怎样让你自己的操作系统支持图形界面。如果你想更好的理解本篇的内容,你需要对操作系统的引导过程有些许了解,这可以参考一下本文的参考文献1,另外,你还需要对汇编语言有所了解,这可以参考一下本文的参考文献2。本文的实验代码由汇编及C语言完成,如果你对C语言不太了解,可以参考一下本文的参考文献3。
标准的GUI界面应当包括图形化界面的显示及对用户输入的支持,而现在在GUI界面下用的最广泛的用户输入设备就是鼠标了。因此本文打算以两部份分别进行描述。上篇描述图形界面的显示,下篇描述对鼠标的支持。
由于知识及水平所限,对于其中不当及错误之处或者您有任何建议,非常欢迎您与我联系。我会在哈工大纯C论坛上(http://purec.binghua.com)上对本文进行跟踪反馈,本文所描述的资料及源代码也可以在上面找到。
一、显卡接口标准VESA简介
所谓标准,其实就是一种协议,比如显卡接口标准,就是显卡与主机之间进行通信的协议,通过这个协议,主机就能操作及控制显卡。比如主机要让显卡在(x,y)画一个点,需要对显卡进行什么操作,这在协议上都有明确的说明与规定,因此,如果我们要操作一个显卡,只需要按照协议上的说明与规定进行就行了,这里的协议就如同一个说明书一样。
随着显卡的发展,先后出现了很多种协议,比如EGA协议,CGA协议,VGA协议等,而现在用得最广泛的是由国际视频电子标准协会(Video Electronics Standards Association)制定的称为VESA的协议,现在最新的协议版本是3.0,不过由于目前并不是所有的显卡均支持此项协议,特别是众多的虚拟机都不支持,因此本文将以2.0版本作为描述的基础,由于各版本是向下兼容的,因此基于2.0的程序完全可以不经修改的应用在3.0版本上。
VESA标准包括了很多的子标准,其中对于操作系统编写最有用的就是VBE标准(Vesa Bios Extension),在实际的系统编写中,我们按照此标准,通过调用BIOS的0x10号中断,而对显卡进行操作,在调用此号中断的时候,ax寄存器中存放的就是你想使用的显卡的功能。比如VESA 2.0标准规定:0x4F00号功能可以返回显卡所支持的VESA标准的信息,0x4F01号功能可以返回所指定的显示模式的信息,诸如行列像素是多少,每像素的字节数是多少之类。调用这些功能是非常方便的,比如,通过阅读VESA标准,我们知道0x4F02号功能可以用来设置显示模式,调用此功能时,bx中存放的是欲设置的显示模式的编号,因此,如果我们想将显卡的显示模式设为0x111模式,那么我们应当编写如下的代码:
|
|
执行了上面的代码之后,显卡就被设置成了0x111号显示模式,那么这个模式有些什么显示特性呢?请看下面的表格:
(表1 VESA标准定义的显示模式(部份))
模式号 | 分辨率 | 色彩 |
---|---|---|
0x100 | 640*400 | 256 |
0x101 | 640*480 | 256 |
0x102 | 800*600 | 16 |
0x103 | 800*600 | 256 |
0x104 | 1024*768 | 16 |
0x105 | 1024*768 | 256 |
0x106 | 1280*1024 | 16 |
0x107 | 1280*1024 | 256 |
0x10D | 300*200 | 1:5:5:5 |
0x10E | 320*200 | 5:6:5 |
0x10F | 320*200 | 8:8:8 |
0x110 | 640*480 | 1:5:5:5 |
0x111 | 640*480 | 5:6:5 |
0x112 | 640*480 | 8:8:8 |
0x113 | 800*600 | |
0x114 | 800*600 | 5:6:5 |
0x115 | 800*600 | 8:8:8 |
0x116 | 1024*768 | 1:5:5:5 |
0x117 | 1024*768 | 5:6:5 |
0x118 | 1024*768 | 8:8:8 |
0x119 | 1280*1024 | 1:5:5:5 |
0x11A | 1280*1024 | 5:6:5 |
0x11B | 1280*1024 | 8:8:8 |
上面的表格列出了VESA标准所定义的部份显示模式,其中色彩有两种表示形式。一种是所谓的“调色板”模式,一种是所谓的“真彩”模式,“调色板”模式主要是为了兼容以前的老式显卡,那种显卡上的显存数量一般说来都非常的少,因此,显卡上一次最多只能存储256种(8位)或更少的16种色彩(4位),比如本表前面部份所示。显卡上把这些色彩组织成为一个表,这个表就称之为一个调色板,然后每点的色彩信息其实就是一个下标,用于从调色板中检索出真正的色彩。比如一个点的色彩是2,则表示使用调色板中的第三个色彩(因为从0开始编号,所以2则对应调色板中第三项)。很显然,不是所有的色彩都能被记录在小小的调色板中,因此一般说来,对于一种给定色彩,我们需要用调色板中与它最相近的色彩进行显示。
现在的显卡一般都有很大的显存,因此它可以完整的存放一个点的色彩信息。我们知道,任何一种色彩都可用不同强度(亮度)的红(R)、绿(G)、兰(B)三种色彩合成,因此,我们要记录或给出一个点的色彩信息,只需要给出红、绿、兰这三种色彩各自的强度(亮度)就行了,因此,就出现了多种不同的编码方法,比如“5:6:5”模式,就表示用最高的5位表示红色的强度,用中间的6位,表示绿色的强度,用最后的5位表示兰色的强度,这样表示一个色彩,总共需要16位,即2B,而“8:8:8”则表示,最高的8位表示红色的强度,随后的8位,表示绿色的强度,最后的8位表示兰色的强度,一个色彩用24位,即3字节进行表示。依次类推。由于这些色彩都被真实的记录下来了,因此这种模式又被称为“真彩”模式,如表1的后面部份所示。
由上面描述可知,我们可以根据我们的需要,选择适当的显示模式,然后调用VBE标准中所定义的中断设置显示卡。
VBE标准描述了大量的功能,这里不打算将它全部描述,只介绍下面行文所需要的功能,如果你想了解整个VBE标准所描述的功能,请参考本文的参考文献4。
下面我们再描述一个下面需要用到的0x4F01功能。这个功能可以把显卡所支持的显示模式的对应信息返回到用户所指定的地址中。它可以如下调用:
|
|
返回的信息是一个256字节的,很庞大的结构,这里我们只介绍我们下面行文感兴趣的部份。
在返回的结构体中偏移量为40的地方,即es:di+40处,用4字节存放了一个线性地址,这就是这个显卡在此模式下显存的线性地址,因此,如果我们直接向这个地址写入数据,那么这数据就会被直接写到显存上,这样就可以显示出我们所需要显示的信息了。这就是所谓的“直接写屏”。
返回的结构体中还包括了该显卡在此模式下每行行像素,列像素等其它众多信息,如果你需要详细了解,请参看本文的参考文献4。
二、用Pyos进行实验
2.1 引导代码分析
令人非常高兴的是,这次实验我们不需要了解太多的基础知识,而就可以很快的进入实验了,下面我们就来看看我们这次的实验。
在实验前,我们需要先设定一个我们需要实验的显示模式,这里我们暂且定为640*480,采用的色彩模式为5:6:5模式,即0x111模式。
我们先来看看我们这次实验的最终结果:
现在,让我们先来看看我们的引导代码:
|
|
这是pyos引导程序(boot.asm)的主函数代码,它现在已经很简单了,最开始,你需要设定一下段寄存器,比如初始化一下数据段寄存器(DS),堆栈段寄存器(SS),及堆栈指针(SP)等,然后,通过read_setup这个子程序,读入setup程序(steup.asm),将其读到SETUP_SEG:SETUP_OFFSET处(SETUP_SEG及SETUP_OFFSET都是在boot.asm最开头定义的常量),关于各子程序的详细代码,请参看本实验的源代码,其中均有很详尽的注释。
下面,让我们看看setup(setup.asm)程序:
|
|
上面这段程序也是非常简单的,首先,重新初始化一下段寄存器,然后设置图形显示模式,之后,读入kernel程序(这是真正的操作系统的内核代码),然后再读入字库(这点在下面会有较为详尽的描述),随后再读入一张hit的图片(hit是“哈尔滨工业大学”的英文缩写,图片采用的是一种bmp格式,这在下面也会有详细描述),之后进行转换到保护模式下的工作,最后切换到保护模式下的kernel程序处,即真正的操作系统内核代码中执行。有关这部分切换到保护模式下的工作,本文的参考文献1中有非常详细的描述,如果你对此部份有疑惑,可以参看一下。下面我们来看看这次的核心子程序set_vesa_model的代码,其余代码请参看本实验的源程序。
|
|
程序非常简单,下面简单的解释一下,进入程序后,我们最先就设置了显卡的显示模式:
|
|
上面这几行代码在前面描述VBE标准时就介绍过了,这里我们设置的是640*480(5:6:5),即对应的0x111模式(见表1),不过,你会发现这里送入bx的是0x4111而不是0x111,这是为什么呢?这主要是因为,我们要使用线性地址模式,也就是说通过直接访问物理内存空存来访问所有的显存空间,因此,这时的送入bx的就应当是0x4000|模式号,对于此处就是:0x4000 | 0x111 = 0x4111(将模式号与0x4000进行与操作,这也是VBE标所规定的)。
随后,我们用0x4F01功能取得此模式下显卡显存的线性地址:
|
|
上面由于 es = TEMP_DATA_SEG,di = VESA,所以读取出来的信息存放于TEMP_DATA_SEG:VESA处,随后,从信息中取出显卡在此模式下显存的线性地址:
|
|
上面代码很简单,由于显卡在此模式下显存的线性地址是存在于返回的信息块中偏移量为40之处,所以,用“mov eax , [ es:VESA + 40 ]” 从中读取到eax中,然后,再把其放入[ fs:1 ]处,由于fs等于TEMP_DATA_SEG,所以,是存放在了TEMP_DATA_SEG:1处。
上面程序中的 SETUP_SEG , VESA , TEMP_DATA_SEG都是在程序最初所定义的常量,详情请参看源代码。
2.2 pyos 内存结构
由于本实验的pyos还没有文件系统及内存管理与分配程序,因此,在本实验中,pyos采用的是磁盘及内存的绝对定位,即所有数据在内存中的位置都是固定的,下面就让我们来看看 pyos 的内存结构。
0x0000:0x7c00 :此处存放的是boot.asm的代码,由系统加电时BIOS自动读入
0x1000:0x0000 :此处存放的是setup.asm的代码,由boot.asm读入
0x2000:0x0000 :此处存放的是内核代码(主要是kernle.c),由setup.asm读入
0x6000:0x0000 :此处存放的是hit的图片数据,由setup.asm读入
0x7000:0x0000 :此处存放的是英文点阵字库数据,由setup.asm读入
0x8000:0x0000 :此处存放的是中文点阵字库数据,由setup.asm读入
0x9000:0x0000 :此处存放的是启动驱动器号,由boot.asm存入
0x9000:0x0001 :此处存放的是显卡显存的线性地址
2.3 pyos 图形驱动
在程序由setup.asm最后通过“jmp dword 0x8:KERNEL_ENTRY”跳入内核代码之后,操作系统开始正式运作,下面就让我们来看看这个内核代码:
|
|
上面的代码非常简单,它首先进行了一下系统初始化,然后进行清屏,pyos清屏采用的是最蜗牛的一种办法,它把整个屏幕当做一个矩形(x:0~639,y:0~479),然后用一种色彩去画这个矩形中的每一个点。vesa_compond_rgb()这个函数用于把用户输入的R(红),G(绿),兰(B)值合成为一个16位长的整数(因为采用的是5:6:5的模式,因此总共一个点的色彩用16位整数表示),然后,它调用了vesa_draw_rect()函数来画这个矩形,我们就来看看这个函数:
|
|
非常简单,它首先画矩形的边框,然后,根据用户的输入参数决定是否调用vesa_fill_rect()函数对这个矩形进行填充。下面我们就来看看这个填充函数:
|
|
晕!太简单了,原来就是不停的调用vesa_draw_point()函数,用同一个色彩画出矩形中的所有点,于是乎,我们还得溯本求源,来看看vesa_draw_point()这个函数:
|
|
这个函数是整个Pyos VESA显卡驱动的根基,所有画线、画矩形、填充矩形都是由它完成的,现在我们就来分析一下,注意下面的一行代码:
|
|
首先,在2.2节中我们知道了0x9000:0001处存放了显卡显存的线性地址(这个线性地址是32位,即4B),因此,把0x9000:0001转换为线性地址就是0x90001,然后,我们在C语言中把它转强行转换成一个指向unsigned int型的指针,然后通过 * 操作符,取出了这个指针,即地址为0x90001处所存的数据,也就是取得了存放于此处的显存的线性地址,然后把它强性转换为一个unsigned short型的指针,这是因为,每个点都是用2B字节,也就是都是用一个unsigned short类型的数据表示的,这个线性地址也是(0,0)点的地址。
随后,我们通过列坐标:x,行坐标:y,算得了(x,y)点的偏移量,最后直接把色彩信息赋值给这个(x,y)点所在的内存地址,那么(x,y)点处就显现出了我们所指定的色彩。实际上就这么简单!
2.4 英文、中文及图形的显示
我们先来谈谈图形,这里主要是指位图,即所谓的BMP格式图形的显示,位图是只这副图上的每一个点(位)都用数据完成的记录下来,它包含了每个点的色彩信息,比如24位位图,就是说这副图上的每个点都用一个24位的数据,即3字节数据保存下来了。当然,被保存下来的数据可以是调色板中的色彩索引值,或者就是RGB数据等。那么到时候,我们就可以读出每一个点的色彩数据,然后,把它们写入对应的显示内存中,就像2.3节所描述的一样,一点一点的把它们给全部画出来,当然,在使用位图的时候,需要知道这副位图每个点用多少字节表示,一行有多少个点,总共有多少行,是一行一行的存放的,还是一列一列的存放的,当然,这些都是实际的实现问题,这里就不多讲述了,你完全可以在了解原理之后自己决定自己的实现方式,创造自己的图形格式。
每个英文字符与每个中文汉字也是一样,它们也都是图,不过,一般来说,这个图只需要用一种色彩显示就可以了,于是,我们就不需要记录下每一点的色彩信息,而只需要记录每个点是否需要显示就行了,不需要显示的点,我们可以记为0,需要显示的点我们可以记为1,因此,如果一行有8个点,我们只需用8位,即1B就可以存储了,第0~7位,对应于实际一行上的0~7点,如果哪一位为1,那么就在显卡上画出对应位的点。
pyos中用的汉字是采用的1616的点阵字库,这种字库中,每个汉字用1616个点表示,即有16行,每行16个点,由前所述,我们可以把每行的16个点,用16位,即2B表示,于是,一个汉字占2*16=32B,这也称之为一个字模,人们把所有的汉字信息即所有的字模,做在一起就形成了一个字库,需要显示时,只需要从字库中取出相应的字模,然后分析它的每一位,最后,在显示屏(显卡)上相应的点上把色彩值赋值给它们就行了。
英文字符也是一样,在pyos中用的是816的英文点阵字库,这种字库中,每个英文用816个点表示,即有16行,每行8个点,即1B的空间,故,一个英文字符需占的空间为1*16=16B,显示的原理及方法均同前所述。
现在的问题就是怎么知道一个英文字符或者一个中文字符在字库中的起始位置呢?对于英文来说,每个字符都有一个ascii码,在英文字库中,英文符号通常都是按ascii码顺序进行存放的,因此,用这个英文的ascii码,乘以每个英文字符需占的空间,就可以得到该英文字符在英文字库中的位置了。
中文就稍微麻烦一些,我们知道,一个中文是由两个字节表示,第一个字节等于汉字的区号加上161,第二个字节等于汉字的位号加上161,汉字是按区号与位号进行存放的,国标汉字中总共有94个区,每个区有94个位,这也是我们常常说的“区位码”,汉字字模就是按着区位码一个区一个区的存放的,因此,知道汉字的区位码后,我们就可以算得该汉字在字库中的位置,比如,一个汉字的第一节为ch1,第二字节为ch2,于是汉字的区号为sector=ch1-161,汉字的位号为position=ch2-161,则汉字在字库中的位置为:pos_in_font=(sector*94+position)*size,其中size是每个汉字在字库中所占的空间。
需要注意的是,上面讲的都是一些原理性描述,而实际中人们并不一定这样使用,比如这次的pyos实验,由于所需要显示的数据非常之少,因此,用不着将那么大的字库全部载入内存,因此,就预先写了一个程序把需要显示的字从字库中提取了出来,然后,把这个生成的小字库载入内存,最后显示的时候,是从小字库中按提取时的顺序依次显示的(请参考本实验源程序中的说明)。当然,怎么从字库中提取出小字库,就需要用到上面原理性的描述了。
有关汉字显示方面的详述描述,可以参看一下本文的参考文献5。
上面所讲的字库都是指点阵字库,在这种字库中每个字都像一个位图,实际使用的还有一种矢量字库,有关这方面的信息,请查阅相关文献。
三、有关本次实验的更进一步的说明
本次实验描述了怎样进行操作系统的GUI界面开发,但描述的非常简单,存在很多不足,这里有几点需要注意一下:
首先,本文第一章所描述的对显卡的设置等操作都是在实模式下完成的,这主要是由于VESA提出的比较早,对保护模式的支持比较有限,现在VESA 3.0提供了一个相对完整的保护模式接口,你可以在保护模式下重新设置显卡的分辨率,显示模式等。但VESA 2.0并没有提供这样的接口,你不能在保护模式下重新设置它们。解决的方法有三种,一是在你的操作系统进入实模式之前,就让用户选择显示模式,当操作系统进入保护模式后,就不再允许用户进行选择(这其实不能算是解决),二是重新切换回保护模式,三是切换到虚拟8086(Virtual 86)模式,后两种方式难度都较大,而且比较麻烦,有兴趣的可以试试。
其次,本实验中相当多的数据,比如每行的显示像素数,每行显示像素的字节数等都是做为已知,设定为固定的,虽然在大多数情况下没有什么问题,但做为一个更通用或更好的系统,应当通过VBE标准,从显卡中动态读取,因为有可能你的显卡并不支持你先前假设的数值,这可以通过第一章中介绍,通过调用0x4F00及0x4F01功能获得相应的数据及信息。
本次pyos采用的显卡模式是线性地址模式或说线性缓冲区模式,即显卡的显存地址被完整而连续的映射到了线性内存空间中,而有些显卡不支持这种模式,而需要使用所谓的页模式,即将显存分成几页,每页64KB或128KB,然后把它们映射到内存中的一块64KB或128KB的区间中,如果你需要显示超过64KB或128KB的部分,需要通过VBE提供的功能调用进行页切换,切换到你指定的页(把你指定的页映射进内存中来)进行操作。
另外,本次pyos在设置显卡模式时,并没有检测是否出错,显卡是否支持所设定的模式,在实际的应用系统中这些都不应是事先假定好的,而必须检测。
这些内容,可以参考一下本文的参考文献4,以获得解决信息。
再后,本次pyos实现的图形驱动太简单太蜗牛了,完全是通过画点来完成,而且只完成了画直线,而不能画斜线,画圆角,而这些涉及到相当一部份图形学的知识,有兴趣的朋友可以研究研究。
最后,这次实验只能看,而用户无法进入操作,还不能称之为一个界面。在下篇里,将描述一下怎样编写鼠标驱动,以弥补这个缺隙
参考文献
1.《操作系统引导探究》(Version 0.02)(谢煜波)http://purec.binghua.com
2.《IBM PC ASSEMBLY LANGUAGE AND PROGRAMMING》(FOURTH EDITIOM)(影印版)(Peter Abel)清华大学出版社
3.《C语言大学实用教程》(苏小红、陈惠鹏、孙志岗)电子工业出版社
4.《Vesa Bios Extension Core Functions Standards》 http://www.vesa.org
5.《点阵汉字显示》(Little Tiger) http://purec.binghua.com
6.VESA BIOS Extension (VBE) 3.0 http://pdos.csail.mit.edu/6.828/2010/readings/hardware/vbe3.pdf
Author 软件开发大郭
LastMod 2021-08-28