PIC

来自osdev
跳到导航 跳到搜索

8259的 “可编程中断控制器” (PIC) 是组成x86架构的最重要的芯片之一。 没有它,x86体系结构就不会是中断驱动的体系结构。 8259A的功能是管理硬件中断,并将其发送到适当的系统 中断。 这允许系统响应设备需求而不损失时间 (相较于轮询设备)。

重要的是要注意,APIC 已经在更现代的系统中取代了8259的PIC,尤其是那些具有多核/处理器的系统。

8259 PIC是做什么的 ?

8259 PIC通过接受几个中断请求并按顺序将它们投送到处理器来控制CPU的中断机制。 例如,当键盘注册一个keyhit时,它会沿着它的中断线 (IRQ 1) 向PIC芯片发送一个脉冲,然后PIC芯片将IRQ转换为系统中断,并发送一条消息来中断CPU的工作。 内核的部分工作是处理这些IRQ并执行必要的过程 (这个例子中是轮询键盘以获取scancode) 或报告用户空间程序中断 (向键盘驱动程序发送消息)。

如果没有PIC,你必须轮询系统中的所有设备,看看他们是否想做任何事情 (信号事件),但是有了PIC,你的系统可以保持运行,直到设备想要触发信号事件,这意味着你不会浪费时间去找设备,你会让设备准备好的时候来找你。(译者注:CPU的接线不可能接上所有提供中断的设备,所以需要通过PIC在中间做汇集)

IBM PC 8259 PIC架构

一开始 (IBM PC和XT),只使用了一个8259 PIC芯片,它为系统提供了8个IRQ。 传统上,BIOS将它们映射到中断8到15 (0x08到0x0F)。以前单PIC机器基本够用了。

IBM PC/AT 8259 PIC架构

IBM PC/AT通过添加第二个8259 PIC芯片扩展了PC体系结构。 这是可能的,因为8259A具有级联中断的能力,只要将一个芯片级联入另一个芯片即可。 这样总共给出了15个中断。 为什么是15而不是16?这是因为当你级联芯片时,PIC需要使用其中一个中断线向另一个芯片发出信号。

因此,在AT中,IRQ线2用于向第二芯片发送信号。 因此,IRQ 2无法由硬件设备使用,而硬件设备已连接到从属PIC上的IRQ 9。 实模式BIOS通常设置为IRQ 9中断处理程序重定向到IRQ 2处理程序。 这样,使用IRQ 2的DOS驱动程序继续工作。 这种双芯片架构仍然在现代系统中可用,并且没有改变 (除了上述APIC架构的出现)。

8259 PIC芯片是如何工作的 ?

现代系统中的两个8259 PIC中的每一个都有8个输入。 当任何输入被触发时,PIC会在内部设置一个位,告诉其中一个输入需要获得服务。 然后,它检查该通道是否被屏蔽,以及是否有中断已经挂起。 如果通道未屏蔽并且没有中断待处理,则PIC将触发中断线。 在从芯片上,这将IRQ 2馈送到主机,并且主机连接到处理器中断线。

当处理器接受中断时,主芯片检查两个PIC中的哪一个负责应答,然后要么向处理器提供中断编号,要么要求从芯片这样做。 应答的PIC查找内部存储的 “向量偏移(vector offset)” 变量,并累加输入线路以形成请求的中断编号。 之后,处理器将查找中断地址并执行相应地操作 (有关更多详细信息,请参见 Interrupts)。

用8259 PIC编程

每个芯片 (主从) 都有一个命令端口和一个数据端口 (下表中给出)。 当没有发出命令时,数据端口允许我们访问8259 PIC的中断掩码。

Chip - Purpose I/O port
Master PIC - Command 0x0020
Master PIC - Data 0x0021
Slave PIC - Command 0x00A0
Slave PIC - Data 0x00A1
  • 每个PIC向量偏移必须被8整除,因为8259A将较低的3位用于特定中断 (0..7) 的中断数。
  • 改变8259 PIC使用的向量偏移的唯一方法是重新初始化它,这解释了为什么代码 “这么长”,以及很多显然没有理由在这里的东西。
  • 如果你打算从保护模式返回真实模式 (出于任何目的),则确实必须将PIC恢复为以前的配置。

Real Mode

Chip Interrupt numbers (IRQ) Vector offset Interrupt Numbers
Master PIC 0 to 7 0x08 0x08 to 0x0F
Slave PIC 8 to 15 0x70 0x70 to 0x77

这些默认BIOS值非常适合实模式编程; 它们不会像在保护模式下那样与任何CPU异常冲突。

保护模式

在保护模式下,IRQ0到7与Intel保留的CPU异常冲突,直到0x1F。 (这是IBM设计错误。) 因此,很难区分IRQ或软件错误。 因此,建议更改PIC的偏移量 (也称为重新映射PIC),以便IRQs使用非保留向量。 一个常见的选择是将它们移动到可用范围的开始 (IRQs 0 .. 0xF -> INT 0x20 .. 0x2F)。 为此,我们需要将主PIC的偏移量设置为0x20,将从PIC的偏移量设置为0x28。 有关代码示例,请参见下文。

代码示例

常用定义

这只是本节其余部分共有的一组定义。有关outb(),inb() 和io_wait() 函数,请参见 本页

#define PIC1		0x20		/* IO base address for master PIC */
#define PIC2		0xA0		/* IO base address for slave PIC */
#define PIC1_COMMAND	PIC1
#define PIC1_DATA	(PIC1+1)
#define PIC2_COMMAND	PIC2
#define PIC2_DATA	(PIC2+1)

中断结束

也许发布给PIC芯片的最常见命令是 “中断结束” (EOI) 命令 (代码0x20)。 在基于IRQ的中断例程结束时发给PIC芯片。 如果IRQ来自主PIC,则仅向主PIC发出此命令就足够了; 但是,如果IRQ来自从属PIC,则必须向两个PIC芯片发出命令。

#define PIC_EOI		0x20		/* End-of-interrupt command code */

void PIC_sendEOI(unsigned char irq)
{
	if(irq >= 8)
		outb(PIC2_COMMAND,PIC_EOI);
	
	outb(PIC1_COMMAND,PIC_EOI);
}

初始化

当你进入保护模式时 (或者甚至开始之前,如果你没有使用 GRUB),你需要给两个PIC的第一个命令是初始化命令 (代码0x11)。 此命令使PIC在数据端口上等待3个额外的 “初始化字”。 这些字节给出PIC以下信息:

  • 其向量偏移。(ICW2)
  • 告诉它是如何连接到主/从的。(ICW3)
  • 提供有关环境的其他信息。(ICW4)
/* reinitialize the PIC controllers, giving them specified vector offsets
   rather than 8h and 70h, as configured by default */

#define ICW1_ICW4	0x01		/* ICW4 (not) needed */
#define ICW1_SINGLE	0x02		/* Single (cascade) mode */
#define ICW1_INTERVAL4	0x04		/* Call address interval 4 (8) */
#define ICW1_LEVEL	0x08		/* Level triggered (edge) mode */
#define ICW1_INIT	0x10		/* Initialization - required! */

#define ICW4_8086	0x01		/* 8086/88 (MCS-80/85) mode */
#define ICW4_AUTO	0x02		/* Auto (normal) EOI */
#define ICW4_BUF_SLAVE	0x08		/* Buffered mode/slave */
#define ICW4_BUF_MASTER	0x0C		/* Buffered mode/master */
#define ICW4_SFNM	0x10		/* Special fully nested (not) */

/*
arguments:
	offset1 - vector offset for master PIC
		vectors on the master become offset1..offset1+7
	offset2 - same for slave PIC: offset2..offset2+7
*/
void PIC_remap(int offset1, int offset2)
{
	unsigned char a1, a2;
	
	a1 = inb(PIC1_DATA);                        // save masks
	a2 = inb(PIC2_DATA);
	
	outb(PIC1_COMMAND, ICW1_INIT | ICW1_ICW4);  // starts the initialization sequence (in cascade mode)
	io_wait();
	outb(PIC2_COMMAND, ICW1_INIT | ICW1_ICW4);
	io_wait();
	outb(PIC1_DATA, offset1);                 // ICW2: Master PIC vector offset
	io_wait();
	outb(PIC2_DATA, offset2);                 // ICW2: Slave PIC vector offset
	io_wait();
	outb(PIC1_DATA, 4);                       // ICW3: tell Master PIC that there is a slave PIC at IRQ2 (0000 0100)
	io_wait();
	outb(PIC2_DATA, 2);                       // ICW3: tell Slave PIC its cascade identity (0000 0010)
	io_wait();
	
	outb(PIC1_DATA, ICW4_8086);
	io_wait();
	outb(PIC2_DATA, ICW4_8086);
	io_wait();
	
	outb(PIC1_DATA, a1);   // restore saved masks.
	outb(PIC2_DATA, a2);
}

请注意io_wait() 调用的存在,因为在较旧的计算机上,有必要给PIC一些时间来对命令做出反应,因为它们可能无法快速响应处理

禁用

如果要使用处理器本地APIC和IOAPIC,则必须首先禁用PIC。 这是通过以下方式完成的:

mov al, 0xff
out 0xa1, al
out 0x21, al

掩码

PIC有一个内部寄存器,称为IMR或中断掩码寄存器。 它是8位宽。 此寄存器是进入PIC的请求线路的位映射。 当设置了一个位时,PIC会忽略该请求并继续正常操作。 请注意,在较高的请求线路上设置掩码不会影响较低的线路。 屏蔽IRQ2会导致从PIC停止触发IRQs。

以下是如何屏蔽IRQ的示例:

void IRQ_set_mask(unsigned char IRQline) {
    uint16_t port;
    uint8_t value;

    if(IRQline < 8) {
        port = PIC1_DATA;
    } else {
        port = PIC2_DATA;
        IRQline -= 8;
    }
    value = inb(port) | (1 << IRQline);
    outb(port, value);        
}

void IRQ_clear_mask(unsigned char IRQline) {
    uint16_t port;
    uint8_t value;

    if(IRQline < 8) {
        port = PIC1_DATA;
    } else {
        port = PIC2_DATA;
        IRQline -= 8;
    }
    value = inb(port) & ~(1 << IRQline);
    outb(port, value);        
}

ISR和IRR

PIC芯片具有两个中断状态寄存器: 服务中寄存器 (ISR In-Service Register) 和中断请求寄存器 (IRR Interrupt Request Register)。 ISR告诉我们正在服务哪些中断,这意味着irq发送到CPU。 IRR告诉我们哪些中断被触发了。 基于中断掩码 (IMR),PIC将从IRR向CPU发送中断,此时它们在ISR中被标记。

可以通过OCW3命令字读取ISR和IRR。 这是发送到具有位3设置的命令端口之一 (0x20或0xa0) 的命令。 要读取ISR或IRR,请将适当的命令写入命令端口,然后读取命令端口 (而不是数据端口)。 要读取IRR,请写入0x0a。 要读取ISR,请写入0x0b。

ISR和IRR各为8位。 以下是如何从两个级联图片中读取16位值的ISR和IRR数据的示例:

#define PIC1_CMD                    0x20
#define PIC1_DATA                   0x21
#define PIC2_CMD                    0xA0
#define PIC2_DATA                   0xA1
#define PIC_READ_IRR                0x0a    /* OCW3 irq ready next CMD read */
#define PIC_READ_ISR                0x0b    /* OCW3 irq service next CMD read */

/* Helper func */
static uint16_t __pic_get_irq_reg(int ocw3)
{
    /* OCW3 to PIC CMD to get the register values.  PIC2 is chained, and
     * represents IRQs 8-15.  PIC1 is IRQs 0-7, with 2 being the chain */
    outb(PIC1_CMD, ocw3);
    outb(PIC2_CMD, ocw3);
    return (inb(PIC2_CMD) << 8) | inb(PIC1_CMD);
}

/* Returns the combined value of the cascaded PICs irq request register */
uint16_t pic_get_irr(void)
{
    return __pic_get_irq_reg(PIC_READ_IRR);
}

/* Returns the combined value of the cascaded PICs in-service register */
uint16_t pic_get_isr(void)
{
    return __pic_get_irq_reg(PIC_READ_ISR);
}

请注意,由于PIC的级联性质,每当设置任何PIC2位时,这些函数将显示位2 (0x0004) 为on。 另请注意,不必每次读取时都重置OCW3命令。 一旦为IRR或ISR设置了它,将来对CMD端口的读取将返回适当的寄存器。 芯片记住你使用了什么OCW3设置。 (免责声明: 我尚未测试最后一部分,但规范就是这么说的。)

虚假IRQs

当IRQ发生时,PIC芯片告诉CPU (通过PIC的INTR线路) 存在中断,CPU确认这一点并等待PIC发送中断向量。 这会产生冲突的情况: 如果IRQ在PIC告诉CPU有中断后消失,而在PIC向CPU发送中断向量之前,CPU会等待PIC告诉它哪个中断向量,但此时PIC又没有一个有效的中断向量来告诉CPU。

为了解决这个问题,PIC告诉CPU一个虚假的中断号。 这是一个虚假的IRQ。 假中断号是对应的PIC芯片的最低优先级中断号 (IRQ 7表示主PIC,IRQ 15表示从PIC)。

中断消失有几个原因。 根据我的经验,最常见的原因是软件在错误的时间发送EOI。 其他原因包括IRQ线路 (或INTR线路) 上的噪声。

处理虚假的IRQ

对于虚假的IRQ,没有真实的IRQ,并且不会设置PIC芯片的相应IRQ的ISR (服务寄存器) 标志。 这意味着中断处理程序不得将EOI发送回PIC以重置ISR标志。

处理IRQ 7的正确方法是首先检查主PIC芯片的ISR,以查看IRQ是虚假的IRQ还是真实的IRQ。 如果它是一个真正的IRQ,那么它与任何其他真正的IRQ一样对待。 如果它是虚假的IRQ,那么你将忽略它 (并且不发送EOI)。

处理IRQ 15的正确方法是类似的,但由于从PIC和主PIC之间的相互作用,这有点棘手。 首先检查从PIC芯片的ISR,看看IRQ是虚假的IRQ还是真实的IRQ。 如果它是一个真正的IRQ,那么它与任何其他真正的IRQ一样对待。 如果它是虚假的IRQ,那么不要将EOI发送到从PIC; 然而,你仍然需要将EOI发送到主PIC,因为主PIC本身不会知道它是从机的虚假IRQ。

还请注意,某些内核 (例如Linux) 会跟踪已发生的虚假IRQ的数量 (例如,通过在发生虚假IRQ时增加计数器)。 这对于检测软件中的问题 (例如在错误的时间发送EOIs) 和检测硬件中的问题 (例如线路噪声) 可能是有用的。


另见

文章

论坛主题

外部链接

de:Programmable Interrupt Controller