第7天 FIFO与鼠标控制

第7天 FIFO与鼠标控制

2020.4.1

1. 获取按键编码(harib04a)

  • 修改程序,让程序在按下一个键以后不结束,而是把所按键的编码在画面上显示出来。

  • 修改int.c中的inthandler21函数:

    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); /*2位16进制数*/
        boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
        putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
    
        return;
    }
    
    • io_out8(PIC0_OCW2, 0x61);这句代码用来通知PIC:“已经知道发生了IRQ1中断了”。
    • 0x60+IRQ号码输出给OWC2就能实现通知PIC的功能。
    • 执行上述代码以后,PIC继续时刻监视着IRQ1中断是否发生。 如果没写这句代码,PIC便不再监视IRQ1中断,不管下次键盘输入什么信息,系统都感知不到。
  • 注意,从编号0x0060的设备输入的8位信息是按键编码。 编号0x0060的设备是键盘。

  • make run后按下键盘按键a:

2. 加快中断处理(harib04b)

  • 所谓中断处理,基本上就是打断CPU原本的工作,加塞其他任务,所以中断必须完成得***干净利索***。中断处理期间,CPU不再接受别的中断请求。 如果处理键盘中断的程序执行太过缓慢,就会出现鼠标运动不连贯等对用户不友好的现象。

  • 处理方法:
    在处理中断的时候,现将按键的编码接收下来,保存在变量中,然后由HariMain偶尔去看看这个变量。如果发现有了数据,就把它显示出来。

  • int.c节选:

    #define PORT_KEYDAT		0x0060
    
    struct KEYBUF keybuf;
    
    void inthandler21(int *esp)
    {
        unsigned char data;
        io_out8(PIC0_OCW2, 0x61);
        data = io_in8(PORT_KEYDAT); /*读取8位数据到CPU*/
        if (keybuf.flag == 0) {
            keybuf.data = data;
            keybuf.flag = 1;
        }
        return;
    }
    

    其中KEYBUF在bootpack.h中的定义是:

    struct KEYBUF {
        unsigned char data, flag;
    };
    
    • 结构体KEYBUF表示键盘输入的缓冲区,用于保存键盘输入的按键编码。
    • flag表示缓冲区状态:flag=0,表示缓冲区为空;flag=1,表示缓冲区为满。
    • 这样的设计有一个问题:
      如果缓冲区中有数据,而这时又来了一个中断,这时,我们只能不做任何处理,权且把这个数据扔掉。(代码中也有体现)
  • 修改bootpack.c的HariMain函数(节选):

    extern struct KEYBUF keybuf; /*keybuf在其他源文件中*/
    ……
    for (;;) {
    	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);
    	}
    }
    
    • 执行io_cli()屏蔽中断,当我们执行其后的操作的时候,不能有其他中断来打扰。
    • 如果flag=0:
      说明按键没有按下,keybuf.data里面没有值保存进来。此时,我们无事可干,执行io_hlt即可。注意,我们已经屏蔽了中断,我们需要先执行io_sti才行。也就是说,io_sti和io_htl都要执行。执行这两个指令(HLT和STI)的函数是io_stihlt.
      【注:】
    • 如果flag=1:
      这说明中断处理函数在keybuf.data中存入了按键编码。先将按键编码放入变量i中,然后将keybuf.flag值为0,然后通过io_sti函数开放中断。 此时已经开放中断,如果来了中断请求也不要紧,因为data的值已经保存了。最后,在中断开放的情况下优哉游哉地显示字符就OK了。
    • 总之,我们需要在屏蔽中断期间做的处理尽可能的少,同时也要让中断处理程序尽可能地简洁。
  • make run,按下CTRL显示1D,松开显示9D

    • 注意:这里和书上的描述有出入,可能是我PC的CPU的型号(i5-8265U)不同导致了这种差异。并未出现书上所讲的***小问题***。
    • 书上的***小问题***:

    • 也就是说我们在定义结构体KEYBUF时:
      struct KEYBUF {
          unsigned char data, flag;
      };
      
      用于存放数据的data小了,它只能存放一个字节。
  • 因此,需要修改程序。让它大一点儿。

3. 制作FIFO缓冲区(harib04c)

  • 解决上述问题的方式很简单:
    制作一个能存储多个字节的缓冲区。
    这个缓冲区显然是FIFO的,而不是FILO的。

  • bootpack.h中KEYBUF的定义:

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

    int.c中有关inthandler21函数:

    void inthandler21(int *esp)
    {
        unsigned char data;
        io_out8(PIC0_OCW2, 0x61);
        data = io_in8(PORT_KEYDAT);
        if (keybuf.next < 32) {
            keybuf.data[keybuf.next] = data;
            keybuf.next++;
        }
        return;
    }
    
    • keybuf.next是计数变量,记载data中有效数据的数量。初始值为0(C编译器为我们初始化了)。
    • 显然,这样写还是有问题,next=32时,便不能再存储数据了。
  • 修改HariMain函数代码(节选):

    extern struct KEYBUF keybuf; /*keybuf在其他源文件中*/
    ……
    for (;;) {
        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[0]中读取数据。
    • 在屏蔽中断的过程中,使用for循环实现数据移送(向前移动),这就违背了屏蔽中断是处理要短小的原则。
    • for循环真的是太浪费时间了。O(n)的时间复杂度,因此,我们需要O(1).

4. 改善FIFO缓冲区(harib04d)

  • 去除for循环!

  • 基本思想:

    • 维护下一个要写入数据的位置和下一个要读出数据的位置。
    • 当下一个写入位置到达数组末尾时,强制下一个写入的位置变成0;当下一个读出的位置到达数组末尾时,强制下一个读出的位置变成0。这样就形成了一个“循环缓冲区”。
    • 这就需要保证缓冲区的大小小于缓冲区的容量。
  • bootpack.h中KEYBUF的定义:

    struct KEYBUF {
        unsigned char data[32];
        int next_r, next_w, len;
    };
    
    • next_r代表下一个要读出数据的位置,next_w代表下一个写入数据的位置,len是缓冲区缓冲区的大小(缓冲区能够记录多少字节的数据)。

    int.c中inthandler21函数:

    void inthandler21(int *esp)
    {
        unsigned char data;
        io_out8(PIC0_OCW2, 0x61);	
        data = io_in8(PORT_KEYDAT);
        if (keybuf.len < 32) { /*len必须小于缓冲区的容量*/
            keybuf.data[keybuf.next_w] = data;
            keybuf.len++;
            keybuf.next_w++;
            if (keybuf.next_w == 32) {
                keybuf.next_w = 0;
            }
        }
        return;
    }
    
    • 函数很简单,负责向缓冲区写入数据。

    HariMain函数代码(节选):

    for (;;) {
    	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);
    	}
    }
    
    • 代码很简单,负责向缓冲区读出数据。
  • 这样,没有使用任何数据移送操作(for循环),同时缓冲区又可以记录大量数据,执行速度又快。

  • make run 正常运行。

5. 整理FIFO缓冲区(harib04e)

  • 截止目前,我们第7天所做的事情都是处理键盘中断的。

  • 有了上面的基础,我们修改一下FIFO缓冲区,让它有通用性,这样我们的鼠标中断也就可以重用代码了。

  • 鼠标一动,就会发送3个字节的数据。

  • 我们新建一个源文件fifo.c,这个文件是专门用于缓冲区的。(如果用面向对象的角度看,这好像是一个类)

  • 首先,是bootpack.h中有关缓冲区结构体FIFO8(8的含义是我们每次读出和写入都是一个字节)的声明:

    struct FIFO8 {
        unsigned char *buf;
        int p, q, size, free, flags;
    };
    
    • 变量buf是一个指针,指向缓冲区的内存起始地址。
    • p代表下一个写入的数据(next_w)
    • q代表下一个读出的数据(next_r)
    • size代表缓冲区的总字节数(容量)
    • free代表缓冲区内没有数据的字节数
    • flags作为记录缓冲区是否溢出
  • fifo.c的fifo8_init函数:

    void fifo8_init(struct FIFO8 *fifo, int size, unsigned char *buf)
    /* 初始化FIFO缓冲区 */
    {
        fifo->size = size;
        fifo->buf = buf;
        fifo->free = size; /* 缓冲区的容量 */
        fifo->flags = 0;
        fifo->p = 0; /* 下一个数据写入位置 */
        fifo->q = 0; /* 下一个要读出的位置 */
        return;
    }
    
  • fifo.c的fifo8_put函数:

    #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--; /*空余位置-1*/
        return 0;
    }
    
    • fifo8_put函数的返回值:-1代表溢出;0代表未溢出。
    • fifo->flags |= FLAGS_OVERRUN;这句代码为什么不用赋值?我认为,可能是按位与比赋值快吧?
    • 函数名fifo8_put中的8的含义还是:向缓冲区存储1字节(8位)信息。
  • fifo.c的fifo8_get函数:

    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++; /*空余位置+1*/
        return data;
    }
    
    • fifo8_get函数的返回值:-1代表缓冲区空了无法读取;非-1:data是读取的数值。
    • 函数名中8的含义还是1字节。
  • fifo.c的fifo8_status函数:

    int fifo8_status(struct FIFO8 *fifo)
    /* 返回缓冲区存储的字节数(len) */
    {
        return fifo->size - fifo->free;
    }
    
  • int.c中的inthandler21函数:

    struct FIFO8 keyfifo;
    
    void inthandler21(int *esp)
    {
        unsigned char data;
        io_out8(PIC0_OCW2, 0x61);	/* 通知PIC,说IRQ-01受理已完成 */
        data = io_in8(PORT_KEYDAT);
        fifo8_put(&keyfifo, data);
        return;
    }
    
    • &keyfifo是获取变量keyinfo的地址。因为fifo8_put函数的第一个参数是一个内存地址。
  • bootpack.c中HariMain函数节选:

    extern struct FIFO8 keyfifo;
    ……
    char s[40], mcursor[256], keybuf[32];
    ……
    fifo8_init(&keyfifo, 32, keybuf);
    ……
    for (;;) {
    	io_cli(); /*屏蔽中断*/
    	if (fifo8_status(&keyfifo) == 0) { /*缓冲区为空*/
    		io_stihlt();
    	} else {
    		i = fifo8_get(&keyfifo); /*从缓冲区读取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);
    	}
    }
    
    • extern struct FIFO8 keyfifo;
      • 告诉C编译器,keyfifo在其他源文件里。
    • char s[40], mcursor[256], keybuf[32];
      • s是用于sprintf函数的数组
      • mcursor是用于存储鼠标像素点信息的数组
      • keybuf是缓冲区的存储空间。
  • 可以看出我们整理FIFO以后,代码显得整洁好看。

  • make run,顺利运行。

6. 总算讲到鼠标了(harib04f)

  • 第7天的内容,截止到现在,我们的鼠标中断处理函数inthandler2c依旧和第6天的函数一模一样。所以说,总算讲到鼠标了。

  • 我们先来讲一下为什么鼠标一直不能用的原因:

    • 从计算机历史来看,鼠标这种装置属于新兴一族。早期的电脑一般都不配备鼠标。一个明显的证据是,分配给鼠标的IRQ是IRQ12,这已经是一个很大的数字了,而键盘的IRQ1就显得“很古老”了。
    • IBM的大叔们认为使用鼠标是极其不方便的(时代的局限性),虽然在主板上做了鼠标用的电路,但是只要不执行激活鼠标的指令,就不会产生鼠标的中断信号(1)。
    • 不产生中断信号的解释是,及时鼠标传来了数据CPU也不会接收。这样的话,鼠标就没有必要产生数据了。所以,初期的鼠标,不管是滑动还是点击,都没有反应(2)。
  • 因此,我么想让鼠标动起来,必须发布指令,让两个装置有效:1.鼠标控制电路;2.鼠标本身。

  • 显然,我们先要让鼠标控制电路有效,再让鼠标本身有效。

  • 鼠标控制电路设定:

    • 鼠标控制电路包含在键盘控制电路中,如果键盘控制电路初始化正常完成,那么鼠标电路控制器的激活也就完成了。
  • bootpack.c中的wait_KBC_sendready函数和init_keyboard函数:

    #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
    
    void wait_KBC_sendready(void)
    {
        /* 等待键盘控制电路准备就绪 */
        for (;;) {
            if ((io_in8(PORT_KEYSTA) & KEYSTA_SEND_NOTREADY) == 0) {
                break;
            }
        }
        return;
    }
    
    void init_keyboard(void)
    {
        /* 初始化键盘控制电路 */
        wait_KBC_sendready();
        io_out8(PORT_KEYCMD, KEYCMD_WRITE_MODE);
        wait_KBC_sendready();
        io_out8(PORT_KEYDAT, KBC_MODE);
        return;
    }
    
    • 函数wait_KBC_sendready:作用是:让键盘控制电路(keyboard controller, KBC)做好准备,等待控制指令的到来。
      • 虽然CPU的电路很快,但是键盘控制电路却没有那么快。如果CPU不顾设备接收数据的能力,只是一个劲儿地发送数据的话,那么势必有些指令得不到执行,从而导致错误的结果。
      • 如果控制电路可以接受CPU的指令了,此时,CPU从设备号0x0064(PORT_KEYSTA)出所读取的1字节数据的倒数第2位(低位开始的低2位)应该是0。 这1字节的数据和KEYSTA_SEND_NOTREADY(0x02)按位与以检查这1字节的数据的第2位是否是0。如果是0,那么退出函数,说明KBC已经做好了准备。
    • 函数init_keyboard的作用是:等待控制键盘(鼠标)电路准备完成,然后发送模式设定指令。指令中包含着设定为何种模式。
      • io_out8(PORT_KEYCMD, KEYCMD_WRITE_MODE);:向设备号为PORT_KEYCMD(0x0064)写入KEYCMD_WRITE_MODE(0x60)。
        0x60的含义是一个指令,是模式设定的指令。
        执行完此行代码以后,也就是说可以设置KBC的模式了。
      • io_out8(PORT_KEYDAT, KBC_MODE);向设备号为0x0060(键盘)写入KBC_MODE(0x47)。这个0x47就是KBC的模式之一,0x47是鼠标模式的模式号码。
      • 这些模式号码以及模式设定指令都是规定好的。这样,就可以激活KBC(包括鼠标)了。
  • 然后在bootpack.c的HariMain函数中调用:

    init_keyboard();  
    

    就可以完成鼠标控制电路的准备了。

  • 发送激活鼠标的指令

    • 发送鼠标激活指令,归根到底还是向键盘控制电路(KBC)发送指令。
  • bootpack.c中的enable_mouse函数:

    #define KEYCMD_SENDTO_MOUSE		0xd4
    #define MOUSECMD_ENABLE			0xf4
    
    void enable_mouse(void)
    {
        /* 激活鼠标 */
        wait_KBC_sendready();
        io_out8(PORT_KEYCMD, KEYCMD_SENDTO_MOUSE);
        wait_KBC_sendready();
        io_out8(PORT_KEYDAT, MOUSECMD_ENABLE);
        return; /* 顺利的话,键盘控制器会返回0xfa */
    }
    
    • 这个函数与init_keyboard函数十分相似。不同点是写入的数据不同。
    • 如果往键盘控制电路发送指令0xd4,下一个数据就会自动发送给鼠标。根据这来发送激活鼠标的指令。
    • io_out8(PORT_KEYCMD, KEYCMD_SENDTO_MOUSE);:向设备号为PORT_KEYCMD(0x0064)写入数据KEYCMD_SENDTO_MOUSE(0xd4),0xd4是一个指令。
    • io_out8(PORT_KEYDAT, MOUSECMD_ENABLE);:向键盘控制电路写入MOUSECMD_ENABLE(0xf4)【实际上这个数据发送给鼠标控制电路】,完成激活鼠标的工作
    • 鼠标一旦激活以后,就会立刻给CPU发送答复信息:“CPU大哥,鼠标小弟我现在就要开始不停地向您发送我的信息了。”这个答复信息用16进制数表示就是0xfa
    • 因为这个答复消息0xfa很快就到CPU了,因此,即使我们保持鼠标完全不动,也一定会产生一个鼠标中断。
  • 再再bootpack.c中掉用鼠标激活函数enable_mouse,这样鼠标就激活了

  • 完成鼠标控制电路设定发送激活鼠标的指令后,这样我们就可以产生鼠标中断了make run一下:

    千呼万唤始出来,鼠标中断终于出来了!

7. 从鼠标接受数据(harib04g)

  • int.c中的inthandler2c函数(鼠标中断处理函数):

    struct FIFO8 mousefifo;
    
    void inthandler2c(int *esp)
    /* 来自PS/2鼠标的中断 */
    {
        unsigned char data;
        io_out8(PIC1_OCW2, 0x64);	/* 通知PIC1:IRQ12的受理已经完成 */
        io_out8(PIC0_OCW2, 0x62);	/* 通知PIC0:IRQ2的受理已经完成 */
        data = io_in8(PORT_KEYDAT);
        fifo8_put(&mousefifo, data);
        return;
    }
    
    • 和inthandler21的不同之处只有送给PIC的中断受理通知。
    • 首先要通知IRQ12受理完成,再通知主PIC。
    • 鼠标中断信号是IRQ12,在从PIC上,是从PIC的第4号,因此有代码io_out8(PIC1_OCW2, 0x64);
    • 从PIC通过IRQ2和主PIC连接,IRQ2是主PIC的第2号,因此有代码io_out8(PIC0_OCW2, 0x62);
    • 这么做是因为,主从PIC的协调不能自主完成,如果程序不教给主PIC(master PIC)该怎么做,它就会忽略从PIC(slave PIC)的下一个中断信号。
  • 取得鼠标数据:
    bootpack.c中代码节选:

    fifo8_init(&mousefifo, 128, mousebuf);
    ……
    for (;;) {
    	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);
    		}
    	}
    }
    
    • 鼠标获得数据的代码和键盘获取数据的方法完全相同!因为鼠标控制电路在键盘控制电路中。
    • 至于传到CPU的数据是来自键盘还是鼠标,需要靠中断号码区分。
    • 鼠标比键盘更快地送出大量数据,所以将鼠标的FIFO缓冲区容量调整到了128字节。
    • 如果键盘和鼠标的FIFO缓冲区都空了,执行HLT。如果不是两者都空,先检查键盘缓冲区,如果有数据就显示出来。如果键盘缓冲区为空,检查鼠标缓冲区,如果有数据,将它显示出来。
  • make run:

    • 刚开始,显示的数据是FA,这说明CPU收到了来自鼠标的答复。
    • 随便动动鼠标以及键盘按键
  • 下一步就是让鼠标指针跟着动起来,这个工作还是留给第8天吧。

8. 重新开始一周感想

  • 现在是2020.04.02 14:17,天气阴,周四。今晚还要在线上开党支部会。
  • 从上周五重新开始毕业设计的工作。不知不觉中已经过去了一个周了。
  • 先说一下进度:143/710=20.1%.五分之一了?还不错嘛!
  • 这一周以来,看了大概有100页(45-143),从第3天到第7天。虽然第3天很久很久之前也看过(但是没有写markdown文档)。
  • 今天,辅导员在大群里发了一张校园樱花的照片,可是开学还没有音信,看来是要错过花期了。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值