《ORANGE’S:一个操作系统的实现》读书笔记(二十)输入输出系统(二)

本文探讨了输出系统中的重要组件——显示器,介绍了终端概念、TTY、视频操作的基本概念,以及如何通过VGA寄存器控制光标位置和屏幕滚动。作者逐步揭示了操作系统如何通过硬件操作实现屏幕内容的显示和控制。
摘要由CSDN通过智能技术生成

上一篇文章记录了输入系统组成部分的键盘,那么本篇文章开始记录输出系统组成的重要部分:显示器。

显示器

你可能会有一点奇怪,为什么我们刚刚还在讨论键盘,怎么现在又转到显示器上了呢?因为随着键盘模块的逐渐完善,我们越来越需要考虑它与屏幕输出之间的关系。I/O包含两个方面——Input和Output——它们总是放在一起的。

我们在新添加终端进程的时候说过,这个进程不仅处理键盘操作,还将处理诸如屏幕输出等内容。所以,在彻底完成键盘驱动之前,我们不得不先来了解一下终端的概念以及显示器的驱动方式。

初识 TTY

如果你用过Linux或者Unix,对TTY就一定不会陌生。很多时候,我们也称之为终端。对于终端最简单而形象的认识是,当你按Alt+F1、Alt+F2、Alt+F3等组合键时,会切换到不同的屏幕。在这些不同的屏幕中,可以分别有不同的输入和输出,相互之间并不受到彼此影响。在某个终端中,如果键入tty命令,执行的结果将是当前的终端号。

实际上,终端的概念不仅仅是Alt+Fn这么简单,但在目前的操作系统中,我们暂时只实现这样简单的终端。

对于不同的TTY,可以理解成如下图所示的样式。

虽然不同的TTY对应的输入设备是同一个键盘,但输出却好比是在不同的显示器上,因为不同的TTY对应的屏幕画面可能是完全不一样的。实际上,我们当然是在使用同一个显示器,画面的不同是因为显示了显存的不同位置罢了。

既然三个CONSOLE公用同一块显存,就必须有一种方式,在切换CONSOLE时,让屏幕显示显存中某个位置的内容。不同担心,通过简单的端口操作,这很容易做到。接下来就会介绍。

基本概念

虽然在标题和文中我们使用“显示器”这个字眼,但是它并不是一个精确的称呼,因为我们操作的对象可能是显卡,或者仅仅是显存。不过没有关系,开始的不精确不代表我们不严谨,因为随着认识的深入,这些概念最终都会清晰起来的。目前,在模糊的地方我们可以暂时使用“视频”这个词。

直到现在才开始介绍视频好像有点晚,因为从一开始我们写那个简单的Boot Sector的时候,就从来没有离开过对视频的操作——如果不是通过屏幕的反馈,我们怎么知道计算机在做些什么呢?

在最初那个Boot Sector中,打印字符是通过BIOS中断来实现的。到了保护模式,BIOS中断无法使用了,我们就在GDT中建立了一个段,它的开始地址是0xB8000,通过段寄存器gs对它进行写操作,从而实现数据的显示。到目前为止,我们对于视频模块的操作也仅限于此,想显示什么就mov什么。

但是,实际上视频是一个很复杂的部分,很多操作的复杂程度非mov能比。显示适配器可以被设置成不同的模式,用来显示更多的色彩、更华丽的图像和动画。当你面对色彩斑斓的图形界面时,当你用PC欣赏电影时,大概能够想出要实现它一定很不容易。

不过,目前的我们不做那么复杂的,我们只要认识开机看到的默认模式就够了,那就是80×25文本模式。在这种模式下,显存大小为32KB,占用的范围为0xB8000~0xBFFFF。每2字节代表一个字符,其中低字节表示字符的ASCII码,高字节表示字符的属性。一个屏幕可以显示25行,每行80个字符。

虽然我们到目前为止还没有介绍过字符属性,但是我们却设置过显示字符的颜色,还编写了一个函数disp_color_str()来显示不同颜色的字符,所以,你可能已经了解一二了。在默认情况下,屏幕上每一个字符对应2字节的定义如下图所示。

可以看到,低字节表示的是字符本身,高字节用来定义字符的颜色。颜色分为前景和背景两部分,各占4位,其中低三位意义是相同的,表示颜色,但最高位作用不同。如果前景最高位为1的话,字符的颜色会比此位为0时亮一些;如果背景最高位为1,则显示出的字符将是闪烁的。更多细节请参考下表。

现在我们来回顾一下之前显示字符的代码:

mov ah, 0Ch    ; 0000:黑底    1100:红字
mov al, 'P'
mov [gs:edi], ax

现在再对照上面的图和表,是不是就可以理解了。

好,我们已经知道一个屏幕可以显示几行几列,又知道一个字符占用几个字节,那么,一个屏幕映射到显存中所占的空间大小就很容易计算了:

80×25×2=4000字节

我们刚才讲过,显存有32KB,每个屏幕才占4KB,所以显存中足以存放8个屏幕的数据。如果我们有3个TTY,分别让它们使用10KB的空间还有剩余。而且在每一个TTY内,还可以实现简单的滚屏功能。

那么,如何能够让系统显示指定位置的内容呢?其实很简单,通过端口操作设置相应的寄存器就可以了。我们马上来介绍。

在继续之前要说明一点,书上假定系统使用的是VGA以上的视频子系统,并假定不使用单色模式。VGA早在1987年就被推出了,所以这样假设不会有什么问题。

寄存器

我们又要和硬件打交道了。说起来,从8259A到8042,再到现在的显示器,对硬件的操作也做了不少,写程序其实没有什么新鲜内容,端口操作而已。不过每种硬件各不相同,我们不得不了解其具体细节。比如VGA系统,它有6组寄存器,如下表所示,我们先来看一下。

从这个表格可以看出,寄存器还真不少,而且有些寄存器读和写的端口是不一样的。寄存器名称全部使用的是英文,这样做不但避免了翻译的偏差,而且有个好处是,我们可以通过Register这个单词是否使用复数来判断寄存器是否只有一个。比如CRT Controller Registers这一组,其中的Data Registers使用的是复数,说明数据寄存器不止一个,如下表所示。

这么多寄存器,只有一个端口0x3D5,怎么来操作其中某一个呢?这就用到Address Register了。我们看到表7.5中每一个寄存器都对应一个索引值,当想要访问其中一个时,只需要先向Address Register写对应的索引值(通过端口0x3D4),然后再通过端口0x3D5进行的操作就是针对索引值对应的寄存器了。如果我们把Data Registers看做一个数组,那么Address Register就相当于数组的下标。

举个例子,如果把索引号为idx的寄存器的值改为new_value,可以这样来做:

out_byte(0x3D4, idx);
out_byte(0x3D5, new_value);

可以看到,只是多了一次端口操作而已。

我们马上来试一下。从字面意思可以知道,Cursor Location High Register和Cursor Location Low Register是用来设置光标位置的,索引号分别是0Eh和0Fh。很久以来我们都没有理会光标位置这个问题,自从Loader中调用BIOS中断显示完第5行的Ready后,它就一直停在那里。那么我们现在修改一下程序,让它跟随我们敲入的每一个字符。

代码 kernel/tty.c,设置光标位置。

        disable_int();
        out_byte(CRTC_ADDR_REG, CURSOR_H);
        out_byte(CRTC_DATA_REG, ((disp_pos / 2) >> 8) & 0xFF);
        out_byte(CRTC_ADDR_REG, CURSOR_L);
        out_byte(CRTC_DATA_REG, (disp_pos / 2) & 0xFF);
        enable_int();

其中,几个宏的定义如下代码所示(其中还定义了我们之后会用到的另外两个寄存器的索引值)。

代码 include/const.h,CRTC相关的宏。

/* VGA */
#define CRTC_ADDR_REG   0x3D4   /* CRT Controller Registers - Addr Register */
#define CRTC_DATA_REG   0x3D5   /* CRT Controller Registers - Data Register */
#define START_ADDR_H    0xC     /* reg index of video mem start addr (MSB) */
#define START_ADDR_L    0xD     /* reg index of video mem start addr (LSB) */
#define CURSOR_H        0xE     /* reg index of cursor position (MSB) */
#define CURSOR_L        0xF     /* reg index of cursor position (LSB) */
#define V_MEM_BASE      0xB8000 /* base of color video memory */
#define V_MEM_SIZE      0x8000  /* 32K: B8000H -> BFFFFH */

之所以disp_pos被2除,是因为屏幕上每个字符对应2字节。

好了,make并运行,效果如下图所示。可以看到,光标开始跟随字符了。

我们不妨乘胜追击,进一步做试验。通过设置Start Address High Register和Start Address Low Register来重新设置显示开始地址,从而实现滚屏的功能。

代码 kernel/tty.c,重新设置显示开始地址。

PUBLIC void in_process(u32 key)
{
    char output[2] = {'\0', '\0'};

    if (!(key & FLAG_EXT)) {
...
    else {
        int raw_code = key & MASK_RAW;
        switch(raw_code) {
            case UP:
                if ((key & FLAG_SHIFT_L) || (key & FLAG_SHIFT_R)) {
                    disable_int();
                    out_byte(CRTC_ADDR_REG, START_ADDR_H);
                    out_byte(CRTC_DATA_REG, ((80 * 15) >> 8) & 0xFF);
                    out_byte(CRTC_ADDR_REG, START_ADDR_L);
                    out_byte(CRTC_DATA_REG, (80 * 15) & 0xFF);
                    enable_int();
                }
                break;
            case DOWN:
                if ((key & FLAG_SHIFT_L) || (key & FLAG_SHIFT_R)) {
                    /* Shift+Down, do nothing */
                }
                break;
            default:
                break;
        }
    }
}

代码的意义是,检查当前按键是否是Shift+↑,如果是,则卷动屏幕至80×15处,即向上卷动15行。让我们来试一下,make并运行,按Shift+↑,果然屏幕向上发生了卷动,效果如下图所示。

也就是说,Start Address High Register和Start Address Low Register两个寄存器可以用来设置从显存的某个位置开始显示。这个特性允许我们把显存划分为不同的部分,然后只需要简单的寄存器设置就可以显示相应位置的内容。

我们已经通过改变VGA寄存器的值实现了光标的移动和屏幕滚动。不过,看到VGA表中那密密麻麻的寄存器名称,心中还是有点发怵,寄存器太多了。这个也不需要担心,寄存器虽多,我们暂时用到的却没有多少。反正我们已经了解了它们的访问方式,等到需要某个功能时,查一下手册就可以了。

公众号

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值