上一章的GDT/IDT内容结束的没头没尾的,这一章会接着介绍。
这一章做了这么两件事情,一是将目前略显冗余的源文件bootpack.c整理了一下,分成了几个具有不同功能的源文件。二是继续上一章节的内容,在初始化中断向量表之后,设置新的中断关联鼠标和键盘。
我觉得这一章的内容虽然短小,却还是挺重要的。
一 分割编译
目前的源文件内容已经比较多了,包含了GDT/IDT的配置初始化,调色板的初始化,屏幕(尺寸等)的初始化,还有绘制光标、显示字符等应用部分。
作者将bootpack.c分成了三个部分:
这边的分割不难,只是对应的Makefile也要对应进行修改,原先是直接编译bootpack.c的内容,现在要编译三个源文件的内容并整合成一个:
在这个过程中,使用通配符%来使得Makefile文件精简化:
%.gas : %.c Makefile
$(CC1) -o $*.gas $*.c
%.nas : %.gas Makefile
$(GAS2NASK) $*.gas $*.nas
%.obj : %.nas Makefile
$(NASK) $*.nas $*.obj $*.lst
二 中断处理
中断处理包含两个部分,一是初始化PIC、二是中断处理程序的制作。下面分别介绍:
【1】初始化PIC(programmable interrupt controller,可编程中断控制器)
在设计上,CPU只能单独处理一个中断,而PIC是将8个中断信号1集合成一个中断信号的装置。PIC监视着输入管脚 的8个中断信号,只要有一个中断信号进来,就将唯一的输出管脚信号变成ON,并通知给CPU:
上面的IRQ即中断请求,一个CPU有16个IRQ,一般都预分配给以下几个硬件使用:
中断请求 | 硬件设备 |
IRQ0 | 系统计时器 |
IRQ1 | 键盘 |
IRQ2 | 可设置中断控制卡 |
IRQ3 | COM2(串行接口2) |
IRQ4 | COM1(串行接口1) |
IRQ5 | 空 |
IRQ6 | 磁盘机 |
IRQ7 | 并行接口 |
IRQ8 | CMOS/时钟 |
IRQ9 | 空 |
IRQ10 | 空 |
IRQ11 | 空 |
IRQ12 | PS/2鼠标 |
IRQ13 | 算术处理器(Arithmetic Processor) |
IRQ14 | Primary(主)IDE控制器 |
IRQ15 | Secondary(从)IDE控制器 |
接着便有了int.c中的PIC初始化:
#include "bootpack.h"
void init_pic(){
/* 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由INT28-2f接收 */
io_out8(PIC1_ICW3, 2 ); /* PIC1由IRQ2连接 */
io_out8(PIC1_ICW4, 0x01 ); /* 无缓冲区模式 */
io_out8(PIC0_IMR, 0xfb ); /* 11111011 PIC1以外全部禁止 */
io_out8(PIC1_IMR, 0xff ); /* 11111111 禁止所有中断 */
return;
}
关于上述的初始化归纳以下几点:
(1)从CPU的角度来看,PIC是外部设备,CPU使 用OUT指令进行操作;
(2)上面的PIC0和PIC1分别指的是主、从PIC;
(3)我们看到这边主要是对两类寄存器进行操作IMR和ICW。
IMR(interrupt mask register)为“中断屏蔽寄存器”,如果某一位的值是1,则该位所对应的IRQ信号被屏蔽,PIC就忽视该路信号,这主要是因为,正在对中断设定进行更改时,如果再接受别的中断会引起混乱,为了防止这种情况的发生,就必须屏蔽中断,当然还有诸如物理因素之类的干扰。
ICW(initial control word)为“初始化控制数据”,共有编号为1-4的四个字节数据。描述如下:
寄存器名 | 描述 |
ICW1 | 与PIC主板配线方式中断信号的电气特性等有关 |
ICW2 | 决定IPR以哪一号中断通知CPU |
ICW3 | ICW3有关主从连接的设定 |
ICW4 | 与PIC主板配线方式中断信号的电气特性等有关 |
对ICW2的设置最为重要,这边单独提出来备忘:
io_out8(PIC0_ICW2, 0x20 ); /* IRQ0-7由INT20-27接收 */
io_out8(PIC1_ICW2, 0x28 ); /* IRQ8-15由INT28-2f接收 */
【2】中断处理程序的制作
PIC初始化之后,下一步就是制作中断处理程序。中断处理程序包含基础的汇编部分、实际应用层的中断服务程序部分以及最终写入注册表部分,这边是这一章的重点,分为下面几个步骤实现:
(1)编写中断服务程序:
根据上面的表格,CPU预分配给鼠标、键盘的中断请求分别时IRQ12和IRQ1,所以编写了中断处理程序(即发生中断时所要调用的程序,这边以键盘中断为例):
/* 键盘中断请求为IRQ1,且上文有IRQ0-7由INT20-27接收,所以这边是inthandler21 */
void inthandler21(int *esp)
/* 来自键盘的中断 */
{
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 (IRQ1) : PS/2 keyboard");
for (;;) {
io_hlt();
}
}
该程序实现的是如果接收到键盘中断,那么就在屏幕上返回打印信息,并使程序保持待机。
(2)汇编实现中断服务程序的返回:
首先,把程序贴出来,在注释中一一说明:
_asm_inthandler21:
PUSH ES ;ES寄存器入栈
PUSH DS ;DS寄存器入栈
PUSHAD ;基础寄存器入栈(包含EAX/ECX/EDX/EBX/ESP/EBP/ESI/EDI)
MOV EAX,ESP
PUSH EAX ;EAX寄存器入栈
MOV AX,SS
MOV DS,AX
MOV ES,AX ;DS和ES调整到与SS相等
CALL _inthandler21 ;调用中断服务程序_inthandler21
POP EAX ;EAX寄存器出栈
POPAD ;基础寄存器出栈(包含EAX/ECX/EDX/EBX/ESP/EBP/ESI/EDI)
POP DS ;DS寄存器出栈
POP ES ;ES寄存器出栈
IRETD ;中断服务程序返回
其中,注意两点:
第一点是,各个寄存器的出栈入栈的操作本质上就是在执行中断服务程序前后保存现场,使其原来的值不被破坏;
第二点是,中断服务程序的返回必须使用IRETD,这是INT的逆过程,而不能用子程序的返回指令RET;
所以上面的函数也可以总结归纳为以下几个步骤:
- 将寄存器的值保存到栈里;
- 然后将DS和ES调整到 与SS相等;
- 再调用_inthandler21;
- 返回以后,将所有寄存器的值再返回到原来的值,然后执行
IRETD;
(3)将编写好的中断处理程序(包含中断服务程序和汇编处理两部分)注册到IDT中断记录表中
GDT/IDT的初始化处理中加入键盘(IRQ1)、鼠标(IRQ12)、并行接口(IRQ7)三个中断处理程序的注册:
void init_gdtidt(void){
struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR*) 0x00270000;
struct GATE_DESCRIPTOR *idt = (struct GATE_DESCRIPTOR*) 0x0026f8000;
int i;
// GDT 初始化
for (i = 0; i < 8192; i++) {
set_segmdesc(gdt + i, 0, 0, 0);
}
// GDT 注册(注册了1段和2段)
set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, 0x4092);
set_segmdesc(gdt + 2, 0x0007ffff, 0x00280000, 0x409a);
load_dgtr(0xffff, 0x00270000);
// IDT 初始化
for (i = 0; i < 256; i++) {
set_gatedesc(idt + i, 0, 0, 0);
}
load_idtr(0x07ff, 0x0026f800);
// IDT 注册(注册了IRQ01、IRQ07、IRQ12三个中断处理程序)
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);
return;
}
其中,set_gatedesc的几个参数分别代表如下含义:
(i)idt+0x21,在IDT的基址0x26f800上偏移0x21,即IRQ1的位置;
(ii)将中断处理程序asm_inthandler21注册在idt中的第0x21位置上;
(iii)2 * 8表示的是asm_inthandler21属于哪一个段,即段号是2。乘以8是因为低3位有着别的意思,这里低3位必须是0;
(iv)AR_INTGATE32为0x008e,这边是将IDT的属性,设定为0x008e,它表示这是用于中断处理的有效设定;
至此,经由中断服务程序制作---->中断处理程序制作(汇编调用中断服务程序并返回)---->中断处理程序写入注册表IDT,便成功将硬件鼠标键盘与CPU的中断关联起来。
三 效果展示
实际运行效果如下:
按下键盘“A”键:
操作鼠标还是没有反应,具体原因将在后面进行探究。