8259A PIC

通常PC机会采用两片8258A芯片级联的方式,将外部硬件设备的中断请求和引脚与处理器的中断接收引脚关联起来。两个芯片级联如下:

Screenshot_20220819_222701

其中一个8259A作为主芯片,与CPU的INTR引脚相连;另一个作为从芯片,与主芯片的IR2引脚相连。

通常情况下,会按照如下情况与外部设备相连:

Screenshot_20220819_223002

一个8259A PIC包含两组寄存器,分别是ICW(初始化命令字)寄存器组和OCW(操作控制字)寄存器组。ICW用于初始化中断控制器,OCW用于操作中断控制寄存器。

PC采用I/O地址映射方式,将8259A PIC的寄存器映射到I/O端口地址空间,因此必须借助INOUT汇编指令才能访问8259A PIC。主8259A芯片的I/O端口地址是20h和21h,从8259A芯片的I/O端口地址是A0h和A1h。内部结构如下:

Screenshot_20220819_223439

通过I/O端口可操作中断控制寄存器的ICW和OCW寄存器组,进而配置IRR、PR、ISR、IMR等寄存器。

  • IRR(Interrput Request Register,中断请求寄存器)用来保存IR0~IR7引脚上接收的中断请求,该寄存器有8bit分别对应外部引脚IR0~IR7。
  • IMR(Interrupt Mask Register,中断屏蔽寄存器)用于级联屏蔽的外部引脚,该寄存器同样有8位分别对应IRR寄存器中的每一位,当IMR中的某一位或几位被置位时,相应的IR引脚的中断请求信号将会被屏蔽,不处理。
  • PR(Priority Resolver,优先级解析器)将从IRR寄存器接收的中断请求中选取最高优先级者,将其发往ISR
  • ISR(In-Service Resigter,正在服务寄存器)记录着正在处理的中断请求,同时8259A芯片还会向CPU发送一个INT信号,而CPU在每执行完一条指令后检测是否接收到中断请求信号。如果收到中断请求信号,处理器将不再继续执行指令,转而向8259A芯片发送一个INTA来应答中断请求信号。8259A芯片收到这个应答信号后,便会把这个中断请求保存到ISR中(置位相应寄存器位)。与此同时,复位IRR寄存器中的对应中断请求信号位,以表示中断请求正在处理。随后,CPU还会发送第二个INTA脉冲信号,其作用是通知8259A芯片发送中断向量号,此时8259A性别会把中断向量号(8位数据)发送到数据总线上供CPU读取。当CPU收到中断向量号后,随即跳转到中断描述符表IDT检索向量号对应的门描述符,并自动执行描述符中的处理程序。

如果8259A芯片采用AEOI(Automatic End of Interrupt,自动结束中断)方式,那么它会在第二个INTA脉冲信号的结尾处复位正在服务寄存器ISR的对应位。如果采用非自动结束方式,那么CPU必须在中断处理程序的结尾处向8259A芯片发送一个EOI(End of Interrupt,结束中断)命令,来复位ISR的对应位。如果中断请求来自级联的8259A芯片,则必须向两个芯片都发送EOI命令。此后8259A继续判断下一个最高优先级的中断,并重复处理过程。

初始化命令字ICW

ICW寄存器组共包括ICW1、ICW2、ICW3、ICW4四个寄存器,必须按照ICW1到ICW4顺序初始化。主8259A芯片的ICW1寄存器映射到I/O端口的20h地址处,ICW2、ICW3、ICW4寄存器映射到I/O端口21h地址处;从8259A芯片的ICW1寄存器映射到I/O端口A0h处,ICW2、ICW3、ICW4映射到I/O端口A1h地址处。

对主从性别而初始化顺序可以是先后式(先配置主芯片的ICW寄存器组,再配置从芯片的ICW寄存器组)或交替式(先配置主/从芯片的ICW1寄存器,再设置主/从芯片的ICW2寄存器,以此类推知道ICW4寄存器)

ICW1寄存器

ICW1寄存器共有8位,各个位的功能描述:

Screenshot_20220819_230011

主从芯片的ICW1寄存器都固定初始化为00010001B(11h)。

ICW2寄存器

ICW2寄存器共有8位,各个位的功能描述:

Screenshot_20220819_230204

对中断向量号没有特殊要求,通常情况下,主8259A芯片的中断向量号设置位20h(占用中断向量号20h~21h),从8259A芯片的中断向量号设置为28h(占用中断向量号28h~2fh)

ICW3寄存器

ICW2寄存器共有8位,对于主/从8259A芯片的ICW3寄存器含义不同,主8259A芯片各个位的功能描述:

Screenshot_20220819_230435

主8259A芯片的ICW3寄存器用于记录各IR引脚与从8259A芯片的级联状态。而从8259A芯片的ICW3寄存器则用于级联其与主8259A芯片的级联状态。

从8259A芯片的ICW3寄存器位:

Screenshot_20220819_230652

从级联结构可以看出,主8259A芯片的ICW3寄存器值被设置为04h,从8259A芯片的ICW3寄存器被设置为02h。

ICW4寄存器

ICW4寄存器有8位,含义如下:

Screenshot_20220819_230858

功能说明:

  • AEOI模式:此模式可使用中断控制寄存器收到CPU发来的第2个INTA中断响应脉冲,自动复位ISR寄存器的对应位
  • EOI模式:此模式下,处理器执行完中断处理程序后,必须手动向中断控制寄存器发送中断结束EOI指令,来复位ISR寄存器的对应位
  • FNM(Fully Nested Mode,全嵌套模式):此模式下,中断请求的优先级按照引脚名从高到低依次为IR0~IR7。如果从8259A芯片的中断请求正在被处理,那么该从芯片将被主芯片屏蔽直到处理结束,即使从芯片产生更高优先级的中断请求也不会得到执行。
  • SFNM(Special Fully Nested Mode,特殊全嵌套模式):该模式与FNM基本相同,不同点在于主芯片不会屏蔽从芯片,主芯片可以接收来自从芯片更高优先级的中断。在中断处理程序返回时,需要先向从芯片发送EOI命令,并检测从芯片的ISR寄存器值,如果ISR寄存器仍有其他中断请求,则无需向主芯片发送EOI命令。

通常情况下,只需要将主/从芯片的ICW4寄存器设置为01h即可。

操作控制字OCW

OCW寄存器组包含OCW1、OCW2、OCW3,用于控制和调整工作期间的中断控制寄存器,这3个寄存器没有操作顺序之分。主8259A芯片的OCW1寄存器映射到I/O端口21h地址处,OCW2、OCW3寄存器映射到I/O端口20h地址处;从8259A的OCW1寄存器映射到I/O端口A1地址处,OCW2、OCW3寄存器映射到I/O端口A0h地址处。

OCW1寄存器

OCW1是中断屏蔽寄存器,共有8位:

Screenshot_20220819_232621

尽量屏蔽不用的中断,防止接收不必要的中断请求,从而导致中断请求过于拥堵。

OCW2寄存器

OCW2依然是个8位寄存器:

Screenshot_20220819_232753

对于OCW2寄存器的D5~D7位,它们可以组合成多种模式:

Screenshot_20220819_232914

循环模式(D7=1):8259A芯片使用一个8位循环队列保存各个引脚的中断请求,当一个中断请求结束后,这个引脚的优先级将自动将为最低,然后排入优先级队列的末尾,依次类推

特殊循环(D7=1,D6=1):特殊循环模式在循环模式的基础上,将D0~D2位指定的优先级设置为最低优先级,随后再按照循环模式循环降低中断请求中的优先级。

OCW3寄存器

OCW3寄存器也是8位寄存器:

Screenshot_20220819_233343

特殊屏蔽模式:在某些场合,中断处理程序能够被更低优先级的中断请求打断,从用特殊屏蔽模式,可在置位IMR寄存器(OCW1寄存器)的同时复位对应的ISR寄存器位,从而可以处理其他优先级的中断请求。

触发中断

中断和异常的处理工作都需要保存和还原任务现场

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
#define SAVE_ALL                                                               \
"cld; \n\t" \
"pushq %rax; \n\t" \
"pushq %rax; \n\t" \
"movq %es, %rax; \n\t" \
"pushq %rax; \n\t" \
"movq %ds, %rax; \n\t" \
"pushq %rax; \n\t" \
"xorq %rax, %rax; \n\t" \
"pushq %rbp; \n\t" \
"pushq %rdi; \n\t" \
"pushq %rsi; \n\t" \
"pushq %rdx; \n\t" \
"pushq %rcx; \n\t" \
"pushq %rbx; \n\t" \
"pushq %r8; \n\t" \
"pushq %r9; \n\t" \
"pushq %r10; \n\t" \
"pushq %r11; \n\t" \
"pushq %r12; \n\t" \
"pushq %r13; \n\t" \
"pushq %r14; \n\t" \
"pushq %r15; \n\t" \
"movq $0x10, %rdx; \n\t" \
"movq %rdx, %ds; \n\t" \
"movq %rdx, %es; \n\t"

entry.S文件的error_code模块极其相似,只不过中断处理程序的调用入口均指向同一中断处理函数do_irq,而且中断触发时不会压入错误码,所以此处无法复用代码。

1
2
3
4
5
6
7
8
9
10
11
#define IRQ_NAME2(nr) nr##_interrupt(void)
#define IRQ_NAME(nr) IRQ_NAME2(IRQ##nr)
#define BUILD_IRQ(nr) \
void IRQ_NAME(nr); \
__asm__(SYMBOL_NAME_STR(IRQ) #nr "_interrupt: \n\t" \
"pushq $0x00 \n\t" SAVE_ALL \
"movq %rsp, %rdi \n\t" \
"leaq ret_from_intr(%rip), %rax \n\t" \
"pushq %rax \n\t" \
"movq $" #nr ", %rsi \n\t" \
"jmp do_irq \n\t");

##用于连接两个宏值,宏展开过程中,会将操作符两边的内容连接起来,组成一个完整的内容。

#操作符可以将后面的内容强制转换位字符串,

leaq ret_from_intr(%rip), %rax用于取得中断处理程序的返回地址ret_from_intr,并将其保存到中断处理程序栈内。由于SAVE_ALL模块使用JMP指令进入中断处理程序do_irq,所以中断处理程序必须借助栈里的返回地址才能构成完整的调用过程(JMP指令在跳转过程中不会压入返回地址,而RET指令在函数返回时却需要返回地址)。使用JMP指令加放回地址的方法来替代CALL指令,可使函数调用过程更灵活

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
BUILD_IRQ(0x20)
BUILD_IRQ(0x21)
BUILD_IRQ(0x22)
BUILD_IRQ(0x23)
BUILD_IRQ(0x24)
BUILD_IRQ(0x25)
BUILD_IRQ(0x26)
BUILD_IRQ(0x27)
BUILD_IRQ(0x28)
BUILD_IRQ(0x29)
BUILD_IRQ(0x2a)
BUILD_IRQ(0x2b)
BUILD_IRQ(0x2c)
BUILD_IRQ(0x2d)
BUILD_IRQ(0x2e)
BUILD_IRQ(0x2f)
BUILD_IRQ(0x30)
BUILD_IRQ(0x31)
BUILD_IRQ(0x32)
BUILD_IRQ(0x33)
BUILD_IRQ(0x34)
BUILD_IRQ(0x35)
BUILD_IRQ(0x36)
BUILD_IRQ(0x37)

void (*interrupt[24])(void) = {
IRQ0x20_interrupt, IRQ0x21_interrupt, IRQ0x22_interrupt, IRQ0x23_interrupt,
IRQ0x24_interrupt, IRQ0x25_interrupt, IRQ0x26_interrupt, IRQ0x27_interrupt,
IRQ0x28_interrupt, IRQ0x29_interrupt, IRQ0x2a_interrupt, IRQ0x2b_interrupt,
IRQ0x2c_interrupt, IRQ0x2e_interrupt, IRQ0x2f_interrupt, IRQ0x30_interrupt,
IRQ0x31_interrupt, IRQ0x32_interrupt, IRQ0x33_interrupt, IRQ0x34_interrupt,
IRQ0x35_interrupt, IRQ0x36_interrupt, IRQ0x37_interrupt};

使用宏函数BUILD_IRQ声明24个中断处理函数的入口代码片段,同时还定义了一个函数指针数组,数组的每个元素都指向宏函数BUILD_IRQ定义一个中断处理函数入口。

初始化主/从9258A中断控制器和中断描述符表IDT内的各门描述符:

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
void interrupt_init() {
int i;
for (i = 32; i < 56; i++) {
set_intr_gate(i, 2, interrupt[i - 32]);
}
color_printk(RED, BLACK, "8259A init \n");

// 8259A master ICW1-4
io_out8(0x20, 0x11);
io_out8(0x21, 0x20);
io_out8(0x21, 0x04);
io_out8(0x21, 0x01);

// 8259A slave ICW1-4
io_out8(0xa0, 0x11);
io_out8(0xa1, 0x28);
io_out8(0xa1, 0x02);
io_out8(0xa1, 0x01);

// 8259A-M/S OCW1
io_out8(0x21, 0x00);
io_out8(0xa1, 0x00);

sti();
}

使用函数set_intr_gate将中断向量、中断处理函数配置到对应的门描述符中。外部硬件设备的中断向量号从32开始。主/从8259A中断控制器进行初始化赋值:

Screenshot_20220820_193524

最后复位主/从8259A中断控制器的中断屏蔽寄存器IMR全部中断屏蔽位,并使能中断(置位EFLAGS标志寄存器中的中断标志位IF

中断处理函数do_irq负责分发中断请求到各个中断处理函数,当前进打印中断向量号,表明处理器正在处理中断处理函数:

1
2
3
4
void do_irq(unsigned long regs, unsigned long nr) { // regs: rep, nr
color_printk(RED, BLACK, "do_irq:%#08x\t", nr);
io_out8(0x20, 0x20);
}

其主要功能是显示当前中断请求的中断向量号,并向主8259A中断控制器发送EOI命令复位ISR寄存器。

程序运行的瞬间结束:

Screenshot_20220820_194709

0x20号中断是时钟中断,在没有配置时钟控制器之前开启时钟中断,从而导致不断有中断请求产生。