前情提要
上一节我们讲了中断,外部中断必须依赖一个硬件,就是可编程中断控制器8259A
一、8259A简介
可屏蔽中断是通过INTR信号线进入CPU的,一般可独立运行的外部设备,如打印机、声卡等,其发出的中断都是可屏蔽中断,都共享这一根INTR信号线通知CPU。大家想想看,任务是串行在CPU上执行的,CPU每次只能执行一个任务,如果同时有多个外设发出中断,而CPU只能先处理一个,它到底先响应哪个呢?还有,为了不使这些中断丢失,是否要为它们单独维护一个中断队列?这些问题如果让CPU来做的话,似乎有些大材小用了,不仅要占用CPU时间,而且还要占用内存来存储中断队列,所以有了中断代理,8259A就是一个中断代理,可编程中断控制器(PIC)。
二、级联8259A
一片8259A只能有8个接口,为了控制很多的外设,所以8259A是可以级联的。
三、信号端口和寄存器
INT:8259A选出优先级最高的中断请求后,发信号通知CPU
INTA:INT Acknowledge,中断响应信号。位于8259A中的INTA接收来自CPU的INTA接口的中断响应信号。
IMR:Interrupt Mask Register,中断屏蔽寄存器,宽度是8位,用来屏蔽某个外设的中断。
IRR:Interrupt Request Register,中断请求寄存器,宽度是8位。它的作用是接受经过IMR寄存器过滤后的中断信号并锁存,此寄存器中全是等待处理的中断,“相当于”5259A维护的未处理中断信号队列。
PR:Priority Resolver,优先级仲裁器。当有多个中断同时发生,或当有新的中断请求进来时,将它与当前正在处理的中断进行比较,找出优先级更高的中断
ISR:In-Service Register,中断服务寄存器,宽度是8位。当某个中断正在被处理时,保存在此寄存器中。
四、8259A处理过程
1、外设发出中断信号
2、检查IMR寄存器中是否已经屏蔽了来自该IRQ接口的中断信号。
3、将该IRQ接口所在IRR寄存器中对应的BIT置1。
4、优先级仲裁器PR会从IRR寄存器中挑选一个优先级最大的中断,IRQ接口号越低,优先级越大。
5、8259A会在控制电路中,通过INT接口向CPU发送INTR信号。
6、CPU在处理完手上代码后,通过INTA接口向8259A的INTA接口回复一个中断响应信号,表示以准备好。
7、8259A在收到这个INTA信号后,立即将刚才选出来的优先级最大的中断在ISR寄存器中对应的BIT置1,在IRR中将该位置0。
8、CPU将再次发送INTA信号给8259A,这一次是想获取中断对应的中断向量号。
9、8259A将此中断向量号通过系统数据总线发送给CPU。
10、CPU从数据总线上拿到中断向量号,用作中断描述符表的索引,跳转到中断处理程序并执行。
11、如果8259A在自动模式下,那么在第8步时,将ISR寄存器相应位置0,如果在手动模式下,则需要CPU发送一个EOL通知。
并不是进入了ISR后的中断就高枕无忧等着CPU了,它还是有可能被换下来的,在8259A发送中断向量号给CPU之前,这时候又来了新的中断,如果它的来源IRQ接口号比ISR中的低,也就是优先级更高,原来ISR中准备上CPU处理的旧中断,其对应的BIT就得清0,同时将它所在的IRR中的相应BIT恢复为1,随后在ISR中将此优先级更高的新中断对应的BIT置1,然后将此新中断的中断向量号发给CPU。
五、8259A编程
既然8259A称为可编程中断控制器,就说明它的工作方式很多,咱们就要通过编程把它设置成需要的样子。对它的编程也很简单,就是对它进行初始化,设置主片与从片的级联方式,指定起始中断向量号以及设置各种工作模式。
中断向量号是逻辑上的东西,它在物理上是8259A上的IRQ接口号。8259A上IRQ号的排列顺序是固定的,但其对应的中断向量号是不固定的,这其实是一种由硬件到软件的映射,通过设置8259A,可以将IRQ接口映射到不同的中断向量号。
在8259A内部有两组寄存器,一组是初始化命令寄存器组,用来保存初始化命令字(Initialization Command Words,ICW),ICW共4个,ICW1~ICW4。另一组寄存器是操作命令寄存器组,用来保存操作命令字(Operation Command Word,OCW),OCW共3个,OCW1~OCW3。所以,我们对8259A的编程,也分为初始化和操作两部分。
ICW1
ICW1用来初始化8259A的连接方式和中断信号的触发方式。连接方式是指用单片工作,还是用多片级联工作,触发方式是指中断请求信号是电平触发,还是边沿触发。
注意,ICW1需要写入到主片的0x20端口和从片的0xA0端口
字段 | 位 | 含义 |
---|---|---|
0 | IC4 | 表示是否要写入ICW4,x86系统中的IC4必须为1 |
1 | SNGL | 若SNGL为1,表示单片,若SNGL为0,表示级联(cascade) |
2 | ADI | 用来设置8085的调用时间间隔,x86不需要设置 |
3 | LTIM | 用来设置中断检测方式,LTIM为0表示边沿触发,LTIM为1表示电平触发。 |
4 | 1 | ICW1的标记 |
5-7 | 000 | 8085处理器专用 |
ICW2
ICW2用来设置起始中断向量号,就是前面所说的硬件IRQ接口到逻辑中断向量号的映射。由于每个8259A芯片上的IRQ接口是顺序排列的,所以咱们这里的设置就是指定IRQ0映射到的中断向量号,其他IRQ接口对应的中断向量号会顺着自动排下去。
注意,ICW2需要写入到主片的0x21端口和从片的0xA1端口
由于咱们只需要设置IRQ0的中断向量号,IRQ1~IRQ7的中断向量号是IRQ0的顺延,所以,咱们只负责填写高5位T3~T7,ID0~ID2这低3位不用咱们负责。由于咱们只填写高5位,所以任意数字都是8的倍数,这个数字表示的便是设定的起始中断向量号。这是有意设计的,低3位能表示8个中断向量号,这由8259A根据8个IRQ接口的排列位次自行导入,IRQ0的值是000,IRQ1的值是001,IRQ2的值便是010…以此类推,这样高5位加低3位,便表示了任意一个IRQ接口实际分配的中断向量号。
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|
T7 | T6 | T5 | T4 | T3 | ID2 | ID1 | ID0 |
ICW3
ICW3仅在级联的方式下才需要(如果ICW1中的SNGL为0),用来设置主片和从片用哪个IRQ接口互连。由于主片和从片的级联方式不一样,对于这个ICW3,主片和从片都有自己不同的结构
注意,ICW3需要写入主片的0x21端口及从片的0xA1端口。
对于主片,ICW3中置1的那一位对应的IRQ接口用于连接从片,若为0则表示接外部设备。比如,若主片IRQ2和IRQ5接有从片,则主片的ICW3为00100100
。
对于从片,要设置与主片8259A的连接方式,“不需要”指定用自己的哪个IRQ接口与主片连接,从片上与主片连接的并不是IRQ接口,设置从片连接主片的方法是只需要在从片上指定主片用于连接自己的那个IRQ接口就行了。在中断响应时,主片会发送与从片做级联的IRQ接口号,所有从片用自己的ICW3的低3位和它对比,若一致则认为是发给自己的。比如主片用IRQ2接口连接从片A,用IRQ5接口连接从片B,从片A的ICW3的值就应该设为00000010
,从片B的ICW3的值应该设为00000101
。所以,从片ICW3中的低3位ID0~ID2就够了,高5位不需要,为0即可。
ICW4
注意,ICW3需要写入主片的0x21端口及从片的0xA1端口。
字段 | 位 | 意义 |
---|---|---|
0 | PM | 微处理器类型,为了兼容老处理器,1表示x86处理器 |
1 | AEOI | 1表示自动结束中断,0表示非自动结束中断,需要在中断处理程序中发送EOI指令结束中断 |
2 | M/S | 缓冲模式下,1表示本8259A是主片,0表示本8259A是从片,非缓冲模式无效 |
3 | BUF | 0表示非缓冲模式,1表示缓冲模式 |
4 | SFNM | 0表示全嵌套模式,1表示特殊全嵌套模式 |
5-7 | 000 | 未定义 |
OCW1
OCW1用来屏蔽连接在8259A上的外部设备的中断信号,实际上就是把OCW1写入了IMR寄存器。这里的屏蔽是说是否把来自外部设备的中断信号转发给CPU。由于外部设备的中断都是可屏蔽中断,所以最终还是要受标志寄存器eflags中的IF位的管束,若IF为0,可屏蔽中断全部被屏蔽,也就是说,在IF为0的情况下,即使8259A把外部设备的中断向量号发过来,CPU也置之不理。
注意,OCW1要写入主片的0x21或从片的0xA1端口
M0~M7对应8259A的IRQ0~IRQ7,某位为1,对应的IRQ上的中断信号就被屏蔽了。否则某位为0的话,对应的IRQ中断信号则被放行。
OCW2
OCW2用来设置中断结束方式和优先级模式。
注意,OCW2要写入到主片的0x20及从片的0xA0端口。
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|
R | SL | EOI | 0 | 0 | L2 | L1 | L0 |
OCW2其中的一个作用就是发EOI信号结束中断。如果使SL为1,可以用OCW2的低3位(L2~L0)来指定位于ISR寄存器中的哪一个中断被终止,也就是结束来自哪个IRQ接口的中断信号。如果SL位为0,L2~L0便不起作用了,8259A会自动将正在处理的中断结束,也就是把ISR寄存器中优先级最高的位清0。
OCW2另一个作用就是设置优先级控制方式,这是用R位(第7位)来设置的。
如果R为0,表示固定优先级方式,即IRQ接口号越低,优先级越高。如果R为1,表明用循环优先级方式,这样优先级会在0~7内循环。如果SL为0,初始的优先级次序为IR0>IR1>IR2>IR3>IR4>IR5>IR6>IR7
。当某级别的中断被处理完成后,它的优先级别将变成最低,将最高优先级传给之前较之低一级别的中断请求,其他依次类推。比如,当前IR3为最高级别中断请求,处理完成后,IR3将变成最低级别,IR4变成最高级别,这一组循环之后的优先级变成了:IR4>IR5>IR6>IR7>IR0>IR1>IR2>IR3
,另外,还可以打开SL开关,使SL为1,再通过L2~L1指定最低优先级是哪个IRQ接口。
高位属性组合表如下
OCW3
OCW3用来设定特殊屏蔽方式及查询方式
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|
/ | ESMM | SMM | 0 | 1 | P | PR | RIS |
注意,OCW3要写入主片的0x20端口或从片的0xA0端口
第7位未用到
第6位的ESMM(Enable Special Mask Mode)和第5位的SMM(Special Mask Mode)是组合在一起用的,用来启用或禁用特殊屏蔽模式。ESMM是特殊屏蔽模式允许位,是个开关。SMM是特殊屏蔽模式位。只有在启用特殊屏蔽模式时,特殊屏蔽模式才有效。也就是若ESMM为0,则SMM无效。若ESMM为1,SMM为0,表示未工作在特殊屏蔽模式。若ESMM和SMM都为1,这才正式工作在特殊屏蔽模式下。
第4~3位的01是OCW3的标识,8259A通过这两位判断是哪个控制字。
P,Poll command,查询命令,当P为1时,设置8259A为中断查询方式,这样就可以通过读取寄存器,如IRS,来查看当前的中断处理情况。
RR,Read Register,读取寄存器命令。它和RIS位是配合在一起使用的。当RR为1时才可以读取寄存器。
RIS,Read Interrupt register Select,读取中断寄存器选择位,顾名思义,就是用此位选择待读取的寄存器。有点类似显卡寄存器中的索引的意思。若RIS为1,表示选择ISR寄存器,若RIS为0,表示选择IRR寄存器。这两个寄存器能否读取,前提是RR的值为1。
问题
8259A就两个端口地址,怎么识别4个ICW和3个OCW的?
ICW1和OCW2、OCW3是用偶地址端口0x20(主片)或0xA0(从片)写入。
ICW2~ICW4和OCW1是用奇地址端口0x21(主片)或0xA1(从片)写入。
以上4个ICW要保证一定的次序写入,所以8259A就知道写入端口的数据是什么了。
OCW的写入与顺序无关,并且ICW1和OCW2、OCW3的写入端口是一致的,那8259A怎样来辨识它们呢?又是自问自答,其实就是各控制字中的第4~3标识位,通过这两位的组合来唯一确定某个控制字。
控制字 | 第4位 | 第3位 |
---|---|---|
ICW1 | 1 | / |
OCW2 | 0 | 0 |
OCW3 | 0 | 1 |
OCW1是怎样确定的呢?OCW是在初始化之后才有效的,所以在初始化之后写入奇地址端口的数据便被认为是OCW1。
8259A的编程其本质上就是写入ICW和OCW。
六、代码实践
本章节的代码我放在了github上,我们可以先看一下仿真结果
因为我们打开了时钟中断,所以这里触发了当前的时钟中断处理函数,当然了,这里只是一个简单的通用处理函数,时钟中断的函数还没有写。
6.1、设置8259A
/* 初始化可编程中断控制器8259A */
static void pic_init(void) {
put_str("----pic_init begin!\n");
/* 初始化主片 */
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
outb (PIC_M_DATA, 0xfe); // 打开主片上IR0,也就是目前只接受时钟产生的中断
outb (PIC_S_DATA, 0xff);
put_str("----pic_init done!\n");
}
由于只是一个硬件的配置,我们直接读写端口就好了
6.2、重要数据结构
因为现在开始代码量比较庞大了,所以后面我们画一个图来更清晰的表明代码的走向。
中断描述符中存储的中断函数地址不直接指向中断处理函数,这样的话相当于软绑定了,我们后面想要添加什么中断,直接处理 idt_table
就好了,向其中相应的位置添加中断函数,而不需要修改idt或者 intr_entry_table
,这些都是确定好了的。
6.3、技巧
由于我们需要自己创建一个中断门描述符,并且把他放在内存中的一个地址内,这里我们首先想到的就是结构体,因为结构体好赋值,当然了,你要是想用64位的int值,一位一位的去存也没关系。
但是结构体长下面的样子
/* 中断门描述符结构体 */
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;
}__attribute__((packed)); // 避免编译器优化,直接给对齐了
其中gcc会对代码进行优化,这里的 uint16_t
为了更高效的执行,会被直接对齐到32位。
为此可以加一个控制参数避免编译器这种优化效果 __attribute__((packed))
,这个指令只对gcc等部分编译器有效,他并不是c语言标准的一部分。
结束语
这一节我们正式进入中断的世界了,后面重要的 0x80
中断也是这么设置的,不过,我们现在先设置一下简单的时钟中断,这里已经打开了时钟中断。