第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中各位的意思。
位 | 名称 | 含义 |
7 | R | 中断优先级是否按照循环方式设置。 R = 0,采用非循环方式; R = 1,采用循环方式。 |
6 | SL | 决定OCW2中L2~L0是否有效。 SL = 0,无效; SL = 1,有效。 |
5 | EOI | 中断结束命令位。 EOI = 1,使当前ISR寄存器的相应位请0.当ICW4中的AEOI为0时,ISR中的相应位置由该命令位清除。 |
4 | 0 | 必须为0,这是OCW2的标识位。 |
3 | 0 | |
2 | L2 | 在SL = 1时,用来确定一个中断优先级的编码。分别对应中断请求级别IRQ0~IRQ7(或IRQ8~IRQ15)。 |
1 | L1 | |
0 | L0 |
通过对照相应位,可以知道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状态寄存器
位 | 名称 | 含义 |
7 | PARITY_EVEN(P_E) | 从键盘获取的奇偶校验错误 |
6 | RCV_TMOUT(R_T) | 接收超时,置1 |
5 | TRANS_TMOUT(T_T) | 发送超时,置1 |
4 | KYBD_INH(K_I) | 1 - 键盘未被禁止 |
3 | CMD_DATA(C_D) | 0 - 输入缓冲去的内容为数据; 1 - 输入缓冲去的内容为命令 |
2 | SYS_FLAG(S_F) | 0 - 加电启动; 1 - 自检通过后启动 |
1 | INPUT_BUF_FULL(I_B_F) | 0 - 8042已将数据取出; 1 - 输入寄存器满 |
0 | OUT_BUF_FULL(O_B_F) | 0 - CPU已将数据取出; 1 - 输出缓冲器满 |
8042控制寄存器
位 | 含义 |
7 | 保留,应该为0 |
6 | 将第二套扫描码翻译为第一套扫描码 |
5 | 1 - 禁止鼠标 |
4 | 1 - 禁止键盘 |
3 | 1 - 忽略状态寄存器的Bit-4 |
2 | 设置状态寄存器的Bit-2 |
1 | 1 - 使能鼠标中断 |
0 | 1 - 使能键盘中断 |
了解了这些寄存器之后我们继续看代码。
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个字节的数据,而我们才读了一个字节,具体的内容会在后面讲解。
今天的内容又结束了,距上一篇的时间已经超过了一周,主要是因为窗口叠加处理那张的代码学起来还是有点小难,顺便还修改了内存管理部分的代码,将数据存储形式从数组改成了链表。下一章鼠标就终于能动起来了,希望大家能继续学习下去。