第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时:
用于存放数据的data小了,它只能存放一个字节。struct KEYBUF { unsigned char data, flag; };
-
因此,需要修改程序。让它大一点儿。
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(包括鼠标)了。
- 函数wait_KBC_sendready:作用是:让键盘控制电路(keyboard controller, 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文档)。
- 今天,辅导员在大群里发了一张校园樱花的照片,可是开学还没有音信,看来是要错过花期了。