需要说明的是,在我们进入保护模式之后,所有的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号