4 Linux pci子系统之pcie设备树解析
在Linux内核中设备树中,定义了一系列属性,用来描述PCIe总线。比如"bus-range"属性,描述PCIe某个domain的总线编号范围;比如"ranges"属性,描述PCIe地址转换。下面将分别介绍这些属性。
简介
在Linux内核中设备树中,定义了一系列属性,用来描述PCIe总线。比如"bus-range"属性,描述PCIe某个domain的总线编号范围;比如"ranges"属性,描述PCIe地址转换。下面将分别介绍这些属性。
设备类型
由于PCIe总线有一些特有的属性,需要在驱动初始化的时候内核自动解析。因此需要在设备树定义设备类型,如下所示。当设备类型为"pci"时,内核就知道这是一个PCI Host Bridge。
// arch/arm64/boot/dts/rockchip/rk3588.dtsi
pcie3x4: pcie@fe150000 {
......
device_type = "pci";
......
};
Linux内核调用__of_node_is_type函数解析设备类型。
// drivers/of/base.c
static bool __of_node_is_type(const struct device_node *np, const char *type)
{
const char *match = __of_get_property(np, "device_type", NULL);
return np && match && type && !strcmp(match, type);
}
PCI域
在Linux内核中,一个PCI设备,通常使用 {domain number}:{bus number}:{device number}:{function number}(比如0000:00:00.0) 描述。
- domain number表示PCI域的编号
- bus number表示总线编号
- device number表示设备编号
- function number表示功能编号
bus number、device number和function number也称之为BDF。PCI域用来给PCI Host Brage编号,有几个PCI Host Brage,就有几个PCI域。通常情况下,各个PCI域之间不能直接通信。在设备树中,PCI域使用"linux,pci-domain"属性描述。
// arch/arm64/boot/dts/rockchip/rk3588.dtsi
pcie3x4: pcie@fe150000 {
......
linux,pci-domain = <0>;
......
};
pcie3x2: pcie@fe160000 {
......
linux,pci-domain = <1>;
......
};
Linux内核调用of_get_pci_domain_nr函数解析PCI域编号。
/**
* This function will try to obtain the host bridge domain number by
* finding a property called "linux,pci-domain" of the given device node.
*
* @node: device tree node with the domain information
*
* Returns the associated domain number from DT in the range [0-0xffff], or
* a negative value if the required property is not found.
*/
int of_get_pci_domain_nr(struct device_node *node)
{
u32 domain;
int error;
error = of_property_read_u32(node, "linux,pci-domain", &domain);
if (error)
return error;
return (u16)domain;
}
在 pci_register_host_bridge 会使用 domain,如下:
- 配置 bridge 设备名为 "pci${domain_nr}:${busnr}"
- 配置 pci bus 设备名为 "${domain_nr}:${busnr}"
static int pci_register_host_bridge(struct pci_host_bridge *bridge)
{
struct device *parent = bridge->dev.parent;
......
// 会在/sys/devices/platform/soc/0.pcie目录下创建pci0000:00 目录
dev_set_name(&bridge->dev, "pci%04x:%02x", pci_domain_nr(bus),
bridge->busnr);
......
// 会在/sys/devices/platform/soc/0.pcie/pci0000:00/pci_bus目录下创建0000:00目录
dev_set_name(&bus->dev, "%04x:%02x", pci_domain_nr(bus), bus->number);
......
}
在 pci_setup_device 中 配置 pci dev名为:"${domain_nr}:${bus_num}:${slot_num}:${pci_num} 如下:
/**
* pci_setup_device - Fill in class and map information of a device
* @dev: the device structure to fill
*
* Initialize the device structure with information about the device's
* vendor,class,memory and IO-space addresses, IRQ lines etc.
* Called at initialisation of the PCI subsystem and by CardBus services.
* Returns 0 on success and negative if unknown type of device (not normal,
* bridge or CardBus).
*/
int pci_setup_device(struct pci_dev *dev)
{
......
// 设置pci dev name,其格式为:domain_nr:bus_num:slot_num:pci_num
dev_set_name(&dev->dev, "%04x:%02x:%02x.%d", pci_domain_nr(dev->bus),
dev->bus->number, PCI_SLOT(dev->devfn),
PCI_FUNC(dev->devfn));
......
}
speed 与 Link lanes
PCIe总线的最大速度由"max-link-speed"属性描述,Link Width由"num-lanes"描述。真实的速度和Link Width由RC、PCIe桥和EP协商决定。
// arch/arm64/boot/dts/rockchip/rk3588.dtsi
pcie3x4: pcie@fe150000 {
......
max-link-speed = <3>;
num-lanes = <4>;
......
}
Linux内核中可使用下面的接口解析"max-link-speed"和"num-lanes"属性。
// drivers/pci/of.c
/**
* This function will try to find the limitation of link speed by finding
* a property called "max-link-speed" of the given device node.
*
* @node: device tree node with the max link speed information
*
* Returns the associated max link speed from DT, or a negative value if the
* required property is not found or is invalid.
*/
int of_pci_get_max_link_speed(struct device_node *node)
{
u32 max_link_speed;
if (of_property_read_u32(node, "max-link-speed", &max_link_speed) ||
max_link_speed > 4)
return -EINVAL;
return max_link_speed;
}
EXPORT_SYMBOL_GPL(of_pci_get_max_link_speed);
// drivers/pci/controller/dwc/pcie-dw-pcie.c
static void rk_pcie_ep_setup(struct rk_pcie *rk_pcie)
{
......
ret = of_property_read_u32(np, "num-lanes", &lanes);
if (ret)
lanes = 0;
......
}
bus-range
"bus-range" 属性定义了此RC bus下 bus_nr的范围。
// arch/arm64/boot/dts/rockchip/rk3588.dtsi
pcie3x4: pcie@fe150000 {
......
bus-range = <0x00 0x0f>;
......
};
pcie3x2: pcie@fe160000 {
......
bus-range = <0x10 0x1f>;
......
};
Linux内核在PCIe Bridge初始化的时候,通过 of_pci_parse_bus_range 解析 bus-range。
/**
* of_pci_parse_bus_range() - parse the bus-range property of a PCI device
* @node: device node
* @res: address to a struct resource to return the bus-range
*
* Returns 0 on success or a negative error-code on failure.
*/
int of_pci_parse_bus_range(struct device_node *node, struct resource *res)
{
u32 bus_range[2];
int error;
error = of_property_read_u32_array(node, "bus-range", bus_range,
ARRAY_SIZE(bus_range));
if (error)
return error;
res->name = node->name;
res->start = bus_range[0];
res->end = bus_range[1];
res->flags = IORESOURCE_BUS;
return 0;
}
EXPORT_SYMBOL_GPL(of_pci_parse_bus_range);
ranges
"ranges" 属性定义了CPU地址到PCI地址的转换关系(outbound memory)。在PCI设备枚举的时候,PCI主机会根据ranges属性,设置对应的memory region,同时将PCI地址设置到PCI设备的寄存器中,当枚举完成后,CPU可以直接通过地址访问PCI设备。"dma-ranges"属性则与"ranges"属性相反,定义PCI地址到CPU地址的转换关系(inbound memory)。
"#address-cells" 定于使用几个cell描述PCI地址,"#size-cells"定义使用几个cell描述地址长度。因此"ranges"属性和"dma-ranges"属性描述的地址意义如下所示。
// arch/arm64/boot/dts/rockchip/rk3588.dtsi
pcie3x4: pcie@fe150000 {
......
#address-cells = <3>
#size-cells = <2>;
/* PCI地址标志 PCI地址高32位 PCI地址低32位 CPU地址高32位 CPU地址低32位 地址长度高32位 地址长度低32位 */
ranges = < 0x00000800 0x0 0xf0000000 0x0 0xf0000000 0x0 0x100000
0x81000000 0x0 0xf0100000 0x0 0xf0100000 0x0 0x100000
0x82000000 0x0 0xf0200000 0x0 0xf0200000 0x0 0xe00000
0xc3000000 0x9 0x00000000 0x9 0x00000000 0x0 0x40000000>;
......
};
// arch/arm64/boot/dts/renesas/r8a774b1.dtsi
pciec0: pcie@fe000000 {
......
#address-cells = <3>;
#size-cells = <2>;
/* Map all possible DDR as inbound ranges */
/* PCI地址标志 PCI地址高32位 PCI地址低32位 CPU地址高32位 CPU地址低32位 地址长度高32位 地址长度低32位 */
dma-ranges = <0x42000000 0 0x40000000 0 0x40000000 0 0x80000000>;
......
};
因此pcie3x4节点中"ranges"属性定义的4个地址段意义如下:
- 一个配置空间,从PCI地址0xf0000000开始,大小为1MB,将映射到Host CPU的0xf0000000地址处,具体的地址根据访问设备的BDF动态映射。
- 一个IO空间,从PCI地址0xf0100000开始,大小为1MB,将映射到Host CPU的0xf0100000地址处。
- 一个32位非预取内存空间,从PCI地址0xf0200000开始,大小为14MB,将映射到Host CPU的0xf0200000地址处。
- 一个64位预取内存空间,从PCI地址0x900000000开始,大小为14GB,将映射到Host CPU的0x900000000地址处。
因此pciec0节点中"dma-ranges"属性定义的地址段意义如下:
从Host CPU的角度看,一个32位非预取内存空间,从PCI地址0x40000000开始,大小为2GB,将映射到Host CPU内存0x40000000地址处。这样设置后,EP的DMA可以直接访问Host CPU的内存。
Linux内核在PCIe Bridge初始化的时候,通过 of_pci_range_parser_one 解析 "range" 属性,并将 range resource 添加到 pci_host_bridge->windows 链表中
struct of_pci_range *of_pci_range_parser_one(struct of_pci_range_parser *parser,
struct of_pci_range *range)
{
const int na = 3, ns = 2;
if (!range)
return NULL;
if (!parser->range || parser->range + parser->np > parser->end)
return NULL;
// ranges = <0x00000800 0x0 0xf1000000 0x0 0xf1000000 0x0 0x100000 // cfg base
// 0x81000000 0x0 0xf1100000 0x0 0xf1100000 0x0 0x100000 // NP-IO
// 0x82000000 0x0 0xf1200000 0x0 0xf1200000 0x0 0xe00000 // NP-MEM32
// 0xc3000000 0x9 0x40000000 0x9 0x40000000 0x0 0x40000000>; // P-MEM64
// 可见 rk3588.dtsi
// 7个元素为一组,
// 第一元素表示属性
// 第二、三为pci域的地址空间
// 第四、五为cpu域的地址空间
// 第六、七为size,表示设备将访问cpu域的地址时,转化为pci域的地址
// 也就是将pci的地址空间映射到 cpu 域的地址空间
range->pci_space = be32_to_cpup(parser->range);
// bit[25:24]
// 0x1 = IORESOURCE_IO
// 0x2 = MEM32
// 0x3 = MEM64
// BIT30 = 1 prefetch
range->flags = of_bus_pci_get_flags(parser->range);
//第二、三个元素
range->pci_addr = of_read_number(parser->range + 1, ns);
//第四、五个元素
range->cpu_addr = of_translate_address(parser->node,
parser->range + na);
// 第六、七个元素
range->size = of_read_number(parser->range + parser->pna + na, ns);
parser->range += parser->np;
/* Now consume following elements while they are contiguous */
while (parser->range + parser->np <= parser->end) {
u32 flags;
u64 pci_addr, cpu_addr, size;
flags = of_bus_pci_get_flags(parser->range);
pci_addr = of_read_number(parser->range + 1, ns);
cpu_addr = of_translate_address(parser->node,
parser->range + na);
size = of_read_number(parser->range + parser->pna + na, ns);
if (flags != range->flags)
break;
if (pci_addr != range->pci_addr + range->size ||
cpu_addr != range->cpu_addr + range->size)
break;
range->size += size;
parser->range += parser->np;
}
return range;
}
"dma-ranges" 通过 of_dma_get_range 解析先管的属性。
// drivers/of/device.c
/**
* of_dma_get_range - Get DMA range info
* @np: device node to get DMA range info
* @dma_addr: pointer to store initial DMA address of DMA range
* @paddr: pointer to store initial CPU address of DMA range
* @size: pointer to store size of DMA range
*
* Look in bottom up direction for the first "dma-ranges" property
* and parse it.
* dma-ranges format:
* DMA addr (dma_addr) : naddr cells
* CPU addr (phys_addr_t) : pna cells
* size : nsize cells
*
* It returns -ENODEV if "dma-ranges" property was not found
* for this device in DT.
*/
int of_dma_get_range(struct device_node *np, u64 *dma_addr, u64 *paddr, u64 *size)
{
struct device_node *node = of_node_get(np);
const __be32 *ranges = NULL;
int len, naddr, nsize, pna;
int ret = 0;
u64 dmaaddr;
if (!node)
return -EINVAL;
while (1) {
struct device_node *parent;
naddr = of_n_addr_cells(node);
nsize = of_n_size_cells(node);
parent = __of_get_dma_parent(node);
of_node_put(node);
node = parent;
if (!node)
break;
ranges = of_get_property(node, "dma-ranges", &len);
/* Ignore empty ranges, they imply no translation required */
if (ranges && len > 0)
break;
/*
* At least empty ranges has to be defined for parent node if
* DMA is supported
*/
if (!ranges)
break;
}
if (!ranges) {
pr_debug("no dma-ranges found for node(%pOF)\n", np);
ret = -ENODEV;
goto out;
}
len /= sizeof(u32);
pna = of_n_addr_cells(node);
/* dma-ranges format:
* DMA addr : naddr cells
* CPU addr : pna cells
* size : nsize cells
*/
dmaaddr = of_read_number(ranges, naddr);
*paddr = of_translate_dma_address(np, ranges);
if (*paddr == OF_BAD_ADDR) {
pr_err("translation of DMA address(%pad) to CPU address failed node(%pOF)\n",
dma_addr, np);
ret = -EINVAL;
goto out;
}
*dma_addr = dmaaddr;
*size = of_read_number(ranges + naddr + pna, nsize);
pr_debug("dma_addr(%llx) cpu_addr(%llx) size(%llx)\n",
*dma_addr, *paddr, *size);
out:
of_node_put(node);
return ret;
}
of_dma_get_range 通过 struct bus_type pci_bus_type 中的 dma_configure,即 pci_dma_configure 在配置dma时解析 "dma-ranges"。
// drivers/pci/pci-driver.c
struct bus_type pci_bus_type = {
.name = "pci",
.match = pci_bus_match,
.uevent = pci_uevent,
.probe = pci_device_probe,
.remove = pci_device_remove,
.shutdown = pci_device_shutdown,
// 创建 sysfs,vendor/device,res,irq等资源的查看
.dev_groups = pci_dev_groups,
// 手动触发pci总线扫描设备,并添加,即枚举
.bus_groups = pci_bus_groups,
.drv_groups = pci_drv_groups,
.pm = PCI_PM_OPS_PTR,
.num_vf = pci_bus_num_vf,
.dma_configure = pci_dma_configure,
};
EXPORT_SYMBOL(pci_bus_type);
// drivers/pci/pci-driver.c
/**
* pci_dma_configure - Setup DMA configuration
* @dev: ptr to dev structure
*
* Function to update PCI devices's DMA configuration using the same
* info from the OF node or ACPI node of host bridge's parent (if any).
*/
static int pci_dma_configure(struct device *dev)
{
struct device *bridge;
int ret = 0;
// 获取 pci_dev所对应 host_bridge 的 device
bridge = pci_get_host_bridge_device(to_pci_dev(dev));
if (IS_ENABLED(CONFIG_OF) && bridge->parent &&
bridge->parent->of_node) {
// 配置当前 pci_dev的dma属性,如mask、是否支持iommu等
ret = of_dma_configure(dev, bridge->parent->of_node, true);
} else if (has_acpi_companion(bridge)) {
struct acpi_device *adev = to_acpi_device_node(bridge->fwnode);
ret = acpi_dma_configure(dev, acpi_get_dma_attr(adev));
}
pci_put_host_bridge_device(bridge);
return ret;
}
中断
PCIe总线中,涉及INTx、MSI、MSI-X中断。INTx中断需要在设备树中配置映射关系。MSI和MSI-X中断需要配置和ITS的映射关系。
INTx中断
由于很多PCI设备只使用INTA中断,若中断很频繁时,这些中断都将集中到INTA上,导致中断效率降低。因此需要将PCI插槽上的INTx中断以旋转(swizzling)方式连接到中断控制器上的不同中断引脚上。设备树需要一种将每个PCI中断信号映射到中断控制器输入的方法。"#interrupt-cells"、"interrupt-map"和"interrupt-map-mask"属性用于描述中断映射。
- "#interrupt-cells"属性表示描述中断需要几个cell
- "interrupt-map-mask"表示PCI插槽(包含了device id信息)和INTx中断编号的掩码
- "interrupt-map"表示INTx和中断控制器的映射关系。
如下所示,phys.hi、phys.mid、phys.low分别表示PCI插槽位置信息,由于在PCIe总线中,INTx中断使用消息机制实现,不需要中断引脚,因此PCI插槽位置信息可以设置为0。
- INTx对应的一列分别表示INTA、INTB、INTC、INTD中断。
- "interrupt-map-mask"和"interrupt-map"的前4列相与得到最终的PCI插槽位置信息和INTx类型。
因此下面分别将INTA、INTB、INTC、INTD映射到pcie3x4_intc中断控制器的0、1、2、3号中断上,实质上pcie3x4_intc是一个虚拟的中断控制器,父中断控制器为GIC,其使用260号中断向GIC提交中断。
// arch/arm64/boot/dts/rockchip/rk3588.dtsi
pcie3x4: pcie@fe150000 {
......
interrupts = <GIC_SPI 263 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 262 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 261 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 260 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 259 IRQ_TYPE_LEVEL_HIGH>;
interrupt-names = "sys", "pmc", "msg", "legacy", "err";
#interrupt-cells = <1>;
/* phys.hi phys.mid phys.low INTx */
interrupt-map-mask = < 0 0 0 7>;
/* phys.hi phys.mid phys.low INTx 映射的中断控制器 中断编号 */
interrupt-map = <0 0 0 1 &pcie3x4_intc 0>, /* INTA */
<0 0 0 2 &pcie3x4_intc 1>, /* INTB */
<0 0 0 3 &pcie3x4_intc 2>, /* INTC */
<0 0 0 4 &pcie3x4_intc 3>; /* INTD */
......
pcie3x4_intc: legacy-interrupt-controller {
interrupt-controller;
#address-cells = <0>;
#interrupt-cells = <1>;
interrupt-parent = <&gic>;
interrupts = <GIC_SPI 260 IRQ_TYPE_EDGE_RISING>;
};
......
}
MSI 和 MSI-X 中断
GICv3及以上版本实现了 ITS(Interrupt Translation Service)。因此在ARM架构上,可基于ITS实现 MSI 或 MSI-X 中断。设备树使用 "msi-map" 属性描述 MSI 或 MSI-X 中断和 msi-controller 的映射关系。
- "msi-map"属性的第一个数据表示MSI Data,即MSI中断向量起始编号,需要配置到PCIe设备的配置空间中
- 第二个数据引用msi-controller节点,msi-controller位于gic节点内
- 第三个数据表示PCIe设备起始16位的Requester ID(BDF)
- 第四个数据表示中断数量,下面申请了4096个中断。
// arch/arm64/boot/dts/rockchip/rk3588.dtsi
pcie3x4: pcie@fe150000 {
......
/* MSI Data msi-controller Requester ID length */
msi-map = <0x0000 &its1 0x0000 0x1000>;
......
};
// arch/arm64/boot/dts/rockchip/rk3588s.dtsi
gic: interrupt-controller@fe600000 {
compatible = "arm,gic-v3";
#interrupt-cells = <3>;
#address-cells = <2>;
#size-cells = <2>;
ranges;
interrupt-controller;
reg = <0x0 0xfe600000 0 0x10000>, /* GICD */
<0x0 0xfe680000 0 0x100000>; /* GICR */
interrupts = <GIC_PPI 9 IRQ_TYPE_LEVEL_HIGH>;
its0: msi-controller@fe640000 {
compatible = "arm,gic-v3-its";
msi-controller;
#msi-cells = <1>;
reg = <0x0 0xfe640000 0x0 0x20000>;
};
its1: msi-controller@fe660000 {
compatible = "arm,gic-v3-its";
msi-controller;
#msi-cells = <1>;
reg = <0x0 0xfe660000 0x0 0x20000>;
};
};更多推荐



所有评论(0)