简介

在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>;
    };
};
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐