实验日期 | 实验项目 |
---|---|
2020.11.12 | 第7天 FIFO与鼠标控制 |
(1).内容概要
加快中断处理:显示文字的功能代码放到了中断处理程序中,这样做可能会导致处理不连贯,不能从网上接收数据等情况的发生。另外中断处理本身就是需要打断CPU的原本的工作,加塞要求进行处理,所以必须处理得干净利索。
(2).关键代码分析
在中断处理程序中的显示字符函数实现
void inthandler21(int *esp)
{
struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
unsigned char data, s[4];
io_out8(PIC0_OCW2, 0x61);//监测IRQ1是否接收中断,IRQ1对应的就是键盘中断
data = io_in8(PORT_KEYDAT);//将获取的键值编码赋值给data
sprintf(s, "%02X", data);//将编码值转换为按照2位16进制数的格式转换为字符串存储在字符串s中。
boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);//绘制显示字符的背景
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);//打印显示的字符
return;
}
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_halt()无限循环中,只有当接收到中断信号,才会开始工作。首先是使用io_cli()屏蔽中断(目的:防止当前中断处理过程中,其他中断对程序执行的干扰),接下来检测缓冲区是否为空,如果为空则取消中断屏蔽,执行hlt,此时,如果收到了PIC的通知,CPU就会被唤醒。如果缓冲区不为空,则记录下键值编码后取消中断屏蔽(此时即使中断进来也不会对已经保存的键值产生影响了),最后就是执行显示字符的功能。
(1).内容概要
(2).关键代码分析
制作FIFO缓冲区
只能存储一个字节的代码解决方案
struct KEYBUF {
unsigned char data[32];//定义了一个可以存储32个字节的字符数组
int next;//该变量是指向下一个数据
};
data = io_in8(PORT_KEYDAT);
if (keybuf.next < 32) {
keybuf.data[keybuf.next] = data;
keybuf.next++;
}
这部分代码是中断处理函数:一旦检测到有中断产生,就立即记录下对应的键值编码。
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);
}
}
这部分代码是在主函数中对存储数据输出,需要注意的就是中断的屏蔽和开启,进行处理数据时,必须对中断进行屏蔽,以免中断接收数据缓冲区的数据造成覆盖。
改善FIFO缓冲区
上述程序中涉及到了每次取出缓冲区中的数据后移动的问题,下面的代码通过一个取出位置,一个存入位置的变量的标记对缓冲区进行了改善。
struct KEYBUF {
unsigned char data[32];
int next_r, next_w, len;
};
这部分代码是存储的结构体,next_r用来指向读出位置,next_w用来指向写入位置,len表示记录下数据的个数。
void inthandler21(int *esp)
{
unsigned char data;
io_out8(PIC0_OCW2, 0x61); /* IRQ-01受付完了をPICに通知 */
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) {//如果写入位置指向最后一个位置,下一个位置是起点,需要重新置0。
keybuf.next_w = 0;
}
}
return;
}
这部分代码是中断处理函数,其之前改善前的代码类似。每次记录下数据后,缓冲区记录数据的长度加一,写入位置指向下一个位置,当写入位置为32(写满了)时,置0后从头开始。
for (;;) {
io_cli();
if (keybuf.len == 0) {
io_stihlt();
} else {
i = keybuf.data[keybuf.next_r];//读出数据
keybuf.len--;//缓冲区存储数据个数减少1
keybuf.next_r++;//读出位置指向下一个
if (keybuf.next_r == 32) {//如果读出位置指向最后一个位置,下一个位置是起点,需要重新置0。
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);
}
}
这部分代码是对读出数据的处理,使用next_r指向读出位置,这样在写入和读出时就不会相互干扰了。
(1).内容概要
(2).关键代码分析
struct FIFO8 {
unsigned char *buf;
int p, q, size, free, flags;
};
这部分代码时换取冲结构体的定义,p表示写入位置,q表示读出位置,buf是用来存储数据的,size表示缓冲区的大小,free表示缓冲区剩余可用大小,flags表示是否溢出。
void fifo8_init(struct FIFO8 *fifo, int size, unsigned char *buf)
{
fifo->size = size;
fifo->buf = buf;
fifo->free = size; /* 缓冲区大小 */
fifo->flags = 0;
fifo->p = 0; /* 下一个数据写入位置*/
fifo->q = 0; /* 下一个数据读出位置*/
}
这部分代码是FIFO的初始化函数,包括对缓冲区的大小和可用的数据区域的大小进行赋值,初始时,写入位置和读出位置都指向0,溢出标志设置为0。
int fifo8_put(struct FIFO8 *fifo, unsigned char data)
{
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;
}
这部分代码是向缓冲区存储一个字节信息的函数。当可用空间大小为0时,设置溢出标志位。使用p指向的写入位置存储数据data,当指向最后一个位置时,p赋值为0。类似地,从缓冲区读出一个字节信息的函数与此类似,不做说明。
int fifo8_status(struct FIFO8 *fifo)
{
return fifo->size - fifo->free;
}
这部分是缓冲区状态判断函数,当返回值为0时,说明缓冲区已满。
有了这些封装在fifo.c文件中的函数,中断处理函数接收数据只需调用fifo8_put函数存储,主函数中调用fifi8_get和fifo8_status函数取出缓冲区中的数据。
(1).内容概要
a.鼠标控制电路:这个电路包含在了键盘控制电路里,当完成了键盘控制电路的初始化,则鼠标电路控制器的激活也完成了。
b.激活鼠标,保证鼠标有效。
从鼠标接收数据和从键盘接收数据是十分类似的,不同的地方在于PIC的中断受理通知,鼠标使用的是IRQ12,IRQ12受理完成后,通过主PIC上2号通过主PIC有鼠标中断产生。接收数据和读取数据和键盘是一致的,使用中断号码来区分数据是来源于鼠标还是键盘。
(2).关键代码分析
void wait_KBC_sendready(void)
{
/* 等待键盘控制电路准备完毕 */
for (;;) {
if ((io_in8(PORT_KEYSTA) & KEYSTA_SEND_NOTREADY) == 0) {
//从设备号码0x0064处读取数据的倒数第2位为0时,说明键盘控制电路可以接收CPU指令了
break;
}
}
return;
}
这部分代码是等到键盘控制电路准备好接收数据,实现就是利用循环,当检测到相应的信号有效时,退出循环,开始准备初始化。
void init_keyboard(void)
{
/* 初始化键盘控制电路 */
wait_KBC_sendready();
io_out8(PORT_KEYCMD, KEYCMD_WRITE_MODE);//设定指令模式为0x60
wait_KBC_sendready();
io_out8(PORT_KEYDAT, KBC_MODE);//设定鼠标模式
return;
}
这部分代码是键盘初始化电路,每次设定模式前需要等待键盘控制电路准备好。
void enable_mouse(void)
{
/* 激活鼠标*/
wait_KBC_sendready();
io_out8(PORT_KEYCMD, KEYCMD_SENDTO_MOUSE);
//向键盘控制电路写入0xd4后,下一个数据就会自动发送给鼠标
wait_KBC_sendready();
io_out8(PORT_KEYDAT, MOUSECMD_ENABLE);//使鼠标能够使用,成功的话向CPU发送一个0xfa作为答复。
return; /*键盘控制其会返回ACK */
}
这部分代码是激活鼠标,使鼠标本身有效
当检测到缓冲区不为空时,需要执行io_stihlt(),这个函数的功能就是取消中断屏蔽后,进行休眠等待状态,为什么不能写成io_sti();io_hlt()呢,还要单独写一个函数实现?
分开写,如果io_sti()之后产生了中断,keybuf里就会存入数据,这时候让CPU进入HLT状态,keybuf里存人的数据就不会被觉察到。根据CPU的规范,机器语言的STI指令之后,如果紧跟着HLT指令,那么就暂不受理这两条指令之间的中断,而要等到HLT指令之后才受理,所以使用io_stihlt函数就能克服这一问题。
harib04f的代码中实现了鼠标控制电路的初始化和激活鼠标的工作,运行代码可以看到鼠标中断被成功调用,而之前的按键部分代码没有做过任何修改,但是按下按键并不会显示按键编码值?
运行程序后,没有移动鼠标也产生了鼠标中断。猜测按下键盘后无法显示键值编码的原因可能是鼠标键值的信息没有接收,无法判断是来自键盘的信息还是来自鼠标的信息,对于这个猜想的正确性,暂时没有成功验证。
这两句程序代码中“io_out8(PIC1_OCW2, 0x64); io_out8(PIC0_OCW2, 0x62); ”第2句是表示IRQ2收到来自从PIC上信号时,通知主PIC,而鼠标对应的是IRQ12,为什么第1句中是0x64(0x60+IRQ号码)?
从下图中可以看出,IRQ12在从PIC上的编号是第4个,所以是0x60+4,由于代码中对主从PIC进行了区分,所以对于每个PIC上的IRQ号码应该都是从1开始编的。
在考核编程中,按下按键后屏幕上显示的键值不变。按了好多次后发现,按下shift后键值才会改变,但是在同学的电脑上尝试是可以按键就发生改变的。
和中文输入法有关,每次按下shift后,再次按下按键键值显示就是因为切换成了英文输入法。解决方案:在每次运行之前,切换成英文输入。
本次实验的创新点是对考核基本要求的进一步完善。主要增加了三个创新点,一是向不同的方向移动,圆形的颜色会发生改变,二是将题目2封装成一个功能,按下p可以在屏幕上打印出变换颜色的姓名和学号,三是在验收后增加的,改变圆形的大小。
第一个创新点是向不同方向移动,圆形的颜色会发生改变
void draw_circular(unsigned char *vram, int xsize, unsigned char c, float x0, float y0,float r)
{
int x, y;
for (y = y0-r; y <= y0+r; y++)
{
for (x = x0-r; x <= x0+r; x++)
{
if((x-x0)*(x-x0)+(y-y0)*(y-y0)<=r*r)//如果坐标(x,y)满足圆形及其内部的方程,则对改变该像素点的颜色。
vram[y * xsize + x] = c;
}
}
return;
}
这部分代码是绘制圆形图案的,关键思想就是确定圆形坐标和半径,利用圆形的坐标方程来对显示的点进行约束。
if(p==0x48)
{
if(my-2>=r)//满足边界条件,绘制圆形
{
my-=2;
draw_circular(binfo->vram, binfo->scrnx, COL8_008484, mx, my+2,r);//覆盖上一次的圆形
init_screen8(binfo->vram, binfo->scrnx, binfo->scrny);//重新加载界面
draw_circular(binfo->vram, binfo->scrnx, c4, mx, my,r);//绘制本次的圆形
}
else
{
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_000000, "It has reached the upper boundary.");//到达边界,打印提示信息
}
}
这部分代码是向上移动的代码,关键思想在于改变圆心的纵坐标,每次都要对是否越界进行判断,如果不越界才绘制图形,此时依然会保留越界前的图形(这时不管如何向上移动图形都不会动),到达边界时,打印字符串提示信息。其余方向的处理类似。在传递颜色参数时定义4个颜色变量,每个方向的移动传递一种颜色,即可实现移动时颜色的变化。
第二个创新点是打印可以颜色动态变化的姓名和学号。
void print_my_infomation(char *vram, int xsize, int x, int y, char c, int *a)
{
extern char hankaku[4096+16*4];
int i=0;
for (i=0;i<15;i++) {
putfont8(vram, xsize, x+i*8, y, c, hankaku + *a * 16);//打印字符串a中的姓名和学号
a+=1;
}
}
这部分代码是将字符数组中的姓名和学号信息打印出来
else if(p==0x19)//显示个人信息
{
int k;
for(k=0;k<6;k++)
{
print_my_infomation(binfo->vram, binfo->scrnx, 8, 16, c[k],a);
int j;
for(j=0;j<100000000;j++);//这个循环改变闪烁的速度
}
}
这是对应按下按键p后的选择分支,6种颜色存储在字符数组中,利用for循环来依次显示6种颜色,内部的for循环是控制颜色改变的速率。
第三个创新点是在验收后完成的,主要是改变圆形的大小,按x可以增大圆形半径,按c可以减小圆形的半径。
else if(p==0x2d)//圆形增大
{
if(mx<=binfo->scrnx-r-2-1&&mx>=r+2&&my<=binfo->scrny-29-r-2&&my>=r+2)
{
r+=2;//半径增大
draw_circular(binfo->vram, binfo->scrnx, c3, mx, my,r);
}
else
{
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_000000, "It's already the largest garden.");//输出最大圆形提示
}
}
这部分代码是改变圆形的半径。半径这个变量需要设置在中断处理的for循环外,利用每次中断按下对应的键去改变半径,设置在中断外的话,下次使用时还是改变后半径的值。如果半径增加到触碰边界时,输出圆形已经是最大的提示信息,并不再增大圆形的大小。减小圆形的操作和增大类似。
本次实验是自制操作系统的第7天,实验主要利用了FIFO缓冲区对按键编码和鼠标编码的信息进行了接收,如何利用收集到的编码值就是本次实验的创新点。实验内容比较简单,但其中涉及到的点还是值得思考的,比如中断的屏蔽和开启顺序,如果这个顺序错误了,则可能会导致数据的接收出错,这其中也可以看出操作系统设计过程中严密完整的逻辑性,需要面面俱到地去考虑问题。