Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 37

PCIe Enumeration: Kernel Device Discovery & Configuration

Recommended resources

Introduction

Welcome back. In our previous lesson, we established a conceptual model of the PCIe subsystem, covering its hierarchical topology (Root Complex, Switches, Endpoints) and the layered protocol that governs communication. We now understand what a PCIe system looks like and the rules for how devices communicate.

This lesson addresses the next logical question: When a system boots, how does the Linux kernel discover which devices are physically connected to the PCIe fabric? This process is called enumeration. Our learning outcome is to explain PCIe enumeration and how the kernel discovers and configures devices during boot.

We will break this down by first understanding the goal of enumeration and the data structures involved, then examining the core algorithm, the low-level access mechanisms, and finally, how this process is anchored in different platform architectures like ACPI (common on x86) and Device Tree (prevalent in the embedded systems you're familiar with).


1. The Goal: Discovering the Hierarchy

At power-on, the kernel's knowledge of the PCIe hierarchy is minimal. It only knows about the existence of the Root Complex and the bus directly attached to it, Bus 0. Everything else is a mystery.

PCIe System Before Enumeration
This diagram shows the initial state of a PCIe system before enumeration. Note that only Bus 0, connected to the Root Complex, is known and numbered. All other buses and devices are yet to be discovered.

The goal of enumeration is to perform a systematic scan to:

  1. Discover every PCIe function on every device.
  2. Assign each function a unique address, known as Bus, Device, Function (BDF).
  3. Read configuration information to understand what each device is and what resources it needs.

To do this, the kernel reads from a special per-device data structure: the Configuration Space.


2. The Configuration Space: A Device's Identity Card

Every PCIe function has a dedicated memory region called the Configuration Space. This space contains all the information the kernel needs to identify and configure the device. While there is also I/O and memory space used for the device's main operation, the configuration space is purely for discovery and setup.

The first 64 bytes of the configuration space are the standardized PCI Header. This header contains the most critical information for enumeration.

GNU/Linux & PCI (Express) - Part 2: Print the PCI Header

To understand what information the kernel is looking for, let's watch this segment from Johannes 4GNU_Linux. It clearly distinguishes between I/O/memory space and configuration space, and then details the most important fields in the PCI header.

Watch from 00:36 to 03:59. Pay close attention to the descriptions of Vendor ID, Device ID, Class Code, and especially the Header Type field.

As the video explained, the key fields for initial discovery are:

  • Vendor ID (VID): A unique number assigned to each hardware vendor (e.g., 0x8086 for Intel).
  • Device ID (DID): A number assigned by the vendor to identify a specific product.
  • Class Code: Identifies the type of device (e.g., network controller, storage controller, display controller).
  • Header Type: This is crucial for enumeration.
    • Type 0: Indicates a standard Endpoint.
    • Type 1: Indicates a PCI-to-PCI Bridge (including ports on a Switch or the Root Complex).

When the kernel scans a potential BDF address, a returned Vendor ID of 0xFFFF signifies that no device is present. Any other value indicates a device has been found. The combination of Vendor ID and Device ID uniquely identifies the hardware, which is how the kernel later matches it to a specific driver. The Header Type tells the kernel whether to continue scanning on the current bus or to start scanning a new bus behind a bridge.


3. The Enumeration Algorithm: A Depth-First Search

The kernel enumerates the PCIe fabric using a depth-first search algorithm. It systematically explores the tree of buses, starting from Bus 0.

Here is the simplified process:

  1. Start with Bus 0: The scan begins on the root bus attached to the Root Complex.
  2. Scan for Devices: The kernel iterates through all possible device numbers (0-31) on the current bus. For each device, it checks function 0.
  3. Check for Device Presence: For each potential BDF (Bus X, Device Y, Function 0), the kernel attempts to read the Vendor ID from its configuration space.
    • If the result is 0xFFFF, there is no device at this location. The kernel moves to the next device number.
    • If a valid Vendor ID is returned, a device exists! The kernel reads the rest of its header.
  4. Handle Multi-Function Devices: If the device has a multi-function header bit set, the kernel also scans functions 1-7 for that device.
  5. Bridge or Endpoint? The kernel checks the Header Type:
    • If Header Type is 0 (Endpoint): The device is a "leaf" in the hierarchy. The kernel stores its information (pci_dev struct) and continues scanning the current bus.
    • If Header Type is 1 (Bridge): This device is a gateway to another bus segment. The kernel must now explore what's behind it.
      a. It configures the bridge's bus registers:
      • Primary Bus: Set to the current bus number.
      • Secondary Bus: Set to a new, unused bus number. This number is now assigned to the bus segment on the other side of the bridge.
        b. Recurse: The kernel calls the enumeration function recursively, passing the new secondary bus number as the target to scan.
        c. Update Subordinate Bus: After the recursive call returns (meaning everything behind the bridge has been scanned), the kernel updates the bridge's Subordinate Bus register to the highest bus number that was discovered behind it. This tells the system the full range of buses managed by this bridge.

This recursive process continues until all paths have been explored. The result is a complete map of the PCIe hierarchy, with every function on every device assigned a unique BDF.

PCIe Enumeration: Bus Number Registers In A Switch
This diagram shows a PCIe topology after enumeration. Notice how the bridges have been configured with Primary, Secondary, and Subordinate bus numbers, creating a fully addressed hierarchy from the initial Bus 0.

4. Accessing the Configuration Space

How does the kernel actually perform the "read from configuration space" step? There are two primary mechanisms.

Legacy Mechanism (x86 I/O Ports)

On older PCI systems and for backward compatibility on x86, configuration space was accessed via two I/O ports:

  • CONFIG_ADDRESS (0xCF8)
  • CONFIG_DATA (0xCFC)

To read a register, the kernel would:

  1. Write a 32-bit value to port 0xCF8 encoding the target BDF and register offset.
  2. Read the 32-bit data value from port 0xCFC.

Linux Kernel: PCI Device Discovery Process

The following article gives a concise, step-by-step example of how these I/O ports are used to find a device on an ACPI-based system. This provides a concrete view of the legacy access method.

Read 'Phase 6: Device Enumeration'. Focus on the example showing how an address is built and written to port 0xCF8, with the result read from 0xCFC.

Modern Mechanism: Enhanced Configuration Access Mechanism (ECAM)

PCI Express introduced a more efficient memory-mapped approach called ECAM. The system firmware reserves a large, contiguous block of physical memory (up to 256 MiB) and maps the configuration space of every possible BDF into this region.

With ECAM, reading a configuration register becomes a simple memory read. The BDF and register offset are used to calculate an address within the ECAM memory region.

The kernel gets the ECAM_Base physical address from the platform firmware (ACPI or Device Tree). This allows the kernel to access any device's configuration space with a direct memory operation, which is much faster than using I/O ports.

Overview of PCI(e) Subsystem - Kishon Vijay Abraham, Texas Instruments

This video segment explains the ECAM mechanism. It visualizes how the CPU's view of memory is translated by the Root Complex into a PCI configuration space access.

Watch the segment from 15:53 to 19:25. It details how the host system uses the 'configurable address space' (part of ECAM) and the address translation unit in the Root Complex to read an endpoint's configuration space.


5. Platform-Specific Implementations: ACPI vs. Device Tree

The enumeration algorithm is generic, but it needs a starting point. The kernel must first find the Root Complex and get its resources (like the ECAM base address). This information is provided by platform-specific firmware.

ACPI (Advanced Configuration and Power Interface)

Commonly used on x86 servers and desktops, ACPI provides the kernel with tables describing the hardware.

  1. Firmware Stage: The BIOS/UEFI creates ACPI tables in memory, including the DSDT (Differentiated System Description Table). The DSDT contains a software description of the PCI Root Bridge.
  2. Kernel Stage:
    • The kernel parses these tables to find the Root Bridge device object (often PNP0A03).
    • The acpi_pci_root driver binds to this object.
    • This driver reads the resources for the root bridge, including the bus number range and the ECAM configuration information from the ACPI MCFG table.
    • With this anchor point established, the kernel's generic PCI core can begin the enumeration process at Bus 0.

The LinkedIn post you read earlier (LINK) provides an excellent summary of this entire flow from Phase 1 to Phase 8.

Device Tree (DT)

In the embedded world, Device Tree is the standard.

  1. Firmware Stage: The bootloader loads the Device Tree Blob (DTB) into memory and passes its address to the kernel.
  2. Kernel Stage:
    • The kernel parses the DTB. It looks for a node that is compatible with a PCI host controller driver. This node describes the Root Complex.
    • The corresponding host controller driver initializes the hardware and gets the resource information (like memory ranges for ECAM) directly from its DT node.
    • Just like with ACPI, once the host bridge is initialized, the generic PCI core takes over and begins enumeration.
    • Dynamic Node Creation: A key feature in modern kernels used for embedded systems is CONFIG_PCI_DYNAMIC_OF_NODES. With this enabled, the base DTB only needs to describe the root complex. As the kernel enumerates and finds new PCI bridges and endpoints, it dynamically creates new device tree nodes for them in memory. This allows the rest of the kernel (e.g., drivers looking for their device) to interact with discovered PCI devices as if they were described in the original DTB.

Using Device Tree Overlays to Support Complex PCI...

This presentation from Bootlin discusses how the kernel handles PCI enumeration in a Device Tree context. It specifically details the dynamic creation of DT nodes, a powerful concept for complex embedded systems.

Review slides 17 through 22. Focus on the distinction between the base DT (with no PCI device nodes) and the process where the kernel creates these nodes at runtime during enumeration. Note the mention of of_pci_make_dev_node() and the properties added, like compatible and reg.


6. Observing Enumeration and Debugging

The primary user-space tool to inspect the results of enumeration is lspci. It reads the information gathered by the kernel and displays the device hierarchy.

Overview of PCI(e) Subsystem - Kishon Vijay Abraham, Texas Instruments

This final clip from the 'Overview of PCI(e) Subsystem' video introduces lspci and shows how to interpret its output.

Watch from 38:38 to 40:00. Note how the output format directly corresponds to the BDF (Bus, Device, Function) numbering scheme.

Interview Scenario

Interviewer: "You've just brought up a new embedded board with a custom FPGA connected via PCIe. The FPGA is supposed to appear as a simple memory device, but it's not visible in /proc/devices, and lspci doesn't list it. What are the first few steps you would take to debug this, and what are the most likely causes?"

Click for Analysis

This is a classic problem that distinguishes between a discovery failure and a driver failure. The fact that lspci doesn't see the device means the problem lies in the enumeration process itself, long before any driver would try to bind to it.

My debugging process would be:

  1. Check Kernel Boot Logs (dmesg): This is the most critical first step. I would grep for "PCI" or the name of my host controller driver.

    • Did the host controller initialize? I'm looking for messages indicating the root complex driver successfully probed. If not, the problem is likely in the Device Tree node for the root complex itself (e.g., status disabled, missing clocks, regulators, or resets).
    • Are there link training errors? The physical link between the root port and the FPGA must be established ("trained") before any higher-level communication can happen. Logs might show messages like "link-up failed" or "failed to train link." This points to a hardware signal integrity issue, a problem with the FPGA's PCIe IP configuration, or incorrect SerDes settings.
  2. Verify Hardware and Firmware:

    • Hardware: Is the FPGA powered correctly? Is the PCIe PERST# (reset) signal being handled correctly by the board's logic?
    • FPGA Firmware: Is the FPGA's bitstream loaded correctly? Within the FPGA design, is the PCIe core configured with a valid Vendor ID (not 0xFFFF) and Device ID? A misconfigured FPGA core is a very common cause.
  3. Dynamic Debug: If the host driver is initializing but still not finding the device, I would enable dynamic debug for the relevant PCI core and host driver files (echo 'file drivers/pci/* +p' > /sys/kernel/debug/dynamic_debug/control). This would flood dmesg with verbose logs showing the enumeration scan step-by-step, which might reveal why it's skipping the device or failing to read its configuration space.

Most Likely Causes (in order):

  1. Physical Link Failure: The link never came up due to signal integrity, power, or reset issues. This is a very common problem on new hardware.
  2. FPGA Configuration Error: The PCIe core inside the FPGA is not configured correctly, is held in reset, or is not presenting a valid Vendor ID on its configuration space registers.
  3. Device Tree Error: The host controller's DT node is missing a critical property (e.g., a phy handle, correct ranges property, or a required clock), preventing it from initializing properly and starting the scan.

Conclusion

In this lesson, we demystified the PCIe enumeration process. You've seen how the kernel transforms an unknown hardware topology into a fully mapped and identified device tree.

Key Takeaways:

  • Enumeration is a depth-first search starting from Bus 0 to discover all PCIe functions.
  • The kernel reads the Configuration Space Header of each potential device to find its Vendor/Device ID and, crucially, its Header Type.
  • Header Type 1 (Bridges) triggers a recursive scan on a newly assigned bus number.
  • Access to configuration space is done via the memory-mapped ECAM on modern systems.
  • The entire process is anchored by platform firmware—ACPI on x86 or Device Tree on embedded systems—which provides the starting information about the Root Complex.

You now understand how the kernel finds a device. In the next lesson, "Write a PCIe driver skeleton that registers with the PCI subsystem and reads device configuration space," we will explore the next step: how a device driver finds and binds to the device that the kernel has just discovered for it.