第7天
PIC初始化之后,再写中断处理函数,然后把中断处理函数的入口地址注册在IDT中。现在重点是中断处理函数如何编写。
PIC中还有一个寄存器OCW,如果键盘发生中断,需要向PIC发送0X60+IRQ号码,执行这行代码之后PIC才会继续监视IRQ1的中断是否发生。OCW设置完毕后,CPU再从端口中读入键盘数据,计算机的硬件规定,键盘数据总共8位,从0x0060端口读入。附上中断处理程序
void inthandler21(int *esp)
{
struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
unsigned char data, s[4];
io_out8(PIC0_OCW2, 0x61);
data = io_in8(PORT_KEYDAT);
sprintf(s, "%02X", data);
boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 23, 31);
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
return;
}
像上面的程序那样在中断处理程序中既要读取端口数据又要显示到屏幕上,作大量的图像处理工作,如果这个时候正好有一个中断进来,计算机就不能处理,只能不作任何反应。为了不影响中断处理,把inthandle21中断处理程序中的图像处理部份拿出来,inthandle21中断处理程序只做一个工作:读取端口数据,把数据放入缓存区,把图像处理工作放在主程序中处理。主程序一直查询缓存区是否有数据,如果有数据就把数据显示到屏幕上。接下来重点就是怎么写缓存区的程序了。
键盘的缓冲区程序其实就是大学学过的队列,也就是能保证先进先出的数据结构。当然我们在写操作系统当然无法用链表,只能用数组实现一个队列。
先定义一个队列的结构体
struct FIFO8
{
unsigned char *buf;
int p;//下一个数据定入地址
int q;//下一个数据读出地址
int size;//队列长度
int free;//表示队列有没有数据的字节数
int flags;//标记队列有没有溢出,最低位是1表示溢出
};
第一步:初始化
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;
return;
}
第二步:存入数据
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;
}
第三步:读取数据
int fifo8_get(struct FIFO8 *fifo)
{
int data;
if (fifo->free == fifo->size)
{
return -1;
}
data = fifo->buf[fifo->q];
fifo->q++;
if (fifo->q == fifo->size)
{
fifo->q = 0;
}
fifo->free++;
return data;
}
附:查询数据量
int fifo8_status(struct FIFO8 *fifo)
{
return fifo->size - fifo->free;
}
键盘的中断码是IRQ1,而鼠标则要晚得多,是IRQ12。如果要让鼠标操作有效必须发行指令,让下面两个装置有效,一个是鼠标控制电路,一个是鼠标本身。首先要激活鼠标控制电路,鼠标控制电路包含在键盘控制电路里。然后发送激活鼠标的指令,其实发送这个指令实际上就是CPU发送数据到鼠标控制器,也就是键盘控制器。
鼠标的中断处理程序和鼠标差不多,也就是从端口中读取数据放入队列中,甚到CPU读取数据的端口号都是一样的。然后在主程序中从鼠标队列中取出数据进行处理。键盘的处理非常简单,从端口读进来的数据就是键盘的扫描码。那么从端口读进来的鼠标数据表示什么意思呢?
第八天
鼠标一开始设置完成会自动发生一次中断,这次中断发送到CPU的数据为0xfa,只是表示鼠标已经设置完成,将会向CPU发送数据。我们每次对鼠标操作都会引起鼠标向CPU发送三次中断,每次中断发送一个字节,一共三个字节,我们要把这三个字节凑到一起处理才是有意义的。
鼠标一次性接收3字节数据,其中第一个字节表示鼠标的动作,第一个字节的高4位取值范围是03,如果出现其它值,说明鼠标出现错误。第一个字节的低4位取值范围8F,如果出现其他值也说明鼠标发生错误,如果第0位为1说明鼠标左键按下,如果第1位为1说明右键被按下。第二个字节为鼠标水平方向移动的多少,正值为右,负值为左。第三个字节为鼠标竖直方向移动多少,正值向上,负值向下。
目前已经能处理鼠标操作传入的3次中断数据,现在就是简单得在画面上显示出来。首先把原来鼠标图像所在的位置给画成背景色,然在计算新的鼠标位置,然后在新的鼠标位置上画一下鼠标图像,然后就感觉鼠标移动了。
接下来插入讲解一下如何从CPU实模式跳入保护模式。我们首先把显卡模式设置好,然后把一些需要BIOS做的工作给做好。将下来就开始跳入保护模式了。关掉CPU级别中断,往ox60号端口写入0xdf,可以让CPU使用超过1M的内存容量。然后设置CR0寄存器,把CR0寄存器读出来,然后把最高位和最低位设置为0,再放入CR0,CPU就跳入保护模式了,进入保护模式后马上要执行JMP指令,才能使接下来的指令正常执行。接下来只要把特定的程序复制到内存中就行了。也就是IDT,GDT,主程序,栈及其他这个所放的位置在内存中放好就行了。
保护模式和实模式的的区别就在于计算内存地址时,是使用段寄存器的值直接指定地址值的一部份呢,还是通过GDT 使用段寄存器的值指定并非实际存在的地址号码。
第九天
鼠标处理告一段落,这一天主要做内存管理,内存管理的第一步是检测计算机内存容量有多大。
先检测CPU有没有缓存,如果CPU是486以上就有缓存,以下就没有缓存也就不需要关闭缓存。检测方法就是看往CPU标志寄存器第18位写入是否有效。然后对CR0寄存器的某些位设置为0,才能关闭掉缓存。关掉缓存之后,开始检测内存容量,检测的方法也很简单,先把内存地址从小到大,每次读一个内存内容,然后把内存内容取否,写入内存,再读一次这个内存,如果新的内容跟预计取反的内容一样说明这个内存地址是有效的。为了提高效率我们没有一个字节一个字节检测,而是每次检测4KB,所以这4KB内存中最一4个字节是有效的,那就认为这4KB都是有效内存。内存大小知道了之后,就可以进行内存管理了。
内存管理的基础是内存分配和内存释放。内存是否使用可以在内存中做一个位图来表示,比如内存有16MB,我们以4KB为一个分配单位,那么一共需要4096位标记,也就是512字节。在这512字节中,如第1位是0,就表示0~0xfff这个内存地址未使用。如果以这种方式管理内存,从代码上来讲比较简单。但是也有不足,那就是分配最小内存地址不灵活,上面的例子中以4KB为一个单位分配,如果只需要1B,那么这样分配就太浪费了。如果位图以1字节为单位的话,那么位图就需要16777216个标记位,也就是2MB个标记位,太浪费了。
还有一种就是这本书使用的内存管理方式,类似于从XXX号地址开始的YYY字节的空间是空着的。可以创建这样一种数据结构
struct FREEINFO
{
unsigned int addr, size;
}
struct MEMMAN
{
int frees;
struct FREEINFO free[1000];
}
struct MEMMAN memman;
memman.frees = 1;
memman.free[0].addr = 0x400000;
memman.free[0].size = 0x07c00000;
如果需要100KB的空间,查看memman中free的情况,找出100MB以上的可用空间即可
for(int i = 0; i < memman.frees; i++)
{
if(memman.free[i].size >= 100 * 1024)
{
"找到可用空间';
"从地址memman.free[i].addr开始的100KB空间;
}
}
用这种方法可以实现内存的管理。