《30天自制操作系统》第6天

第6天 分割编译与中断处理

1.分割源文件

由于bootpack.c代码行数太多,不便于观看也不便于修改,于是根数函数的功能将它分割为几个文件,下图为示意图。

还需要修改一下Makefile,流程如下:

由于只是简单把代码转移一下位置,就不展示完成后的界面了。


2.整理Makefile

现在Makefile又太长了,对他进行修改。

%.gas: %.c
	$(CC1) -o $*.gas $*.c

%.nas: %.gas
	$(GAS2NASK) $*.gas $*.nas

%是Makefile中的通配符,可以将大量同类型的文件,只用一条规则就完成构建。而且普通生成规则比这种规则优先级更高,所以即使产生冲突也没有问题。$*就是%的内容,你可能会问为什么不都使用%?因为下面是不能使用%的。

因为主程序还是没改变,这次也不放界面出来。


3.整理头文件

学过C语言的人应该都知道头文件,把编写的函数放在头文件中,方便其他文件进行调用,也可以使得在模块化编程中减少重复的代码。这次新建一个头文件,命名为bootpack.h,把要用到的函数都放到这里面来。

/* asmhead.nas */
struct BOOTINFO {
    char cyls;  /* 启动区读硬盘督导何处为止 */
    char leds;  /* 启动时键盘LED的状态 */
    char vmode; /* 显卡模式为多少位彩色 */
    char reserve;
    short scrnx, scrny; /* 画面分辨率 */
    char *vram;
};

#define ADR_BOOTINFO    0x00000ff0

/* naskfunc.nas */
void io_hlt(void);
void io_cli(void);
void io_out8(int port, int data);
int io_load_eflags(void);
void io_store_eflags(int eflags);
void load_gdtr(int limit, int addr);
void load_idtr(int limit, int addr);

/* graphic.c */
void init_palette(void);
void set_palette(int start, int end, unsigned char *rgb);
void boxfill8(unsigned char *vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1);
void init_screen(char *vram, int x, int y);
    (以下略)

在C文件中添加bootpack.h的引用,又可以减少不少重复代码。

由于主程序流程依然没有改变,本节也无可展示内容。


4.初始化PIC

IDT和GDT的内容都在第五章中解释了,如果遗忘了可以再去看看。

但就算我们完成了IDT和GDT的初始化还是不能用中断,因为还没有初始化PIC。

PIC的意思是可编程中断控制器(Programmable Interrupt Controller),每个PIC可以处理8个中断信号,但8个中断信号不够用,便采取了PIC级联的方式增加处理中断信号的数目,可处理的中断信号数从8个变成了15个,其连接图如下所示:

8259A芯片就是PIC芯片(我的收藏的博客中有两篇介绍8259A的博客,本文的内容大多也来源于此),因为从片占据了主片的IR2,所以一共只能处理15个中断信号。

接下来看看代码吧。

void init_pic(void) /* PIC初始化 */
{
    io_out8(PIC0_IMR, 0xff);    /* 禁止所有中断 */
    io_out8(PIC1_IMR, 0xff);    /* 禁止所有中断 */

    io_out8(PIC0_ICW1, 0x11);   /* 边沿触发模式(edge trigger mode) */
    io_out8(PIC0_ICW2, 0x20);   /* IRQ0~7有INT20~27接收 */
    io_out8(PIC0_ICW3, 1 << 2); /* PIC1有IRQ2连接 */
    io_out8(PIC0_ICW4, 0x01);   /* 无缓冲区模式 */

    io_out8(PIC1_ICW1, 0x11);   /* 边沿触发模式(edge trigger mode) */
    io_out8(PIC1_ICW2, 0x28);   /* IRQ8~15由IRQ2连接 */
    io_out8(PIC1_ICW3, 0x2);    /* PIC1有IRQ2连接 */
    io_out8(PIC1_ICW4, 0x01);   /* 无缓冲区模式 */

    io_out8(PIC0_IMR, 0xfb);    /* 11111011 PIC1以外全部禁止 */
    io_out8(PIC1_IMR, 0xff);    /* 11111111 禁止所有中断 */
}

在bootpack.h中定义了这些端口:

#define PIC0_ICW1   0x0020
#define PIC0_OCW2   0x0020
#define PIC0_IMR    0x0021
#define PIC0_ICW2   0x0021
#define PIC0_ICW3   0x0021
#define PIC0_ICW4   0x0021
#define PIC1_ICW1   0x00a0
#define PIC1_OCW2   0x00a0
#define PIC1_IMR    0x00a1
#define PIC1_ICW2   0x00a1
#define PIC1_ICW3   0x00a1
#define PIC1_ICW4   0x00a1

很奇怪,为什么有不少端口号是一样的啊?这样分得清要对哪个进行操作吗?这些问题之后会做解释。其中PIC0开头的是8259A主片的端口,PIC1开头的是8259A从片的端口。

8259A芯片中有4个ICW寄存器(Initial Control Word,初始化控制数据),对ICW寄存器的操作要连续进行,即按照ICW1、ICW2、ICW3、ICW4的顺序进行,所以,即使ICW2、ICW3、ICW4的地址一样,8259A芯片也能分得出是对哪个寄存器进行操作。下面对ICW寄存器做主要的介绍。

ICW1

当发送的字节第5比特位(D4)=1,端口地址为0x0020或0x00a0时,表示对ICW1进行操作。

名称含义
D7A7这几位对8086/88处理器无用。
D6A6
D5A5
D41恒为1。
D3LTIM

0 - 边沿触发方式;

1 - 电平触发中断方式。

D2ADI对8086/88系统无用。
D1SNGL

0 - 多片8259A芯片;

1 - 单片8259A芯片。

D0IC4

0 - 不需要ICW4;

1 - 需要ICW4。

在linux0.11内核中,ICW1被设置为0x11,表示中断请求是边沿触发,多片8259A芯片级联且需要发送ICW4。

ICW2

用于设置芯片送出的终端号的高5位,在设置了ICW1之后,当地址为0x0021或0x00a1时,表示对ICW2进行设置。

名称含义
D7A15/T7在使用8086/88处理器的系统或兼容系统中,T7~T3是中断号的高5位,与8259A芯片自动设置的低3位(8259A按IR0~IR7三维编码值自动填入)组成一个8位中断号。8259A在收到第2个中断响应脉冲时,把此中断号送到数据线上,以供CPU读取。
D6A14/T6
D5A13/T5
D4A12/T4
D3A11/T3
D2A10一般都设置为0
D1A9
D0A8

Linux-0.11系统把主片的ICW2设置为0x20,表示主片中断请求0~7对应的中断号是0x20~0x27,把从片的ICW2设置成0x28,表示从片中断请求8~15级对应的中断号是0x28~0x2f。另外0x00~0x1f的中断号要么是系统异常中断,要么就是厂家预留给以后使用的,所以不能用。

ICW3

D7D6D5D4D3D2D1D0
主片S7S6S5S4D3D2D1D0
从片00000ID2ID1ID0

对于主片,Si=1表示IRi与从片级联,则该中断请求引脚的信号来自从片。

对于从片,ID2~ID0三个比特位对应个从片的标识号,即连接到主片的中断级。当某个从片接收到级联线(CAS2~CAS0)输入的值与自己的ID2~ID0相等时,从片应该向数据总线发送自己当前被选中的中断请求的中断号。

Linux-0.11把主片的ICW3设置为0x04,即S2=1,其余各位为0,表示主片的IR2连接一个从片,从片的ICW3被设置为0x02,即其标识号为2,表示此从片连接到主片的IR2引脚。因此,中断优先级的排列次序为:0级最高,1级次之,接下来是8~15级,最后是主片的3~7级。

ICW4

当ICW1的D0置为1时,表示需要ICW4。

名称含义
D70恒为0
D60
D50
D4SFNM

0 - 普通全嵌套方式

1 - 特殊全嵌套方式

D3BUF

0 - 非缓冲方式

1 - 缓冲方式

D2M/S

0 - 缓冲方式下从片

1 - 缓冲方式下主片

D1AEOI

0 - 非自动结束中断方式

1 - 自动结束中断方式

D0MPM

0 - MCS80/85系统

1 - 8086/88处理器系统

Linux-0.11中主片和从片的ICW4都被设置为0x01.表示设置为普通全嵌套方式、非缓冲方式、非自动结束中断方式,并用于8086及其兼容机。

ICW的内容就介绍结束了,但代码还有PIC0_IMR的定义,这是什么?在8259A芯片中,除了ICW寄存器,还有OCW (操作命令字,Operation Command Words)寄存器,被用来设置和管理8259A的工作方式。8259A中一共有3个OCW寄存器,代码中的PIC0_IMR就是OCW1,下面是关于OCW1的介绍。

OCW1

地址为0x21或0xA1,用于对8259A中IMR(中断屏蔽寄存器)进行读写操作。

D7D6D5D4D3D2D1D0
名称M7M6M5M4M3M2M1M0

若Mi=1,则屏蔽对应的中断请求级IRi;若Mi=0,则允许IRi。另外,屏蔽高优先级中断并不会影响低优先级中断的请求。

由于本次没有使用OCW2和OCW3,在这里就不做介绍了。

代码讲解完毕,开始运行程序吧。

虽然界面还是没有改变,但这是为了接下来使用鼠标和键盘做铺垫。


5.中断处理程序的制作

void inthandler21(int *esp)     /* 来自PS/2键盘的中断 */
{
    struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
    boxfill8(binfo->vram, binfo->scrnx, COL8_000000, 0, 0, 32 * 8 - 1, 15);
    putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, "INT 21 (IRQ-1) : PS/2 keyboard");
    while (1)
    {
        io_hlt();
    }    
}

这是键盘的中断服务函数,目前就只是让它显示一段文字就可以了,更复杂的操作等到之后再添加。

_asm_inthandler21:
    PUSH    ES
    PUSH    DS
    PUSHAD
    MOV EAX, ESP
    PUSH    EAX
    MOV AX, SS       ; C语言认为SS、ES、DS是一个段
    MOV DS, AX       ; 在执行C语言之前把它们设置成一样的
    MOV ES, AX       ; 不然函数inthandler21就不能顺利执行
    CALL    _inthandler21
    POP EAX
    POPAD
    POP DS
    POP ES
    IRETD

这是中断保存现场的操作,另一部分操作由硬件自动完成就不用我们操心了(这是机组的知识,相信大家都知道的吧)。

PUSHAD指令:把通用寄存器压入栈,当操作数大小是32位时,入栈的顺序是:EAX、ECX、EDX、EBX、ESP、EBP、ESI、EDI.

POPAD指令:把栈中的值弹出到通用寄存器中,当操作室大小是32位时,出栈顺序是:EDI、ESI、EBP、ESP、EBX、EDX、ECX、EAX。

栈属于数据结构的知识,书上也讲得很详细,我就不赘述了(其实就是我不想讲)。

中断服务函数写好了,但系统怎么调用它呢?系统怎么知道函数的位置呢?还记得我们写的IDT的设定吗?我们只要将程序入口地址传入其中,系统就会在中断时根据这个地址执行中断服务程序,如下是代码部分:

    /* IDT的设定 */
    set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32);
	set_gatedesc(idt + 0x27, (int) asm_inthandler27, 2 * 8, AR_INTGATE32);
	set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);

2 * 8表示使用第2段,乘8是因为低3位不能用(应该是因为段寄存器低3位不能用的缘故)。AR_INTGATE32为0x8e,我之前博客介绍过IDT结构,这个意思是此段存在于内容,特权级为0(最高),是中断描述符。

来运行看看效果吧。

点击键盘就会显示字符串,但鼠标则是没什么反应,主要是因为鼠标的设置还没有完成,那是下一天的事情了。


今天的内容还是比较多的,主要是寄存器的介绍,不过对应着数据慢慢看还是不难,要等到学到内存管理,用到一些算法上的知识的时候,才会让人有些理不清逻辑。今天的内容就到此为止,期待你收看下一天的博客(虽然还没有写)( ̄▽ ̄)~*。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值