需要说明的是,在我们进入保护模式之后,所有的BISO中断程序都不能够使用了,所以我们只能,通过I/O端口操作来操作显示器上的光标和显示字符。要在显示器上显示一个字符首先要确定光标的位置,在光标所在的位置显示字符,并将光标移到下一个位置。在显示器中有一系列的寄存器。先来看一下各个寄存器的功能:
寄存器名称 |
索引号 |
写端口 |
读端口 |
读端口 |
地址寄存器 |
-- |
0x3D5/0x3B5 |
|
0x3D4/0x3B5 |
水平扫描总时间 |
0x00 |
0x3D5/0x3B5 |
|
0x3D5/0x3B5 |
水平显示结束 |
0x01 |
0x3D5/0x3B5 |
|
0x3D5/0x3B5 |
水平消隐开始 |
0x02 |
0x3D5/0x3B5 |
|
0x3D5/0x3B5 |
水平消隐结束 |
0x03 |
0x3D5/0x3B5 |
|
0x3D5/0x3B5 |
水平回扫开始 |
0x04 |
0x3D5/0x3B5 |
|
0x3D5/0x3B5 |
水平回扫结束 |
0x05 |
0x3D5/0x3B5 |
|
0x3D5/0x3B5 |
垂直扫描总时间 |
0x06 |
0x3D5/0x3B5 |
|
0x3D5/0x3B5 |
溢出 |
0x07 |
|
|
0x3D5/0x3B5 |
行扫描预置 |
0x08 |
0x3D5/0x3B5 |
|
0x3D5/0x3B5 |
最大扫描行 |
0x09 |
0x3D5/0x3B5 |
|
0x3D5/0x3B5 |
光标起始 |
0x0A |
0x3D5/0x3B5 |
|
0x3D5/0x3B5 |
光标结束 |
0x0B |
0x3D5/0x3B5 |
|
0x3D5/0x3B5 |
显存起始地址(高) |
0x0C |
0x3D5/0x3B5 |
0x3D5/0x3B5 |
0x3D5/0x3B5 |
显存起始地址(低) |
0x0D |
0x3D5/0x3B5 |
0x3D5/0x3B5 |
0x3D5/0x3B5 |
光标位置(高位) |
0x0E |
0x3D5/0x3B5 |
0x3D5/0x3B5 |
0x3D5/0x3B5 |
光标位置(低位) |
0x0F |
0x3D5/0x3B5 |
0x3D5/0x3B5 |
0x3D5/0x3B5 |
垂直回扫开始 |
0x10 |
0x3D5/0x3B5 |
|
0x3D5/0x3B5 |
垂直回扫结束 |
0x11 |
0x3D5/0x3B5 |
|
0x3D5/0x3B5 |
光笔地址〔高位) |
0x10 |
仅EGA有效只读 |
0x3D5/0x3B5 |
0x3D5/0x3B5 |
光笔地址(低位) |
0x11 |
仅EGA有效只读 |
0x3D5/0x3B5 |
0x3D5/0x3B5 |
垂直显示结束 |
0x12 |
0x3D5/0x3B5 |
|
0x3D5/0x3B5 |
偏移/逻辑屏宽度 |
0x13 |
0x3D5/0x3B5 |
|
0x3D5/0x3B5 |
下划线位置 |
0x14 |
0x3D5/0x3B5 |
|
0x3D5/0x3B5 |
垂直消隐开始 |
0x15 |
0x3D5/0x3B5 |
|
0x3D5/0x3B5 |
垂直消隐结束 |
0x16 |
0x3D5/0x3B5 |
|
0x3D5/0x3B5 |
模式控制 |
0x17 |
0x3D5/0x3B5 |
|
0x3D5/0x3B5 |
行比较 |
0x18 |
0x3D5/0x3B5 |
|
0x3D5/0x3B5 |
为了节省计算机的端口数,这些显示器寄存器采用了地址方式来进行读写。具体来说就是使用了一个地址寄存器,通过这个寄存器的索引来访问其它寄存器。比如:想要访问光标位置(高位,它的索引是14)这个寄存器,就先要向0x03d4输出它的索引14,然后再向0x3d5端口输出光标高位的值。想要访问光标位置(低位,它的索引是15)这个寄存器,就先要向0x03d4输出它的索引15,然后再向0x3d5端口输出光标低位的值。
我们在工程中创建一个叫作printf的文件夹,并在其下创建一个printf.c文件,同时在include/kernel下创建一个printf.h的头文件。如下图所示:
在printf.c中,我们实现了两个函数,分别是get_cursor和set_curso,取得当前光标位置和设置光标位置。代码如下:
/***
* 取得光标位置
* return: 光标的线性位置
*/
u16 get_cursor()
{
//告诉地址寄存器要接下来要使用14号寄存器
outb_p(14, 0x03d4);
//从光标位置高位寄存器读取值
u8 cursor_pos_h = inb_p(0x03d5);
//告诉地址寄存器要接下来要使用15号寄存器
outb_p(15, 0x03d4);
//从光标位置高位寄存器读取值
u8 cursor_pos_l = inb_p(0x03d5);
//返回光标位置
return (cursor_pos_h << 8) | cursor_pos_l;
}
想要设置光标的位置也很简单,把outb_p修改成inb_p,由向端口输出改为从端口输入,分别向14号和15号寄存器输出光标位置的高8位数值和低8位数值即可:
/***
* 设置光标位置
* u16 x: 光标的横坐标
* u16 y: 光标的纵坐标
*/
void set_cursor(u16 x, u16 y)
{
//计算光标的线性位置
u16 cursor_pos = y * 80 + x;
//告诉地址寄存器要接下来要使用14号寄存器
outb_p(14, 0x03d4);
//向光标位置高位寄存器写入值
outb_p((cursor_pos >> 8) & 0xff, 0x03d5);
//告诉地址寄存器要接下来要使用15号寄存器
outb_p(15, 0x03d4);
//向光标位置高位寄存器写入值
outb_p(cursor_pos & 0xff, 0x03d5);
}
接下来在start_kernel显示Hello World!处加入光标控制函数,让显示器每显示一个字符就将光标移动到下一个位置:
//全局字符串指针变量
char *str = "Hello World!";
//内核启动程序入口
int start_kernel(int argc, char **args)
{
//显存地址
char *p = (char *) 0xb8000;
//显示str的内容到显示器上
for (int i = 0; str[i] != '\0'; i++)
{
//显示字符
p[i * 2] = str[i];
//取得光标位置
u16 cursor_pos = get_cursor();
//下一个光标位置
cursor_pos++;
//设置光标到新位置
set_cursor(cursor_pos % 80, cursor_pos / 80);
}
//永无休止的循环
for (;;)
{
}
return 0;
}
将printf.c加入到Makefile当中,并执行make all命令。运行虚拟机lidqos可以看到显示器的光标位置已经发生了改变,运行结果正确:
源代码的下载地址为:
https https://github.com/magicworldos/lidqos.git
git git@github.com:magicworldos/lidqos.git
subverion https://github.com/magicworldos/lidqos
branch v0.7
Copyright © 2015-2023 问渠网 辽ICP备15013245号