往期内容
本文章相关专栏往期内容,PCI/PCIe子系统专栏:
- 嵌入式系统的内存访问和总线通信机制解析、PCI/PCIe引入
- 深入解析非桥PCI设备的访问和配置方法
- PCI桥设备的访问方法、软件角度讲解PCIe设备的硬件结构
- 深入解析PCIe设备事务层与配置过程
- PCIe的三种路由方式
- PCI驱动与AXI总线框架解析(RK3399)
- 深入解析PCIe地址空间与寄存器机制:从地址映射到TLP生成的完整流程
- PCIe_Host驱动分析_地址映射
- PCIe_Host驱动分析_设备枚举
- PCI/PCIe设备INTx中断机制和MSI中断机制
- MSI-X中断机制、MSI/MSI-X操作流程详解
Uart子系统专栏:
- 专栏地址:Uart子系统
- Linux内核早期打印机制与RS485通信技术
-- 末片,有专栏内容观看顺序interrupt子系统专栏:
- 专栏地址:interrupt子系统
- Linux 链式与层级中断控制器讲解:原理与驱动开发
-- 末片,有专栏内容观看顺序pinctrl和gpio子系统专栏:
专栏地址:pinctrl和gpio子系统
编写虚拟的GPIO控制器的驱动程序:和pinctrl的交互使用
-- 末片,有专栏内容观看顺序
input子系统专栏:
- 专栏地址:input子系统
- input角度:I2C触摸屏驱动分析和编写一个简单的I2C驱动程序
-- 末片,有专栏内容观看顺序I2C子系统专栏:
- 专栏地址:IIC子系统
- 具体芯片的IIC控制器驱动程序分析:i2c-imx.c-CSDN博客
-- 末篇,有专栏内容观看顺序总线和设备树专栏:
- 专栏地址:总线和设备树
- 设备树与 Linux 内核设备驱动模型的整合-CSDN博客
-- 末篇,有专栏内容观看顺序
目录
- 往期内容
- 1.前言
- 2.中断处理流程
- 2.设备树和支持的中断
- 3.注册PCIe控制器的handle
- 4.创建控制器的domain
- 5.创建映射关系
-
- [5.1 如何映射](#5.1 如何映射)
- [5.2 代码体现](#5.2 代码体现)
1.前言
参考资料:
- 《PCI_SPEV_V3_0.pdf》6.8节
- 《devicetree-specification-v0.2.pdf》
开发板资料:
分析的文件:
linux-4.4_rk3399\drivers\pci\host\pcie-rockchip.c
📎pcie-rockchip.c
2.中断处理流程
要分析PCIe设备中断号的分配过程,需要从RK3399的芯片资料开始学习。
层级结构为:PCIe设备 => PCIe控制器 => GIC =>CPU
设备发生中断:PICe设备1 --> PCIe控制器 --> GIC --> CPU
那就从软件上来讲,也就是处理流程,根据之前在中断子系统说讲的,如下:
(1)CPU肯定是会去跳到vector向量表,去对中断现场进行保存,调用GIC中的handle函数,这里假设是gic_handle_irq (\Linux-4.9.88\drivers\irqchip\irq-gic-v3.c)
(2)GIC的handle函数会执行:
- 读GIC的寄存器,去获得hwirq_1硬件中断号 ,假设是80
- 得到硬件中断号了,那接下来肯定就是在domain找到其对于的virq_1,假设是221
- 得到硬件中断号了,就可以找到其PCIe控制器的irq_desc中断描述符,也就是irq_desc[virq_1]
- 调用irq_desc[virq_1].handle_irq,这个函数是PCIe控制器提供的
(3)irq_desc[virq_1].handle_irq假设是rockchip_pcie_legacy _int handler(\Linux-4.9.88\drivers\pci\host\pcie-rockchip.c)
- PCIe控制器要做的事情也和GIC差不多,获取设备硬件中断hwirq_2,这里是10
- 获取virq_2:10 --> 225(假设)
- 得到PCIe设备1的irq_desc:irq_desc[virq_2]
- 调用irq_desc[virq_2].irq_handle
(4)调用irq_desc[virq_2].irq_handle???其实就是调用PCIe设备驱动request_irq的func,也就是irq_desc[virq_2].action.handler(func)
这里只是对流程进行简单讲解,还有一些mask屏蔽中断的操作,在中断子系统专栏有讲。
2.设备树和支持的中断
c
gic: interrupt-controller@fee00000 {
compatible = "arm,gic-v3";
#interrupt-cells = <4>;
#address-cells = <2>;
#size-cells = <2>;
ranges;
interrupt-controller;
/* 省略 */
};
pcie0: pcie@f8000000 {
compatible = "rockchip,rk3399-pcie";
#address-cells = <3>;
#size-cells = <2>;
aspm-no-l0s;
clocks = <&cru ACLK_PCIE>, <&cru ACLK_PERF_PCIE>,
<&cru PCLK_PCIE>, <&cru SCLK_PCIE_PM>;
clock-names = "aclk", "aclk-perf",
"hclk", "pm";
bus-range = <0x0 0x1f>;
max-link-speed = <1>;
linux,pci-domain = <0>;
msi-map = <0x0 &its 0x0 0x1000>;
interrupts = <GIC_SPI 49 IRQ_TYPE_LEVEL_HIGH 0>,
<GIC_SPI 50 IRQ_TYPE_LEVEL_HIGH 0>,
<GIC_SPI 51 IRQ_TYPE_LEVEL_HIGH 0>;
interrupt-names = "sys", "legacy", "client";
#interrupt-cells = <1>;
interrupt-map-mask = <0 0 0 7>;
interrupt-map = <0 0 0 1 &pcie0_intc 0>,
<0 0 0 2 &pcie0_intc 1>,
<0 0 0 3 &pcie0_intc 2>,
<0 0 0 4 &pcie0_intc 3>;
};
对于RK3399,PCIe控制器可以向GIC发出3个中断:sys、legacy、client:
- sys:下图中Event ID为81,就是SPI 49号中断(81=32+49),用来处理一些系统性的中断,比如电源状态、热拔插
- legacy:用来处理PCIe设备发来的INTA/INTB/INTC/INTD中断
- client:跟外接的PCIe设备通信时,可能会发送传输错误,用这个中断来处理
接下来的代码分析就是以legacy为主
3.注册PCIe控制器的handle
c
interrupts = <GIC_SPI 49 IRQ_TYPE_LEVEL_HIGH 0>,
<GIC_SPI 50 IRQ_TYPE_LEVEL_HIGH 0>,
<GIC_SPI 51 IRQ_TYPE_LEVEL_HIGH 0>;
interrupt-names = "sys", "legacy", "client";
对应的设备树部分,根据PCIe设备选择的是哪种中断方式,来设置PCIe控制器的handle,这里是 <GIC_SPI 50 IRQ_TYPE_LEVEL_HIGH 0>,也就是legacy的方式,其实50+32就是硬件中断号,PCIe控制器要发送到中断控制器的hwirq82
rockchip_pcie_init_irq_domain
中是否完成映射?
rockchip_pcie_init_irq_domain
是创建一个 IRQ domain 的过程。这个函数通过调用irq_domain_add_linear
创建了一个线性 IRQ domain,这个 domain 是 PCIe
控制器用于管理挂接到其上面的设备的 INTx 中断(如 INTA、INTB、INTC、INTD)。此时,还没有具体到 PCIe
设备的中断映射,只是为 PCIe 控制器建立了一个 虚拟中断控制器域,供后续 PCIe 设备使用。- 当 PCIe 设备扫描并初始化时,会根据该域为设备映射中断号,所以这个函数仅仅是 为 PCIe 控制器的 INTx 引脚建立域,具体的映射会在之后处理设备中断时完成。
pcie0_intc
是虚拟中断控制器,它是管理 INTA# 的设备吗?
pcie0_intc
是一个虚拟的中断控制器节点,它负责管理连接到 PCIe 控制器的所有 PCIe 设备的 INTx 中断 (包括 INTA、INTB、INTC、INTD)。每个 INTx 中断引脚会根据设备树的interrupt-map
进行映射。通过设备树的interrupt-map
,会为连接到 PCIe 控制器的设备的中断分配具体的中断号。- 并不是专门对应 INTA# ,而是通过映射机制为连接到 PCIe 的多个设备分配中断号。PCIe 设备的 INTx 中断(INTA, INTB, INTC, INTD)会映射到对应的硬件中断,
pcie0_intc
管理所有挂在 PCIe
控制器上的设备中断。
- PCIe 设备中断映射的过程:代码流程解析
这个流程实际上是描述了 PCIe 设备初始化时的中断号分配过程 。在这个过程中,系统会扫描挂接到 PCIe
控制器的设备,并为每个设备分配中断号。流程大致如下:
设备扫描与初始化:
- 调用
pci_scan_root_bus
和相关函数递归扫描并初始化挂接在 PCIe 总线上的所有设备。pci_scan_single_device
负责扫描单个 PCIe 设备,通过pci_setup_device
设置其硬件资源,并通过pci_device_add
添加设备。解析设备中断信息:
- 在
pci_device_add
中,调用pcibios_add_device
,最终调用of_irq_parse_and_map_pci
来解析设备的中断信息。of_irq_parse_and_map_pci
通过读取 PCIe 设备的中断引脚号(INTx
),调用设备树的interrupt-map
,映射设备的中断号。这里解析设备树中interrupt-map
的过程是在
of_irq_parse_raw
中完成的。分配中断号:
irq_create_of_mapping
最终为该设备创建一个中断号,并将其存储在pci_dev->irq
中。此时,PCIe 设备的中断号已经完成映射。设备中断号的存储:
- 映射得到的中断号会存放在
pci_dev->irq
中,后续可以通过该中断号为设备注册中断处理函数。
- 映射的具体过程
- 映射的过程 发生在调用
of_irq_parse_and_map_pci
及其后续调用的of_irq_parse_raw
函数时。此时,设备的 PCIe 配置空间中的中断引脚号(INTx)会通过
interrupt-map
的定义映射到虚拟中断控制器(如pcie0_intc
),并得到一个实际的中断号。- 比如
interrupt-map = <0 0 0 1 &pcie0_intc 0>,
,当设备配置空间中的interrupt pin
为1
时(即使用 INTA# 中断),映射到&pcie0_intc 0
,并得到相应的中断号。
- 总结:映射与域的关系
rockchip_pcie_init_irq_domain
的作用是为 PCIe 控制器创建一个域,供后续设备映射中断号使用。这是一个准备工作,域的创建并不代表已经完成了映射,而是为 PCIe 设备的中断号分配提供了基础。- 映射真正发生在
of_irq_parse_and_map_pci
函数中,它解析设备树的interrupt-map
并将 PCIe 设备的中断号与虚拟中断控制器pcie0_intc
进行关联,最终得到具体的硬件中断号。
c
static void rockchip_pcie_legacy_int_handler(struct irq_desc *desc)
{
//........
irq = platform_get_irq_byname(pdev, "legacy"); //---(1)
if (irq < 0) {
dev_err(dev, "missing legacy IRQ resource\n");
return -EINVAL;
}
irq_set_chained_handler_and_data(irq,
rockchip_pcie_legacy_int_handler,
rockchip); // ------(2)
//........
}
(1)这里 platform_get_irq_byname()
函数通过设备树中的 interrupt-names
获取与 "legacy"
对应的 GIC 中断线(即 GIC_SPI 50
),并将其传递给 irq_set_chained_handler_and_data()
来设置中断处理函数。
(2) 将 GIC 中断处理与 PCIe 控制器的中断处理流程连接起来。设置了PCIe控制器的irq_desc[].handle_irq 为 rockchip_pcie_legacy_int_handler,采用legacy的中断方式,设置函数如下:
c
void
irq_set_chained_handler_and_data(unsigned int irq, irq_flow_handler_t handle,
void *data)
{
unsigned long flags;
struct irq_desc *desc = irq_get_desc_buslock(irq, &flags, 0);
if (!desc)
return;
desc->irq_common_data.handler_data = data;
__irq_do_set_handler(desc, handle, 1, NULL);
irq_put_desc_busunlock(desc, flags);
}
其实也很好懂,上锁,根据virq获取中断描述符,设置其data和handler_irq,解锁
rockchip_pcie_legacy_int_handler函数要进一步分辨设置的是INTA还是INTB、INTC、INTD中断:
上图,每个INTx#都有对应的irq_desc中断描述符,发生中断后PCIe控制器调用了rockchip_pcie_legacy_int_handler,去分辨
c
static void rockchip_pcie_legacy_int_handler(struct irq_desc *desc)
{
struct irq_chip *chip = irq_desc_get_chip(desc);
struct rockchip_pcie *rockchip = irq_desc_get_handler_data(desc);
struct device *dev = rockchip->dev;
u32 reg;
u32 hwirq;
u32 virq;
chained_irq_enter(chip, desc);
reg = rockchip_pcie_read(rockchip, PCIE_CLIENT_INT_STATUS); // ----(1)
reg = (reg & PCIE_CLIENT_INTR_MASK) >> PCIE_CLIENT_INTR_SHIFT;
while (reg) {
hwirq = ffs(reg) - 1;
reg &= ~BIT(hwirq);// ----(2)
virq = irq_find_mapping(rockchip->irq_domain, hwirq);// ----(3)
if (virq)
generic_handle_irq(virq);
else
dev_err(dev, "unexpected IRQ, INT%d\n", hwirq);
}
chained_irq_exit(chip, desc);
}
(1)读取中断状态寄存器 :这两行代码从 PCIe 设备读取中断状态寄存器(PCIE_CLIENT_INT_STATUS
)。该寄存器通常包含有关当前触发的中断的信息。通过将寄存器值与中断掩码 (PCIE_CLIENT_INTR_MASK
) 进行按位与操作,然后通过右移操作,提取出实际的中断状态。
(2)处理每个中断:
- 逐个处理中断 :
while (reg)
循环用来处理寄存器中每一个设置为 1 的中断标志。 - 获取硬件中断号 :
hwirq = ffs(reg) - 1;
通过调用ffs
(find first set)函数来查找reg
中最右侧的 1 的位置,得到的值就是当前中断的硬件 IRQ(hwirq
)。 - 清除当前中断 :
reg &= ~BIT(hwirq);
通过清除当前处理中断的位来避免重复处理。
(3)映射和触发虚拟中断:
- 查找虚拟 IRQ :通过
irq_find_mapping
函数将硬件 IRQ (hwirq
) 映射到对应的虚拟 IRQ (virq
)。这个映射关系是在初始化时通过irq_domain
创建的(下小点会讲到),它定义了硬件 IRQ 和内核中的虚拟 IRQ 之间的关系。 - 触发 IRQ 处理程序 :如果找到有效的虚拟 IRQ,调用
generic_handle_irq(virq)
触发相应的中断处理程序。这个函数会调用与virq
相关联的中断处理程序,执行实际的中断处理逻辑。
4.创建控制器的domain
PCIe控制器的节点里有一个更下一级的中断控制器,这是一个虚拟的中断控制器,是对PCIe控制器的进一步的信息设置。创建的域实际上是 PCIe 控制器的 ,它主要用于管理连接到该 PCIe 控制器的所有 PCIe 设备的中断映射关系。具体来说,创建的 IRQ domain 会负责映射 PCIe 设备的中断号,确保每个设备的中断号与系统的中断控制器之间进行正确的转换。
c
pcie0: pcie@f8000000 {
#address-cells = <3>;
#interrupt-cells = <1>;
interrupt-map-mask = <0 0 0 7>;
interrupt-map = <0 0 0 1 &pcie0_intc 0>,
<0 0 0 2 &pcie0_intc 1>,
<0 0 0 3 &pcie0_intc 2>,
<0 0 0 4 &pcie0_intc 3>;
pcie0_intc: interrupt-controller {
interrupt-controller;
#address-cells = <0>;
#interrupt-cells = <1>;
};
};
在代码里,对于pcie0_intc
会创建出一个IRQ domain:
c
static int rockchip_pcie_init_irq_domain(struct rockchip_pcie *rockchip)
{
struct device *dev = rockchip->dev;
struct device_node *intc = of_get_next_child(dev->of_node, NULL); //获取pcie0_intc节点
if (!intc) {
dev_err(dev, "missing child interrupt-controller node\n");
return -EINVAL;
}
rockchip->irq_domain = irq_domain_add_linear(intc, 4, //创建外设的domain
&intx_domain_ops, rockchip);
//通过调用 irq_domain_add_linear() 创建 IRQ 域并将其与设备树中的中断控制器节点关联。
if (!rockchip->irq_domain) {
dev_err(dev, "failed to get a INTx IRQ domain\n");
return -EINVAL;
}
return 0;
}
rockchip_pcie_init_irq_domain
中是否完成映射?
rockchip_pcie_init_irq_domain
是创建一个 IRQ domain 的过程。这个函数通过调用irq_domain_add_linear
创建了一个线性 IRQ domain,这个 domain 是 PCIe 控制器用于管理挂接到其上面的设备的 INTx 中断(如 INTA、INTB、INTC、INTD)。此时,还没有具体到 PCIe 设备的中断映射,只是为 PCIe 控制器建立了一个 虚拟中断控制器域,供后续 PCIe 设备使用。- 当 PCIe 设备扫描并初始化时,会根据该域为设备映射中断号,所以这个函数仅仅是 为 PCIe 控制器的 INTx 引脚建立域,具体的映射会在之后处理设备中断时完成。
pcie0_intc
是虚拟中断控制器,它是管理 INTA# 的设备吗? ----- 个人想法
pcie0_intc**
是一个虚拟的中断控制器节点,它负责管理连接到 PCIe 控制器的所有 PCIe 设备的 INTx 中断 (包括 INTA、INTB、INTC、INTD)。每个 INTx 中断引脚会根据设备树的interrupt-map
进行映射。通过设备树的interrupt-map
,会为连接到 PCIe 控制器的设备的中断分配具体的中断号。- 并不是专门对应 INTA# ,而是通过映射机制为连接到 PCIe 的多个设备分配中断号。PCIe 设备的 INTx 中断(INTA, INTB, INTC, INTD)会映射到对应的硬件中断,
pcie0_intc
管理所有挂在 PCIe 控制器上的设备中断。
5.创建映射关系
5.1 如何映射
interrupt-map
和 interrupt-map-mask
的作用
interrupt-map-mask
定义了从 PCI/PCIe 配置空间中的哪些字段参与中断映射。例如,在这个例子中,interrupt-map-mask = <0 0 0 7>;
表示只有配置空间中最后 3 位有效,它们参与中断映射。interrupt-map
定义了设备树中的中断映射关系,将 PCIe 设备的中断映射到主机系统的中断控制器。
**interrupt-map = <0 0 0 1 &pcie0_intc 0>, ...**
-
<0 0 0 1>
:这四个数字的含义分别是:- 第 1 个数字 (0):表示设备号。在 PCIe 中,0 通常代表某个设备的地址,参与设备编号映射。
- 第 2 个数字 (0) :表示功能号。对于多功能设备,功能号用于区分同一个 PCIe 设备的不同功能模块,这里是
0
。 - 第 3 个数字 (0) :表示中断引脚 (INTx)。这个数字代表哪个 INT 引脚触发了中断。在 PCIe 中,中断引脚可以是 INTA, INTB, INTC 或 INTD。这种机制源自传统的 PCI 中断机制,在这里对应的值是
0
(表示 INTA)。 - 第 4 个数字 (1) :代表中断线号,即设备的中断编号。这里的
1
表示是设备的第 1 号中断。
-
&pcie0_intc
:这是一个设备树的节点引用,指向 PCIe 控制器下的虚拟中断控制器pcie0_intc
,表示该设备的中断需要交由pcie0_intc
处理。 -
0
:这是最后一个参数,通常表示硬件 IRQ 号或中断标志。在这个例子中,它是0
,表示这是第 0 号硬件中断线。
**interrupt-map-mask = <0 0 0 7>;**
0 0 0 7
:表示只有设备号(第 3 位)和中断引脚号(第 4 位)有效,参与映射。实际上,它用最后 3 位来判断是哪个 PCIe 设备、哪个功能模块、哪个中断引脚触发了中断。
中断映射的流程:
- PCIe 配置空间中的
interrupt pin
字段 :这个字段记录了设备使用的中断引脚(INTA、INTB、INTC 或 INTD)。比如当PCIe设备的配置空间中interrupt pin
为1
,表示使用 INTA# 引脚触发的中断。 - PCIe 地址映射 :根据 PCIe 设备的 bus、device、function 号,以及
interrupt pin
,组合出0 0 0 1
。 - 与
interrupt-map-mask
进行与操作 :0 0 0 1
与interrupt-map-mask = <0 0 0 7>;
进行按位与运算,只保留最后 3 位。因此0 0 0 1
会与0 0 0 7
计算后仍然是0 0 0 1
,表示 INTA# 对应的中断号。 - 映射到
interrupt-map
:根据interrupt-map
,<0 0 0 1 &pcie0_intc 0>
表示 PCIe 设备的 INTA# 中断被映射到虚拟中断控制器pcie0_intc
,并由其处理为第0
号硬件中断。应该就是指这个设备发出的就是0号硬件中断
那么它在代码中是如何体现出来的???看下面
5.2 代码体现
c
rockchip_pcie_probe
bus = pci_scan_root_bus(&pdev->dev, 0, &rockchip_pcie_ops, rockchip, &res);
pci_scan_root_bus_msi
pci_scan_child_bus
pci_scan_slot
dev = pci_scan_single_device(bus, devfn);
dev = pci_scan_device(bus, devfn); //----(1)
struct pci_dev *dev;
dev = pci_alloc_dev(bus);
pci_setup_device
pci_read_bases(dev, 6, PCI_ROM_ADDRESS);
pci_device_add(dev, bus); //-----(2.1)
pcibios_add_device(struct pci_dev *dev)
dev->irq = of_irq_parse_and_map_pci(dev, 0, 0); //具体内容看下面
of_irq_parse_and_map_pci //-----(2.2)
ret = of_irq_parse_pci(dev, &oirq);
rc = pci_read_config_byte(pdev, PCI_INTERRUPT_PIN, &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);
rc = of_irq_parse_raw(laddr, out_irq);
return irq_create_of_mapping(&oirq); //------(3)
这个流程实际上是描述了 PCIe 设备初始化时的中断号分配过程。在这个过程中,系统会扫描挂接到 PCIe 控制器的设备,并为每个设备分配中断号。流程大致如下:
-
设备扫描与初始化:
- 调用
pci_scan_root_bus
和相关函数递归扫描并初始化挂接在 PCIe 总线上的所有设备。 pci_scan_single_device
负责扫描单个 PCIe 设备,通过pci_setup_device
设置其硬件资源,并通过pci_device_add
添加设备。
- 调用
-
解析设备中断信息:
- 在
pci_device_add
中,调用pcibios_add_device
,最终调用of_irq_parse_and_map_pci
来解析设备的中断信息。 ------(2.1) - 映射的过程 发生在调用
of_irq_parse_and_map_pci
及其后续调用的**of_irq_parse_raw**
函数时。此时,设备的 PCIe 配置空间中的中断引脚号(INTx)会通过interrupt-map
的定义映射到虚拟中断控制器(如pcie0_intc
),并得到一个实际的中断号。-----(2.2) - 比如
interrupt-map = <0 0 0 1 &pcie0_intc 0>,
,当设备配置空间中的interrupt pin
为1
时(即使用 INTA# 中断),映射到&pcie0_intc 0
,并得到相应的中断号。
- 在
-
分配虚拟中断号:
irq_create_of_mapping
最终为该设备创建一个虚拟中断号,并将其存储在pci_dev->irq
中。此时,PCIe 设备的中断号已经完成映射。(然后就可以通过request_irq函数,使用这个pci_dev->irq来为该驱动设置其中断处理函数handler)
rockchip_pcie_init_irq_domain
的作用是为 PCIe 控制器创建一个域,供后续设备映射中断号使用。这是一个准备工作,域的创建并不代表已经完成了映射,而是为 PCIe 设备的中断号分配提供了基础。
映射真正发生在 of_irq_parse_and_map_pci
函数中,它解析设备树的 interrupt-map
并将 PCIe 设备的中断号与虚拟中断控制器 pcie0_intc
进行关联,最终得到PCIe设备具体的硬件中断号。