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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Deepin Linux
Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
