在实模式下,CPU与外设通信的手段主要依赖BIOS中断,在保护模式下,则主要使用in/out指令对外设的控制寄存器直接进行读写。
对显示控制器的访问将会涉及比较多的I/O指令,但该模块的思想比较简单,主要就是在内核中维护相关的状态,比如光标的位置、显示器在显存中的起始地址等。本节不会实现一个非常完备的显示器控制功能,这是第5章的核心任务,本节只要实现简单的打印即可,所以只对光标进行操作。
显示控制器系统包含6组寄存器,如表3-1所示。
表3-1 显示控制器的寄存器
这个表非常复杂,但读者不必担心,本节所使用的控制光标的代码非常简单,只使用了其中的两个寄存器。其他寄存器完全可以等后面使用的时候再研究。这一节使用的寄存器主要是CRT Controller Data Register这一组寄存器。这组寄存器又包括了24个寄存器,如表3-2所示。
表3-2 CRT Controller Data Register
与光标相关的寄存器是14号和15号寄存器,将光标位置的高8位写入14号寄存器,低8位写入15号寄存器,即可实现光标移动的功能。
由表3-1可知,CRTC控制器寄存器组的24个寄存器对应的端口都是0x3D5,如何对这些寄存器进行区分呢?这就需要使用地址寄存器(Address Register)。先将寄存器的索引号写入地址寄存器,也就是端口0x3D4,然后再将值写入0x3D5即可。如果把寄存器组看成一个数组,那么地址寄存器就像这个数组的下标。搞清楚了这一点,移动光标的功能就很容易实现了,如代码清单3-3所示。
1 /* kernel/chr_drv/console.c */
2 #include<asm/io.h>
3 #include<asm/system.h>
4
5 //部分代码略
6
7 static unsigned longorigin;
8 static unsigned longscr_end;
9 static unsigned longpos;
10 static unsigned longx,y;
11 static unsigned longtop,bottom;
12 static unsigned longattr=0x07;
13
14 static inline void gotoxy(int new_x,unsigned int new_y){
15 if(new_x>video_num_columns||new_y>=video_num_lines)
16 return;
17
18 x=new_x;
19 y=new_y;
20 pos=origin+y *video_size_row+(x<<1);
21 }
22
23 static inline void set_cursor(){
24 cli();
25 outb_p(14,video_port_reg);
26 outb_p(0xff & ((pos-video_mem_base)>>9),video_port_val);
27 outb_p(15,video_port_reg);
28 outb_p(0xff & ((pos-video_mem_base)>>1),video_port_val);
29 sti();
30 }
31
32 void con_init(){
33 //部分代码略
34 origin=video_mem_base;
35 scr_end=video_mem_base+video_num_lines *video_size_row;
36 top=0;
37 bottom=video_num_lines;
38
39 gotoxy(ORIG_X,ORIG_Y);
40 set_cursor();
41 }
在控制台初始化程序(代码清单3-2)中,video_port_reg被初始化为0x3D4,而video_port_val则被初始化为0x3D5。现在,这两个初始化操作就容易理解了。
全局变量origin中记录了当前控制台显存的起始位置,实际上就是0xb8000,video_ num_columns记录了一行需要占用多少字节的存储空间。
全局变量x和y记录了光标的二维坐标,但寄存器使用的是显存地址。所以第20行使用了一个公式将二维坐标转换成显存地址,注意计算时x坐标要左移1位,也就是乘以2,因为每个字符都占据两个字节,第一个字节是ASCII值,第二个字节是该字符的属性。
至于set_cursor函数,在理解了寄存器组的原理以后就很简单了,先向地址寄存器中写入寄存器序号:光标位置的高地址,序号是14;光标位置的低地址,序号是15。而在计算高低地址时,需要多右移一位,原因也在于每个字符占两个字节。
第39行的ORIG_X和ORIG_Y用于从0x90000的位置取出光标位置。不知你是否还记得,setup模块借助BIOS中断读取到光标位置以后,又把这个值写入0x90000,在讲解setup模块时介绍过,这个值在操作系统进入保护模式以后还会再使用,这里就是使用的地方了。这行代码的作用是将光标的初始位置刷新给寄存器。
第24行和第29行所使用的函数定义在asm/system.h文件中。sti和cli的实现如下所示:
1 /* include/asm/system.h */
2 #ifndef_SYSTEM_H
3 #define_SYSTEM_H
4
5 #define sti() __asm__("sti"::)
6 #define cli() __asm__("cli"::)
7 #define nop() __asm__("nop"::)
8 #define iret() __asm__("iret"::)
9
10 #endif
可见,sti和cli都是一个宏函数,它们不过是对内嵌汇编语句的封装,这样写有利于C源文件的可读性。out_p的实现位于io.h中,它的实现也很简单,这里就不再详细列出了,读者可以通过查看随书代码仓库里的代码自己学习。