中断上半部和下半部是什么?
操作系统是中断驱动的,中断发生后会执行相应的中断处理程序,我们希望 CPU 中断响应的时间越短越好,这样便能响应更多设备的中断。但是中断处理程序还是需要完整执行的,不能光为了提高中断响应效率而只执行部分中断处理程序 。 于是,把中断处理程序分为上半部和下半部两部分,把中断处理程序中需要立即执行的部分(分分钟不能耽误的部分)划分到上半部,这部分是要限时执行的,所以通常情况下只完成中断应答或硬件复位等重要紧迫的工作。而中断处理程序中那些不紧急的部分则被推迟到下半部中去完成。由于中断处理程序的上半部是刻不容缓要执行的,所以上半部是在关中断不被打扰的情况下执行的。当上半部执行完成后就把中断打开了,下半部也属于中断处理程序,所以中断处理程序下半部则是在开中断的情况下执行的,如果有新的中断发生,原来这个旧中断的下半部就会被换下 CPU,先执行新的中断处理程序的上半部,等待线程调度机制为旧中断处理程序择一 日期(就是指调度算法认为的某个恰当时机)后,再调度其上 CPU 完成其下半部的执行。
对于中断是否无视 eflags 中的 IF 位,可以这么理解:
- 首先,只要是导致运行错误的中断类型都会无视 IF 位,不受 IF 位的管束,如 NMI 、异常。
- 其次,由于 int n 型的软中断用于实现系统调用功能,不能因为 IF 位为 0 就不顾用户请求,所以
为了用户功能正常,软中断必须也无视 IF 位。
中断描述符表中不仅仅有中断描述符,还可以有任务门描述符和陆阱门描述符 。 由于表中所有描述符都是记录一段程序的起始地址,相当于通向某段程序的“大门”,所以,中断描述符表中的描述符有自己的名称一一门 。
寄存器入栈情况及顺序,这里不再讨论有关特权检查的内容。
- 处理器根据中断向量号找到对应的中断描述符后,拿 CPL 和中断门描述符中选择子对应的目标代码段的 DPL 比对,若 CPL 权限比 DPL 低,即数值上 CPL > DPL,这表示要向高特权级转移,需要切换到高特权级的拢。这也意味着当执行完中断处理程序后,若要正确返回到当前被中断的进程,同样需要将 栈恢复为此时的旧拢。于是处理器先临时保存当前旧栈 SS 和 ESP 的值,记作 SS_old 和 ESP_old,然后在TSS 中找到同目标代码段 DPL 级别相同的栈加载到寄存器 SS 和 ESP 中,记作 SS_new 和 ESP_new ,再将之前临时保存的 SS old 和 ESP old 压入新栈备份,以备返回时重新加载到栈段寄存器 SS 和栈指针 ESP 。由于 SS_old 是 16 位数据, 32 位模式下的栈操作数是 32 位,所以将 SS_old 用 0 扩展其高 16 位,成为 32位数据后入栈 。 此时新栈内容如图 7-8 中 A 所示。
- 在新栈中压入 EFLAGS 寄存器,新栈内容如图 7-8 中 B 所示。
- 由于要切换到目标代码段,对于这种段间转移,要将 CS 和 EIP 保存到 当前栈中备份,记作 CS_old 和 EIP_old,以便中断程序执行结束后能恢复到被中断的进程。同样 CS_old 是 16 位数据, 需要用 0 填充其高 16 位,扩展为 32 位数据后入栈。 此时新栈内容如图 7-8 中 C 所示。当前栈是新栈,还是旧栈,取决于第 1 步中是否涉及到特权级转移。
- 某些异常会有错误码,此错误码用于报告异常是在哪个段上发生的,也就是异常发生的位置,所以错误码中包含选择子等信息, 一会介绍。错误码会紧跟在 EIP 之后入栈,记作 ERROR_CODE。此时新栈内容如图 7-8 中 D 所示。
为了安全起见,处理器在返回到被中断过程中也要再进行一次特权级检查,下面咱们聊聊这个从中断处理程序返回的过程,假设此时已经手动将 ERROR_CODE 从栈中弹出,栈顶己位于正确的位置,即指向 EIP_old
- 当处理器执行到 iret 指令时,它知道要执行远返回,首先需要从栈中返回被中断进程的代码段选
择子 CS_old 及指令指针 EIP_old。这时候它要进行特权级检查。先检查栈中 CS 选择子 CS_old,根据其 RPL 位,即未来的 CPL,判断在返回过程中是否要改变特权级。 - 栈中 cs 选择子是 CS_old ,根据 CS_old 对应的代码段的 DPL 及 CS_old 中的 RPL 做特权级检查,规则不再赘述。如果检查通过,随即需要更新寄存器 cs 和 EIP。由于 CS_old 在入栈时已经将高 16 位扩充为 0,现在是 32 位数据,段寄存器 CS 是 16 位,故处理器丢弃 CS_old 高 16 位,将低 16 位加载到 CS。将 EIP_old 加载到 EIP 寄存器,之后栈指针指向 EFLAGS 。如果进入中断时未涉及特权级转移,此时栈指针是 ESP_old (说明在之前进入中断后,是继续使用旧栈)。否则栈指针是 ESP_new (说明在之前进入中断后用的是 TSS 中记录的新栈)。
- 将栈中保存的 EFLAGS 弹出到标志寄存器 EFLAGS 。如果在第 1 步中判断返回后要改变特权级,此时栈指针是 ESP_new,它指向栈中的 ESP_old。否则进入中断时属于平级转移,用的是旧栈,此时栈指针是 ESP_old,栈中己无因此次中断发生而入栈的数据,栈指针指向中断发生前的栈顶。
- 如果在第 1 步中判断出返回时需要改变特权级,也就是说需要恢复旧栈,此时便需要将 ESP_old 和 SS_old 分别加载到寄存器 ESP 及 SS,丢弃寄存器 SS 和 ESP 中原有的 SS_new 和 ESP_new,同时进行特权级检查。补充,由于 SS_old 在入栈时己经由处理器将高 16 位填充为 0,现在是 32 位数据,所以在重新加载到栈段寄存器 SS 之前,需要将 SS_old 高 16 位剥离丢弃,只用其低 16 位加载 SS 。
错误码本质上就是个描述符选择子,通过低 3 位属性来修饰此选择子指向是哪个表中的哪个描述符。
EXT 表示 EXTernal event,即外部事件,用来指明中断源是否来自处理器外部,如果中断源是不可屏蔽中断 NMI 或外部设备, EXT 为 1 ,否则为 0 。
IDT 表示选择子是否指向中断描述符表 IDT, IDT 位为 1 ,则表示此选择子指向中断描述符表,否则指向全局描述符表 GDT 或局部描述符表 LDT 。
TI 和选择子中 TI 是一个意思,为 0 时用来指明选择子是从 GDT 中检索描述符,为 1 时是从 LDT 中检索描述符。当然,只有在 IDT 位为 0 时 TI 位才有意义。
选择子高 13 位索引就是选择子中用来在表中索引描述符用的下标。
中断返回时, iret 指令并不会把错误码从栈中弹出,所以在中断处理程序中需要手动用栈指针跨过错误码或将其弹出。否则栈顶处若不是 EIP (EIP_old)的话, iret 返回时将会载入错误的值到后续寄存器。
通常能够压入错误码的中断属于中断向量号在 0~32 之内的异常,而外部中断(中断向量号在 32~255 之间)和 int 软中断并不会产生错误码。通常我们并不用处理错误码。
中断描述符表本质上就是中断处理程序地址数组,而中断向量号便是此数组的索引下标,这就是中断向量号是个整数的原因。 CPU 不支持“数组名[索引]”的形式,那是高级语言编译器支持的东西,它最终也要编译转换成某种内存寻址方式之一,必须得用最基本的形式一一地址来访问内存。当 CPU 接收到 8259A 送来的中断向量号后要将其乘以 8,再加上中断描述符表的起始地址,经过内存寻址,最终定位到目标中断处理程序。
汇编宏定义——预编译
;单行宏定义
%define ERROR_CODE nop
;多行宏定义
%macro mul_add 3 ; 宏名字 参数个数
mov eax, %l
add eax, %2
add eax, %3
%endmacro
;调用:
mul_add 45, 24, 33 ;其中%1是45, %2是24, %3是33
汇编版本的中断处理程序
定义了 33 个中断处理程序。每个中断处理程序都一样,就是调用字符串打印函数 put_str 来打印宇符串“ interrupt occur!”,之后直接退出中断。
[bits 32]
%define ERROR_CODE nop ; 若在相关的异常中cpu已经自动压入了错误码,为保持栈中格式统一,这里不做操作.
%define ZERO push 0 ; 若在相关的异常中cpu没有压入错误码,为了统一栈中格式,就手工压入一个0
extern put_str ;声明外部函数
section .data
intr_str db "interrupt occur!", 0xa, 0
global intr_entry_table
intr_entry_table:
%macro VECTOR 2
section .text
intr%1entry: ; 每个中断处理程序都要压入中断向量号,所以一个中断类型一个中断处理程序,自己知道自己的中断向量号是多少
%2
push intr_str
call put_str
add esp,4 ; 跳过参数
; 如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI
mov al,0x20 ; 中断结束命令EOI
out 0xa0,al ; 向从片发送
out 0x20,al ; 向主片发送
add esp,4 ; 跨过error_code
iret ; 从中断返回,32位下等同指令iretd
section .data
dd intr%1entry ; 存储各个中断入口程序的地址,形成intr_entry_table数组
%endmacro
VECTOR 0x00,ZERO
VECTOR 0x01,ZERO
VECTOR 0x02,ZERO
VECTOR 0x03,ZERO
VECTOR 0x04,ZERO
VECTOR 0x05,ZERO
VECTOR 0x06,ZERO
VECTOR 0x07,ZERO
VECTOR 0x08,ERROR_CODE
VECTOR 0x09,ZERO
VECTOR 0x0a,ERROR_CODE
VECTOR 0x0b,ERROR_CODE
VECTOR 0x0c,ZERO
VECTOR 0x0d,ERROR_CODE
VECTOR 0x0e,ERROR_CODE
VECTOR 0x0f,ZERO
VECTOR 0x10,ZERO
VECTOR 0x11,ERROR_CODE
VECTOR 0x12,ZERO
VECTOR 0x13,ZERO
VECTOR 0x14,ZERO
VECTOR 0x15,ZERO
VECTOR 0x16,ZERO
VECTOR 0x17,ZERO
VECTOR 0x18,ERROR_CODE
VECTOR 0x19,ZERO
VECTOR 0x1a,ERROR_CODE
VECTOR 0x1b,ERROR_CODE
VECTOR 0x1c,ZERO
VECTOR 0x1d,ERROR_CODE
VECTOR 0x1e,ERROR_CODE
VECTOR 0x1f,ZERO
VECTOR 0x20,ZERO
编译器会将属性相同的 section 合并到同一个大的 segment 中,而且,为了显得更靠谱一点,我们在 kemel.S 中对所有的数据 section 都用了同一个名字.data, 编译之后,所有中断处理程序的地址都会乖乖地作为数组 intr_entry_table 的元
素紧凑地排在一起。
创建IDT,安装中断处理程序
/*中断门描述符结构体*/
struct gate_desc {
uint16_t func_offset_low_word; //中断处理代码在段内偏移量的低位
uint16_t selector; //中断处理代码段选择子
uint8_t dcount; //此项为双字计数字段,是门描述符中的第4字节。此项固定值,不用考虑
uint8_t attribute; //属性
uint16_t func_offset_high_word; //中断处理代码在段内偏移量的高位
};
// 静态函数声明,非必须
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function);
static struct gate_desc idt[IDT_DESC_CNT]; // idt是中断描述符表,本质上就是个中断门描述符数组
typedef void* intr_handler;
extern intr_handler intr_entry_table[IDT_DESC_CNT]; // 声明引用定义在kernel.S中的中断处理函数入口数组
/* 创建中断门描述符 */
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) {
p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000FFFF;
p_gdesc->selector = SELECTOR_K_CODE;
p_gdesc->dcount = 0;
p_gdesc->attribute = attr;
p_gdesc->func_offset_high_word = ((uint32_t)function & 0xFFFF0000) >> 16;
}
/*初始化中断描述符表*/
static void idt_desc_init(void) {
int i;
for (i = 0; i < IDT_DESC_CNT; i++) {
make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]);
}
put_str("idt_desc_init done\n");
}
用内联汇编实现端口I/O函数
#ifndef __LIB_IO_H
#define __LIB_IO_H
#include "stdint.h"
/* 向端口port写入一个字节*/
static inline void outb(uint16_t port, uint8_t data) {
/*********************************************************
a表示用寄存器al或ax或eax,对端口指定N表示0~255, d表示用dx存储端口号,
%b0表示对应al,%w1表示对应dx */
asm volatile ( "outb %b0, %w1" : : "a" (data), "Nd" (port));
/******************************************************/
}
/* 将addr处起始的word_cnt个字写入端口port */
static inline void outsw(uint16_t port, const void* addr, uint32_t word_cnt) {
/*********************************************************
+表示此限制即做输入又做输出.
outsw是把ds:esi处的16位的内容写入port端口, 我们在设置段描述符时,
已经将ds,es,ss段的选择子都设置为相同的值了,此时不用担心数据错乱。*/
asm volatile ("cld; rep outsw" : "+S" (addr), "+c" (word_cnt) : "d" (port));
/******************************************************/
}
/* 将从端口port读入的一个字节返回 */
static inline uint8_t inb(uint16_t port) {
uint8_t data;
asm volatile ("inb %w1, %b0" : "=a" (data) : "Nd" (port));
return data;
}
/* 将从端口port读入的word_cnt个字写入addr */
static inline void insw(uint16_t port, void* addr, uint32_t word_cnt) {
/******************************************************
insw是将从端口port处读入的16位内容写入es:edi指向的内存,
我们在设置段描述符时, 已经将ds,es,ss段的选择子都设置为相同的值了,
此时不用担心数据错乱。*/
asm volatile ("cld; rep insw" : "+D" (addr), "+c" (word_cnt) : "d" (port) : "memory");
/******************************************************/
}
#endif
它们都是对底层硬件端口直接操作的,通常由设备的驱动程序来调用,不用说,为了快速响应,函数调用上需要更加高效。而且,操作系统是为了让用户程序编写、执行更加方便才诞生的,归根结底是为了用户程序服务,所以它会让处理器的大多数时间花在 3 特权级的用户程序上。为了让处理器更多地为用户程序服务,操作系统(包括硬件驱动程序)必须减少自己占用处理器的时间,所以,对硬件端口的操作,只要求一个字一一快。
但一般的函数调用需要涉及到现场保护及恢复现场,即函数调用前要把相关的栈、返回地址( CS 和EIP )保存到栈中,函数执行完返回后再将它们从栈中恢复到寄存器。栈毕竟是内存,速度低很多,而且入栈、出栈这么多内存操作,对于想方设法提速的内核程序来说是无法忍受的。
因此,为了提速,在我们的实现中,函数的存储类型都是 static,并且加了 inline 关键字,它建议处理器将函数编译为内嵌的方式。内嵌函数大家都清楚吧,就是将所调用的函数体的内容,在该函数的调用处,原封不动地展开,这样编译后的代码中将不包含 call 指令,也就不属于函数调用了,而是顺次执行。虽然这会让程序大一些,但减少了函数调用及返回时的现场保护及恢复工作,提升了效率还是值得的 。
设置8259A
#define PIC_M_CTRL 0x20 // 这里用的可编程中断控制器是8259A,主片的控制端口是0x20
#define PIC_M_DATA 0x21 // 主片的数据端口是0x21
#define PIC_S_CTRL 0xa0 // 从片的控制端口是0xa0
#define PIC_S_DATA 0xa1 // 从片的数据端口是0xa1
#define IDT_DESC_CNT 0x21 // 目前总共支持的中断数
/* 初始化可编程中断控制器8259A */
static void pic_init(void) {
/* 初始化主片 */
outb (PIC_M_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
outb (PIC_M_DATA, 0x20); // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
outb (PIC_M_DATA, 0x04); // ICW3: IR2接从片.
outb (PIC_M_DATA, 0x01); // ICW4: 8086模式, 正常EOI
/* 初始化从片 */
outb (PIC_S_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
outb (PIC_S_DATA, 0x28); // ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
outb (PIC_S_DATA, 0x02); // ICW3: 设置从片连接到主片的IR2引脚
outb (PIC_S_DATA, 0x01); // ICW4: 8086模式, 正常EOI
/* 打开主片上IR0,也就是目前只接受时钟产生的中断 */
outb (PIC_M_DATA, 0xfe);
outb (PIC_S_DATA, 0xff);
put_str(" pic_init done\n");
}
加载IDT,开启中断
/*完成有关中断的所有初始化工作*/
void idt_init() {
put_str("idt_init start\n");
idt_desc_init(); // 初始化中断描述符表
pic_init(); // 初始化8259A
/* 加载idt */
uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 16));
asm volatile("lidt %0" : : "m" (idt_operand));
put_str("idt_init done\n");
}
lidt 的操作数也要符合 IDTR 寄存器的结构,所以恼的操作数也必须是48 位,前 16 位是界限 h由,后 32 位是基址,只不过这 48 位的数据必须位于内存中
由于 C 语言中没有 48 位的数据类型,所以我们用 64 位的变量 idt_operand 来代替,这是没问题的,lidt 中会取出 48 位数据做操作数,所以咱们只要保证 64 位变量中的前 48 位数据是正确的就行。为了给 lidt 凑出 48 位的操作数,在第 76 行。
- 先用 sizeof(idt) - 1 得到 idt 的段界限 limit,这用作低 16 位的段界限。
- 接下来再将 idt 的地址挪到高 32 位即可,这可以通过把 idt 地址左移 16 位的形式实现。由于数组名便是地址,即指针,故先将其转换成整数才能参与后面的左移运算。考虑到 32 位地址经过左移操作后,高位将被丢弃,万一原地址高 16 位不是 0,这样会造成数据错误,故需要将 idt 地址转换成 64 位整型后再进行左移操作,这样其高 32 位都是 0,经过左移操作依然能够保证其精度。由于指针只能转换成相同大小的整型,故 32 位的指针不能直接转换成 64 位的整型,所以采取迂回的作法,先将其转换成 uint32_t,再将其转换成 uint64_t,之后再对这个 64 位的无符号整型数据进行左移 16 位操作。这样 idt 地址被移到了16~48 位,低 16 位自动填充为 0 。
- 之后再将以上两步的结果通过“按位或”运算符 ‘|’ 组合到一起后,存储到变量 idt_operand 中。虽然经过以上的三步得到的操作数是 64 位,但由于 lidt 的操作数是从内存地址处获得的,所以 lidt 依然只在该地址处(&idt_operand)取其中的 48 位数据当作操作数。
gcc -I lib/kernel/ -I lib/ -I kernel/ -m32 -c -fno-builtin -o kernel/main.o kernel/main.c
nasm -f elf -o lib/kernel/print.o lib/kernel/print.S
nasm -f elf -o kernel/kernel.o kernel/kernel.S
gcc -I lib/kernel/ -I lib/ -I kernel/ -m32 -c -fno-builtin -o kernel/interrupt.o kernel/interrupt.c
gcc -I lib/kernel/ -I lib/ -I kernel/ -m32 -c -fno-builtin -o kernel/init.o kernel/init.c
ld -Ttext 0xc0001500 -e main -m elf_i386 -s -o kernel.bin kernel/main.o kernel/init.o kernel/interrupt.o lib/kernel/print.o kernel/kernel.o
C语言实现通用中断函数
char* intr_name[IDT_DESC_CNT]; // 用于保存异常的名字
intr_handler idt_table[IDT_DESC_CNT]; // 定义中断处理程序数组.在kernel.S中定义的intrXXentry只是中断处理程序的入口,最终调用的是ide_table中的处理程序
/* 通用的中断处理函数,一般用在异常出现时的处理 */
static void general_intr_handler(uint8_t vec_nr) {
if (vec_nr == 0x27 || vec_nr == 0x2f) { // 0x2f是从片8259A上的最后一个irq引脚,保留
return; //IRQ7和IRQ15会产生伪中断(spurious interrupt),无须处理。
}
put_str("int vector: 0x");
put_int(vec_nr);
put_char('\n');
}
/* 完成一般中断处理函数注册及异常名称注册 */
static void exception_init(void) { // 完成一般中断处理函数注册及异常名称注册
int i;
for (i = 0; i < IDT_DESC_CNT; i++) {
/* idt_table数组中的函数是在进入中断后根据中断向量号调用的,
* 见kernel/kernel.S的call [idt_table + %1*4] */
idt_table[i] = general_intr_handler; // 默认为general_intr_handler。
// 以后会由register_handler来注册具体处理函数。
intr_name[i] = "unknown"; // 先统一赋值为unknown
}
//给各异常命名,方便排查错误
intr_name[0] = "#DE Divide Error";
intr_name[1] = "#DB Debug Exception";
intr_name[2] = "NMI Interrupt";
intr_name[3] = "#BP Breakpoint Exception";
intr_name[4] = "#OF Overflow Exception";
intr_name[5] = "#BR BOUND Range Exceeded Exception";
intr_name[6] = "#UD Invalid Opcode Exception";
intr_name[7] = "#NM Device Not Available Exception";
intr_name[8] = "#DF Double Fault Exception";
intr_name[9] = "Coprocessor Segment Overrun";
intr_name[10] = "#TS Invalid TSS Exception";
intr_name[11] = "#NP Segment Not Present";
intr_name[12] = "#SS Stack Fault Exception";
intr_name[13] = "#GP General Protection Exception";
intr_name[14] = "#PF Page-Fault Exception";
// intr_name[15] 第15项是intel保留项,未使用
intr_name[16] = "#MF x87 FPU Floating-Point Error";
intr_name[17] = "#AC Alignment Check Exception";
intr_name[18] = "#MC Machine-Check Exception";
intr_name[19] = "#XF SIMD Floating-Point Exception";
}
extern idt_table ;idt_table是C中注册的中断处理程序数组
section .data
global intr_entry_table
intr_entry_table:
%macro VECTOR 2
section .text
intr%1entry: ; 每个中断处理程序都要压入中断向量号,所以一个中断类型一个中断处理程序,自己知道自己的中断向量号是多少
%2 ; 中断若有错误码会压在eip后面
; 以下是保存上下文环境
push ds
push es
push fs
push gs
pushad ; PUSHAD指令压入32位寄存器,其入栈顺序是: EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI
; 如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI
mov al,0x20 ; 中断结束命令EOI
out 0xa0,al ; 向从片发送
out 0x20,al ; 向主片发送
push %1 ; 不管idt_table中的目标程序是否需要参数,都一律压入中断向量号,调试时很方便
call [idt_table + %1*4] ; 调用idt_table中的C版本中断处理函数
jmp intr_exit
section .data
dd intr%1entry ; 存储各个中断入口程序的地址,形成intr_entry_table数组
%endmacro
section .text
global intr_exit
intr_exit:
; 以下是恢复上下文环境
add esp, 4 ; 跳过中断号
popad
pop gs
pop fs
pop es
pop ds
add esp, 4 ; 跳过error_code
iretd
VECTOR 0x00,ZERO
VECTOR 0x01,ZERO
VECTOR 0x02,ZERO
VECTOR 0x03,ZERO
VECTOR 0x04,ZERO
VECTOR 0x05,ZERO
VECTOR 0x06,ZERO
VECTOR 0x07,ZERO
VECTOR 0x08,ERROR_CODE
VECTOR 0x09,ZERO
VECTOR 0x0a,ERROR_CODE
VECTOR 0x0b,ERROR_CODE
VECTOR 0x0c,ZERO
VECTOR 0x0d,ERROR_CODE
VECTOR 0x0e,ERROR_CODE
VECTOR 0x0f,ZERO
VECTOR 0x10,ZERO
VECTOR 0x11,ERROR_CODE
VECTOR 0x12,ZERO
VECTOR 0x13,ZERO
VECTOR 0x14,ZERO
VECTOR 0x15,ZERO
VECTOR 0x16,ZERO
VECTOR 0x17,ZERO
VECTOR 0x18,ERROR_CODE
VECTOR 0x19,ZERO
VECTOR 0x1a,ERROR_CODE
VECTOR 0x1b,ERROR_CODE
VECTOR 0x1c,ZERO
VECTOR 0x1d,ERROR_CODE
VECTOR 0x1e,ERROR_CODE
VECTOR 0x1f,ZERO
VECTOR 0x20,ZERO
8253时钟
#define IRQ0_FREQUENCY 100
#define INPUT_FREQUENCY 1193180
#define COUNTER0_VALUE INPUT_FREQUENCY / IRQ0_FREQUENCY
#define CONTRER0_PORT 0x40
#define COUNTER0_NO 0
#define COUNTER_MODE 2
#define READ_WRITE_LATCH 3
#define PIT_CONTROL_PORT 0x43
/* 把操作的计数器counter_no、读写锁属性rwl、计数器模式counter_mode写入模式控制寄存器并赋予初始值counter_value */
static void frequency_set(uint8_t counter_port, \
uint8_t counter_no, \
uint8_t rwl, \
uint8_t counter_mode, \
uint16_t counter_value) {
/* 往控制字寄存器端口0x43中写入控制字 */
outb(PIT_CONTROL_PORT, (uint8_t)(counter_no << 6 | rwl << 4 | counter_mode << 1));
/* 先写入counter_value的低8位 */
outb(counter_port, (uint8_t)counter_value);
/* 再写入counter_value的高8位 */
outb(counter_port, (uint8_t)counter_value >> 8);
}
/* 初始化PIT8253 */
void timer_init() {
put_str("timer_init start\n");
/* 设置8253的定时周期,也就是发中断的周期 */
frequency_set(CONTRER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE);
put_str("timer_init done\n");
}