🚀 前言
截止目前为止,我们的键盘输入还是没有完成,有人说,博主博主,这不是中断的内容吗?是的,但是在中断初始化部分其实并没有实现键盘输入。这部分的实现交给了控制台输出化,通过这一章节的初始化后,我们就可以和操作系统进行交互了。本文对应书中第16回。希望各位给个三连,拜托啦,这对我真的很重要!!!
🏆 开启键盘输入中断
首先单独解决一个问题,就是键盘输入的问题,对应的函数在main函数里面的控制台初始化tty_init()
函数里面的con_init()
函数内。详细代码如下:
void main(void)
{
···
tty_init();
···
}
void tty_init(void)
{
rs_init();
con_init();
}
void con_init(void)
{
···
set_trap_gate(0x21,&keyboard_interrupt);
···
}
可以看到调用的依旧是中断那一节所提到的set_trap_gate
函数,关于这个函数以及中断的详细知识可以查看我之前的博客:linux0.11内核源码修仙传第六章——中断初始化。这里简单提一下,这个函数的作用就是设置了键盘中断函数,中断号是0x21
,对应的中断处理函数为keyboard_interrupt
。在设置完成之后,main函数在进入死循环前最后会使用sti()
函数开启中断,这样键盘就会开始生效,可以和操作系统进行交互啦!~
🏆 字符显示
光是开启键盘与操作系统的交互还不行,我们还需要将我们输入的内容输出到显示屏上供我们查看。那么就会面临一个问题, 如何将键盘输入的字符显示到屏幕上呢 ?
📃如何显示一个字符
我们首先立下一个共识,键盘输入的字符都是进入内存的,而屏幕上的显示肯定也是到内存中取数据的。以字符’a’为例,假设我们可以随意操作内存,如何将这个字符显示出来?
内存中有一块专门的区域是和显存映射的 ,这块区域写入什么,就相当于写在显存上,而往显存写数据,就相当于在屏幕上输出文本了。其在内存的位置如下:
假设写一行汇编语句,是往0xB8000
处写入字符'a'
:
mov [0xB8000], 'a'
那么就会往内存0xB8000
处写入一个字符a
,只要写入,屏幕上就会显示a
。
那么像是如果想要error那种红色,或者警告那种黄色呢?实际上这片内存每两个字节表示一个显示在屏幕上的字符,第一个字符是编码,即要显示的内容;第二个字符代表颜色。如果要显示hello,代码应该如下所示
mov [0xB8000], 'h'
mov [0xB8002], 'e'
mov [0xB8004], 'l'
mov [0xB8006], 'l'
mov [0xB8008], 'o'
📃显示模式
详细的代码处理依旧是在con_init()
这个超长的函数里,首先把这个函数的框架梳理出来:
#define ORIG_VIDEO_MODE ((*(unsigned short *)0x90006) & 0xff)
void con_init(void)
{
···
if (ORIG_VIDEO_MODE == 7) /* Is this a monochrome display? */
{
···
if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10) {···}
else {...}
···
}
else /* If not, it is color. */
{
···
if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10) {···}
else {...}
···
}
···
}
这里面其实就是找出当前的显示模式,即只需要看一个即可,这里可以选择的是两个,一个是单色模式(monochrome display),一个是色彩模式(color)。模式取决于ORIG_VIDEO_EGA_BX
变量,而这个变量依旧是在临时变量区,内存的0x90006
处,此处再把那张表拿出来,详情可见:linux0.11内核源码修仙传第二章——setup.s
在知道前面这里是选择显示模式后,将con_init
函数缩写如下:
void con_init(void)
{
register unsigned char a;
char *display_desc = "????";
char *display_ptr;
// 获取显示模式相关参数
video_num_columns = (((*(unsigned short *)0x90006) & 0xff00) >> 8);
video_size_row = video_num_columns * 2;
video_num_lines = 25;
video_page = (*(unsigned short *)0x90004);
video_erase_char = 0x0720;
// 显存映射的内存区域
video_mem_start = 0xb8000;
video_port_reg = 0x3b4;
video_port_val = 0x3b5;
video_mem_end = 0xba000;
// 滚动屏幕操作时的信息
origin = video_mem_start;
scr_end = video_mem_start + video_num_lines * video_size_row;
top = 0;
bottom = video_num_lines;
// 定位光标并开启键盘中断
gotoxy(ORIG_X,ORIG_Y);
set_trap_gate(0x21,&keyboard_interrupt);
outb_p(inb_p(0x21)&0xfd,0x21);
a=inb_p(0x61);
outb_p(a|0x80,0x61);
outb(a,0x61);
}
可以看到这个函数其实就做了四件事:
- 获取显示模式相关信息 :获取 0x90006 地址处的数据,就是获取显示模式等相关信息(见上表)。
- 显存映射的内存区域 :显存映射的内存地址范围,由于假设是CGA 类型的文本模式,所以映射的内存是从 0xB8000 到 0xBA000。
- 滚动屏幕操作时的信息 :设置一些滚动屏幕时需要的参数,定义顶行和底行是哪里,这里顶行就是第一行,底行就是最后一行。
- 定位光标并开启键盘中断 :把光标定位到之前保存的光标位置处(取内存地址 0x90000 处的数据),然后设置并开启键盘中断。
📃光标
众所周知,在控制台,除了输入,还有光标也是重要的构成。 光标其实本质上就是x和y坐标对应的内存指针 ,往这个位置进行读取或者写入。上一小节中,定位光标使用的是gotoxy
函数。
static inline void gotoxy(unsigned int new_x,unsigned int new_y)
{
if (new_x > video_num_columns || new_y >= video_num_lines)
return;
x=new_x;
y=new_y;
pos=origin + y*video_size_row + (x<<1);
}
gotoxy
中x表示光标的列,y表示光标的行,pos是对应位置的内存指针。现在如果
_keyboard_interrupt:
...
call _do_tty_interrupt
...
void do_tty_interrupt(int tty)
{
copy_to_cooked(tty_table+tty);
}
void copy_to_cooked(struct tty_struct * tty) {
...
tty->write(tty);
...
}
// 控制台时 tty 的 write 为 con_write 函数
void con_write(struct tty_struct * tty) {
...
__asm__("movb _attr,%%ah\n\t"
"movw %%ax,%1\n\t"
::"a" (c),"m" (*(short *)pos)
:"ax");
pos += 2;
x++;
...
}
最后一个函数con_write
是内联汇编,就是将字符c
写入pos
指向的内存,相当于往屏幕上进行输出。之后pos
加两个字节,x
向后调整,即光标向右调整一个位置。
在右侧到达边界后,就需要进行换行,其实就是把光标计算出一个新值,使其处于下一行开头。
void con_write(struct tty_struct * tty) {
...
if (x>=video_num_columns) {
x -= video_num_columns;
pos -= video_size_row;
lf();
}
...
}
static void lf(void) {
if (y+1<bottom) {
y++;
pos += video_size_row;
return;
}
...
}
其具体做法也很简单,con_write
函数负责监测是否到达边界并设置x
和pos
,lf
函数中负责调整行的位置,即y以及据此接着调整对应pos
指向的内存位置。
滚屏效果也是可以实现的,同样也是先检测再调整。当光标位置x和y出现在最后一行的最后一列时,将每一行的字符复制到上一行。向上滚动也是同理,这里看一个向下滚动的函数:
static void scrdown(void)
{
__asm__("std\n\t"
"rep\n\t"
"movsl\n\t"
"addl $2,%%edi\n\t" /* %edi has been decremented by 4 */
"movl _video_num_columns,%%ecx\n\t"
"rep\n\t"
"stosw"
::"a" (video_erase_char),
"c" ((bottom-top-1)*video_num_columns>>1),
"D" (origin+video_size_row*bottom-4),
"S" (origin+video_size_row*(bottom-1)-4)
:"ax","cx","di","si");
}
这里简化掉了显示模式选择,直接看实现方式。里面是同样是采用内联汇编的方式,其中简单来说就是,操作循环次数的是ECX
寄存器,将行数赋值给这个寄存器后通过rep指令循环操作。stosw
是串操作,rep
对其进行多次执行。串操作的输入参数如下,每个值赋值的寄存器则分别是ax,cx,di,si
:
"a" (video_erase_char)
:把video_erase_char
的值赋给寄存器 AX,此值会在stosw
操作中用于填充屏幕的最后一行。"c" ((bottom - top - 1) * video_num_columns >> 1)
:将计算得到的值赋给寄存器 ECX,这个值决定了 rep movsl 和 rep stosw 操作的重复次数。"D" (origin + video_size_row * bottom - 4)
:把计算得到的地址赋给寄存器 EDI,该地址是数据复制的目标地址。"S" (origin + video_size_row * (bottom - 1) - 4)
:把计算得到的地址赋给寄存器 ESI,该地址是数据复制的源地址。
采用这些想法,可以实现常见的控制台操作:回车,删除,清屏等,对应函数名称如下:
// 定位光标的
static inline void gotoxy(unsigned int new_x, unsigned int new_y){}
// 滚屏,即内容向上滚动一行
static void scrup(void){}
// 光标同列位置下移一行
static void lf(int currcons){}
// 光标回到第一列
static void cr(void){}
...
// 删除一行
static void delete_line(void){}
🎯总结
本文主要介绍了开启键盘中断,使得我们可以与控制台进行交互,同时如何将字符显示到屏幕上。其中字符显示到屏幕上的本质就是映射一部分内存为显存,然后往该内存里面写东西就可以显示到屏幕上。对于其中输入位置以及滚动,清屏,换行等操作则是通过代表屏幕坐标的x
,y
以及对应位置的内存坐标指针pos
进行算法编写实现的。
📖参考资料
[1] linux源码趣读
[2] 一个64位操作系统的设计与实现