那么,既然可以绘制字符了,为何不绘制一个字符串呢?
绘制字符串我们绘制一个字符,主要做了:
- 根据字符的形状写出font数据,这个数组中每个char能够表明一行像素的颜色。16个char 就能够表明16行像素的颜色,也就是说16个char 就能够表示一个字符了。
- 把font中定义的16个char 用putfont8函数绘制到指定位置x,y就行了
那么,如何绘制一个字符串,也就是说,如何绘制很多个字符串?
一个一个绘制就行了呗。重复使用putfont8函数,使用是,更改一下x,y的取值就行了。
不过使用putfont8前,还需要准备好font数组。
刚才我们指示准备了字符A的font数组。要显示字符串,字符串里面必然包含26个字母,以及26个字母的大小写,不仅仅包含字母,还有各种标点符号等。
所以,要想显示字符串,首先就要构造出每个字符,标点符号的font数组。
我们使用一个现成的font数组,别人已经构建好的数组。
这个数组比较好用。
假设这个数组的首地址hankaku,那么hankaku 0x41*16就得到了字符‘A’首地址,
因为字符A的字符编码是0x41,所以,我们可以这样写:hankaku 'A'*16就得到了字符'A'的首地址。使用putfont8(,,,,, hankaku 'A'*16)就可以绘制字符A了。
也就是说:hankaku 'B'*16就是字符'B'
hankaku 'a'*16就是小写字符a.
所以,如果要绘制字符串:”ABC123“,可以使用如下代码:
oid HariMain(void)
{
struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;
extern char hankaku[4096];
init_palette();
init_screen(binfo->vram, binfo->scrnx, binfo->scrny);
putfont8(binfo->vram, binfo->scrnx, 8, 8, COL8_FFFFFF, hankaku 'A' * 16);
putfont8(binfo->vram, binfo->scrnx, 16, 8, COL8_FFFFFF, hankaku 'B' * 16);
putfont8(binfo->vram, binfo->scrnx, 24, 8, COL8_FFFFFF, hankaku 'C' * 16);
putfont8(binfo->vram, binfo->scrnx, 40, 8, COL8_FFFFFF, hankaku '1' * 16);
putfont8(binfo->vram, binfo->scrnx, 48, 8, COL8_FFFFFF, hankaku '2' * 16);
putfont8(binfo->vram, binfo->scrnx, 56, 8, COL8_FFFFFF, hankaku '3' * 16);
for (;;) {
io_hlt();
}
}
注意到第4行,extern char hankaku[4096],表示hankaku这个数组不在本代码中编译,在其他代码中编译了。这个hankaku就是我们使用的字符库。
新的问题又来了,这个显示程序使用其他不太方便,显示字符串还需要把每个字符单独拆开去写,并且自己还要设定每个字符的坐标x,y, 能不能只指定一个字符串" mingminglashi"就可以了?
能,所以有如下函数putfonts8_asc:
void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s)
{
extern char hankaku[4096];
for (; *s != 0x00; s ) {
putfont8(vram, xsize, x, y, c, hankaku *s * 16);
x = 8;
}
return;
}
这个函数第4行的for循环,便利出字符串中的每个字符。
第5行,调用putfont8函数显示字符
第6行,设定下一个字符的显示位置。
现在可以显示任意字符串就比较方便了。
那么如果我要显示一个变量的值呢? 比如“ a=5",该如何操作?
这样操作就行:
sprintf(s, "a = %d", 5);
putfonts8_asc(binfo->vram, binfo->scrnx, 16, 64, COL8_FFFFFF, s);
先用sprint将a=5做成字符串,然后再用putfonts8_asc将字符串显示。
因为我们是用c在写程序,所以,可以使用sprintf函数。
字符串的显示就到这里。自己写的操作系统可以显示字符了
鼠标的显示既然可以显示字符串了,下一步不就是键盘打字,然后显示了嘛
这样,表面看起来,就是一个真正的操作系统了:可以用键盘来控制显示屏来显示任意字符,任意图像了。
操作系统本来就是为了让人方便地使用键盘,鼠标来控制cpu来帮人工作的嘛。
因为键盘的控制和鼠标的控制基本相同,并且,鼠标的使用更加直观,所以我们就先讲鼠标如何显示,以及如何操作,再讲键盘的操作,他们两个非常的相似。
那么如何显示鼠标?
鼠标就是一个箭头的形状嘛,如下:
我们可以把鼠标绘制到一个16x16的长方形中,在长方形中,有的位置需要显示的颜色与背景不同。
所以,显示鼠标与显示一个字符还是非常相似的。不同的是,字符所在的长方形是16x8的,而鼠标是16x16的。
显示鼠标,也是分两步:
- 构建表明鼠标颜色的数组
- 将数组用for循环赋值到显示缓冲区里。
关于构建表明鼠标每个像素颜色的数组,有如下代码:
#define COL8_000000 0
#define COL8_FF0000 1
#define COL8_00FF00 2
#define COL8_FFFF00 3
#define COL8_0000FF 4
#define COL8_FF00FF 5
#define COL8_00FFFF 6
#define COL8_FFFFFF 7
#define COL8_C6C6C6 8
#define COL8_840000 9
#define COL8_008400 10
#define COL8_848400 11
#define COL8_000084 12
#define COL8_840084 13
#define COL8_008484 14
#define COL8_848484 15
void init_mouse_cursor8(char *mouse, char bc)
{
static char cursor[16][16] = {
"**************..",
"*OOOOOOOOOOO*...",
"*OOOOOOOOOO*....",
"*OOOOOOOOO*.....",
"*OOOOOOOO*......",
"*OOOOOOO*.......",
"*OOOOOOO*.......",
"*OOOOOOOO*......",
"*OOOO**OOO*.....",
"*OOO*..*OOO*....",
"*OO*....*OOO*...",
"*O*......*OOO*..",
"**........*OOO*.",
"*..........*OOO*",
"............*OO*",
".............***"
};
int x, y;
for (y = 0; y < 16; y ) {
for (x = 0; x < 16; x ) {
if (cursor[y][x] == '*') {
mouse[y * 16 x] = COL8_000000;
}
if (cursor[y][x] == 'O') {
mouse[y * 16 x] = COL8_FFFFFF;
}
if (cursor[y][x] == '.') {
mouse[y * 16 x] = bc;
}
}
}
return;
}
使用init_mouse_cursor8函数,就可以设字符数组mouse的值,mouse就可以表示鼠标所在长方形中每个像素的颜色。
关于把mouse用for循环绘制到显示缓冲区里:
void putblock8_8(char *vram, int vxsize, int pxsize,
int pysize, int px0, int py0, char *buf, int bxsize)
{
int x, y;
for (y = 0; y < pysize; y ) {
for (x = 0; x < pxsize; x ) {
vram[(py0 y) * vxsize (px0 x)] = buf[y * bxsize x];
}
}
return;
}
写了函数putblock8_8来将buf中的设置的值赋值到vram中。
buf中就是鼠标的像素颜色,vram就是显示缓冲区。
到此:使用init_mouse_cursor8函数和publock8_8函数,就可以完成鼠标的显示了,总体代码如下:
void HariMain(void)
{
struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;
char s[40], mcursor[256];
int mx, my;
init_palette();
init_screen8(binfo->vram, binfo->scrnx, binfo->scrny);
mx = (binfo->scrnx - 16) / 2;
my = (binfo->scrny - 28 - 16) / 2;
init_mouse_cursor8(mcursor, COL8_008484);
putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16);
sprintf(s, "(%d, %d)", mx, my);
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, s);
for (;;) {
io_hlt();
}
}
11,12行分别使用了init_mouse_cursor8和putblock8_8。
9,10设定了要显示鼠标的位置
显示效果:
至此,我们在窗口上显示了鼠标,看起来有点操作系统的样子了。
不过这个鼠标还不会动。
如何使这个鼠标随着人的操作动起来?
利用中断函数。
鼠标相对于CPU来说,是外部设备。鼠标一动,cpu就会有响应。
这个响应就是通过中断函数来实现的。
鼠标中断函数前的准备工作CPU的中断函数用汇编操作的话,会非常的简单。
不过汇编工作在16位的工作模式下,我们这里使用C语言写操作系统了,就必须工作在32位模式下.
那么在32位模式下,在使用中断函数前,必须自己定义中断函数。然后将中断函数的地址放在表中,叫中断函数表:interrupt descriptor table, IDT.
也就是说,我们需要把原来16位模式下,鼠标中断函数调用出来,放在我们自己定义的IDT中断函数表里。
中断函数表放在内存中某个位置,然后把这个位置放在CPU的寄存器里,这个寄存器叫IDTR,中断函数表首地址寄存器。
以上中断函数表,以及相应寄存器准备好后,
在鼠标有动作的时候,触发中断动作,cpu就去查询IDTR寄存器中的值,这个值就是中断函数表在内存中的首地址,然后就可以从中断函数表中找到相应的中断函数去执行了。
总之: 想得到鼠标的响应,就必须使用中断函数,要想使用中断函数,就必须设定IDT。
还有,IDT里的中断函数体放在哪里呢?我们知道中断函数表,只存储了中断函数的地址。中断函数我们需要放在内存中某个位置的,所以,还要指名中断函数所在的内存位置。
注意到,中断函数是属于我们所写的操作系统的函数,只允许操作系统本身来访问的。
为了指名中断函数所在的位置,需要对内存的使用进行管理,这里就使用一个表来管理内存,叫做全局内存段的定义表,或者叫描述表/记录表,golobal segment descriptor table,GDT.
这这个表格中,定义了对整个内存的分段情况,每段内存的权限情况等。
这个表的开始地址页需要放在其对用的寄存器GDTR中,CPU才会依照GDT所定义的方式去访问内存。
所以,想得到鼠标的响应,我们要先设置GDT,在GDT中存放中断函数,再设置IDT,IDT中存放中断函数的地址, 然后才可以在中段函数中获取到鼠标的状态。
设定GDT,使用以下set_segmdesc函数
void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar)
{
if (limit > 0xfffff) {
ar |= 0x8000; /* G_bit = 1 */
limit /= 0x1000;
}
sd->limit_low = limit & 0xffff;
sd->base_low = base & 0xffff;
sd->base_mid = (base >> 16) & 0xff;
sd->access_right = ar & 0xff;
sd->limit_high = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0);
sd->base_high = (base >> 24) & 0xff;
return;
}
其中,unsigned int limit, 是当前内存段的大小
int base,当前内存段的开始地址
int ar,当前内存段的属性,可读,可写,可执行等属性的设置,是否分页访问内存等
这个函数,将内存段的大小,内存段的开始地址, 以及属性存放到结构体struct SEGMENT_DESCRIPTOR *sd中, sd就是结构体的首地址。
使用set_segmdesc去定义内存的使用规则GDT代码如下:
struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT;
// 开始地址0x0000 0000, 内存大小:0xffff ffff ,4GB,
set_segmdesc(gdt 1, 0xffffffff, 0x00000000, AR_DATA32_RW);
set_segmdesc(gdt 2, LIMIT_BOTPAK, ADR_BOTPAK, AR_CODE32_ER);
load_gdtr(LIMIT_GDT, ADR_GDT);
第3行,limit=0xffff ffff=4GB, base=0x0000 0000, ar = AR_DATA32_RW=0x4092,
第3行定义了一个大小为limit=4GB的内存段,开始地址为0x0000 0000, 并且定义这段内存是操作系统专用的可读可写,但不可执行的。 也就是操作系统用于存放数据的内存。
补充知识:在set_segmdesc内部第3行,当limit>0xfffff时,将ar的最高位设置为1。这是为什么呢?这是指:当内存大小超过0xfffff=1M后,就使用“页”来访问内存了,而定义用“页”的概念来访问内存,需要更改内存段的属性,将其ar的最高位设定为1即可,即:ar |= 0x8000.
第4行定义了一个大小为LIMIT_BOTPAK=0x0007ffff=512KB的内存,它以地址ADR_BOTPAK=0x0028 0000开始,它的属性为AR_CODE32_ER,可执行可读,这个内存段是指存放操作系统程序的内存段。就是我们现在正在写的,bootpack.c这个文件存放的内存段。所以它的开始地址是0x0028 0000,
第5行,load_gdtr,是将GDT的首地址ADR_GDT,以及大小LIMIT_GDT放入寄存器。涉及到寄存器的函数,都是汇编函数,在文件naskfunc.nas中的实现为:
_load_gdtr: ; void load_gdtr(int limit, int addr);
MOV AX,[ESP 4] ; limit
MOV [ESP 6],AX
LGDT [ESP 6]
RET
注意第1行定义了gdt,它是以ADR_GDT为开始地址的结构体。
整体有关内存分段管理的代码为:
truct SEGMENT_DESCRIPTOR {
short limit_low, base_low;
char base_mid, access_right;
char limit_high, base_high;
};
#define ADR_GDT 0x00270000
#define LIMIT_GDT 0x0000ffff
#define ADR_BOTPAK 0x00280000
#define LIMIT_BOTPAK 0x0007ffff
#define AR_DATA32_RW 0x4092 // 操作系统使用的数据断,可读,可写
#define AR_CODE32_ER 0x409a // 操作系统使用的存放程序的内存段,可读,可执行
void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar)
{
if (limit > 0xfffff) {
ar |= 0x8000; /* G_bit = 1 */
limit /= 0x1000;
}
sd->limit_low = limit & 0xffff;
sd->base_low = base & 0xffff;
sd->base_mid = (base >> 16) & 0xff;
sd->access_right = ar & 0xff;
sd->limit_high = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0);
sd->base_high = (base >> 24) & 0xff;
return;
}
struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT;
int i;
for (i = 0; i <= LIMIT_GDT / 8; i ) {
set_segmdesc(gdt i, 0, 0, 0);
}
// 开始地址0x0000 0000, 内存大小:0xffff ffff ,4GB,
set_segmdesc(gdt 1, 0xffffffff, 0x00000000, AR_DATA32_RW);
set_segmdesc(gdt 2, LIMIT_BOTPAK, ADR_BOTPAK, AR_CODE32_ER);
load_gdtr(LIMIT_GDT, ADR_GDT);
这里,在内存中设定了两个段:一个是数据段,是gdt中的第1个内存段的使用规则,开始地址为0x0000 0000,总内存大小为4GB,它规定了一个操作系统专用的数据存放区域。
一个程序段,是gdt中定义的第2个内存段的使用规则,开始地址为0x0028 0000,大小为512KB,它规定了一个操作系统专用存放代码的存放区域
我们刚才所说的中断函数,就要存放在程序段里。
好了,定义好了内存段了,也就是定义好中断函数所存放的内存区域了,下面就来定义中断函数表IDT了,定义这个表的总体代码为:
struct GATE_DESCRIPTOR {
short offset_low, selector;
char dw_count, access_right;
short offset_high;
};
#define ADR_IDT 0x0026f800 // IDT的地址
#define LIMIT_IDT 0x000007ff
void set_gatedesc(struct GATE_DESCRIPTOR *gd, int offset, int selector, int ar)
{
gd->offset_low = offset & 0xffff;
gd->selector = selector;
gd->dw_count = (ar >> 8) & 0xff;
gd->access_right = ar & 0xff;
gd->offset_high = (offset >> 16) & 0xffff;
return;
}
struct GATE_DESCRIPTOR *idt = (struct GATE_DESCRIPTOR *) ADR_IDT;
for (i = 0; i <= LIMIT_IDT / 8; i ) {
set_gatedesc(idt i, 0, 0, 0);
}
load_idtr(LIMIT_IDT, ADR_IDT); // 高速cpu,IDT的在内存中的存放地址
set_gatedesc(idt 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32);
set_gatedesc(idt 0x27, (int) asm_inthandler27, 2 * 8, AR_INTGATE32);
set_gatedesc(idt 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);
以上代码就是设置IDT的所有代码,设定完成后,就可以等待中断程序,比如鼠标动作的发生了
第13行,定义了asm_inthandler21函数,作为中断函数表的第0x21号个中断函数,因为把它放在了idt 0x21位置处,一般在什么位置,就称为几号中断函数,所以,这里就称其为0x21号中断函数。
在第14,15行定义了0x27,0x2c号中断函数。
注意到set_gatedesc函数的第3个参数:2*8,指的就是GDT中定义的第2个内存段,操作系统代码bootpack.c所在的内存段。那么为什么要2*8呢?因为完整的描述一段内存的作用,需要如结构体SEGMENT_DESCRIPTOR所示的8个字节的数据,所以,第2个段的开始地址就是2*8了
关于IDT的代码解释完了,IDT所定义的中断函数的意义是什么?
中断函数asm_inthandler21是处理键盘中断过程的函数。
中断函数asm_inthandler2c是关于鼠标的中断处理函数。
也就是说,要处理鼠标,键盘的中断,只需要详细的完成这两个中断处理函数就可以了,这两个函数获取到鼠标和键盘的状态,然后我们再把鼠标的状态改变反馈到我们绘制的鼠标上。比如鼠标位置改变了,那么我们绘制鼠标的位置页要改变。
比如键盘按下某个键了,我们就在屏幕上输入相应的字符。
这些功能,我们就就在day06来讨论吧,毕竟中断函数的编写,以及对中断后的处理也是需要花费一些精力的。
总结:今天是第5天,我们尝试在屏幕上显示了字符串,并且显示了鼠标。
这让当前这个界面,有点像操作系统的样子了
但是,当前这个鼠标是个静止的鼠标,所以。我们第一步就是先让鼠标可以操作
然后再让键盘可以操作。
这就是今后几天的目标。
在控制鼠标之前,需要先设置寄存器GDTR, 并声称一个表GDT,然后设置IDTR,并生成一个表IDT.
我们可以把获取鼠标按键,移动等代码放在IDT定义的中断函数中去实现,后续再像办法显示到界面上来。