PART 1
首先,计算机的中断根据其来源可分为内部和外部。外部中断常常是由外部设备发起,或是计算机遇到了某些遭难性错误而发生,相对于内部中断的发生频率来说要小些。因此也仅做了解。
而内部中断则要更加常见,根据其发出中断来源分为软中断和异常。软中断是由软件主动或被动发起的,一般是 “INT” 族的指令主动调用的。这类指令均已在处理器中编码,并通过数据线连接到芯片上。即实际向处理器发出中断的是8259A芯片组。8259A芯片的几个IRQ接口(Interrupt ReQuest:中断请求接口)已经预先和其他的可能发出中断的设备连接好了,对应关系如下(但这些IRQ并不是所有引脚,8259A每个芯片似乎有28个引脚)。
如IRQ0的时钟中断会在处理器加电以后自动且定期地向8259A芯片发出中断(定期:根据8253计数器的设定频率发生)。
接下来,只要对8259A芯片进行编程,就能够实现硬件层面的中断控制了,诸如中断屏蔽或中断优先级等。编程仅分为初始化和操作,通过ICW1ICW4(Initialization Command Words)初始化,OCW1OCW3(Operation Command Words)操作。
ICW1:规定8259的连接方式(单片或级联)与中断源请求信号的有效形式(边沿或电平触发)
ICW2(中断类型码字):设置中断类型码的初始化命令字
ICW3(级连控制字):标志主片/从片的初始化命令字
ICW4(中断结束方式字):方式控制初始化命令字
注:
ICW必须按照顺序分别写入主片和从片,ICW1写入主片0x20,从片0xA0端口;ICW2~4写入主片0x21,从片0xA1端口。
OCW1:用于对中断屏蔽寄存器IMR进行读/写。
OCW2:用于设定中断优先级
OCW3:设置或清除特殊屏蔽方式和读取寄存器状态(IRR 和 ISR)
注:
OCW无写入顺序要求,OCW1写入主片0x21,从片0xA1端口;OCW2~3写入主片0x20,从片0xA0端口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| /* 初始化主片 */ 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);
|
PART 2
现在,中断已经会正常触发了。但仅仅只是触发中断而已,触发以后的关键——中断处理程序还没能实现。中断发生流程如下:
设备发出中断信号给8259芯片,芯片检测是否屏蔽该设备发出的中断,若未屏蔽,则通知处理器发生中断且告知处理器中断号,否则直接忽略该信号。处理器收到中断信号以后,先将上下文保存,然后关闭中断,访问IDTR(Interrupt Descriptor Table Register)获取中断描述表,以中断号为索引获得对应的中断描述符,通过描述符内容调用对应的中断处理程序。
注:此处所指的上下文是指SS、ESP、EFLAGS、CS、EIP,以及所有通用寄存器。但如果没有发生特权级转移,SS和ESP则不需要被保存,直接沿用即可。
另外需要注意的是,中断的特权级转移同样指会从低特权级向高特权级转移。因此同样也必须要求,触发中断的调用者特权级低于或等于被调用者的特权级
门描述符如下:主要用到中断门描述符(8 Byte)
此类描述符将构成中断描述符表,并将其起始地址加载进IDTR(IDT Register)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| struct gate_desc { uint16_t func_offset_low_word; uint16_t selector; uint8_t dcount; 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) { 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"); } //intr_entry_table是中断处理函数的入口函数,仅做保存上下文和调用真正的处理函数这两个工作
static void exception_init(void) { int i; for (i = 0; i < IDT_DESC_CNT; i++) { idt_table[i] = general_intr_handler; intr_name[i] = "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"; } //idt_table是真正的中断处理函数 //intr_entry_table中的中断处理函数入口函数中存在指令: //call [idt_table + %1*4]
|
PART 3
时钟频率。
计算机是时钟分为外部时钟和内部时钟。
内部时钟由主板上的晶体振荡器产生,或称之为“晶振”。处理器和南北桥的通信基于该频率,称之为“外频”。外频×倍频=主频,处理器取指、执行的时钟周期基于主频。内部时钟出厂时固定,一般是最快的,单位常为纳秒ns。
外部时钟是处理器与外部设备之间通信时采用的时序。一般是毫秒ms或秒s级别。
处理器的速度显然是远快于外部设备的,但只要外部设备需要同计算机进行数据交换,就需要将双方的时钟按照一定比例同步。
一个简单例子是:
处理器会在每次中断的时候从外部设备的固定端口读取数据。
假设处理器频率100HZ,外部设备只能接受最高10HZ的传输速率,为了保证数据的稳定传输,就需要将处理器发送中断的频率降低到10HZ。我们显然不能真的去降低处理器的频率,那样未免有过多的浪费了,因此我们另外引入一个“计时器”,让这个计算器代替处理器去发出时钟中断信号,这样就能保证处理器原有的运行频率,同时降低发出中断的频率了。
当然,处理器要比100HZ,外部设备一般也不会慢到10HZ,这里只是大个好懂的比方罢了。
例中的“计时器”便是指8253芯片。该芯片自带三个计数器,每个计数器自带三个寄存器。
计数初值寄存器、减法寄存器、输出锁存器三个寄存器的功能根据名字便能大概理解了。就是将计数初值寄存器放入减法寄存器,同时将计数器的GATE引脚置1,减法计数器就会在每个CLK到来时降低1,当其值为0时,将会发送信号并停止计数/重新开始。不过值得在意的是,计数器的CLK引脚连接的是10MHZ脉冲,而8253的频率只有2MHZ。
8253的编程更加容易,其只有一个控制字。三个计数器分别对应的端口是0x40~0x42。控制字结构如下:
而0x43端口将用于写入初值。
但需要注意的是,计数器0的发送端已经和8259A芯片的IRQ0连接好了。也就是说,默认加电以后,这里面就会被自动赋予一个初值并开始发送时钟中断信号。所以时钟中断信号一开始就从这里发出,想要调节频率,只需要修改计数器0中的初值寄存器中的值即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| /* 把操作的计数器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"); }
|
插画ID:93758526