This lecture is part of EmbeddedPathashala’s free Linux kernel development course. In the previous lecture we looked at PCI/PCIe bus topology and the modern pci_driver API. In this lecture we go one level deeper into PCI bus enumeration — how the kernel tells devices apart, how it numbers buses, and how it walks the whole PCIe fabric at boot time. If you are building drivers for real hardware, understanding PCI bus enumeration is what lets you make sense of lspci output, sysfs entries, and probe-time data instead of treating them as magic numbers.
What You Will Learn
- How the kernel distinguishes a PCI endpoint (type 0) from a PCI-to-PCI bridge (type 1)
- The five fields the kernel uses to identify every PCI/PCIe device
- How Primary, Secondary, and Subordinate bus numbers are assigned to a bridge
- The hard limits on buses, devices, and functions in a PCI topology, and where they come from
- How the kernel walks a PCIe fabric using a depth-first enumeration algorithm
- The Bus-Device-Function (BDF) addressing scheme and how to read it from a
pci_dev - How to write an original kernel module that reads identification fields and enumerates devices
Prerequisites
- Lecture 1 of this chapter — PCI/PCIe basics, topology, and the
pci_driverregistration API - Comfort building and loading out-of-tree kernel modules
- Basic C and familiarity with reading kernel logs via
dmesg
PCI Header Types: Endpoint vs Bridge
Every PCI/PCIe function exposes a 256-byte (or 4 KB for PCIe extended) configuration space. Byte offset 0x0E of that space is the Header Type register, and it is the very first thing configuration software checks about a device. Two values matter for basic PCI bus enumeration:
- Type 0 — an endpoint device. Its configuration header describes Base Address Registers (BARs), interrupt pins, and subsystem IDs — the actual functional device (NIC, GPU, NVMe controller, and so on).
- Type 1 — a PCI-to-PCI bridge. Its configuration header does not describe a functional device at all; instead it describes a downstream bus, including the bus numbers the software assigns during enumeration.
This distinction matters because bridge devices are recursively expanded during enumeration, while endpoint devices are leaf nodes — enumeration software stops descending once it hits a type 0 header.
Device Identification Fields
Every PCI/PCIe function is uniquely identified using a small set of fields, all readable straight from configuration space:
| Field | Config Offset | Purpose |
|---|---|---|
| Vendor ID | 0x00 | Identifies the silicon manufacturer (e.g. Intel, Broadcom) |
| Device ID | 0x02 | Identifies the specific device from that vendor |
| Revision ID | 0x08 | Silicon revision/stepping of the device |
| Class Code | 0x0A – 0x0B | Generic function the device implements (network, storage, display, bridge, etc.) |
| Header Type | 0x0E | Layout of the rest of the header — type 0 (endpoint) or type 1 (bridge) |
Vendor ID and Device ID together are usually enough to match a driver to a device — this is exactly what the struct pci_device_id table you saw in Lecture 1 does. Revision ID, Class Code, and Header Type give configuration software extra detail when Vendor/Device ID alone isn’t sufficient, such as when a generic class driver needs to bind to any device of a certain type regardless of vendor.
You’ve already seen these fields without necessarily realizing it, if you’ve ever run:
$ lspci -vvv -s 00:1f.3
00:1f.3 Audio device: Intel Corporation Device 51ca (rev 11)
Every one of those pieces of information — bus:device.function, vendor name, device ID, revision — is read straight out of the fields in the table above. You can also read the raw values directly from sysfs, without any tool:
$ cd /sys/bus/pci/devices/0000:00:1f.3/
$ cat vendor
0x8086
$ cat device
0x51ca
$ cat revision
0x11
$ cat class
0x040300
$ cat header_type
0x00
Reading Identification Fields From a Driver
Inside a kernel driver, the vendor and device ID are already cached for you in pci_dev->vendor and pci_dev->device — no config space read needed. Everything else, you read directly with the pci_read_config_*() family of functions. Here is an original demo probe function that prints out full identification for whatever device it binds to:
#include <linux/module.h>
#include <linux/pci.h>
#define EP_DRV_NAME "ep_pci_id_demo"
static const struct pci_device_id ep_id_table[] = {
{ PCI_DEVICE(PCI_ANY_ID, PCI_ANY_ID) },
{ 0, }
};
MODULE_DEVICE_TABLE(pci, ep_id_table);
static int ep_pci_id_probe(struct pci_dev *pdev,
const struct pci_device_id *id)
{
u8 hdr_type, rev_id;
u16 class_dev;
const char *hdr_str;
pci_read_config_byte(pdev, PCI_HEADER_TYPE, &hdr_type);
pci_read_config_byte(pdev, PCI_REVISION_ID, &rev_id);
pci_read_config_word(pdev, PCI_CLASS_DEVICE, &class_dev);
hdr_str = (hdr_type & 0x7f) == PCI_HEADER_TYPE_BRIDGE ?
"type 1 (bridge)" : "type 0 (endpoint)";
dev_info(&pdev->dev,
"ep_pci_id_demo: BDF=%s vendor=0x%04x device=0x%04x "
"rev=0x%02x class=0x%04x header=%s\n",
pci_name(pdev), pdev->vendor, pdev->device,
rev_id, class_dev, hdr_str);
/* We only inspect the device here, we don't own it */
return -ENODEV;
}
static struct pci_driver ep_pci_id_driver = {
.name = EP_DRV_NAME,
.id_table = ep_id_table,
.probe = ep_pci_id_probe,
};
module_pci_driver(ep_pci_id_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Demo: read PCI device identification fields");
Returning -ENODEV from probe() is intentional here — since id_table matches every PCI device on the system, we don’t want to actually claim ownership of anything; we only want the core to call our probe() once per device so we can log its identification fields.
Bridge Configuration: Primary, Secondary, Subordinate Bus
When configuration software finds a type 1 (bridge) header, it has to program three extra fields before that bridge’s downstream bus becomes usable:
- Primary Bus Number — the bus the bridge itself lives on (its upstream side)
- Secondary Bus Number — the new bus number immediately downstream of this bridge
- Subordinate Bus Number — the highest bus number reachable anywhere downstream of this bridge
During the first enumeration pass, software doesn’t yet know how deep a branch goes, so the subordinate field is temporarily set to 0xFF — the maximum possible bus number — as a placeholder. Once that branch is fully explored, the real subordinate value is written back in. In the kernel, these three fields correspond to config space offsets PCI_PRIMARY_BUS (0x18), PCI_SECONDARY_BUS (0x19), and PCI_SUBORDINATE_BUS (0x1A), and are visible from a bridge’s struct pci_bus as bus->primary, bus->busn_res.start, and bus->busn_res.end.
Topology Limits: Why 256 / 32 / 8
PCI bus enumeration has fixed numeric limits, and they come directly from how many bits are reserved for addressing in the BDF scheme:
| Level | Range | Bits |
|---|---|---|
| Bus | 0 – 255 | 8 bits |
| Device | 0 – 31 | 5 bits |
| Function | 0 – 7 | 3 bits |
Bus number 0 is always reserved for the root complex. Every external PCIe lane, whether it originates directly from the CPU or from a downstream switch, sits behind a PCI-to-PCI bridge and therefore gets its own new bus number — only the downstream (secondary) side of a bridge counts as a new bus during enumeration.
The Depth-First Enumeration Algorithm
PCI bus enumeration is a classic depth-first search (DFS) starting at the root complex. In plain terms:
- Start at bus 0 (the root complex).
- Scan each device/function slot on the current bus.
- If a function’s header type says “bridge,” assign it the next free bus number (always at least one greater than the bus it lives on), then immediately recurse into that new bus before looking at any sibling devices.
- If a function’s header type says “endpoint,” it’s a leaf — nothing to recurse into.
- Once a branch has no more bridges to descend into, backtrack up to the next unexplored sibling bridge and repeat.
This “go as deep as possible before backtracking” behavior is exactly what makes it depth-first rather than breadth-first — the enumerator commits fully to one branch of the fabric before it moves sideways to the next.
Bus 0
00:02.0 → Bus 1
Bus 1 → internal Bus 2
02:00.0 → Bus 3
02:01.0 → Bus 4
02:02.0 → Bus 5
03:00.0
04:00.0
05:00.0
Walking this with DFS: the enumerator finds the upstream bridge at 00:02.0, assigns it bus 1, and immediately descends. On bus 1 it finds the switch’s internal bridge, assigns bus 2, and descends again. On bus 2 it finds three downstream-port bridges in sequence — each gets the next free bus number (3, 4, 5) and is immediately descended into before moving to its sibling. Each of those downstream buses terminates in a single endpoint, so the enumerator backtracks after each one until the whole switch is fully numbered.
BDF Addressing: Bus-Device-Function
Every function enumerated this way gets a unique BDF — Bus, Device, Function — written as three hex bytes without the 0x prefix, e.g. 03:00.0 means bus 0x03, device 0x00, function 0x0. When the function is omitted or irrelevant, only bus:device is shown, e.g. 03:00. Inside the kernel, BDF components are packed into a single pci_dev->devfn byte and unpacked with two macros:
u8 bus = pdev->bus->number;
u8 dev = PCI_SLOT(pdev->devfn); /* upper 5 bits */
u8 func = PCI_FUNC(pdev->devfn); /* lower 3 bits */
pr_info("BDF = %02x:%02x.%d\n", bus, dev, func);
You rarely need to do this by hand though — pci_name(pdev) already returns the fully formatted domain:bus:device.function string, which is exactly what you saw printed in the demo probe function earlier.
Code Example: Enumerating Every PCI Device From a Module
The kernel already enumerates the PCI fabric for you at boot, so a driver never re-implements the DFS algorithm itself. What a driver can do is walk the already-built list of discovered devices using pci_get_device(). Here’s an original standalone module that logs the BDF and identification fields of every PCI device currently known to the system:
#include <linux/module.h>
#include <linux/pci.h>
static int __init ep_pci_enum_init(void)
{
struct pci_dev *pdev = NULL;
int count = 0;
pr_info("ep_pci_enum: walking discovered PCI devices\n");
while ((pdev = pci_get_device(PCI_ANY_ID, PCI_ANY_ID, pdev)) != NULL) {
pr_info("ep_pci_enum: BDF=%s vendor:device=%04x:%04x class=0x%06x\n",
pci_name(pdev), pdev->vendor, pdev->device, pdev->class);
count++;
}
pr_info("ep_pci_enum: total devices found = %d\n", count);
return 0;
}
static void __exit ep_pci_enum_exit(void)
{
pr_info("ep_pci_enum: unloaded\n");
}
module_init(ep_pci_enum_init);
module_exit(ep_pci_enum_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Demo: enumerate discovered PCI devices via pci_get_device");
Build and Run Walkthrough
# Makefile
obj-m += ep_pci_enum.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
$ make
$ sudo insmod ep_pci_enum.ko
$ dmesg | tail -n 8
[ 1234.567890] ep_pci_enum: walking discovered PCI devices
[ 1234.567901] ep_pci_enum: BDF=0000:00:00.0 vendor:device=8086:1237 class=0x060000
[ 1234.567909] ep_pci_enum: BDF=0000:00:01.0 vendor:device=8086:7000 class=0x060100
[ 1234.567917] ep_pci_enum: BDF=0000:00:02.0 vendor:device=1234:1111 class=0x030000
[ 1234.567925] ep_pci_enum: BDF=0000:00:1f.3 vendor:device=8086:51ca class=0x040300
[ 1234.567930] ep_pci_enum: total devices found = 4
$ sudo rmmod ep_pci_enum
Header Type 0 vs Type 1 Fields at a Glance
| Aspect | Type 0 (Endpoint) | Type 1 (Bridge) |
|---|---|---|
| Represents | Functional device (NIC, GPU, storage…) | PCI-to-PCI bridge / downstream port |
| Key extra fields | Base Address Registers (BARs 0-5) | Primary/Secondary/Subordinate bus numbers |
| Role in enumeration | Leaf node — enumeration stops here | Recursed into — new bus discovered behind it |
| BAR count | Up to 6 | Up to 2 |
Real-World Use Cases
- Writing a driver that must distinguish a switch/bridge chip from the endpoint devices behind it
- Debugging why a device isn’t showing up under the expected BDF after a hotplug event
- Building diagnostic or inventory tooling that walks the PCIe fabric similarly to
lspci -t - Understanding IOMMU groupings and PCIe passthrough, both of which are organized around BDF addressing
Common Mistakes and Troubleshooting
- Assuming BDF numbers are stable across reboots. Bus numbers depend on enumeration order and firmware/BIOS behavior; don’t hardcode them in production code — match on Vendor/Device ID instead.
- Forgetting to mask the header type register. Bit 7 of the header type byte is the multi-function flag, not part of the type value — always mask with
& 0x7fbefore comparing againstPCI_HEADER_TYPE_NORMALorPCI_HEADER_TYPE_BRIDGE. - Confusing Class Code with Device ID. Class Code tells you the generic category (e.g. “network controller”); it does not identify which vendor’s chip it is.
- Not accounting for the subordinate bus placeholder. If you read bus numbers mid-enumeration (which you normally never do from driver code), the temporary
0xFFsubordinate value is not the final topology.
Best Practices
- Match drivers on Vendor ID/Device ID (and Class Code for generic drivers), never on BDF
- Use
pci_name(pdev)instead of manually formatting BDF strings - Prefer the cached
pdev->vendor/pdev->devicefields over re-reading config space when the value is already available - Always release references obtained from
pci_get_device()if you break out of the loop early — the kernel drops the reference automatically when the loop runs to completion (returns NULL), but an earlybreakneeds an explicitpci_dev_put() - Security consideration: never trust config-space-derived values from a device as inherently safe input — validate ranges before using them to index into arrays
- Performance consideration: config space reads are relatively slow I/O operations; cache identification fields at probe time rather than re-reading them on a hot path
Interview Questions
- What is the difference between a Type 0 and a Type 1 PCI configuration header?
- Why is the subordinate bus number temporarily set to 0xFF during enumeration?
- How many bits are used for Bus, Device, and Function in a BDF address, and what do those bit widths limit?
- Why is PCI bus enumeration described as depth-first rather than breadth-first?
- Where would you find the Vendor ID and Device ID inside
struct pci_dev, and where would you find them in raw configuration space?
Summary and Key Takeaways
PCI bus enumeration is how every PCI/PCIe device in a system gets discovered and given a stable, addressable identity before any driver ever touches it. The header type field tells software whether it’s looking at a functional endpoint or a bridge to more devices; the identification fields (Vendor ID, Device ID, Revision ID, Class Code) are what drivers actually match against; and the depth-first bus-numbering algorithm, bounded by the 256-bus/32-device/8-function limits, is what produces the BDF address you see in every lspci listing. Once these pieces click, config-space output and driver probe data stop being opaque numbers and start being a readable map of the machine’s PCIe topology.
Continue This Free Linux Kernel Development Course
Practice by loading the ep_pci_enum module on real hardware and cross-checking its output against lspci -t. Next up: PCI resource management — BARs, I/O vs memory-mapped regions, and requesting resources safely in a driver.
FAQ
What is PCI bus enumeration?
PCI bus enumeration is the process configuration software (usually firmware, then the kernel) uses to discover every PCI/PCIe device in a system, assign bus numbers to bridges, and build the full device topology using a depth-first search starting from the root complex.
What’s the difference between Vendor ID and Device ID?
Vendor ID identifies the manufacturer of the silicon (e.g. a specific company), while Device ID identifies the specific product from that vendor. Together they uniquely identify a device model and are the primary fields drivers match against.
Why does a bridge need Primary, Secondary, and Subordinate bus numbers?
A PCI-to-PCI bridge connects an upstream bus (Primary) to a new downstream bus (Secondary), and everything reachable further downstream falls within the Subordinate bus range. All three numbers together tell software exactly which part of the topology sits behind that bridge.
Why can a PCI system only have 256 buses?
The bus number field in a BDF address is 8 bits wide, and 8 bits can represent exactly 256 distinct values (0-255), which is a hardware/protocol-level limit, not a software choice.
What does BDF mean in Linux PCI drivers?
BDF stands for Bus-Device-Function, the three-part hexadecimal address (e.g. 03:00.0) that uniquely identifies a PCI/PCIe function. It’s exactly what pci_name() returns for a given struct pci_dev.
Is PCI bus enumeration done by the kernel or the BIOS/firmware?
Both can do it. Firmware typically performs an initial enumeration and bus number assignment at boot; the Linux kernel re-walks and can re-enumerate the fabric, particularly important for hotplug scenarios like adding a PCIe device after boot.
How do I read a device’s Class Code from a running Linux system?
Run cat /sys/bus/pci/devices/<BDF>/class, or use lspci -vvv which decodes the raw class code into a human-readable device category.
What’s the difference between pci_get_device() and probe()-based discovery?
pci_get_device() lets you walk the already-enumerated list of devices from anywhere in kernel code, useful for diagnostics. A pci_driver‘s probe() callback, by contrast, is invoked automatically by the PCI core for each device matching your id_table, and is the correct mechanism for a real driver that intends to own a device.
Where can I learn more in this free embedded Linux course?
This lecture is part of EmbeddedPathashala’s free Linux kernel development course, which also covers character devices, platform drivers, Device Tree, I2C/SPI, DMA, and PCI/PCIe driver development from the ground up.
