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

第八天 鼠标控制与32位模式切换

1.鼠标解读(1)

首先要说明一下,鼠标发送回来的数据是三个字节一组的,所以我们要以三个字节为单位处理鼠标数据。

    unsigned char mouse_dbuf[3], mouse_phase;

    enable_mouse();
    mouse_phase = 0;

while (1)
    {
        io_cli();
        if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) == 0)
        {
            io_stihlt();
        }
        else if (fifo8_status(&keyfifo) != 0)
        {
            i = fifo8_get(&keyfifo);
            io_sti();
            sprintf(s, "%02X", i);
            boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
            putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
        }
        else if (fifo8_status(&mousefifo) != 0)
        {
            i = fifo8_get(&mousefifo);
            io_sti();
            if (mouse_phase == 0)       /* 等待鼠标的0xfa状态 */
            {
                if (i == 0xfa)
                {
                    mouse_phase = 1;
                }
            }
            else if (mouse_phase == 1)  /* 等待鼠标的第1字节 */
            {
                mouse_dbuf[0] = i;
                mouse_phase = 2;
            }
            else if (mouse_phase == 2)  /* 等待鼠标的第2字节 */
            {
                mouse_dbuf[1] = i;
                mouse_phase = 3;
            }
            else if (mouse_phase == 3)  /* 等待鼠标的第3字节 */
            {
                mouse_dbuf[2] = i;
                mouse_phase = 1;
                sprintf(s, "%02X %02X %02X", mouse_dbuf[0], mouse_dbuf[1], mouse_dbuf[2]);      /* 将鼠标的3个字节显示出来 */
                boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 32 + 8 * 8 - 1, 31);
                putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);
            }
        }
    }

添加的代码的逻辑是:先将回应信号(0xfa)舍弃,在读取完鼠标发送的三个字节后,将他们显示在屏幕上。

运行得到的结果如图所示:

移动鼠标和按下按键会使数据发生改变,这些数据的详细内容会放在第3节讲解。


2.稍事整理

HariMain有点乱,作者在这节就只进行了代码的整理。

struct MOUSE_DEC
{
    unsigned char buf[3], phase;
};

void enable_mouse(struct MOUSE_DEC *mdec);
int mouse_decode(struct MOUSE_DEC *mdec, unsigned char dat);

void HariMain(void)
{
    ...

    struct MOUSE_DEC mdec;

    ...

    enable_mouse(&mdec);

    while (1)
    {
        io_cli();
        if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) == 0)
        {
            io_stihlt();
        }
        else if (fifo8_status(&keyfifo) != 0)
        {
            i = fifo8_get(&keyfifo);
            io_sti();
            sprintf(s, "%02X", i);
            boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
            putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
        }
        else if (fifo8_status(&mousefifo) != 0)
        {
            i = fifo8_get(&mousefifo);
            io_sti();
            if (mouse_decode(&mdec, i) != 0)  /* 等待鼠标的第3字节 */
            {
                sprintf(s, "%02X %02X %02X", mdec.buf[0], mdec.buf[1], mdec.buf[2]);      /* 将鼠标的3个字节显示出来 */
                boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 32 + 8 * 8 - 1, 31);
                putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);
            }
        }
    }
}

void enable_mouse(struct MOUSE_DEC *mdec)     /* 激活鼠标 */
{
    wait_KBC_sendready();
    io_out8(PORT_KEYCMD, KEYCMD_SENDTO_MOUSE);  /* 把发往0x60端口的数据发给鼠标 */
    wait_KBC_sendready();
    io_out8(PORT_KEYDAT, MOUSECMD_ENABLE);      /* 允许鼠标向主机发送数据包 */
    /* 顺利的话,键盘控制器会返送回ACK(0xfa) */
    mdec->phase = 0;
}

int mouse_decode(struct MOUSE_DEC *mdec, unsigned char dat)
{
    if (mdec->phase == 0)       /* 等待鼠标返回ACK(0xfa) */
    {
        if (dat == 0xfa)
        {
            mdec->phase = 1;
        }
        return 0;
    }
    else if (mdec->phase == 1)  /* 等待鼠标的第1字节 */
    {
        mdec->buf[0] = dat;
        mdec->phase = 2;
        return 0;
    }
    else if (mdec->phase == 2)  /* 等待鼠标的第2字节 */
    {
        mdec->buf[1] = dat;
        mdec->phase = 3;
        return 0;
    }
    else if (mdec->phase == 3)  /* 等待鼠标的第3字节 */
    {
        mdec->buf[2] = dat;
        mdec->phase = 1;
        return 1;
    }
    return -1;          /* 应该不会到这里来 */
}

此次修改添加了记录鼠标数据的结构体,便于操作。

主要逻辑没有发生变化就不展示效果了,反正和上一节一样。


3.鼠标解读

在讲代码之前,我要向大家介绍PS/2鼠标。

标准的PS/2鼠标支持以下输入:X(左右)位移、Y(上下)位移、左键、中键、右键(没有滚轮)。标准的PS/2鼠标以3字节的数据包格式发送给主机,三个数据意义如下:

76543210
第一字节Y溢出X溢出Y符号X符号1中键右键左键
第一字节X方向移动
第一字节Y方向移动
  • Y溢出:0 - 没有溢出;1 - 表示Y坐标的变化量超出-256~255的范围。
  • X溢出:0 - 没有溢出;1 - 表示X坐标的变化量超出-256~255的范围。
  • Y符号:Y坐标变化的符号位。1 - 表示负数,即鼠标向下移动。
  • X符号:X坐标变化的符号位。1 - 表示负数,即鼠标向右移动。
  • 中键:1 - 中键按下
  • 右键:1 - 右键按下
  • 左键:1 - 左键按下
  • 第二、三字节表示鼠标在X、Y坐标上的变化量。

了解了这些知识后我们再来看代码。

struct MOUSE_DEC
{
    unsigned char buf[3], phase;
    int x, y, btn;
};

int mouse_decode(struct MOUSE_DEC *mdec, unsigned char dat)
{
    if (mdec->phase == 0)       /* 等待鼠标返回ACK(0xfa) */
    {
        if (dat == 0xfa)
        {
            mdec->phase = 1;
        }
        return 0;
    }
    else if (mdec->phase == 1)  /* 等待鼠标的第1字节 */
    {
        if ((dat & 0xc8) == 0x08)   /* 如果第一字节正确 */
        {
            mdec->buf[0] = dat;
            mdec->phase = 2;
        }
        return 0;
    }
    else if (mdec->phase == 2)  /* 等待鼠标的第2字节 */
    {
        mdec->buf[1] = dat;
        mdec->phase = 3;
        return 0;
    }
    else if (mdec->phase == 3)  /* 等待鼠标的第3字节 */
    {
        mdec->buf[2] = dat;
        mdec->phase = 1;
        mdec->btn = mdec->buf[0] & 0x07;
        mdec->x = mdec->buf[1];
        mdec->y = mdec->buf[2];

        if ((mdec->buf[0] & 0x10) != 0)
        {
            mdec->x |= 0xffffff00;
        }
        if ((mdec->buf[0] & 0x20) != 0)
        {
            mdec->y |= 0xffffff00;
        }
        mdec->y = -mdec->y;
        return 1;
    }
    return -1;          /* 应该不会到这里来 */
}

mouse_decode函数在0xfa阶段,等到了0xfa信号就到第一字节的阶段。在第一字节阶段,有这样一个判断(dat & 0xc8) == 0x08,参考上面的表,第一字节的bit3始终为一,这段代码就是检测是否接受的是第一字节。第二字节阶段没什么好说的。第三字节阶段代码中,mdec->btn = mdec->buf[0] & 0x07; mdec->x = mdec->buf[1]; mdec->y = mdec->buf[2];分别保存按键状态,x和y方向的移动量。但这段代码我不太理解:mdec->x |= 0xffffff00;这段代码是对x方向的数据进行处理,但这不是取反码、补码或移码操作。

        else if (fifo8_status(&mousefifo) != 0)
        {
            i = fifo8_get(&mousefifo);
            io_sti();
            if (mouse_decode(&mdec, i) != 0)  /* 等待鼠标的第3字节 */
            {
                sprintf(s, "[lcr %4d %4d]", mdec.x, mdec.y);
                if ((mdec.btn & 0x01) != 0)
                {
                    s[1] = 'L';
                }
                if ((mdec.btn & 0x02) != 0)
                {
                    s[3] = 'R';
                }
                if ((mdec.btn & 0x04) != 0)
                {
                    s[2] = 'C';
                }
                boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 32 + 15 * 8 - 1, 31);
                putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);
            }
        }

当三个字节取完后,根据不同的按键状态显示不同的信息。

完成了这些步骤,我们就可以移动鼠标了!


4.移动鼠标指针

        else if (fifo8_status(&mousefifo) != 0)
        {
            i = fifo8_get(&mousefifo);
            io_sti();
            if (mouse_decode(&mdec, i) != 0)  /* 等待鼠标的第3字节 */
            {
                sprintf(s, "[lcr %4d %4d]", mdec.x, mdec.y);
                if ((mdec.btn & 0x01) != 0)
                {
                    s[1] = 'L';
                }
                if ((mdec.btn & 0x02) != 0)
                {
                    s[3] = 'R';
                }
                if ((mdec.btn & 0x04) != 0)
                {
                    s[2] = 'C';
                }
                boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 32 + 15 * 8 - 1, 31);
                putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);
                boxfill8(binfo->vram, binfo->scrnx, COL8_008484, mx, my, mx + 15, my + 15);
                mx += mdec.x;
                my += mdec.y;
                if (mx < 0)
                {
                    mx = 0;
                }
                if (my < 0)
                {
                    my = 0;
                }
                if (mx > binfo->scrnx - 16)
                {
                    mx = binfo->scrnx - 16;
                }
                if (my > binfo->scrny - 16)
                {
                    my = binfo->scrny - 16;
                }
                sprintf(s, "(%3d, %3d)", mx, my);
                boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 0, 79, 15);
                putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, s);
                putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16);
            }
        }

这段代码的代码逻辑是:接收鼠标发来的三个数据,并对数据进行处理,得到按键信息和移动信息,将这些信息显示在屏幕上,然后刷新鼠标的位置。代码虽然长,但理解起来并不难。

效果如下。

能完成鼠标的移动是真的不容易(在理解代码的情况下)。不过这也只是一个简陋的程序,不足之处也有许多,之后也会有改进的方案,使它变得更加完美。


5.通向32位模式之路

一开始学习这本书的时候,我是抱着稳扎稳打的心态,一定要理解每一句代码。但是,作者在之前直接就在asmhead.nas添加了不少内容,我当时人都懵了,这么多行代码书里面怎么没有解释,网上也找不到答案,也只得放一放,想着等学完这本书再去学学汇编搞明白,结果作者把解释放在了第8章。。。

; 使PIC不接受一切中断
; 根据AT兼容机的规格,如果要初始化PIC
; 必须把它放在CLI之前,否则有时会挂起
; 随后进行PIC的初始化
    MOV AL, 0xff
    OUT 0x21, AL
    NOP                 ; 有的机型不能炼虚使用OUT指令,加上NOP延时
    OUT 0xa1, AL

    CLI                 ; 禁止CPU层面插队

如注释所示,这段代码用于禁止中断。

; 为了能够从CPU访问1MB以上的存储器,设定A20 GATE
    MOV AL, 0x02
    OUT 0x92, AL        ; 92h端口的bit1控制着A20,为1时打开

这段代码和书上的不一样,但实现的功能是一样的,都是设定A20 GATE(至于什么是A20 GATE,请自行百度查询,在此讲解的话篇幅会变很长),我也是因为书上的代码太不好理解了,才在网上搜索相关解释,结果发现有很简单的实现方法,我现在学完了第11章也没有出什么错误。

; 保护模式转移
[INSTRSET "i486p"]      ; 使用486指令的记述

    LGDT    [GDTR0]     ; 暂定GDT设定
    MOV EAX, CR0
    AND EAX, 0x7fffffff ; 将bit31设置为0(禁止分页)
    OR  EAX, 0x00000001 ; 把bit0设置为1(转换为保护模式)
    MOV CR0, EAX
    JMP pipelineflush

pipelineflush:
    MOV AX, 1 * 8       ; 可读区段32位
    MOV DS, AX
    MOV ES, AX
    MOV FS, AX
    MOV GS, AX
    MOV SS, AX

LGDT指令,不管三七二十一,把随意准备的GDT给读进来。对于这个暂定的GDT,我们以后还要重新设置。然后将CR0这一特殊的32位寄存器的值代人EAX,并将最高位置为0,最低位置为1,再将这个值返回给CR0寄存器,其意义是禁止分页和转换为保护模式。CR0,也就是 control register0,是一个非常重要的寄存器,只有操作系统才能操作它。

保护模式与先前的16位模式不同,段寄存器的解释不是16倍,而是能够使用GDT。在这种模式下,应用程序既不能随便改变段的设定,又不能使用操作系统专用的段。操作系统受到CPU的保护,所以称为保护模式。

这个JMP指令看起来是不是很多余?因为变成保护模式后,机器语言的解释要发生变化。CPU为了加快指令的执行速度而使用了管道( pipeline)这一机制,就是说,前一条指令还在执行的时候,就开始解释下一条甚至是再下一条指令。因为模式变了,就要重新解释一遍,所以加入了JMP指令。

而且在程序中,进入保护模式以后,段寄存器的意思也变了(不再是乘以16后再加算的意思了),除了CS以外所有段寄存器的值都从0x000变成了0x0008。CS保持原状是因为如果CS也变了,会造成混乱,所以只有CS要放到后面再处理。0x0008,相当于“gdt+1”的段。

; boookpack的转送
    MOV ESI, bootpack   ; 传送源
    MOV EDI, BOTPAK     ; 传送地址
    MOV ECX, 512 * 1024 / 4 ; 一共转移512KB数据
    CALL    memcpy

; 顺便将磁盘数据也向原来的位置传送
; 首先从引导扇区开始
    MOV ESI, 0x7c00     ; 传送源
    MOV EDI, DSKCAC     ; 传送地址
    MOV ECX, 512 / 4    ; IPL有512字节
    CALL    memcpy

    MOV ESI, DSKCAC0 + 512  ; 传送源
    MOV EDI, DSKCAC + 512   ; 传送地址
    MOV ECX, 0
    MOV CL, BYTE [CYLS] ; 读取柱头数
    IMUL    ECX, 512 * 18 * 2 / 4   ; 读取磁盘的字节数 / 4
    SUB ECX, 512 / 4    ; 减去IPL的部分
    CALL memcpy

ECX在这里记录的是要传送的次数,因为memcpy是以4字节为单位传送的,所以传送次数就是字节数除以4。memcpy功能的代码如下所示:

memcpy:
    MOV EAX, [ESI]
    ADD ESI, 4
    MOV [EDI], EAX
    ADD EDI, 4          ; 每次从原地址复制4个字节到目的地址
    SUB ECX, 1
    JNZ memcpy          ; 如果减法的结果不是0,则进入memcpy
    RET

在ESI中放入传送源地址,在EDI中放入传送目的地址,在ECX中放入要传送的次数,再调用memcpy就可以实现复制了。

; 启动bootpack
    MOV EBX, BOTPAK
    MOV ECX, [EBX + 16]
    ADD ECX, 3          ; ECX +=3
    SHR ECX, 2          ; ECX /= 4
    JZ  skip            ; 没有可转发的东西
    MOV ESI, [EBX + 20] ; 传送源
    ADD ESI, EBX
    MOV EDI, [EBX + 12] ; 传送地址
    CALL    memcpy

skip:
    MOV ESP, [EBX + 12] ; 堆栈初始值
    JMP DWORD 2 * 8 : 0x0000001b

现在,操作系统的内存分布如下:

0x00000000 - 0x000fffff : 虽然在启动中会多次使用,但之后就变空。(1MB)

0x00100000 - 0x00267fff : 用于保存软盘的内容。(1440KB)

0x00268000 - 0x0026f7ff : 空(30KB)

0x0026f800 - 0x0026ffff : IDT(2KB)

0x00270000 - 0x0027ffff : GDT (64KB)

0x00280000 - 0x002fffff : bootpack.hrb(512KB)

0x00300000 - 0x003fffff : 栈及其他(1MB)

0x00400000 -                : 空

接着是剩余的部分:

    ALIGNB  16          ; 填充字节0直到地址能被16整除

GDT0:
    RESB    8           ; 选择器(NULL selector)
    DW  0xffff, 0x0000, 0x9200, 0x00cf  ; 可读区段32位
    DW  0xffff, 0x0000, 0x9a28, 0x0047  ; 可执行段32位(用于bootpack)
    DW  0

GDTR0:
    DW  8 * 3 - 1       ; 16位的段上限
    DD  GDT0            ; 32位的段起始地址

    ALIGNB  16

bootpack:

ALIGNB指令的意思是,一直添加DB0,直到地址能被16整除为止,如果最初的地址能被16整除,则ALIGNB指令不作任何处理。

GDTR0是LGDT指令,意思是通知GDT0有GDT了。在GDT0里,写人了16位的段上限,和32位的段起始地址。

最后在HariMain中要存在io_sti()函数,用以开放中断,接收硬件数据。


本节的内容还是很简单的,实现鼠标的移动也是一件令人振奋的事情。不过,最后一节的内容我也不太清楚,所以没能讲得很详细,请大家见谅。在后面的内容中,涉及到硬件操作的内容会变少,而涉及到算法理解的内容会增多,所以需要更多的时间去理解代码。希望大家能继续学习下去!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值