PCI 总线学习笔记(四)
PCI 总线学习系列,参考自
技术大牛博客: PCIe 扫盲系列博文连载目录篇
书籍:王齐老师的《PCI Express 体系结构导读》
下面的文章中加入了自己的一些理解和实际使用中遇到的一些场景,供日后查询和回忆使用
1、INTx 中断
PCI 总线使用 INTA#、INTB#、INTC# 和 INTD# 信号向处理器发出中断请求。这些中断请求信号为低电平有效(电平触发,这点要和边沿触发区别开来),并与处理器的中断控制器连接。在 PCI 体系结构中,这些中断信号属于边带信号(Sideband Signals),PCI 总线规范并没有明确规定在一个处理器系统中如何使用这些信号,因为这些信号对于 PCI 总线是可选信号。所谓边带信号是指这些信号在 PCI 总线中是可选信号,而且只能在一个处理器系统的内部使用,并不能离开这个处理器环境。
1.1、中断信号与中断控制器的连接关系
PCI 总线规范没有规定 PCI 设备的 INTx 信号如何与中断控制器的 IRQ_PINx# 信号相连,这为系统软件的设计带来了一定的困难,为此系统软件通常使用中断路由表存放 PCI 设备的 INTx 信号与中断控制器的连接关系。在 x86 处理器系统中,BIOS 可以提供这个中断路由表,而在 PowerPC 处理器中 Firmware 也可以提供这个中断路由表。
在一些简单的嵌入式处理器系统中,Firmware 并没有提供中断路由表,此时系统软件开发者需要事先了解 PCI 设备的 INTx 信号与中断控制器的连接关系(了解后在代码中固定变量的值)。此时外部设备与中断控制器的连接关系由硬件设计人员指定。
我们假设在一个处理器系统中,共有 3 个 PCI 插槽(分别为 PCI 插槽 A、B 和 C),这些 PCI 插槽与中断控制器的 IRQ_PINx 引脚(分别为IRQW#、IRQX#、IRQY#和IRQZ#)可以按照下图所示的拓扑结构进行连接。
此时,PCI 插槽 A、B、C 的 INTA#、INTB# 和 INTC# 信号将分散连接到中断控制器的 IRQW#、IRQX# 和 IRQY# 信号,而所有 INTD# 信号将共享一个 IRQZ# 信号。采用这种连接方式时,整个处理器系统使用的中断请求信号,其负载较为均衡。而且这种连接方式保证了每一个插槽的 INTA# 信号都与一根独立的 IRQx# 信号对应,从而提高了 PCI 插槽中断请求的效率。
INTx 中断在设计时,就要注意负载均衡的问题
在一个处理器系统中,多数 PCI 设备仅使用 INTA# 信号,很少使用 INTB# 和 INTC# 信号,而 INTD# 信号更是极少使用。在 PCI 总线中,PCI 设备配置空间的 Interrupt Pin 寄存器记录该设备究竟使用哪个INTx信号。
1.2、中断信号与 PCI 总线的连接关系
上面的情况还比较简单,是 PCI 设备与中断控制器直接相连。现代处理器,还经常使用 PCI 桥进行 PCI 总线扩展,情况更为复杂。
在 PCI 总线中,INTx 信号属于边带信号。PCI 桥也不会处理这些边带信号。这给 PCI 设备将中断请求发向处理器带来了一些困难,特别是给挂接在 PCI 桥之下的 PCI 设备进行中断请求带来了一些麻烦。
在一些嵌入式处理器系统中,这个问题较易解决。因为嵌入式处理器系统很清楚在当前系统中存在多少个 PCI 设备,这些 PCI 设备使用了哪些中断资源。在这类处理器系统中,可能并不含有 PCI 桥,因而 PCI 设备的中断请求信号与中断控制器的连接关系较易确定。在多数情况下,嵌入式处理器系统使用的 PCI 设备仅使用 INTA# 信号进行中断请求,所以只要将这些 INTA# 信号挂接到中断控制器的独立 IRQ_PIN# 引脚上即可。这样每一个 PCI 设备都可以独占一个单独的中断引脚。
而在 x86 处理器系统中,这个问题需要 BIOS 参与来解决。在 x86 处理器系统中,有许多 PCI 插槽,处理器系统并不知道在这些插槽上将要挂接哪些 PCI 设备,而且也并不知道这些 PCI 设备到底需不需要使用所有的 INTx# 信号线。因此 x86 处理器系统必须要对各种情况进行处理。
x86 处理器系统还经常使用 PCI 桥进行 PCI 总线扩展,扩展出来的 PCI 总线还可能挂接一些 PCI 插槽,这些插槽上 INTx# 信号仍然需要处理。PCI 桥规范并没有要求桥片传递其下 PCI 设备的中断请求。事实上多数 PCI 桥也没有为下游 PCI 总线提供中断引脚 INTx#,管理其下游总线的 PCI 设备。但是 PCI 桥规范推荐(不是一定)使用下面的表建立下游 PCI 设备的 INTx 信号与上游 PCI 总线 INTx 信号之间的映射关系:
这里可以理解为,将下游 PCI 设备的 INTx 信号线,接到上游提供 INTx 中断引脚的 PCI 总线上。例如接到 HOST 主桥上,由 HOST 主桥对 INTx 中断进行统一整合后,发送到 CPU 的中断控制器上
我们举例说明该表的含义。在 PCI 桥下游总线上的 PCI 设备,如果其设备号为 0,那么这个设备的 INTA# 引脚将和 PCI 总线的 INTA# 引脚相连;如果其设备号为 1,其 INTA# 引脚将和 PCI 总线的 INTB# 引脚相连;如果其设备号为 2,其 INTA# 引脚将和 PCI 总线的 INTC# 引脚相连;如果其设备号为 3,其 INTA# 引脚将和 PCI 总线的 INTD# 引脚相连…
这里可以理解为,将下游 PCI 设备的 INTx 信号线,接到上游提供 INTx 中断引脚的 PCI 总线上。
如果接到 PCI 桥上时,则会由 PCI 桥对 INTx 中断进行第二次路由,再接往上一级的 PCI 桥… 直到找到 HOST 主桥为止。由 HOST 主桥对 INTx 中断进行统一整合后,发送到 CPU 的中断控制器上。
从上面可以看出,我们从 PCI 设备配置空间的 Interrupt Pin 寄存器读出的 INTx 中断线,可能并不是实际在 CPU 端产生的 INTx 中断。
例如我们从 Interrupt Pin 寄存器读出 PCI 设备使用的 INTA 中断,但实际到 CPU 端时,产生的是 INTD 中断。因为有 PCI 桥的存在,INTA 中断可能经过了多次路由
在 x86 处理器系统中,由 BIOS 或者 APCI 表记录 PCI 总线的 INTA~D# 信号与中断控制器之间的映射关系,保存这个映射关系的数据结构也被称为中断路由表。大多数 BIOS 使用上表中的映射关系,这也是绝大多数 BIOS 支持的方式。如果在一个 x86 处理器系统中,PCI 桥下游总线的 PCI 设备使用的中断映射关系与此不同,那么系统软件程序员需要改动 BIOS 中的中断路由表。
1.3、小结
其实从上面的信息不难看出,虽然设备可能使用的是 INTA# 中断,但是真正到了 CPU 的中断控制器端,可能是 IRQA#、 IRQB#,也可能是 IRQC#,因为中间可能经过了多次路由。而不同芯片厂家、板卡厂家的硬件设计不同,导致对于软件开发人员来说,需要详细了解硬件上的设计与规则,而后软件端才能做出相应的处理。
实际 PCI 总线驱动开发中,我们要实现的就是 pci_host_bridg
结构中的 map_irq
函数,来实现 INTx 中断映射功能。
2、Linux 中的 INTx 中断路由
在 pci_host_bridg
结构中,有一个函数 map_irq
,该函数就是对 INTx 中断映射的实现。函数入参为 PCI 设备 Interrupt Pin 寄存器读出的值,返回值为最终经过路由到 CPU 端对应的中断号。
该函数的实现通常和具体的板卡相关(Linux 内核也有一个默认实现,就是 PCI 协议中推荐的 INTx 映射方式),在板卡相关的 pci 驱动中实现。
struct pci_host_bridge {
struct device dev;
struct pci_bus *bus; /* Root bus */
struct pci_ops *ops;
void *sysdata;
int busnr;
struct list_head windows; /* resource_entry */
u8 (*swizzle_irq)(struct pci_dev *, u8 *); /* Platform IRQ swizzler */
int (*map_irq)(const struct pci_dev *, u8, u8);
void (*release_fn)(struct pci_host_bridge *);
......
}
以 Linux 源码为例:
int dw_pcie_host_init(struct pcie_port *pp)
{
struct pci_host_bridge *bridge;
......
bridge = devm_pci_alloc_host_bridge(dev, 0);
if (!bridge)
return -ENOMEM;
......
bridge->map_irq = of_irq_parse_and_map_pci;
......
}
of_irq_parse_and_map_pci
即为 Linux 提供的默认 INTx 中断映射的实现。有些板卡在硬件设计上,如果不遵循 PCI 协议中推荐的 INTx 中断映射的实现,也可以自己实现。
2.1 源码详解
函数调用关系:
of_irq_parse_and_map_pci
----of_irq_parse_pci
|----pci_swizzle_interrupt_pin
----irq_create_of_mapping
of_irq_parse_pci
函数的主体就是,根据当前设备,逐步往上遍历其父节点、父节点的父节点…,每找到一个父节点,就进行一次 INTx 中断映射(映射方式就是 PCI 协议中推荐的映射方式),直到找到 HOST 主桥为止。具体映射的方法由 pci_swizzle_interrupt_pin
函数实现。
/**
* of_irq_parse_pci - Resolve the interrupt for a PCI device
* @pdev: the device whose interrupt is to be resolved
* @out_irq: structure of_irq filled by this function
*
* This function resolves the PCI interrupt for a given PCI device. If a
* device-node exists for a given pci_dev, it will use normal OF tree
* walking. If not, it will implement standard swizzling and walk up the
* PCI tree until an device-node is found, at which point it will finish
* resolving using the OF tree walking.
*/
static int of_irq_parse_pci(const struct pci_dev *pdev, struct of_phandle_args *out_irq)
{
......
/* Now we walk up the PCI tree */
for (;;) {
/* Get the pci_dev of our parent */
ppdev = pdev->bus->self;
/* Ouch, it's a host bridge... */
if (ppdev == NULL) {
ppnode = pci_bus_to_OF_node(pdev->bus);
/* No node for host bridge ? give up */
if (ppnode == NULL) {
rc = -EINVAL;
goto err;
}
} else {
/* We found a P2P bridge, check if it has a node */
ppnode = pci_device_to_OF_node(ppdev);
}
/*
* Ok, we have found a parent with a device-node, hand over to
* the OF parsing code.
* We build a unit address from the linux device to be used for
* resolution. Note that we use the linux bus number which may
* not match your firmware bus numbering.
* Fortunately, in most cases, interrupt-map-mask doesn't
* include the bus number as part of the matching.
* You should still be careful about that though if you intend
* to rely on this function (you ship a firmware that doesn't
* create device nodes for all PCI devices).
*/
if (ppnode)
break;
/*
* We can only get here if we hit a P2P bridge with no node;
* let's do standard swizzling and try again
*/
pin = pci_swizzle_interrupt_pin(pdev, pin);
pdev = ppdev;
}
......
}
可以看到,我们最终找到了当前设备使用的 INTx 中断对应的 CPU 端产生的 INTx 中断(变量pin)。完成了INTx 中断路由。
u8 pci_swizzle_interrupt_pin(const struct pci_dev *dev, u8 pin)
{
int slot;
if (pci_ari_enabled(dev->bus))
slot = 0;
else
slot = PCI_SLOT(dev->devfn);
return (((pin - 1) + slot) % 4) + 1;
}
pci_swizzle_interrupt_pin
函数的实现逻辑,其实就是 PCI 协议中,推荐的 INTx 中断映射的方式:

2.2 设备树中的参数解析
设备树中,和 INTx 中断相关的,就是 interrupt-map-mask
和 interrupt-map
两个属性。还记得我们在 3.1 章节讲解的 of_irq_parse_pci
函数么,该函数完成了中断路由(映射)后,会根据最终的映射结果(INTA or INTB or INTC or INTD),寻找对应 CPU 端的中断控制器的中断号,这个寻找的过程,会去解析设备树、使用 interrupt-map-mask
和 interrupt-map
两个属性(of_irq_parse_raw),如下:
static int of_irq_parse_pci(const struct pci_dev *pdev, struct of_phandle_args *out_irq)
{
......
/* Now we walk up the PCI tree */
for (;;) {
......
/*
* We can only get here if we hit a P2P bridge with no node;
* let's do standard swizzling and try again
*/
pin = pci_swizzle_interrupt_pin(pdev, pin);
......
}
out_irq->np = ppnode;
out_irq->args_count = 1;
out_irq->args[0] = pin;
laddr[0] = cpu_to_be32((pdev->bus->number << 16) | (pdev->devfn << 8));
laddr[1] = laddr[2] = cpu_to_be32(0);
/*
* 解析设备树节点,根据 pin 值找到对应的 CPU 端中断控制器的中断号
*/
rc = of_irq_parse_raw(laddr, out_irq);
......
}
以下面代码为例:
pcie3x1: pcie@fe270000 {
compatible = "rockchip,rk3568-pcie";
#address-cells = <3>;
#size-cells = <2>;
bus-range = <0x0 0xf>;
......
interrupt-map-mask = <0 0 0 7>;
interrupt-map = <0 0 0 1 &pcie3x1_intc 0>,
<0 0 0 2 &pcie3x1_intc 1>,
<0 0 0 3 &pcie3x1_intc 2>,
<0 0 0 4 &pcie3x1_intc 3>;
......
pcie3x1_intc: legacy-interrupt-controller {
interrupt-controller;
#address-cells = <0>;
#interrupt-cells = <1>;
interrupt-parent = <&gic>;
interrupts = <GIC_SPI 157 IRQ_TYPE_EDGE_RISING>;
};
};
- interrupt-map-mask 定义了 interrupt-map 中哪些字段有效,用于筛选子设备的中断信息
- interrupt-map 字段则用来描述设备(这里指的就是 HOST Bridge )和中断控制器之间的映射关系
- interrupt-map 的格式可以分为以下几个部分:
<子中断设备地址><子中断设备标识符><父中断控制器的引用><父中断控制器地址><父中断线的标识符>
- 子中断设备地址:因为 #address-cells = <3>,所以地址由 3 个 u32组成。但是由于 interrupt-map-mask 前 3 个 u32 都是 0 ,所以,该地址不需要关心。
- 子中断设备标识符:PCI 设备所使用的 INTx 中断线。1 表示 INTA,2 表示 INTB …
- 父中断控制器的引用:指向了父中断控制器节点
- 父中断控制器地址:因为父中断控制器 pcie3x1_intc 的 #address-cells = <0>,所以这里就省略了
- 父中断线的标识符:父中断控制器的中断线
再以下面的代码为例:
pcie: pcie {
compatible = "pci-host-ecam-generic";
device_type = "pci";
#address-cells = <3>;
#size-cells = <2>;
#interrupt-cells = <1>;
reg = <0x0 0x40000000 0x0 0x10000000>;
msi-parent = <&its>;
bus-range = <0x0 0xff>;
interrupt-map-mask = <0x0 0x0 0x0 0x7>;
interrupt-map = <0x0 0x0 0x0 0x1 &gic 0x0 0x0 0 28 4>,
<0x0 0x0 0x0 0x2 &gic 0x0 0x0 0 29 4>,
<0x0 0x0 0x0 0x3 &gic 0x0 0x0 0 30 4>,
<0x0 0x0 0x0 0x4 &gic 0x0 0x0 0 31 4>;
......
}
以 interrupt-map 字段中的第一条为例:
0x0 0x0 0x0
:子中断设备地址:因为#address-cells = <3>
,所以地址由 3 个 u32 组成。但是由于interrupt-map-mask
前 3 个 u32 都是 0 ,所以,该地址不需要关心。0x1
:当前 PCI 设备使用的是 INTA 中断引脚&gic
:表明了当前设备的父中断控制器引用为 gic0x0 0x0 0
:对于 GIC 控制器来说,表示 GIC_SPI 类型中断28
:表明了 INTA 中断引脚对应连接到 GIC 的 28 号中断线4
:对 GIC 控制器来说,触发方式为高电平有效
关于设备树中的中断解析,可参考这篇文章:
设备树中断配置解析
如果想深入研究 Linux 下的设备树节点详情,可参考这篇文章:
The Devicetree
2.3 关于 map_irq 接口的调用逻辑
pci_device_probe
-> pci_assign_irq
这其中,pci_device_probe
接口为每个 PCI 设备都会调用一次
void pci_assign_irq(struct pci_dev *dev)
{
u8 pin;
u8 slot = -1;
int irq = 0;
struct pci_host_bridge *hbrg = pci_find_host_bridge(dev->bus);
if (!(hbrg->map_irq)) {
pci_dbg(dev, "runtime IRQ mapping not provided by arch\n");
return;
}
/* If this device is not on the primary bus, we need to figure out
which interrupt pin it will come in on. We know which slot it
will come in on 'cos that slot is where the bridge is. Each
time the interrupt line passes through a PCI-PCI bridge we must
apply the swizzle function. */
pci_read_config_byte(dev, PCI_INTERRUPT_PIN, &pin);
/* Cope with illegal. */
if (pin > 4)
pin = 1;
if (pin) {
/* Follow the chain of bridges, swizzling as we go. */
if (hbrg->swizzle_irq)
slot = (*(hbrg->swizzle_irq))(dev, &pin);
/*
* If a swizzling function is not used map_irq must
* ignore slot
*/
irq = (*(hbrg->map_irq))(dev, slot, pin);
if (irq == -1)
irq = 0;
}
dev->irq = irq;
pci_dbg(dev, "assign IRQ: got %d\n", dev->irq);
/* Always tell the device, so the driver knows what is
the real IRQ to use; the device does not use it. */
pci_write_config_byte(dev, PCI_INTERRUPT_LINE, irq);
}
3、MSI 中断
PCI 总线 V2.2 规范还定义了一种新的中断机制,即 MSI 中断机制。MSI 中断机制采用存储器写总线事务(Memory Write)向处理器系统提交中断请求,其实现机制是向 HOST 处理器指定的一个存储器地址写指定的数据。
存储器地址、数据都保存在设备配置空间的 MSI Capability 中,由设备主动去产生写事务,这是一个硬件过程。在 PCI 设备驱动初始化时,会把存储器地址和数据写到设备配置空间的 MSI Capability 中
这个存储器地址一般是中断控制器规定的某段存储器地址范围,而且数据也是事先安排好的数据,通常含有中断向量号。HOST 主桥会将 MSI 这个特殊的存储器写总线事务进一步翻译为中断请求,提交给处理器。目前 PCIe 和 PCI-X 设备必须支持 MSI 中断机制,但是 PCI 设备并不一定都支持 MSI 中断机制。
目前 MSI 中断机制虽然在 PCIe 总线上已经成为主流,但是在 PCI 设备中并不常用。即便是支持 MSI 中断机制的 PCI 设备,在设备驱动程序的实现中也很少使用这种机制。首先 PCI 设备具有 INTx# 信号可以传递中断,而且这种中断传送方式在 PCI 总线中根深蒂固。其次 PCI 总线是一个共享总线,传递 MSI 中断需要占用 PCI 总线的带宽,需要进行总线仲裁等一系列过程,远没有使用 INTx# 信号线直接。
一个设备最高支持 32 个 MSI 中断向量,因此向处理器系统提交中断请求的同时,可以通知处理器系统产生该中断的原因,即通过不同中断向量号表示中断请求的来源。当处理器系统执行中断服务例程时,不需要读取 PCI 设备的中断状态寄存器,获得中断请求的来源,从而在一定程度上提高了中断处理的效率 。
下面是和 MSI 相关的几个寄存器详细说明:
如下图所示,MSI有四种类型
其中 Capability ID 的值是只读的,05h 表示支持 MSI 功能。
-
Next Capability Pointer 也是只读的,其用于查找下一个 Capability Structure 的位置,其值为 00h 则表示到达Linked List的最后了
-
Message Control Register 用于确定 MSI 的格式与支持的功能等信息,如下图所示:
-
15:9 :系统软件读取该字段时将返回全零,对此字段写无意义
-
8: 表示支持带中断 Masking 的结构;如果为 0,表示不支持带中断 Masking 的结构(只读)
-
7 :该位为 1 时,表示支持 64 位地址结构;如果为0,表示只能支持带 32 位地址结构 (只读)
-
6:4:该字段可读写,表示软件实际分配给当前 PCI 设备的中断向量数目
-
3:1:表示当前 PCI 设备最多可以使用几个中断向量号(只读)
-
0:该位可读写,是 MSI 中断机制的使能位
-
-
Message Address:当 MSI Enable 位有效时,该寄存器存放目标存储器写事务地址的低 32 位,该寄存器的 32:2 字段有效,系统软件可以对该字段进行读写操作,位[1:0]为 0
-
Message UpperAddress:当 MSI Enable 位有效时,该寄存器存放目标存储器写事务地址的高 32 位
存储器地址、数据都保存在设备配置空间的 MSI Capability 中。当设备产生中断时,由设备主动去产生写事务,这是一个硬件过程。我们需要做的只是在 PCI 总线在初始化时,把存储器地址和数据写到设备配置空间的 MSI Capability 中
- Next Pointer:下一个 MSI Capability 结构体的地址
- Message Data[15:0]:当 MSI Enable 位有效时,该字段存放 MSI 报文使用的中断数据,PCI 设备可以通过改变 Message Data 中的数据发送不同的中断请求(具体里面的数据该如何组织,完全是由 PCI 控制器、中断控制器决定的)
在 MSI 模式 下,PCI 设备只有 一个 MSI 结构(capability),但它可以支持多个中断(Multiple Messages)。
系统软件在配置 MSI 时,会:
1、分配一组连续的中断向量(如 vector 32 - 35)
2、填充 Message Data 的基础值(通常是最低编号的向量,例如 vector 32)
3、设置 MME(Multiple Message Enable)字段,允许设备修改 Message Data 的低位(MSI 中,硬件是可以主动修改 Message Data 字段的)
- Mask Bits(Optional):中断掩码寄存器。PCI 总线规定当一个设备使用 MSI 中断机制时,最多使用 32 个中断向量,一个设备最多发送 32 种中断请求,所以Mask Bits为 32 位,应位为 1 时表示相应的中断被屏蔽
- Pending Bits(Optional):待处理中断寄存器。该字段对于系统软件可读,PCI 设备内部逻辑可以改变该字段的值。该字段的长度为 32 位,与 Mask 字段配合使用,当一个中断对应的 Mask bit 位为1(屏蔽)时,Pending Bits 字段对应的位将被 PCI 内部逻辑置一,此时 PCI 设备并不会向中断控制器发送 MSI 中断报文;当系统软件将对应的 Mask bit 位由 1 改为 0 时,PCI 设备向中断控制器发送 MSI 中断报文(在 PCI 设备驱动程序开发时,有时需要 Mask Bits 和 Pending Bits 字段配合防止处理器丢失中断请求)
4、错误处理
PCI 设备可以通过奇偶校检来检测到来自 AD 上的地址或者数据的错误,并通过 PERR# 或者SERR# 报告错误。但是需要注意的是,PCI Spec 并未规定任何硬件层面上的错误处理或者恢复机制,因此,这些错误都只能通过软件进行处理。