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

第7天 FIFO与鼠标控制

 

温馨提示:这章的程序我在作者提供的模拟器上运行的效果不理想,而在虚拟机上没有问题,所以我建议大家使用虚拟机运行这章的程序。(如果在模拟器上运行起来没问题就不用管这条提示了)

1.获取按键编码

继续对键盘进行操作,这次是要获取所按键的编码。

#define PORT_KEYDAT 0x0060

void inthandler21(int *esp)
{
    struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
    unsigned char data, s[4];
    io_out8(PIC0_OCW2, 0x61);   /* 通知PIC IRQ-01已经受理完毕 */
    data = io_in8(PORT_KEYDAT);

    sprintf(s, "%02x", data);
    boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
    putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
    while (1)
    {
        io_hlt();
    }    
}

首先了解OCW2中各位的意思。

名称含义
7R

中断优先级是否按照循环方式设置。

R = 0,采用非循环方式;

R = 1,采用循环方式。

6SL

决定OCW2中L2~L0是否有效。

SL = 0,无效;

SL = 1,有效。

5EOI

中断结束命令位。

EOI = 1,使当前ISR寄存器的相应位请0.当ICW4中的AEOI为0时,ISR中的相应位置由该命令位清除。

40必须为0,这是OCW2的标识位。
30
2L2在SL = 1时,用来确定一个中断优先级的编码。分别对应中断请求级别IRQ0~IRQ7(或IRQ8~IRQ15)。
1L1
0L0

通过对照相应位,可以知道io_out8(PIC0_OCW2, 0x61);的意思是通知PIC已经知道IRQ1中断发生了,并对硬件进行了处理。

0x0060端口是8042键盘输入输出缓冲寄存器。8042是与鼠标键盘操作相关的芯片,CPU通过I/O端口直接和8042芯片通信,获得键盘的扫描码或发送各种键盘命令。data = io_in8(PORT_KEYDAT);这段程序就是读取键盘的扫描码。

之后的程序就是把键盘的扫描码输出到屏幕上,并陷入死循环。

如下是运行的画面。

由于处于死循环,每次只能显示一个键的扫描码。


2.加快中断处理

中断讲的就是一个快,所以一般运行较慢的代码都是放在主程序中进行处理。

struct KEYBUF
{
    unsigned char data, flag;
};

#define PORT_KEYDAT 0x0060

struct KEYBUF keybuf;

void inthandler21(int *esp)
{
    unsigned char data;
    io_out8(PIC0_OCW2, 0x61);   /* 通知PIC IRQ-01已经受理完毕 */
    data = io_in8(PORT_KEYDAT);
    if (keybuf.flag == 0)
    {
        keybuf.data = data;
        keybuf.flag = 1;
    }
}

在中断处理程序中,对是否发生中断进行记录,在主程序中对劲具体处理。

    while (1)
    {
        io_cli();
        if (keybuf.flag == 0)
        {
            io_stihlt();
        }
        else
        {
            i = keybuf.data;
            keybuf.flag = 0;
            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);
        }
    }

在主程序中,首先禁止中断,如果不禁止中断的话,后面的程序运行的结果就会不一样。试想,你先按下A,在程序还未运行到i = keybuf.data;时按下B,之后 i 的值就变成了B,输出的就是B的扫描码,这明显不是我们想要的效果。

接下来判断是否有键盘中断发生,没有就恢复中断并让CPU休息一下,有就显示键盘扫描码。

运行的情况如下:

由于中断中去除了死循环,按下不同键就可以显示不同的扫描码了,可以发现,按下和松开会出现不同的扫描码。


3.制作FIFO缓冲区

虽然上面的程序效果看似不错,但是在按下Ctrl键的时候会漏掉一个字符。现在就来改进程序。

struct KEYBUF
{
    unsigned char data[32];
    int next;
};

void inthandler21(int *esp)
{
    unsigned char data;
    io_out8(PIC0_OCW2, 0x61);   /* 通知PIC IRQ-01已经受理完毕 */
    data = io_in8(PORT_KEYDAT);
    if (keybuf.next < 32)
    {
        keybuf.data[keybuf.next] = data;
        keybuf.next++;
    }
}

作者的FIFO缓冲区就是队列,只不过是一个实现过程较为复杂的队列,更实用的队列会在下一节介绍。

data数组用来保存从键盘传来的数据,next代表data数组中的数据个数。

若data数组未满,就向data数组中写入数据。代码逻辑很简单。

如下是boootpack.c做出的修改:

    while (1)
    {
        io_cli();
        if (keybuf.next == 0)
        {
            io_stihlt();
        }
        else
        {
            i = keybuf.data[0];
            keybuf.next--;
            for (j = 0; j < keybuf.next; j++)
            {
                keybuf.data[j] = keybuf.data[j + 1];
            }
            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);
        }
    }

这段代码的逻辑就是,如果键盘发送数据了,就将它读出来,并把data数组后面的数据往前挪,再讲数据显示在屏幕上。

来看看实现效果吧。


4.改善FIFO缓冲区

上一节的FIFO实现起来既麻烦,效率也低,这次会对它做修改。

struct KEYBUF
{
    unsigned char data[32];
    int next_r, next_w, len;
};

void inthandler21(int *esp)
{
    unsigned char data;
    io_out8(PIC0_OCW2, 0x61);   /* 通知PIC IRQ-01已经受理完毕 */
    data = io_in8(PORT_KEYDAT);
    if (keybuf.len < 32)
    {
        keybuf.data[keybuf.next_w] = data;
        keybuf.len++;
        keybuf.next_w++;
        if (keybuf.next_w == 32)
        {
            keybuf.next_w = 0;
        }
    }
}

这次采用的是循环队列的逻辑(如果不懂队列的话,最好还是看看数据结构的知识,后面的算法知识会变得越来越多,不早早学会,之后会学得更吃力),书中有图片讲解,很生动形象。

接下来还需要修改bootpack.h:

    while (1)
    {
        io_cli();
        if (keybuf.len == 0)
        {
            io_stihlt();
        }
        else
        {
            i = keybuf.data[keybuf.next_r];
            keybuf.len--;
            keybuf.next_r++;
            if (keybuf.next_r == 32)
            {
                keybuf.next_r = 0;
            }
            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);
        }
    }

采用循环队列就不需要对数据进行挪动了,程序的运行速度也能得到提高。

运行的结果和上一节没有什么区别就不放图了。


5.整理FIFO缓冲区

虽然一开始讲的就是键盘,但FIFO才是这章的主角之一啊(看看这章的标题),所以就继续对它做处理吧。

首先定义FIFO的结构体:

struct FIFO8
{
    unsigned char *buf;
    int p, q, size, free, flags;
};

buf为缓冲区的地址;p为下一数据的写入地址;q为下一个数据的读出地址;size为缓冲区的总字节数;free为没有数据的字节数;flags用来检测是否发生过溢出。

void fifo8_init(struct FIFO8 *fifo, int size, unsigned char *buf)   /* 初始化FIFO8缓冲区 */
{
    fifo->size = size;
    fifo->buf = buf;
    fifo->free = size;      /* 缓冲区的大小 */
    fifo->flags = 0;
    fifo->p = 0;            /* 下一个数据写入位置 */
    fifo->q = 0;            /* 下一个数据读出位置 */
}

fifo8_init是FIFO结构体的初始化函数,这个相信无需多讲。

#define FLAGS_OVERRUN   0x0001

int fifo8_put(struct FIFO8 *fifo, unsigned char data)   /* 向FIFO传送数据并保存 */
{
    if (fifo->free == 0)    /* 没有空余位置了,溢出 */
    {
        fifo->flags |= FLAGS_OVERRUN;
        return -1;
    }
    fifo->buf[fifo->p] = data;
    fifo->p++;
    if (fifo->p == fifo->size)
    {
        fifo->p = 0;
    }
    fifo->free--;
    return 0;
}

fifo8_put是往FIFO缓冲区存储1字节信息的函数。如果没有空余空间,就将flags设置为溢出状态,之后就可以得知是否缺失过信息。

int fifo8_get(struct FIFO8 *fifo)   /* 从FIFO取得一个数据 */
{
    int data;
    if (fifo->free == fifo->size)   /* 如果缓冲区为空,则返回-1 */
    {
        return -1;
    }
    data = fifo->buf[fifo->q];
    fifo->q++;
    if (fifo->q == fifo->size)
    {
        fifo->q = 0;
    }
    fifo->free++;
    return data;
}

fifo8_get用于从FIFO中取出一个字符数据。如果FIFO中没有字符就直接-1,否则读取数据,并改变下一个数据的读出地址。

int fifo8_status(struct FIFO8 *fifo)    /* 报告一下到底积攒了多少数据 */
{
    return fifo->size - fifo->free;
}

fifo8_status用来检查FIFO的状态,看FIFO中是否有数据。

键盘的中断服务函数也变得比之前简洁多了:

void inthandler21(int *esp)     /* 来自PS/2键盘的中断 */
{
    unsigned char data;
    io_out8(PIC0_OCW2, 0x61);   /* 通知PIC IRQ-01已经受理完毕 */
    data = io_in8(PORT_KEYDAT);
    fifo8_put(&keyfifo, data);
}

主函数也应该做些修改:

    char s[40], mcursor[256], keybuf[32];

    fifo8_init(&keyfifo, 32, keybuf);

    while (1)
    {
        io_cli();
        if (fifo8_status(&keyfifo) == 0)
        {
            io_stihlt();
        }
        else
        {
            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);
        }
    }

虽然做了修改,但其执行的内容依旧相同,所以也不放图了。


6.总算讲到鼠标了

鼠标的相关讲解书上已经讲得够多了,就让我们直接看看程序吧。

#define PORT_KEYDAT				0x0060
#define PORT_KEYSTA				0x0064
#define PORT_KEYCMD				0x0064
#define KEYSTA_SEND_NOTREADY	0x02
#define KEYCMD_WRITE_MODE		0x60
#define KBC_MODE				0x47

这里又定义了不少东西,让我们来一一了解。

在之前就介绍过,0x0060是8042键盘输入/输出寄存器,用于发送键盘命令或获得键盘的扫描码;0x0064是8042键盘命令/状态寄存器,保存的是8042芯片状态和控制信息。另外介绍一下,8042芯片是一种键盘鼠标的专用的I/O接口,可以通过它实现对键盘鼠标的操作。具体的信息可以到LINUX设备驱动之Intel8042芯片驱动(一)_lee-yu的博客-CSDN博客中查看,一下的内容大多也出自这篇博客。

再来讲讲状态寄存器和命令寄存器中的详细内容吧。

8042状态寄存器

名称含义
7PARITY_EVEN(P_E)从键盘获取的奇偶校验错误
6RCV_TMOUT(R_T)接收超时,置1
5TRANS_TMOUT(T_T)发送超时,置1
4KYBD_INH(K_I)1 - 键盘未被禁止
3CMD_DATA(C_D)

0 - 输入缓冲去的内容为数据;

1 - 输入缓冲去的内容为命令

2SYS_FLAG(S_F)

0 - 加电启动;

1 - 自检通过后启动

1INPUT_BUF_FULL(I_B_F)

0 - 8042已将数据取出;

1 - 输入寄存器满

0OUT_BUF_FULL(O_B_F)

0 - CPU已将数据取出;

1 - 输出缓冲器满

8042控制寄存器

含义
7保留,应该为0
6将第二套扫描码翻译为第一套扫描码
51 - 禁止鼠标
41 - 禁止键盘
31 - 忽略状态寄存器的Bit-4
2设置状态寄存器的Bit-2
11 - 使能鼠标中断
01 - 使能键盘中断

了解了这些寄存器之后我们继续看代码。

void wait_KBC_sendready(void)   /* 等待键盘控制电路准备完毕 */
{
    while (1)
    {
        if ((io_in8(PORT_KEYSTA) & KEYSTA_SEND_NOTREADY) == 0)
        {
            break;
        }
    }
}

void init_keyboard(void)     /* 初始化键盘控制电路 */
{
    wait_KBC_sendready();
    io_out8(PORT_KEYCMD, KEYCMD_WRITE_MODE);    /* 把发往0x60端口的数据会写入8042的控制寄存器 */
    wait_KBC_sendready();
    io_out8(PORT_KEYDAT, KBC_MODE);     /* 不禁止鼠标键盘,并使能鼠标键盘中断 */
}

wait_KBC_sendready函数的作用是检查8042芯片状态寄存器的Bit-1是否为0,即8042是否已经将之前的数据取走。若8042未将数据取走则一直等待到它取走为止。

init_keyboard函数中,io_out8(PORT_KEYCMD, KEYCMD_WRITE_MODE);是向0x64端口发送0x60命令,0x60命令的含义是:把发往0x60端口的数据写入8042控制寄存器。io_out8(PORT_KEYDAT, KBC_MODE);是向0x60端口发送0x47命令,将0x47代入控制寄存器中,可以知道此指令是不禁止键盘鼠标的使用,允许键盘鼠标的中断。但仅仅是开中断还不够,我们还得从鼠标中获取鼠标的数据。

#define KEYCMD_SENDTO_MOUSE		0xd4
#define MOUSECMD_ENABLE			0xf4

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

enable_mouse函数中,io_out8(PORT_KEYCMD, KEYCMD_SENDTO_MOUSE);是向0x64端口发送0xd4命令,0xd4命令的含义是:把发往0x60端口的数据发给鼠标。io_out8(PORT_KEYDAT, MOUSECMD_ENABLE);是向0x60端口发送0xf4命令,由于上一条命令的原因,这条命令会发送给鼠标,0xf4命令的含义是:允许鼠标向主机发送数据包。完成这些操作后,8042会返回一个回应信号(0xfa)。

接下来运行代码,会得到如下界面:


7.从鼠标接受数据

虽然成功触发了鼠标中断,但这不是最终目的,我们得最终目的是让鼠标在屏幕上动起来,因此鼠标的鼠标是必不可少的。

键盘和鼠标的原理几乎相同,程序也就十分相似。

struct FIFO8 mousefifo;

void inthandler2c(int *esp)     /* 来自PS/2鼠标的中断 */
{
    unsigned char data;
    io_out8(PIC1_OCW2, 0x64);   /* 通知PIC1 IRQ-12已经受理完毕 */
    io_out8(PIC0_OCW2, 0x62);   /* 通知PIC0 IRQ-02已经受理完毕 */
    data = io_in8(PORT_KEYDAT);
    fifo8_put(&mousefifo, data);
}

由于鼠标的中断在PIC1上,PIC1级联在PIC0的IRQ2上,所以在处理不仅要对IRQ12进行处理,还需对IRQ2进行处理。

接着获取鼠标的信息,方法和键盘完全相同。


    fifo8_init(&mousefifo, 128, mousebuf);

    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();
            sprintf(s, "%02X", i);
            boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 47, 31);
			putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);
        }
    }

因为鼠标输送的数据比键盘的要多得多,所以将它的缓冲区设置到了128字节。代码逻辑很简单,相信不用我多讲。

执行的界面如下:

效果看起来不错,不过现在的代码实现的东西还很简单,毕竟鼠标一次性会发送3个字节的数据,而我们才读了一个字节,具体的内容会在后面讲解。


今天的内容又结束了,距上一篇的时间已经超过了一周,主要是因为窗口叠加处理那张的代码学起来还是有点小难,顺便还修改了内存管理部分的代码,将数据存储形式从数组改成了链表。下一章鼠标就终于能动起来了,希望大家能继续学习下去。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值