Understanding Linux PCI Drivers: A Complete Beginner’s Guide

This article walks readers through the fundamentals of Linux PCI/PCIe buses, explains the kernel PCI subsystem, details core data structures such as struct pci_dev, struct pci_driver, and struct pci_device_id, and provides step‑by‑step code examples for device enumeration, driver matching, resource allocation, and practical driver implementation.

Deepin Linux
Deepin Linux
Deepin Linux
Understanding Linux PCI Drivers: A Complete Beginner’s Guide

Linux PCI Bus Overview

PCI (Peripheral Component Interconnect) is a local bus introduced by Intel in 1991, providing plug‑and‑play, automatic resource allocation and a 32‑ or 64‑bit data path at 33 MHz or 66 MHz (e.g., 33 MHz × 32 bit = 133 MB/s).

The Linux PCI subsystem bridges devices such as NICs, SSDs, GPUs and FPGAs to the CPU and memory, handling enumeration, resource allocation and interrupt management.

Core Data Structures

struct pci_dev

Defined in include/linux/pci.h. Important members: vendor and device – IDs assigned by the PCI SIG. subsystem_vendor, subsystem_device – identify a specific subsystem. class – 16‑bit class code; high byte identifies the base class (e.g., PCI_CLASS_NETWORK_ETHERNET). pin – legacy INTx pin. irq – assigned interrupt number; replaced by MSI/MSI‑X vectors after pci_alloc_irq_vectors().

// Example: print vendor/device IDs in probe
static int my_pci_probe(struct pci_dev *pdev,
                         const struct pci_device_id *id)
{
    pr_info("PCI Vendor ID: 0x%04x, Device ID: 0x%04x
",
            pdev->vendor, pdev->device);
    return 0;
}

struct pci_driver

Represents a PCI driver. Key fields: name – unique driver name. id_table – pointer to an array of struct pci_device_id that lists supported devices. probe – called after a matching device is found. remove – called when the device is removed or the driver is unloaded.

static struct pci_driver my_pci_driver = {
    .name     = "my_pci_demo",
    .id_table = my_pci_ids,
    .probe    = my_pci_probe,
    .remove   = my_pci_remove,
};
module_pci_driver(my_pci_driver);

struct pci_device_id

Used for matching devices to drivers. Members: vendor,

device
subvendor

,

subdevice
class

,

class_mask
driver_data

– private driver data.

Three helper macros simplify definitions:

PCI_DEVICE(vendor, device)
PCI_DEVICE_SUB(vendor, device, subvendor, subdevice)
PCI_DEVICE_CLASS(class << 8, mask)
static const struct pci_device_id my_pci_ids[] = {
    PCI_DEVICE(0x8086, 0x1234),                     // Intel example
    PCI_DEVICE_SUB(0x8086, 0x1234, 0x8086, 0x5678),
    PCI_DEVICE_CLASS(PCI_CLASS_NETWORK_ETHERNET << 8, 0xffff00),
    { PCI_DEVICE(0x8086, 0x1234), .driver_data = (kernel_ulong_t)"private data" },
    { 0, }
};
MODULE_DEVICE_TABLE(pci, my_pci_ids);

PCI Driver Operation

Device Enumeration

During boot the kernel initializes the PCI subsystem, sets bus parameters and recursively scans the PCI hierarchy. For each device it reads the 256‑byte configuration space, extracts vendor and device IDs, creates a struct pci_dev and stores the information for later matching.

Driver Matching and Probe

After enumeration the kernel walks the list of registered struct pci_driver objects, compares each driver’s id_table with the device’s IDs and selects the first matching entry. The kernel then invokes the driver’s probe function.

Resource Allocation in Probe

A typical probe performs:

Enable the device with pci_enable_device().

Reserve I/O and memory regions via pci_request_regions().

Map required BARs using pci_iomap() (e.g., first BAR).

Allocate interrupt vectors with pci_alloc_irq_vectors() or register a legacy IRQ with request_irq().

static int my_pci_probe(struct pci_dev *pdev,
                         const struct pci_device_id *ent)
{
    int err;

    err = pci_enable_device(pdev);
    if (err)
        return err;

    err = pci_request_regions(pdev, "my_pci_dev");
    if (err)
        goto err_req;

    void __iomem *bar0 = pci_iomap(pdev, 0,
                                   pci_resource_len(pdev, 0));
    if (!bar0) {
        err = -EIO;
        goto err_iomap;
    }

    err = request_irq(pdev->irq, my_interrupt_handler,
                      IRQF_SHARED, "my_pci_dev", pdev);
    if (err)
        goto err_irq;

    /* Additional device‑specific initialization */
    return 0;

err_irq:
    pci_iounmap(pdev, bar0);
err_iomap:
    pci_release_regions(pdev);
err_req:
    pci_disable_device(pdev);
    return err;
}

Remove Function

The remove callback releases resources in reverse order:

static void my_pci_remove(struct pci_dev *pdev)
{
    free_irq(pdev->irq, pdev);
    pci_iounmap(pdev, pci_resource_start(pdev, 0));
    pci_release_regions(pdev);
    pci_disable_device(pdev);
}

Character Device Interface (Optional)

After successful probing a driver may create a character device to expose functionality to user space.

struct cdev my_cdev;
struct class *my_class;
struct device *my_device;

cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
int err = cdev_add(&my_cdev, MKDEV(major, minor), 1);
if (err)
    return err;

my_class = class_create(THIS_MODULE, "my_pci_class");
if (IS_ERR(my_class))
    return PTR_ERR(my_class);

my_device = device_create(my_class, NULL, MKDEV(major, minor),
                          NULL, "my_pci_device");
if (IS_ERR(my_device))
    return PTR_ERR(my_device);

PCI Subsystem Workflow Summary

Kernel boots → PCI subsystem initialization → recursive bus scan.

For each device: read configuration space → create struct pci_dev → store IDs.

Driver registration via pci_register_driver() adds a struct pci_driver to the kernel list.

Matching: kernel compares device IDs with each driver’s id_table. First match triggers probe.

Probe: enable device, request regions, map BARs, allocate or register interrupts, perform device‑specific setup.

Remove: free interrupt, unmap BARs, release regions, disable device.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

kernelCLinuxEmbeddedPCIDriverDevice
Deepin Linux
Written by

Deepin Linux

Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.