一 显示字体(汉字)
字符的显示其实跟上一章矩形的显示类似,也是往指定的像素块中写颜色值,只不过字符的显示回更精细一些,而不是一大块一大块的简单赋值。
显示8*16字符的函数如下,通过直接向VRAM中的地址赋值来显示字符:
/**
* @brief 通过直接向 VRAM 中的地址赋值来显示字符
* @param vram
* @param xsize
* @param x
* @param y
* @param c 颜色 color
* @param font 显示的排列位置的描述数组
*/
void putfont8(char *vram, int xsize, int x, int y, char c, char *font){
int i;
char *p, d /* data */;
for (i = 0; i < 16; i++) {
p = vram + (y + i) * xsize + x;
d = font[i];
if ((d & 0x80) != 0) { p[0] = c; }
if ((d & 0x40) != 0) { p[1] = c; }
if ((d & 0x20) != 0) { p[2] = c; }
if ((d & 0x10) != 0) { p[3] = c; }
if ((d & 0x08) != 0) { p[4] = c; }
if ((d & 0x04) != 0) { p[5] = c; }
if ((d & 0x02) != 0) { p[6] = c; }
if ((d & 0x01) != 0) { p[7] = c; }
}
return;
}
一般情况下,想显示字母或者数字,8*16也就够了,然而若想显示汉字,需要16*16的像素块,我这边随便选取了一个汉字“弦”(汉字unicode编码为5f26),我用emwin自带的文字处理器fontcvt,转换得到宋体“弦”字的编码如下:
/* Start of unicode area <CJK Unified Ideographs> */
GUI_CONST_STORAGE unsigned char acGUI_Fontsongti16_5F26[ 32] = { /* code 5F26 */
________,_X______,
XXXXX___,__X_____,
____X___,__X_____,
____X_XX,XXXXXXX_,
____X___,_X______,
_XXXX___,_X______,
_X______,X___X___,
_X_____X,____X___,
_X____XX,XXXX____,
_XXXX___,__X_____,
____X___,_X______,
____X___,X___X___,
____X__X,_____X__,
____X_XX,XXXXXX__,
_X_X___X,_____X__,
__X_____,________};
将其转换为16进制码,并调用,这边注意区别与8*16字符的16*16的实现是有区别的(前者遍历数组长度为16,后者遍历数组长度为32):
//调用
void HariMain(void)
{
struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;
static char font_xian[32] = {
0x00,0x40,0xF8,0x20,0x80,0x20,0x0b,0xfe,0x80,0x40,0x78,0x40,0x40,0x88,0x41,0x80,
0x43,0xf0,0x78,0x20,0x80,0x40,0x80,0x88,0x09,0x04,0x0b,0xfc,0x51,0x04,0x20,0x00
};
init_palette();
init_screen(binfo->vram, binfo->scrnx, binfo->scrny);
putfont16(binfo->vram, binfo->scrnx, 10, 10, COL8_FFFFFF, font_xian);
for (;;) {
io_hlt();
}
}
//显示字符
void putfont16(char *vram, int xsize, int x, int y, char c, char *font)
{
int i;
char *p, d /* data */;
for (i = 0; i < 32; i++) {
p = vram + (y + i/2) * xsize + x + 8*(i%2); //注意这边的区别
d = font[i];
if ((d & 0x80) != 0) { p[0] = c; }
if ((d & 0x40) != 0) { p[1] = c; }
if ((d & 0x20) != 0) { p[2] = c; }
if ((d & 0x10) != 0) { p[3] = c; }
if ((d & 0x08) != 0) { p[4] = c; }
if ((d & 0x04) != 0) { p[5] = c; }
if ((d & 0x02) != 0) { p[6] = c; }
if ((d & 0x01) != 0) { p[7] = c; }
}
return;
}
实际效果如下:
书中作者的字库使用了现成的HANKAKU字库,里面基本上涵盖了常用的ASCII字符(不含中文),如果需要使用中文,有两种方案:
【1】若使用的汉字比较多,需要自行增加新的字库文件(建立新的编译链);
【2】若使用的汉字比较少,在bootpack.c添加几个常用汉字的静态定义即可,或者修改书中作者提供的hankaku.txt文件亦可;
二 显示字符串
基于引入的hankaku字库,构造显示字符串的函数如下:
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;
}
与此同时,包含c标准输入输出流stdio.h之后(#include <stdio.h>),就可以引用sprintf函数(sprintf只对内存进行操作,而printf函数不可避免地会使用到操作系统的功能,所以这边不用printf函数):
char s[40];
sprintf(s, "scrny = %d", binfo->scrny);
三 显示符号图形(鼠标指针形状)
与显示字符类似,我们需要用显示图形来创建一个鼠标的指针形状(这边还只是静态图形,后面做完与鼠标外设的联动之后,就可以自由移动了):
void init_mouse_cursor8(char *mouse, char bc)
/* マウスカーソルを準備(16x16) */
{
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;
}
这边附一个正常点的鼠标光标图案:
static char cursor[16][16] = {
"*...............",
"**..............",
"*O*.............",
"*OO*............",
"*OOO*...........",
"*OOOO*..........",
"*OOOOO*.........",
"*OOOOOO*........",
"*OOOOOOO*.......",
"*OOOO*****......",
"*OO*O*..........",
"*O*.*O*.........",
"**..*O*.........",
"*....*O*........",
".....*O*........",
"......*........."
};
四 GDT与IDT初相识
【1】分段机制(GDT:全局段号记录表):
为了解决内存重叠的问题,需要引入分段机制。32位的CPU使用32条地址线,能区分2^32=4G个内存地址。每个内存地址都有1Byte的内容。
分段,就是将4GB的内存分成很多块(block),每一块的起始地址都看作0来处理。有了这个功能,任何程序都可以先写上一句"ORG 0",一个应用程序就不会占用别人的内存空间,这样就可以同时运行多个程序。像这样分割出来的块,就称为段(segment)。还有一种"分页"的技术,这里不讨论。
为了表示一个段,需要记录以下信息:
-
段的起始地址
-
段的大小
-
段的管理属性(禁止写入,执行,系统专用等)
这些信息需要用8个字节保存。使用段的方式是和调色板神似的:DS是16位,理论上能够表示2^16=65536个段。但由于CPU设计上的原因,低3位不能用,因此DS只能表示2^13=8192个段(即第0个~第8191个)。
要存储8192个段,就需要占用8192*8=65536Byte=64KB的内存空间。这64KB的数据就称为GDT (Global segment Descriptor Table)即"全局段号记录表"。
将这64K的GDT整齐地排列在内存某处,再将其起始地址和有效设定个数放在CPU内被称作GDTR的48bit寄存器中,GDT的设定就完成了。
段的起始地址、大小、管理属性这些信息是按bit保存的,十分复杂,暂时不予理会。
【2】中断机制(IDT:中断记录表):
CPU应对外设的情况变化或者内部错误时,一般有轮询和中断两种机制。
轮询比较耗时,所以要使用鼠标键盘,就必须使用中断机制,即设置IDT(IDT的设置必须要在GDT设置完成之后)。
IDT(Interrupt Descriptor Table)即"中断记录表"。IDT记录了0~255的中断号与调用函数之间的对应关系。当发生了123号中断,就会调用对应的函数。其设置方式与GDT是相似的,IDT的每一项也需要8Byte保存,这8Byte里包括中断处理函数名(即C语言中的函数指针)。
void init_gdtidt(void){
struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR*) 0x00270000;
struct GATE_DESCRIPTOR *idt = (struct GATE_DESCRIPTOR*) 0x0026f8000;
int i;
// GDT 初始化
for (i = 0; i < 8192; i++) {
set_segmdesc(gdt + i, 0, 0, 0);
}
set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, 0x4092);
set_segmdesc(gdt + 2, 0x0007ffff, 0x00280000, 0x409a);
load_dgtr(0xffff, 0x00270000);
// IDT 初始化
for (i = 0; i < 256; i++) {
set_gatedesc(idt + i, 0, 0, 0);
}
load_idtr(0x07ff, 0x0026f800);
return;
}
void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar){
if (limit > 0x000fffff) {
ar |= 0x8000;
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;
}
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;
}
// void load_dgtr(int limit, int addr);
// void load_idtr(int limit, int addr);
_load_gdtr: ; void load_gdtr(int limit, int addr);
MOV AX,[ESP+4] ; limit
MOV [ESP+6],AX
LGDT [ESP+6]
RET
_load_idtr: ; void load_idtr(int limit, int addr);
MOV AX,[ESP+4] ; limit
MOV [ESP+6],AX
LIDT [ESP+6]
RET
这边注意以下四点:
(1)段号为1的段,上限值为0xffffffff,即大小正好是4GB,地址是0,它表示的是CPU所能管理的全部内存本身,段的属性设为0x4092,属性的含义暂且不表。
(2)段号为2的段,它的大小是512KB,地址是0x280000 这正好是为bootpack.hrb(C 转化成的与汇编可连接文件)而准备的用这个段,就可以执行bootpack.hrb,因为 bootpack.hrb 是以 ORG 0 为前提翻译成的机器语言。
(3)LGDT/LIDT分别是汇编中的加载全局段号描述符/加载中断描述符命令。
(4)这边IDT内存是从0x26f800--0x26ffff的2KB区间,GDT内存是从0x270000--0x27ffff的64KB区间,后面C语言段bootpack.hrb(第2段)是从0x280000--0x2fffff的512KB区间: