一步步编写操作系统 71 直接操作显卡,编写自己的打印函数71-74

一直以来,我们在往屏幕上输出文本时,要么利用bios中断,要么利用系统调用,这些都是依赖别人的方法。咱们还用过一个稍微有点独立的方法,就是直接写显存,但这貌似又没什么含量。如今我们要写一个打印函数了,似乎,我们马上就要站起来了

之前我们讲述了有关显卡的知识,但当时怕影响兄弟们的学习积极性,我们并没有说把有关显卡的寄存器罗列出来。话说,出来混早晚要还的,躲得过初一躲不过十五。如今我们需要通过端口来控制显卡的行为,有些问题还是要面对的。

之前咱们对显卡的操作和对普通内存操作是一样的,打印字符时,就是往显存中mov一些字符的ascii码和属性,那还是我们在显存默认的文本模式下。您想,我们都爱看视频、电影,话说十年前第一次看到DVD版本的电影时我都被震撼到了,当时看的是《星河战队》,清晰到毛发可见的程度,何况大家现在都偏爱蓝光高清版本,咳咳,说远了,总之能够让我们看到如此炫丽的画面,这都是显卡的功劳,这说明显卡还可以工作在彩色图形模式。对于显卡的操作可不是咱们之前的mov来mov去就行了。不过我们也并不需要那么复杂的功能,咱们还是在80*25的文本模式下转悠,而且还只是简单的操作。

之前我们已经对硬盘有过端口操作了,无非就是用in和out指令加不同的端口号,对显卡也是如此。显卡中的寄存器很多,不,是非常多,这里按照它们在图形管线(位于cpu和video之间)中的位置的顺序给大家介绍下,见下表

 

如您所见,表中列出的寄存器的数量似乎没我说的那么恐怖,不要高兴的太早,马上就要让大伙儿难过了,其实这些只是寄存器的目录而已,这有没有让大家想起了周星驰主演的电影《鹿鼎记》中,天地会总舵主陈近南让韦小宝练武功时的场景,拿出了一本不算太厚的“武功秘密”,起初小宝还很高兴,但陈近南告诉他这只是个目录,而且是练了之后才九死一生,否则就十死无生^_^。

好了,下节到解释,本节到此,现来玩哦。

接上文,请见“一步步编写操作系统 71 直接操作显卡,编写自己的打印函数1” 下面解释下显卡寄存器的内容。

以上所说的目录其实就是寄存器分组,在这些寄存器中也不全是分组。前四组寄存器属于分组,它们有一个特征,就是被分成了两类寄存器,即Address Register和Data Register。这两个寄存器是干吗的呢?这得先从寄存器为什么要分成组开始说。

端口实际上就是IO接口电路上的寄存器,为了能访问到这些cpu外部的寄存器,计算机系统为这些寄存器统一编址,一个寄存器被赋予一个地址,这些地址可不是我们所说的内存地址,内存地址是用来访问内存用的,其范围取决于地址总线的宽度,而寄存器的地址范围是0~65535(Intel系统)。这些地址就是我们所说的端口号,用专门的IO指令in和out来读写这些寄存器。至于计算机内部访问端口怎么实现的,这是硬件工程师的事,咱们暂且奉行拿来主义,认同这个事实就够了。

IO接口电路上的寄存器数量有多有少,这要看具体的外设了,我这么说您就明白了,这里给寄存器分组的原因是,显卡(显示器的IO接口电路)上的寄存器太多了,如果一个寄存器就要占用一个系统端口的话,这得多浪费硬件资源,万一别的硬件也这么干,这63336个地址可就捉襟见肘了。所以计算机系统说了,我不管你们内部有多少寄存器,给你们的端口地址是有数的,你们自己内部协调吧。

计算机工程师是非常聪明的,把数据结构中数组的知识用到了硬件中。他们把每一个寄存器分组视为一个寄存器数组,提供个寄存器用于指定数组下标,再提供个寄存器用于对索引所指向的数组元素(也就是寄存器)进行输入输出操作。这样用这两个寄存器就能够定位寄存器数组中的任何寄存器啦。

这两个寄存器就是各组中的Address Register和Data Register。Address Register做为数组的索引(下标),Data Register做为寄存器数组中该索引对应的寄存器,它相当于所对应的寄存器的窗口,往此窗口读写的数据都作用在索引所对应的寄存器上。

所以,对这类分组的寄存器操作方法是,先在Address Register中指定寄存器的索引值,用来确定所操作的寄存器是哪个,然后在Data Register寄存器中对所索引的寄存器进行读写操作。

上面CRT Controller Registers寄存器组中的Address Register和Data Register的端口地址有些特殊,它的端口地址并不固定,具体值取决于Miscellaneous Output Register寄存器中的Input/Output Address Select字段,现在咱们看一下这个寄存器。

 

和大家坦白一点,显卡参数还需要专业人士来解释,由于咱们用不到这么高深的设置,加之我对显卡没有深入学习,所以这里面有好多参数术语,我不敢随意翻译成中文,担心误导大家,所以我直接把此寄存器各字段的英文描述搬过来了,至于中文的意思,大家仁者见仁智者见智吧,请您见谅。

 

好了,简直了,就这样吧,晚安。

 

 

万事开头难,我们先从简单的打印字符开始。这个功能类似c语言中的putchar,每次只打印一个字符,由于此函数咱们是在内核中实现的,暂且将其命名为put_char。

在这之前,为了开发方便,我们定义一些数据类型。主要是参考了linux的/usr/include/stdint.h文件,有环境的同学可以自行看下,没环境的同学,请看图

 

该文件在我目前的linux版本上是320行,这里只是冰山一角,里面各种宏显得好高大上啊,不过请放心,把这个图贴出来就是为了“吓唬”大家的^_^,咱们不会写这么复杂,不信请看代码:

 1	#ifndef __LIB_STDINT_H
 2	#define __LIB_STDINT_H
 3	typedef signed char int8_t;
 4	typedef signed short int int16_t;
 5	typedef signed int int32_t;
 6	typedef signed long long int int64_t;
 7	typedef unsigned char uint8_t;
 8	typedef unsigned short int uint16_t;
 9	typedef unsigned int uint32_t;
10	typedef unsigned long long int uint64_t;
11	#endif

怎么样,确实是很简单吧。以后我们采用的任何数据类型就要用这些定义好的啦。估计大家也注意到啦,咱们定义的stdint.h文件位于lib目录下,也就是说我新建了个lib目录做来专门存放各种库文件。不仅如此,在lib目录下还建立了user和kernel两个子目录,以后供内核使用的库文件就放在lib/kernel/下,lib/user/中是用户进程使用的库文件。

我们要实现的字符打印函数叫put_char,它是用汇编语言写的。因为要和显卡打交道啦,里面涉及到端口的读写操作,目前还是用纯汇编文件较方便,以后慢慢发展起来后,咱们会采取内联汇编的方式。

直接上代码啦,我们的打印函数统统在print.S文件中完成,该文件是各种打印函数的核心,重中之重,这里先给大家介绍下它的处理流程:

  1. 备份寄存器现场。
  2. 获取光标坐标值,光标坐标值是下一个可打印字符的位置。
  3. 获取待打印的字符。
  4. 判断字符是否为控制字符,若是回车符、换行符、退格符三种控制字符之一,则进入相应的处理流程。否则,其余字符都被粗暴地认为是可见字符,进入输出流程处理。
  5. 判断是否需要滚屏
  6. 更新光标坐标值,使其指向下一个打印字符的位置。
  7. 恢复寄存器现场,退出。

该文件相对来说又有点长,故需要将其拆分成3部分,先给大伙儿呈上其第一部分,代码:

 1 TI_GDT equ 0
 2 RPL0 equ 0
 3 SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
 4
 5 [bits 32]
 6 section .text
 7 ;------------------------ put_char -----------------------------
 8 ;功能描述:把栈中的1个字符写入光标所在处
 9 ;-------------------------------------------------------------------
 10 global put_char
 11 put_char:
 12 pushad ;备份32位寄存器环境
 13 ;需要保证gs中为正确的视频段选择子,
 ;为保险起见,每次打印时都为gs赋值
 14 mov ax, SELECTOR_VIDEO ; 不能直接把立即数送入段寄存器 
 15 mov gs, ax
 16
 17 ;;;;;;;;; 获取当前光标位置 ;;;;;;;;;
 18 ;先获得高8位
 19 mov dx, 0x03d4 ;索引寄存器
 20 mov al, 0x0e ;用于提供光标位置的高8位
 21 out dx, al
 22 mov dx, 0x03d5 ;通过读写数据端口0x3d5来获得或设置光标位置
 23 in al, dx ;得到了光标位置的高8位
 24 mov ah, al
 25
 26 ;再获取低8位
 27 mov dx, 0x03d4
 28 mov al, 0x0f
 29 out dx, al
 30 mov dx, 0x03d5
 31 in al, dx
 32
 33 ;将光标存入bx
 34 mov bx, ax
 35 ;下面这行是在栈中获取待打印的字符
 36 mov ecx, [esp + 36] ;pushad压入4×8=32字节,
 ;加上主调函数4字节的返回地址,故esp+36字节
 37 cmp cl, 0xd ;CR是0x0d,LF是0x0a
 38 jz .is_carriage_return
 39 cmp cl, 0xa
 40 jz .is_line_feed
 41
 42 cmp cl, 0x8 ;BS(backspace)的asc码是8
 43 jz .is_backspace
 44 jmp .put_other
 45 ;;;;;;;;;;;;;;;;;;

下节我们再解释代码吧,再来玩哦。

 

 

接前文,下面把代码解释一下。

 1 TI_GDT equ 0
 2 RPL0 equ 0
 3 SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
 4
 5 [bits 32]
 6 section .text
 7 ;------------------------ put_char -----------------------------
 8 ;功能描述:把栈中的1个字符写入光标所在处
 9 ;-------------------------------------------------------------------
 10 global put_char
 11 put_char:
 12 pushad ;备份32位寄存器环境
 13 ;需要保证gs中为正确的视频段选择子,
 ;为保险起见,每次打印时都为gs赋值
 14 mov ax, SELECTOR_VIDEO ; 不能直接把立即数送入段寄存器 
 15 mov gs, ax
 16
 17 ;;;;;;;;; 获取当前光标位置 ;;;;;;;;;
 18 ;先获得高8位
 19 mov dx, 0x03d4 ;索引寄存器
 20 mov al, 0x0e ;用于提供光标位置的高8位
 21 out dx, al
 22 mov dx, 0x03d5 ;通过读写数据端口0x3d5来获得或设置光标位置
 23 in al, dx ;得到了光标位置的高8位
 24 mov ah, al
 25
 26 ;再获取低8位
 27 mov dx, 0x03d4
 28 mov al, 0x0f
 29 out dx, al
 30 mov dx, 0x03d5
 31 in al, dx
 32
 33 ;将光标存入bx
 34 mov bx, ax
 35 ;下面这行是在栈中获取待打印的字符
 36 mov ecx, [esp + 36] ;pushad压入4×8=32字节,
 ;加上主调函数4字节的返回地址,故esp+36字节
 37 cmp cl, 0xd ;CR是0x0d,LF是0x0a
 38 jz .is_carriage_return
 39 cmp cl, 0xa
 40 jz .is_line_feed
 41
 42 cmp cl, 0x8 ;BS(backspace)的asc码是8
 43 jz .is_backspace
 44 jmp .put_other
 45 ;;;;;;;;;;;;;;;;;;

put_char函数中以后我们任何一个打印功能的核心,所以光它的实现就要112行,这似乎是我们目前写过的最长的一个函数了,我保证以后也没有这么长的啦。好啦,长归长,不过也没什么难度,下面咱们开讲啦。

put_char的打印原理是直接写显存,在32位保护模式下对内存的操作是“[段基址(选择子):段内偏移量]”,所以这就涉及到视频段选择子啦。一直以来我们都是用段寄存器gs来存储视频段选择子,以后也是,所以得保证在写显存之前,gs中的值是正确的选择子。第14~15行是我们为GS寄存器赋值的代码,别小看这两行,大有来头,可不亚于摊上大事呢,吼吼,待咱们把put_char函数说完再跟大家好好说道说道吧,大家要做好心理准备。咱们先说别的。

第1~3行是定义了视频段的选择子,由于只需要这三行,专门定义个配置文件有点不值当的,所以直接在这定义了,好的习惯是放在配置文件中,大家在实践中不要学我。

第10行是通过关键字global把函数put_char导出为全局符号,这样对外部文件便可见了,外部文件通过声明便可以调用。

第11行开始定义函数put_char。

第12行是用pushad指令备份32位寄存器的环境,按理说用到哪些寄存器就要备份哪些,我这里是偷懒行为,将8个32位全部备份了。PUSHAD是push all double,该指令压入所有双字长的寄存器,这里的“所有”一共是8个,它们的入栈先后顺序是: EAX->ECX->EDX->EBX->ESP->EBP->ESI->EDI,EAX是最先入栈。

第14~15行是为gs安装正确的选择子,原因如前所述完事再说。

我们在打印字符时,通常都不用指定字符显示的坐标位置,大家也没觉得有什么奇怪,原因是字符是在当前光标的位置处显示的,而且光标的位置会一直更新顺延,我们的字符一直跟着光标走,似乎光标就是字符的导航一样,而我们已经习惯了跟随光标。我想大伙儿已经清楚了光标和字符的关系了,对,它们的关系就是没有任何关系^_^。“光标在哪字符就在哪”,这是我们人为有意设置的,我们是在光标处打印字符。也就是说,我们也可以不在光标处打印字符,让光标和字符的位置分开。这一点在理论上就能证明,我们知道打印字符本质上就是把字符写入在显存中的某个地址处。在文本模式80*25下的显存可以显示80*25=2000个字符,每个字符占2字节,低字节是字符的ascii码,高字节是前景色和背景色属性,所以在4000字节的显存空间中,只要起始地址为偶数的任意2字节我们都可以写入字符,您看,这哪里是光标能限制的。光标只是个亮点,用来吸引用户眼球的,它能够帮助咱们快速找到屏幕上的活跃位置,它本身与字符显示的位置没有关系。

有点长,下节再说吧。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值