hurlex <八> 完成中断请求和定时器中断

2014-09-13 posted in [hurlex开发文档]

在上一章中我们完成了中断处理程序的框架,本章在其基础上讨论中断请求的实现。

我们在上一章中提到,外设的所有中断由中断控制芯片8259A统一汇集之后连接到CPU的INTR引脚。1这章我们就来探究8259APIC的初始化和实现定时器的中断处理。

8259APIC每一片可以管理8个中断源,显然一般情况下设备数量会超过这个值。这里就要提到IBM PC/AT 8259A PIC架构了,IBM的设计方案是使用8259APIC的级联功能,使用两片级联(分为主、从片)的方式来管理硬件中断。其中主片的INT端连接到CPU的INTR引脚,从片的INT连接到主片的IR2引脚。结构如下图所示:

8259A PIC级联

图中时钟中断连接在主片的IRQ0引脚,键盘中断连接在了主片的IRQ1引脚。其它的引脚暂时用不到就不说了。在上一张描述中断描述符表时我们知道了0~31号中断是CPU使用和保留的,用户可以使用的中断从32号开始。所以这里的IRQ0对应的中断号就是32号,IRQ1就是33号,然后以此类推。

理论就暂时阐述到这里,接下来是实现代码。首先是对8259A PIC的初始化,在设置中断描述符表的函数init_idt最前面加入如下代码:

  1. // 初始化中断描述符表
  2. void init_idt()
  3. {
  4. // 重新映射 IRQ 表
  5. // 两片级联的 Intel 8259A 芯片
  6. // 主片端口 0x20 0x21
  7. // 从片端口 0xA0 0xA1
  8. // 初始化主片、从片
  9. // 0001 0001
  10. outb(0x20, 0x11);
  11. outb(0xA0, 0x11);
  12. // 设置主片 IRQ 从 0x20(32) 号中断开始
  13. outb(0x21, 0x20);
  14. // 设置从片 IRQ 从 0x28(40) 号中断开始
  15. outb(0xA1, 0x28);
  16. // 设置主片 IR2 引脚连接从片
  17. outb(0x21, 0x04);
  18. // 告诉从片输出引脚和主片 IR2 号相连
  19. outb(0xA1, 0x02);
  20. // 设置主片和从片按照 8086 的方式工作
  21. outb(0x21, 0x01);
  22. outb(0xA1, 0x01);
  23. // 设置主从片允许中断
  24. outb(0x21, 0x0);
  25. outb(0xA1, 0x0);
  26. ... ...
  27. }

对8259A PIC具体的设置我们不再阐述,这种资料网上铺天盖地的都是。相信结合注释很容易理解这个简单的初始化过程。

完成了初始化之后,我们继续添加对IRQ处理函数的添加。首先是在idt.h头文件末尾添加如下内容:

  1. // IRQ 处理函数
  2. void irq_handler(pt_regs *regs);
  3. // 定义IRQ
  4. #define IRQ0 32 // 电脑系统计时器
  5. #define IRQ1 33 // 键盘
  6. #define IRQ2 34 // 与 IRQ9 相接,MPU-401 MD 使用
  7. #define IRQ3 35 // 串口设备
  8. #define IRQ4 36 // 串口设备
  9. #define IRQ5 37 // 建议声卡使用
  10. #define IRQ6 38 // 软驱传输控制使用
  11. #define IRQ7 39 // 打印机传输控制使用
  12. #define IRQ8 40 // 即时时钟
  13. #define IRQ9 41 // 与 IRQ2 相接,可设定给其他硬件
  14. #define IRQ10 42 // 建议网卡使用
  15. #define IRQ11 43 // 建议 AGP 显卡使用
  16. #define IRQ12 44 // 接 PS/2 鼠标,也可设定给其他硬件
  17. #define IRQ13 45 // 协处理器使用
  18. #define IRQ14 46 // IDE0 传输控制使用
  19. #define IRQ15 47 // IDE1 传输控制使用
  20. // 声明 IRQ 函数
  21. // IRQ:中断请求(Interrupt Request)
  22. void irq0(); // 电脑系统计时器
  23. void irq1(); // 键盘
  24. void irq2(); // 与 IRQ9 相接,MPU-401 MD 使用
  25. void irq3(); // 串口设备
  26. void irq4(); // 串口设备
  27. void irq5(); // 建议声卡使用
  28. void irq6(); // 软驱传输控制使用
  29. void irq7(); // 打印机传输控制使用
  30. void irq8(); // 即时时钟
  31. void irq9(); // 与 IRQ2 相接,可设定给其他硬件
  32. void irq10(); // 建议网卡使用
  33. void irq11(); // 建议 AGP 显卡使用
  34. void irq12(); // 接 PS/2 鼠标,也可设定给其他硬件
  35. void irq13(); // 协处理器使用
  36. void irq14(); // IDE0 传输控制使用
  37. void irq15(); // IDE1 传输控制使用

然后是idt_s.s中添加相应的处理过程:

  1. ; 构造中断请求的宏
  2. %macro IRQ 2
  3. [GLOBAL irq%1]
  4. irq%1:
  5. cli
  6. push byte 0
  7. push byte %2
  8. jmp irq_common_stub
  9. %endmacro
  10. IRQ 0, 32 ; 电脑系统计时器
  11. IRQ 1, 33 ; 键盘
  12. IRQ 2, 34 ; IRQ9 相接,MPU-401 MD 使用
  13. IRQ 3, 35 ; 串口设备
  14. IRQ 4, 36 ; 串口设备
  15. IRQ 5, 37 ; 建议声卡使用
  16. IRQ 6, 38 ; 软驱传输控制使用
  17. IRQ 7, 39 ; 打印机传输控制使用
  18. IRQ 8, 40 ; 即时时钟
  19. IRQ 9, 41 ; IRQ2 相接,可设定给其他硬件
  20. IRQ 10, 42 ; 建议网卡使用
  21. IRQ 11, 43 ; 建议 AGP 显卡使用
  22. IRQ 12, 44 ; PS/2 鼠标,也可设定给其他硬件
  23. IRQ 13, 45 ; 协处理器使用
  24. IRQ 14, 46 ; IDE0 传输控制使用
  25. IRQ 15, 47 ; IDE1 传输控制使用
  26. [GLOBAL irq_common_stub]
  27. [EXTERN irq_handler]
  28. irq_common_stub:
  29. pusha ; pushes edi, esi, ebp, esp, ebx, edx, ecx, eax
  30. mov ax, ds
  31. push eax ; 保存数据段描述符
  32. mov ax, 0x10 ; 加载内核数据段描述符
  33. mov ds, ax
  34. mov es, ax
  35. mov fs, ax
  36. mov gs, ax
  37. mov ss, ax
  38. push esp
  39. call irq_handler
  40. add esp, 4
  41. pop ebx ; 恢复原来的数据段描述符
  42. mov ds, bx
  43. mov es, bx
  44. mov fs, bx
  45. mov gs, bx
  46. mov ss, bx
  47. popa ; Pops edi,esi,ebp...
  48. add esp, 8 ; 清理压栈的 错误代码 ISR 编号
  49. iret ; 出栈 CS, EIP, EFLAGS, SS, ESP
  50. .end:

最后是init_idt函数构造IRQ的相关描述符和具体的IRQ处理函数了。

  1. // 初始化中断描述符表
  2. void init_idt()
  3. {
  4. ... ...
  5. idt_set_gate(31, (uint32_t)isr31, 0x08, 0x8E);
  6. idt_set_gate(32, (uint32_t)irq0, 0x08, 0x8E);
  7. idt_set_gate(33, (uint32_t)irq1, 0x08, 0x8E);
  8. idt_set_gate(34, (uint32_t)irq2, 0x08, 0x8E);
  9. idt_set_gate(35, (uint32_t)irq3, 0x08, 0x8E);
  10. idt_set_gate(36, (uint32_t)irq4, 0x08, 0x8E);
  11. idt_set_gate(37, (uint32_t)irq5, 0x08, 0x8E);
  12. idt_set_gate(38, (uint32_t)irq6, 0x08, 0x8E);
  13. idt_set_gate(39, (uint32_t)irq7, 0x08, 0x8E);
  14. idt_set_gate(40, (uint32_t)irq8, 0x08, 0x8E);
  15. idt_set_gate(41, (uint32_t)irq9, 0x08, 0x8E);
  16. idt_set_gate(42, (uint32_t)irq10, 0x08, 0x8E);
  17. idt_set_gate(43, (uint32_t)irq11, 0x08, 0x8E);
  18. idt_set_gate(44, (uint32_t)irq12, 0x08, 0x8E);
  19. idt_set_gate(45, (uint32_t)irq13, 0x08, 0x8E);
  20. idt_set_gate(46, (uint32_t)irq14, 0x08, 0x8E);
  21. idt_set_gate(47, (uint32_t)irq15, 0x08, 0x8E);
  22. // 255 将来用于实现系统调用
  23. idt_set_gate(255, (uint32_t)isr255, 0x08, 0x8E);
  24. ... ...
  25. }
  26. // IRQ 处理函数
  27. void irq_handler(pt_regs *regs)
  28. {
  29. // 发送中断结束信号给 PICs
  30. // 按照我们的设置,从 32 号中断起为用户自定义中断
  31. // 因为单片的 Intel 8259A 芯片只能处理 8 级中断
  32. // 故大于等于 40 的中断号是由从片处理的
  33. if (regs->int_no >= 40) {
  34. // 发送重设信号给从片
  35. outb(0xA0, 0x20);
  36. }
  37. // 发送重设信号给主片
  38. outb(0x20, 0x20);
  39. if (interrupt_handlers[regs->int_no]) {
  40. interrupt_handlers[regs->int_no](regs);
  41. }
  42. }

结合代码中详细的注释和本章开始的8259A PIC的结构图,详细很容易理解这个处理过程。其实IRQ和ISR的处理过程很类似:

  • ISR的处理过程是 (isr0 - isr31) -> isr_common_stub -> isr_handler -> 具体的ISR处理函数。

  • IRQ的处理过程是 (irq0 - irq15) -> irq_common_stub -> irq_hanlder -> 具体的IRQ处理函数。

写到这里具体的IRQ处理过程就完成了,以后只需要设置好相应的处理函数就好了,接下来我们实现时钟中断的产生和处理。

时钟中断对于操作系统内核来说很重要的一种中断,它使得CPU无论在执行任何用户或者内核的程序时,都能定义的将执行权利交还到CPU手中来。2除了记录时间之外,时钟中断的处理函数里通常都是对进程的调度处理。

具体的时钟中断源是8253/8254 Timer产成的,要按照需要的频率产生中断,需要先配置8253/8254 Timer芯片。代码如下:

  1. #include "timer.h"
  2. #include "debug.h"
  3. #include "common.h"
  4. #include "idt.h"
  5. void timer_callback(pt_regs *regs)
  6. {
  7. static uint32_t tick = 0;
  8. printk_color(rc_black, rc_red, "Tick: %d\n", tick++);
  9. }
  10. void init_timer(uint32_t frequency)
  11. {
  12. // 注册时间相关的处理函数
  13. register_interrupt_handler(IRQ0, timer_callback);
  14. // Intel 8253/8254 PIT芯片 I/O端口地址范围是40h~43h
  15. // 输入频率为 1193180,frequency 即每秒中断次数
  16. uint32_t divisor = 1193180 / frequency;
  17. // D7 D6 D5 D4 D3 D2 D1 D0
  18. // 0 0 1 1 0 1 1 0
  19. // 即就是 36 H
  20. // 设置 8253/8254 芯片工作在模式 3 下
  21. outb(0x43, 0x36);
  22. // 拆分低字节和高字节
  23. uint8_t low = (uint8_t)(divisor & 0xFF);
  24. uint8_t hign = (uint8_t)((divisor >> 8) & 0xFF);
  25. // 分别写入低字节和高字节
  26. outb(0x40, low);
  27. outb(0x40, hign);
  28. }

对应的头文件如下:

  1. #ifndef INCLUDE_TIMER_H_
  2. #define INCLUDE_TIMER_H_
  3. #include "types.h"
  4. void init_timer(uint32_t frequency);
  5. #endif // INCLUDE_TIMER_H_

8253/8254 Timer有三种工作模式,我们使用第三种。init_timer函数的参数是所需的时钟中断的频率,具体的设置原理不再赘述。最后,修改入口函数进行测试:

  1. #include "console.h"
  2. #include "debug.h"
  3. #include "gdt.h"
  4. #include "idt.h"
  5. #include "timer.h"
  6. int kern_entry()
  7. {
  8. init_debug();
  9. init_gdt();
  10. init_idt();
  11. console_clear();
  12. printk_color(rc_black, rc_green, "Hello, OS kernel!\n");
  13. init_timer(200);
  14. // 开启中断
  15. asm volatile ("sti");
  16. return 0;
  17. }

最后编译执行,我们看到了如下的输出:

8253/8254 Timer 中断

  • 这里肯定会有读者提出来现代的计算机主板上早就使用APIC(Advanced Programmable Interrupt Controller,高级可编程中断控制器)来进行外设的中断管理了。没错,但是我相信在本科阶段的微机原理和接口技术中学的是8259APIC(Programmable Interrupt Controller),而且无论硬件怎么发展,始终会兼容以前的接口。本着大家熟悉易理解的原则,我们依旧使用兼容的8259APIC(Programmable Interrupt Controller, 可编程中断控制器)的设置方法进行设置。

  • 当然了,屏蔽中断就没办法了。

原文:

http://wiki.0xffffff.org/posts/hurlex-8.html