《30天自制操作系统》第4天

第4天 C语言与画面显示的练习

1.用C语言实现内存写入

现在想要在屏幕上显示点什么东西,需要向VRAM中数据,虽然C语言可以写内存,但我们还是先用汇编来完成这个工作吧。现在在naskfunc.c中添加点东西。

_write_mem8:    ; void write_mem8(int addr, int data);
    MOV ECX, [ESP + 4]      ; [ESP + 4]中存放的是地址,蒋其读入ECX
    MOV AL, [ESP + 8]       ; [ESP + 8]中存放的是数据,将其读入AL
    MOV [ECX], AL
    RET

在第二天的博客中有一张CPU内存部分寄存器的表,SP表示16位的栈指针寄存器,而此处ESP表示32位的栈指针寄存器。函数的参数会按地址从低到高的顺序存放在栈中,可以通过ESP取出参数。

这段代码的含义就是把data数据存入内存的addr地址中。另外,要与C语言联合使用的话,能自由使用的寄存器就只有EAX、ECX和EDX,因此不能随意更改代码中的寄存器。

naskfunc.c中还添加了一个INSTRSET指令,由于书中有介绍就不多说了。

汇编部分完成了,就开始写C语言代码了。

void io_hlt(void);
void write_mem8(int addr, int data);

void HariMain(void)
{
    int i;

    for (i = 0xa0000; i <= 0xaffff; i++)
    {
        write_mem8(i, 15);  /* MOV BYTE [i], 15 */
    }

    while (1)
    {
        io_hlt();
    }
}

不会C语言语法的读者就先去学学C语言吧,相信我,那会事半功倍的。

这段代码是将地址0xa0000~0xaffff的内存全部写入15,然后一直死循环。

来运行看看效果吧。

一片白色,正是我们想要的效果。


2.条纹图案

改成条纹图案的操作倒是很简单,只需要改一行代码:

for (i = 0xa0000; i <= 0xaffff; i++)
{
    write_mem8(i, i & 0x0f);  /* MOV BYTE [i], 15 */
}

这里用到了与运算,如之前所说,不介绍C语言的基础语法,相信大家也能看懂。

这段代码就是把调色板的16种颜色全部显示出来,并循环往复,我们设置的屏幕宽度是320像素,是16的整数倍,所以相同的颜色会在同一列出现,形成条纹。

最后的运行效果如下:

这图看久了眼睛都觉得有点花。


3.挑战指针

指针作为C语言的核心,自然是重中之重。什么?你没学过指针?那你学的什么C语言。什么?感觉指针太难了,学不会?那我只能说C语言不适合你,写操作系统也不适合你,去学Java、Python或Php吧。

来看看使用指针后的代码吧。

int i;      /* int为32位 */
char *p;    /* BYTE型地址 */

for (i = 0xa0000; i <= 0xaffff; i++)
{
	p = (char*) i;
	*p = i & 0x0f;  /* 代替了write_mem8(i, i & 0x0f); */
}

p = (char*) i;这句代码中对i进行了类型转换,虽然在这里字符指针和整型数据都占4字节,但编译器它不听啊,你不按规矩来,他就报警告,虽然警告没什么影响,但眼不见心不烦嘛,0 error 0 warning听起来多舒服。

运行一下看结果。


4.指针的应用(1)

对代码做点小小的修改:

p = (char *) 0xa0000;   /* 给地址变量赋值 */

for (i = 0; i <= 0xffff; i++)
{
	*(p + i) = i & 0x0f;
}

图我就不放了,反正效果看上面的就行了。


5.指针的应用(2)

对代码做点小小的修改:

p = (char *) 0xa0000;   /* 给地址变量赋值 */

for (i = 0; i <= 0xffff; i++)
{
	p[i] = i & 0x0f;
}

这和数组操作差不多。

图也不放了,偷懒。


6.色号设定

既然都能显示颜色了,那自定义要显示的颜色?当然可以!但是我们只能设置最多256种颜色,至于为什么只能设置256位,还记得我们在asmhead.nas中设置的8位彩色模式吧。这个8位彩色模式,是由程序员随意指定0~255的数字所对应的颜色的。1个数字对应一种颜色,这种方式被称为调色板。

来看看代码吧。

void init_palette(void)
{
    static unsigned char table_rgb[16 * 3] = 
    {
        0x00, 0x00, 0x00,   /* 0:黑 */
        0xff, 0x00, 0x00,   /* 1:亮红 */
        0x00, 0xff, 0x00,   /* 2:亮绿 */
        0xff, 0xff, 0x00,   /* 3:亮黄 */
        0x00, 0x00, 0xff,   /* 4:亮蓝 */
        0xff, 0x00, 0xff,   /* 5:亮紫 */
        0x00, 0xff, 0xff,   /* 6:浅亮蓝 */
        0xff, 0xff, 0xff,   /* 7:白 */
        0xc6, 0xc6, 0xc6,   /* 8:亮灰 */
        0x84, 0x00, 0x00,   /* 9:暗红 */
        0x00, 0x84, 0x00,   /* 10:暗绿 */
        0x84, 0x84, 0x00,   /* 11:暗黄 */
        0x00, 0x00, 0x84,   /* 12:暗蓝 */
        0x84, 0x00, 0x84,   /* 13:暗紫 */
        0x00, 0x84, 0x84,   /* 14:浅暗蓝 */
        0x84, 0x84, 0x84    /* 15:暗灰 */
    };
    set_palette(0, 15, table_rgb);
}

这个函数的作用就是定义颜色数据,然后将数据传入下一个函数中进行处理。

这里我们得先讲讲一些汇编代码,因为下面的C语言程序需要用到汇编语言编写的函数。作者搞事啊,一下子蹦出这么多函数,我就挑几个来讲讲好了。

_io_in8:    ; int io_in8(int port)
    MOV EDX, [ESP + 4]      ; port
    MOV EAX, 0
    IN  AL, DX
    RET

_io_out8:   ; void io_out8(int port, int data);
    MOV EDX, [ESP + 4]      ; port
    MOV AL, [ESP + 8]       ; data
    OUT DX, AL
    RET

_io_in8用于从端口读入一个字节,_io_out8用于向端口输出一个字节。其中,IN和OUT都是汇编中的I/O操作指令,IN用于从端口读取数据,OUT用于向端口写入数据。

如:IN AL, 21H表示从21H端口读取一个字节数据到AL。

IN AX, 21H表示从21H端口读取两个字节数据到AX(21H端口的数据放入AL,22端口的数据放入AH)

OUT 21H, AL表示将AL的值写入21H端口

OUT 21H, AX表示将AX的值写入端口地址21H开始的两个字节(AL写入21H端口,AH写入22H端口)

注:当I/O地址大于FFH时,地址需放入DX中,而且数据传输只能通过使用EAX,AX或AL。

_io_cli:    ; void io_cli(void);
    CLI
    RET

_io_sti:    ; void io_sti(void);
    STI
    RET

CLI指令用于禁止中断发生,STI指令用于允许中断发生。所以这两函数的功能也显而易见。

_io_load_eflags:    ; int io_load_eflags(void);
    PUSHFD          ; 将标志寄存器数据压入栈中
    POP EAX
    RET

_io_store_eflags:   ; void io_store_eflags(int eflags);
    MOV EAX, [ESP + 4]
    PUSH    EAX
    POPFD           ; 将数据写入标志寄存器中
    RET

这两函数的主要内容都已经在注释写好了,而且关于栈操作书上也写得很清楚,我就不多说了。接下来继续介绍C语言文件的函数。

void set_palette(int start, int end, unsigned char *rgb)
{
    int i, eflags;
    eflags = io_load_eflags();  /* 记录中断许可标志的值 */
    io_cli();                   /* 将中断许可标志置为0,禁止中断 */
    io_out8(0x03c8, start);
    for (i = start; i <= end; i++)
    {
        io_out8(0x03c9, rgb[0] / 4);
        io_out8(0x03c9, rgb[1] / 4);
        io_out8(0x03c9, rgb[2] / 4);
        rgb += 3;
    }
    io_store_eflags(eflags);    /* 复原中断许可标志 */
}

首先用io_load_eflags保存状态寄存器的值,然后用io_cli将中断标志位置0(中断标志位在状态寄存器上),禁止中断,最后复原状态寄存器的值,被修改的中断标志就会被修改回来。

代码中分别对0x3C8和0x3C9端口进行了操作,这两个端口的操作步骤如下:

  1. 屏蔽中断
  2. 将想要设定的调色板号码写入0x3C8,紧接着,按R、G、B的顺序写入0x3C9。如果还想继续设定下一个调色板,则省略调色板号码,再按照RGB的顺序写入0x3C9就行了。
  3. 恢复中断

程序的结果就是把调色板的数字0对应黑色,1对应红色,……,15对应暗灰。其中有个奇怪的地方就是,io_out8(0x03c9, rgb[0] / 4);这段代码中,当rgb[0]=0xff时,代码中为rgb[0]或rgb[0] / 4的效果是一样的;但当rgb[0]=0x84时,效果也不一样了。而且,当rgb[0]=0xff时,代码为io_out8(0x03c9, rgb[0] / 4);和代码为io_out8(0x03c9, 0xcf);的效果不一样,也不知道是为什么。如果有人知道请务必在下方评论告诉我。

主程序并没有什么变化。让我们看看修改后的效果。

可以看到从左到右依次是黑、红、绿、黄……,和我们设定的一样。


7.绘制矩阵

绘制矩阵是完成靠C语言程序逻辑实现的,而且并没有什么难度,相信读者花一点时间也能琢磨出一个实现绘制矩阵的算法。

void boxfill8(unsigned char *vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1)
{
    int x, y;
    for (y = y0; y <= y1; y++)
    {
        for (x = x0; x <= x1; x++)
        {
            vram[y * xsize + x] = c;
        }
    }
}

如下图所示:

首先从(x0, y0)点开始填充像素点,直到(x1, y0),然后换行继续填充,直到(x1, y1)。很简单的程序逻辑。

说来惭愧,之前竟然把调色板的工作方式给忘了,所以面对COL8_000000这个定义是数字时还想了一会儿是怎么回事。因为调色板的工作方式,一个数字代表一个颜色,只要往VRAM中写入数字,屏幕上会显示数字相应的颜色。

主函数中添加了绘制矩阵的代码,没什么好讲的,就直接看效果吧。

也不知道五彩斑斓的黑的颜色怎么定义,我想画出来。


8.今天的成果

大家期待的界面它来了!

先看看代码。

void HariMain(void)
{
    char *vram;    /* BYTE型地址 */
    int xsize, ysize;

    init_palette();         /* 设定调色板 */

    vram = (char *) 0xa0000;   /* 给地址变量赋值 */
    xsize = 320;
    ysize = 200;

    boxfill8(vram, xsize, COL8_008484, 0,          0,          xsize - 1,  ysize - 29);
    boxfill8(vram, xsize, COL8_C6C6C6, 0,          ysize - 28, xsize - 1,  ysize - 28);
    boxfill8(vram, xsize, COL8_FFFFFF, 0,          ysize - 27, xsize - 1,  ysize - 27);
    boxfill8(vram, xsize, COL8_C6C6C6, 0,          ysize - 26, xsize - 1,  ysize - 1);

    boxfill8(vram, xsize, COL8_FFFFFF, 3,          ysize - 24, 59,         ysize - 24);
    boxfill8(vram, xsize, COL8_FFFFFF, 2,          ysize - 24, 2,          ysize - 4);
    boxfill8(vram, xsize, COL8_848484, 3,          ysize - 4,  59,         ysize - 4);
    boxfill8(vram, xsize, COL8_848484, 59,         ysize - 23, 59,         ysize - 5);
    boxfill8(vram, xsize, COL8_000000, 2,          ysize - 3,  59,         ysize - 3);
    boxfill8(vram, xsize, COL8_000000, 60,         ysize - 24, 60,         ysize - 3);

    boxfill8(vram, xsize, COL8_848484, xsize - 47, ysize - 24, xsize - 4,  ysize - 24);
    boxfill8(vram, xsize, COL8_848484, xsize - 47, ysize - 23, xsize - 47, ysize - 4);
    boxfill8(vram, xsize, COL8_FFFFFF, xsize - 47, ysize - 3,  xsize - 4,  ysize - 3);
    boxfill8(vram, xsize, COL8_FFFFFF, xsize - 3,  ysize - 24, xsize - 3,  ysize - 3);

    while (1)
    {
        io_hlt();
    }
}

老实说,代码没什么好讲的。我就是来水字数的。下面是界面效果。

这界面应该能和85年的Windows界面有的一拼。


今天的内容又结束了。本以为出完第3天的博客之后要等半个月才能写出这篇来,结果只花了3天时间。其中,2天学第7章的内容,1天时间写这篇博客。。。虽然博客简陋,内容也不多,也是花了我3小时才写出来的。愿你能继续看第5天的博客!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值