peter@home:~$

Using Linux device-tree with PCIe devices

Recently for work, we have been going through the process of removing any use of the deprecated /sys/class/gpio paths for interacting with GPIOs. This has been accomplished by defining these GPIOs in the device-tree in association with some driver - using a pre-existing one like gpio-leds or gpio-keys, extending an existing driver, or creating an entirely new driver.

Most of these have been pretty straightforward, they have been devices that already are typically found on the device tree, mainly platform devices (devices that are more-or-less independent of any bus). But recently I needed to do the same for some devices that don’t typically have device-tree entries: PCIe devices. These are auto-enumerated by the kernel, so most of the time they don’t need any help for detection. But in this case, I needed to find a way to represent these devices in the device-tree so I could add configuration values to them.

I tried looking online for any documentation on doing this, but it seems that it’s a relatively uncommon need. Most posts/questions/articles I could find on the subject seemed to be about setting up the PCIe ports (root complex ports?) that are directly on an SoC. In my case, this much was already done, and these are commonly provided by the vendor (though in some instances they may need to be modified) if the SoC has good support. I was able to find one Stack Overflow question that gave me some clues on how to go about it. I was also able to find a little more in the kernel documentation, but again most of that was about setting up root ports/bridges.

Initially, I more-or-less got it working on a couple products through trial-and-error. I always just kept the regs values as all zeros, and didn’t include the ranges values. On these products, it was a lot simpler since the PCIe devices were connected directly to the SoC. In the end, I was able to get away with something similar to the following:

&pcie0 { /* Typically defined in a dtsi for the SoC */
    status = "okay";

    pcie@0,0 { /* PCIe bridge/root */
        #address-cells = <3>;
        #size-cells    = <2>;
        reg = <0 0 0 0 0>;

        device_name: pcie@0 {
            compatible = "pci<vendor ID>,<device ID>";

            reg = <0 0 0 0 0>;

            /* Your stuff goes here */
        }
    }
}

And this worked great for those simpler products, but we also had a product that uses a PCIe switch on one of the busses. I attempted to get it working through dumb trial-and-error again, but this time I wasn’t having any luck. I went through /sys/bus/pci/devices to find the hierarchy, which got me a good bit of the way there, but the driver still was not picking up the entries.

Eventually I remembered seeing this LKML patch that I had initially dismissed as I didn’t fully read it and didn’t know it was in mainline and usable on our platform. So I enabled the kernel config option (PCI_DYNAMIC_OF_NODES - Create Device tree nodes for PCI devices) and loaded the new kernel on my device. At first, this didn’t fully work - it seems that it doesn’t always fully auto-generate the entries if there are any conflicting entries in the device-tree. So I temporarily removed those entries, and then it fully generated the PCIe device-tree (apart from the devices). In order to read it in a sensible way, I copied the contents of /sys/firmware/devicetree to my computer, and used dtc to convert it into dts format (dtc -I fs -O dts <path> > tree.dts).

At this point, I just tried replicating the same structure it had. I think the most important part I was missing was proper values for the reg field. The first bridge has them as all zeros, so it makes sense that that also worked for me. But the switch/nested bridges had non-zero values for these. After adding those, it still wasn’t working correctly. After adding some debug prints to show which items in the PCIe hierarchy properly picked up fwnode items, I realized that it was trying to use another node I hadn’t moved yet for one of the bridges, and not the node that actually had the device as a child. (My current guess is that for bridges where they are alone at the same location in the hierarchy, a reg of all zeros will work, but it might not if the bridge has neighbors at the same level, or if it’s device ID is not zero. I’m just glad to have gotten it working, and don’t really feel like trying a bunch of permutations to figure out all the rules.)

So in the end, I ended up with a device-tree closer to this:

&pcie0 {
    status = "okay";

    pcie@0,0 { /* Root */
        #address-cells = <3>;
        #size-cells = <2>;
        reg = <0 0 0 0 0>;

        pcie@0,0 { /* Switch */
            #address-cells = <3>;
            #size-cells = <2>;
            reg = <0x10000 0x00 0x00 0x00 0x00>;

            pcie@1,0 { /* Switch */
                #address-cells = <3>;
                #size-cells = <2>;
                reg = <0x20800 0x00 0x00 0x00 0x00>;

                device_0: pcie@0 { /* Device */
                    compatible = "pci<pid>,<vid>";
                    reg = <0 0 0 0 0>;

                    /* Properties go here */
                };
            };

            pcie@3,0 { /* Switch */
                #address-cells = <3>;
                #size-cells = <2>;
                reg = <0x21800 0x00 0x00 0x00 0x00>;

                device_1: pcie@0 { /* Device */
                    compatible = "pci<pid>,<vid>";
                    reg = <0 0 0 0 0>;

                    /* Properties go here */
                };
            };
        };
    };
};

And in case it might be useful to anyone else, this is the code I used to see what PCIe bus nodes were picking up fwnodes:

strict pci_dev *dev;

...

struct pci_bus *p_bus = dev->bus;
while (p_bus && (p_bus->parent != p_bus)) {
    if (p_bus->dev.fwnode) {
        const char *name = fwnode_get_name(p_bus->dev.fwnode);
        dev_warn(&dev->dev, "PCI parent bus has fwnode: %s", name);
    } else {
        dev_warn(&dev->dev, "PCI parent bus has no fwnode");
    }

    p_bus = p_bus->parent;
}

if (dev->dev.fwnode) {
    const char *name = fwnode_get_name(dev->dev.fwnode);
    dev_warn(&dev->dev, "Device has fwnode: %s", name);
} else {
    dev_warn(&dev->dev, "Device has no fwnode");
}