8259A
8259A是可编程中断控制器,通过与CPU的INTR引脚相连,向CPU传送外部中断。下图是来自于Intel的8259A的datasheet中的结构:
IR0 ~ IR7是外部中断源,所以每片8259A可以处理8个级别的中断向量,同时8259A可以串联使用,datasheet上说,不外加电路的情况下,通过串联可以实现64个级别的中断向量。不过实际使用中通常采用两片8259A串联的方式。下图来自于《x86/x64体系探索及编程》中关于串联的8259A:
每个8259A的IR引脚都和一条IRQ线链接,处理一个中断源,主片上的IR0 ~ IR7对应IRQ0 ~ IRQ7,从片上的IR0 ~ IR7对应IRQ8 ~ IRQ15,由于串联使用IR2,所以从片上的IR2同时对应IRQ2和IRQ9。这16个IRQ对应不同的硬件中断,具体请参考《IRQ》。
8259A的IR引脚是有优先级的,编号越小的优先级越高,所以IR0最高,IR7最低,但是在级联的情况下,从片链接到了主片的IR2上这样就造成了中断源的优先级从高到低的顺序是:IRQ0, IRQ1, IRQ8 ~ IRQ15,IRQ3 ~ IRQ7。
中断响应
8259A内部有三种8位的寄存器,分别是中断请求状态寄存器IRR,中断服务状态寄存器ISR,中断屏蔽状态寄存器IMR。这三个寄存器都是8位的,每一位对应一个IRQ。
IRR
IRR寄存器是用来标记到达的中断请求的。当一个中断请求到达8259A的一个引脚的时候,对应的IRR上的位就被设置,表示该引脚上有一个中断到来了。例如IRQ0中断到达了,那么主片上的IRR的bit 0就被设置。如过IRQ10中断到达了,那么从片上的IRR的bit 2被设置。
ISR
ISR寄存器是用来记录被处理器处理的中断请求的。当一个中断请求到达并且被记录到IRR之后,8259A在适当的时候通知处理器产上了一个这样的中断,等待处理器处理,这个时候ISR对应的位就会被设置,IRR对应的位就被清除。例如8259A通知处理器有一个IRQ0中断等待处理,并且得到处理器允许,那么主片的ISR的bit 0被设置,同时IRR bit 0被清除。
IMR
IMR寄存器是用来记录屏蔽的中断请求的。当8259A打算屏蔽某一中断的时候就将IMR对应的位设置成1。例如打算屏蔽IRQ0,那么主片上的IMR bit 0被设置。当主片上的IMR bit 2被设置的时候,从片上的所有中断请求都被屏蔽。
中断处理过程
8259A的中断响应过程如下:
- 当某条IRQ线上发生了中断请求,对应的8259A设置IRR响应的位,表示发生了中断请求。
- 查看IMR是否屏别了该中断,如果没有屏蔽则给CPU发送INTR。
- CPU在接收到INTR之后会回复INTA。
- 当收到第一个INTA之后8259A进行优先级仲裁,优先级高的中断得到响应,设置响应的ISR位,并且清空对应的IRR位。
- 在收到第二个INTA之后将响应的中断向量通过数据总线传递给CPU。
- 如果是AEOI模式,在第二个INTA处理完成之后ISR对应的位自动清空,否则必须接收到一个EOI之后8259A才能清空对应的ISR位。
命令字
8259A的寄存器被成为命令字,分为初始化命令字ICW以及操作命令字OCW,每片8259A都含有4个ICW:ICW1 ~ ICW4,以及3个OCW:OCW1 ~ OCW3。顾名思义ICW就是用来初始化8259A的,OCW使用来操作8259A的。8259A中的寄存器采用I/O地址映射的方式,使用IN和OUT指令访问这些寄存器:
- 端口0x20:8259A主片的ICW1,OCW2以及OCW3。
- 端口0x21:8259A主片的ICW2 ~ ICW4以及OCW1。
- 端口0xa0:8259A从片的ICW1,OCW2以及OCW3。
- 端口0xa1:8259A从片的ICW2 ~ ICW4以及OCW1。
在使用8259A之前先要对其进行初始化,初始化的流程是分别写入主片和从片的ICW1 ~ ICW4。
ICW1
主片和从片的ICW1的格式是一样的,只是映射的I/O端口不同:
这是Intel 8259A datasheet上关于ICW1的结构,时间已经很久远了,现在有些位已经不是用了,例如D2和D3,必须都是0,还有D5~D7只在MCS-80/85模式下有效,现在也必须是0。
ICW2
ICW2使用来设置中断向量的,结构如下:
在8086/8088系统中只使用了D3~D7,D0~D2必须是0。D3~D7设置的是IRQ0中断向量的高5位,当前8259A上的其他IRQ的中断向量依次顺延。例如主片的ICW2的值是0x20那么IRQ0的中断向量就是0x20,IRQ1的中断向量就是0x21,以此类推,IRQ7的中断向量就是0x27。如果从片上的ICW2的值是0x28那么IRQ8的中断向量就是0x28,IRQ9就是0x29,以此类推IRQ15就是0x2f。
ICW3
ICW3在主片和从片上的结构是不同的,主片的结构:
主片中的每一位表示相应的IR是否链接了从片,例如只有IR2链接了从片,那么主片的ICW3的值就是0x04。
从片的结构:
从片的ICW3使用低3位表示从片标识代码(slave identification code),这个代码大体的意思应该是表示从片本身链接到主片的哪个IR上。例如从片链接到主片的IR2上时,从片的ICW3的值就是0x02。
ICW4
主片和从片的ICW4是一样的,使用来设置EOI模式以及嵌套模型等的:
SFNM是设置嵌套模式的,主要是用来处理从片上的中断,当SFNM=0的时候,从片上的中断会使主片的ISR的bit 2被置位,同时屏蔽后续从片上的优先级更高的中断。当SFNM=1的时候当主片的ISR的bit 2被置位时不会屏蔽从片上优先级更高的中断。
OCW1
OCW1对应的就是8259A中的IMR:
当主片上的OCW1的D2是1的时候,整个从片都被屏蔽。
OCW2
OCW2用来设置中断优先级以及发送EOI命令:
R,SL,EOI三位的组合值表示了不同的操作。L0~L2表示操作的IRQ值。
OCW3
OCW3有多种用途,可以用来读取IRR/ISR,设置poll模式或者Special Mask Mode:
Non-Special Mask Mode的时候当IRQ在处理的过程中ISR响应的位被设置,在发送EOI命令之前,8259A会屏蔽优先级低的IRQ。在Special Mask Mode中,当IRQ被响应的过程中,8259A不会查看ISR,直接根据IRR和IMR来处理中断,这样即使优先级低于正在被处理的IRQ的中断到达的时候,也会得到响应。
例子
通过一个简单的例子来了解一下如何对8259A进行编程,要控制8259A之前先要对其进行初始化:
###############################################################
# init 8259a
init_8259a:
## master 8259a
# write ICW1
movb $0x11, %al
out %al, $MASTER_ICW1_PORT
# write ICW2
movb $0x20, %al
out %al, $MASTER_ICW2_PORT
# write ICW3
movb $0x04, %al
out %al, $MASTER_ICW3_PORT
# write ICW4
movb $0x01, %al
out %al, $MASTER_ICW4_PORT
## slave 8259a
# write ICW1
movb $0x11, %al
out %al, $SLAVE_ICW1_PORT
# write ICW2
movb $0x28, %al
out %al, $SLAVE_ICW2_PORT
# write ICW3
movb $0x02, %al
out %al, $SLAVE_ICW3_PORT
# write ICW4
movb $0x01, %al
out %al, $SLAVE_ICW4_PORT
ret
初始化就是按照主从片的顺序依次写ICW。
除了初始化8259A以外还要有中断处理函数,这里我们只处理了timer和keyboard中断,这两个中断的处理函数如下:
###############################################################
# keyboard_handler
keyboard_handler:
jmp do_keyboard_handler
kmsg: .asciz ">>> now: enter keyboard handler"
kmsg1: .asciz "exit the keyboard handler<<<"
do_keyboard_handler:
movl $kmsg, %esi
call puts
call println
call dump_8259_imr
call dump_8259_irr
call dump_8259_isr
movl $kmsg1, %esi
call puts
call println
call println
call write_master_EOI
iret
###############################################################
# timer_handler
timer_handler:
jmp do_timer_handler
tmsg: .asciz ">>> now: enter timer handler"
tmsg1: .asciz "exit the timer handler<<<"
do_timer_handler:
movl $tmsg, %esi
call puts
call println
call dump_8259_imr
call dump_8259_irr
call dump_8259_isr
movl $tmsg1, %esi
call puts
call println
call println
call write_master_EOI
call disable_timer
call enable_keyboard
iret
这样准备工作都完成了,下面就是在开启保护模式和分页之后调用上面的函数来操作8259A了:
# set timer_handler and keyboard_handler
movl $PIC8259A_TIMER_VECTOR, %esi
movl $timer_handler, %edi
call __set_interrupt_handler
movl $KEYBOARD_VECTOR, %esi
movl $keyboard_handler, %edi
call __set_interrupt_handler
call init_8259a
call disable_timer
call disable_keyboard
sti
movl $0xffffff, %ecx
l1:
nop
loop l1
call dump_8259_imr
call dump_8259_irr
call dump_8259_isr
call println
call enable_timer
jmp .
在安装了中断处理函数以及初始化了8259A之后,使用sti指令来开启中断。然后使用dump函数来打印出8259A各个寄存器的值。
sti指令允许cpu处理中断了,但是前面调用disable_keyboard让8259A来屏蔽keyboard中断,所以这个时候只有timer中断:
timer_handler执行过程中开启了keyboard中断,并且关闭了timer中断。这样在后续当窗口获得焦点的情况下按键盘任意键就会触发keyboard中断,并且执行keyboard_handler:
参考
《x86/x64体系探索及编程》
《8259A PROGRAMMABLE INTERRUPT CONTROLLER (8259A/8259A-2)》 ---- Intel 8259A datasheet