导读:
现在我们将试着在屏幕上显示点东西。为此,我们需要一种管理屏幕滚动的方法。同时,能在屏幕上显示不同的颜色也是一件美妙的事情。 幸运的是,VGA显卡使这很容易实现:为了在显示器上显示内容,显卡给定了一个内存块,我们只需向内存中写入字符和属性对。 VGA控制器会自动地把更新的内容画到屏幕上。滚动屏幕是由我们的内核软件来维护的。从技术上说,这是我们的第一个驱动程序,现在我们就开始编写。
如上面所提到的,字符缓存只是在我们地址空间中的一块内存。这块缓存在0xB8000的物理内存位置上。 缓存的类型为“short”,这意味着缓存中的每一项内容都是由16位组成的,而不是我们通常认为的8位。 缓存中的每一个16位元素,都可以被分为“高8位”和“低8位”。低8位代表需要显示的字符。高8位通常定义了这个字符的前景色和背景色。
15 12 11 8 7 0
背景色 前景色 字符
16位中的高8位被称为“属性位”,低8位被称为“字符位”。正如你在上面的表格中看到的,每一个16位元素中,属性位又被分为2个4位的块:一个代表背景色,另一个代表前景色。 现在因为只用4位来表示颜色的原因,最多只可能有16种不同的颜色可供选择,(使用公式:(位数 ^ 2) - 4^2 = 16 )。以下是16种颜色表。
值 颜色 值 颜色
0 黑 8 深灰
1 蓝 9 淡蓝
2 绿 10 淡绿
3 青绿 11 淡青绿
4 红 12 淡红
5 品红 13 淡品红
6 棕 14 淡棕
7 淡灰 15 白
最后,为了能处理内存中特定的索引内容,我们需要使用有一个公式。 字符型的内存是一个简单的“线性”(或平坦)的内存区域,但是显示控制器使它看起来像一个80x25的16位矩阵。 在内存中,文字的每一行都是相等的;前后相互连接。 因此我们试着把屏幕变为平行的线。完成这个过程的最好方法是用公式:
index = (y_value * width_of_screen) + x_value;
如果我们要控制(3,4)位置上的字符,使用这个公式,就得到 4 * 80 + 3 = 323。 也就是说,在屏幕(3,4)位置上操作,就等同于如下操作:
unsigned short *where = (unsigned short *)0xB8000 + 323;
*where = character | (attribute <<8);
以下内容是'scrn.c'文件,这个文件中包含了我们处理屏幕显示时要用到的函数。 我们include了'system.h'文件,这样我们就能使用outportb,memcpy,memset,memsets和strlen了。 我们使用的滚动屏幕功能是十分有趣的: 我们从第1行开始操作字符缓存(而不是第0行),然后把它复制到第0行上去,实际上就是把整个屏幕向上移动了一行。最后,我们用一行带有属性的空格写满最后一行。 这个文件中的putch函数可能是最复杂的一个了,同样也是最大的一个。 因为它需要处理换行("/n"),回车("/r")和退格("/b")。 如果你想要的话,你可以接着处理警告字符("/a" - ASCII 7), 处理时应该会发出一声短beep。 如果你需要的话,我已经编写了settextcolor函数来设置字符的颜色。
#include ="">
/* 这些内容定义了我们的文字指针,背景和前景颜色(属性),和xy坐标。 */
unsigned short *textmemptr;
int attrib = 0x0F;
int csr_x = 0, csr_y = 0;
/* 滚动屏幕 */
void scroll(void)
{
unsigned blank, temp;
/* 把空格定义为空白字符...我们也要设置他的背景颜色 */
blank = 0x20 | (attrib <<8);
/* 25行是结尾,也就是说,我们要把它滚动上去 */
if(csr_y >= 25)
{
/* 把当前的字符块向上移动一行 */
temp = csr_y - 25 + 1;
memcpy (textmemptr, textmemptr + temp * 80, (25 - temp) * 80 * 2);
/* 最后,我们把最后一行设置为我们定义的空白字符。 */
memsetw (textmemptr + (25 - temp) * 80, blank, 80);
csr_y = 25 - 1;
}
}
/* 更新硬件光标: 在输入的字符之后的那一行上显示一个闪烁。 */
void move_csr(void)
{
unsigned temp;
/* 在线性的内存块中找到索引的公式。表示为:
* Index = [(y * width) + x] */
temp = csr_y * 80 + csr_x;
/* 向VGA控制器的CRT控制寄存器发送14和15标志。
* 他们是索引字符的高位和低位,这个字符显示在硬件光标的闪烁处。
* 想知道更多细节,你可以查看VGA规范的编程文档。一个相当好的文档在
* http://www.brackeen.com/home/vga */
outportb(0x3D4, 14);
outportb(0x3D5, temp >> 8);
outportb(0x3D4, 15);
outportb(0x3D5, temp);
}
/* 清空屏幕 */
void cls()
{
unsigned blank;
int i;
/* 同样的,我们需要一个用来做空白的'short'颜色 */
blank = 0x20 | (attrib <<8);
/* 用带有空白颜色的空白画满整个屏幕 */
for(i = 0; i <25; i++)
memsetw (textmemptr + i * 80, blank, 80);
/* 更新我们的虚拟光标,然后移动物理光标 */
csr_x = 0;
csr_y = 0;
move_csr();
}
/* 在屏幕上显示单个字符 */
void putch(unsigned char c)
{
unsigned short *where;
unsigned att = attrib <<8;
/* 处理退格时,把光标向前一动一格 */
if(c == 0x08)
{
if(csr_x != 0) csr_x--;
}
/* 处理一个跳格时,增加光标的x值,但只移动到可以被8整除的位置上。 */
else if(c == 0x09)
{
csr_x = (csr_x + 8) &~(8 - 1);
}
/* 处理一个回车, 直接把光标放回到最前面 */
else if(c == '/r')
{
csr_x = 0;
}
/* 我们用DOS和BIOS的方法来处理新换行:我们把CR看作和换行一同出现。
* 我们把x放到最前面,然后增加y值 */
else if(c == '/n')
{
csr_x = 0;
csr_y++;
}
/* 任何比空格大的字符都是可以被打印出来的,包括空格本身。
* 用来从线性的内存快中找出索引的公式表示为:
* Index = [(y * width) + x] */
else if(c >= ' ')
{
where = textmemptr + (csr_y * 80 + csr_x);
*where = c | att; /* 字符 AND 属性: 颜色 */
csr_x++;
}
/* 如果光标到达了屏幕宽度的边缘,我们就插入另一行 */
if(csr_x >= 80)
{
csr_x = 0;
csr_y++;
}
/* 如果需要的话,滚动屏幕,并移动光标 */
scroll();
move_csr();
}
/* 使用以上的方法,输出一个字符串 */
void puts(unsigned char *text)
{
int i;
for (i = 0; i
现在我们将试着在屏幕上显示点东西。为此,我们需要一种管理屏幕滚动的方法。同时,能在屏幕上显示不同的颜色也是一件美妙的事情。 幸运的是,VGA显卡使这很容易实现:为了在显示器上显示内容,显卡给定了一个内存块,我们只需向内存中写入字符和属性对。 VGA控制器会自动地把更新的内容画到屏幕上。滚动屏幕是由我们的内核软件来维护的。从技术上说,这是我们的第一个驱动程序,现在我们就开始编写。
如上面所提到的,字符缓存只是在我们地址空间中的一块内存。这块缓存在0xB8000的物理内存位置上。 缓存的类型为“short”,这意味着缓存中的每一项内容都是由16位组成的,而不是我们通常认为的8位。 缓存中的每一个16位元素,都可以被分为“高8位”和“低8位”。低8位代表需要显示的字符。高8位通常定义了这个字符的前景色和背景色。
15 12 11 8 7 0
背景色 前景色 字符
16位中的高8位被称为“属性位”,低8位被称为“字符位”。正如你在上面的表格中看到的,每一个16位元素中,属性位又被分为2个4位的块:一个代表背景色,另一个代表前景色。 现在因为只用4位来表示颜色的原因,最多只可能有16种不同的颜色可供选择,(使用公式:(位数 ^ 2) - 4^2 = 16 )。以下是16种颜色表。
值 颜色 值 颜色
0 黑 8 深灰
1 蓝 9 淡蓝
2 绿 10 淡绿
3 青绿 11 淡青绿
4 红 12 淡红
5 品红 13 淡品红
6 棕 14 淡棕
7 淡灰 15 白
最后,为了能处理内存中特定的索引内容,我们需要使用有一个公式。 字符型的内存是一个简单的“线性”(或平坦)的内存区域,但是显示控制器使它看起来像一个80x25的16位矩阵。 在内存中,文字的每一行都是相等的;前后相互连接。 因此我们试着把屏幕变为平行的线。完成这个过程的最好方法是用公式:
index = (y_value * width_of_screen) + x_value;
如果我们要控制(3,4)位置上的字符,使用这个公式,就得到 4 * 80 + 3 = 323。 也就是说,在屏幕(3,4)位置上操作,就等同于如下操作:
unsigned short *where = (unsigned short *)0xB8000 + 323;
*where = character | (attribute <<8);
以下内容是'scrn.c'文件,这个文件中包含了我们处理屏幕显示时要用到的函数。 我们include了'system.h'文件,这样我们就能使用outportb,memcpy,memset,memsets和strlen了。 我们使用的滚动屏幕功能是十分有趣的: 我们从第1行开始操作字符缓存(而不是第0行),然后把它复制到第0行上去,实际上就是把整个屏幕向上移动了一行。最后,我们用一行带有属性的空格写满最后一行。 这个文件中的putch函数可能是最复杂的一个了,同样也是最大的一个。 因为它需要处理换行("/n"),回车("/r")和退格("/b")。 如果你想要的话,你可以接着处理警告字符("/a" - ASCII 7), 处理时应该会发出一声短beep。 如果你需要的话,我已经编写了settextcolor函数来设置字符的颜色。
#include ="">
/* 这些内容定义了我们的文字指针,背景和前景颜色(属性),和xy坐标。 */
unsigned short *textmemptr;
int attrib = 0x0F;
int csr_x = 0, csr_y = 0;
/* 滚动屏幕 */
void scroll(void)
{
unsigned blank, temp;
/* 把空格定义为空白字符...我们也要设置他的背景颜色 */
blank = 0x20 | (attrib <<8);
/* 25行是结尾,也就是说,我们要把它滚动上去 */
if(csr_y >= 25)
{
/* 把当前的字符块向上移动一行 */
temp = csr_y - 25 + 1;
memcpy (textmemptr, textmemptr + temp * 80, (25 - temp) * 80 * 2);
/* 最后,我们把最后一行设置为我们定义的空白字符。 */
memsetw (textmemptr + (25 - temp) * 80, blank, 80);
csr_y = 25 - 1;
}
}
/* 更新硬件光标: 在输入的字符之后的那一行上显示一个闪烁。 */
void move_csr(void)
{
unsigned temp;
/* 在线性的内存块中找到索引的公式。表示为:
* Index = [(y * width) + x] */
temp = csr_y * 80 + csr_x;
/* 向VGA控制器的CRT控制寄存器发送14和15标志。
* 他们是索引字符的高位和低位,这个字符显示在硬件光标的闪烁处。
* 想知道更多细节,你可以查看VGA规范的编程文档。一个相当好的文档在
* http://www.brackeen.com/home/vga */
outportb(0x3D4, 14);
outportb(0x3D5, temp >> 8);
outportb(0x3D4, 15);
outportb(0x3D5, temp);
}
/* 清空屏幕 */
void cls()
{
unsigned blank;
int i;
/* 同样的,我们需要一个用来做空白的'short'颜色 */
blank = 0x20 | (attrib <<8);
/* 用带有空白颜色的空白画满整个屏幕 */
for(i = 0; i <25; i++)
memsetw (textmemptr + i * 80, blank, 80);
/* 更新我们的虚拟光标,然后移动物理光标 */
csr_x = 0;
csr_y = 0;
move_csr();
}
/* 在屏幕上显示单个字符 */
void putch(unsigned char c)
{
unsigned short *where;
unsigned att = attrib <<8;
/* 处理退格时,把光标向前一动一格 */
if(c == 0x08)
{
if(csr_x != 0) csr_x--;
}
/* 处理一个跳格时,增加光标的x值,但只移动到可以被8整除的位置上。 */
else if(c == 0x09)
{
csr_x = (csr_x + 8) &~(8 - 1);
}
/* 处理一个回车, 直接把光标放回到最前面 */
else if(c == '/r')
{
csr_x = 0;
}
/* 我们用DOS和BIOS的方法来处理新换行:我们把CR看作和换行一同出现。
* 我们把x放到最前面,然后增加y值 */
else if(c == '/n')
{
csr_x = 0;
csr_y++;
}
/* 任何比空格大的字符都是可以被打印出来的,包括空格本身。
* 用来从线性的内存快中找出索引的公式表示为:
* Index = [(y * width) + x] */
else if(c >= ' ')
{
where = textmemptr + (csr_y * 80 + csr_x);
*where = c | att; /* 字符 AND 属性: 颜色 */
csr_x++;
}
/* 如果光标到达了屏幕宽度的边缘,我们就插入另一行 */
if(csr_x >= 80)
{
csr_x = 0;
csr_y++;
}
/* 如果需要的话,滚动屏幕,并移动光标 */
scroll();
move_csr();
}
/* 使用以上的方法,输出一个字符串 */
void puts(unsigned char *text)
{
int i;
for (i = 0; i