In-Depth Reading: Chapter 12 of ‘Linux Device Drivers’ – PCI Drivers

Hello, everyone. I am a programmer.

This article is approximately 3200 words long. Today, I finished reading Chapter 12 – PCI Drivers from the 3rd edition of ‘Linux Device Drivers’. This chapter is a key part of driver development that interacts with actual hardware buses. The book provides a detailed explanation of the complete lifecycle of writing a Linux PCI device driver, from driver registration, device detection, resource allocation and mapping, to final cleanup and unregistration. While organizing my reading notes, I sometimes add my own understanding or information obtained through AI queries. If there are any inaccuracies in my descriptions, please feel free to point them out in the comments section.

By following my public account, you can obtain e-books related to Linux (including the 3rd edition of ‘Linux Device Drivers’) and commonly used development tools. A document list is provided at the end of this article.

In-Depth Reading: Chapter 12 of 'Linux Device Drivers' - PCI Drivers

1. Basics of PCI Bus

Concepts and Design Goals of PCI: PCI (Peripheral Component Interconnect) is a high-performance local bus designed to replace the ISA bus. It has the following key goals:

[1]. Higher Performance: Achieve better performance when transferring data between computers and peripherals. This is accomplished by using higher clock frequencies (typically 33MHz, 66MHz, or even 133MHz) and wider data buses (32-bit or 64-bit), enabling faster data transfer between computers and peripherals.

[2]. Platform Independence: The PCI specification is designed to be as platform-independent as possible.

[3]. Simplified System Operation (Auto-configuration): PCI devices are “jumperless” and can be automatically configured by firmware (such as BIOS) during the system boot phase, simplifying the addition and removal of peripherals.

PCI Addressing:

Each PCI device is uniquely identified by a bus number, device number, and function number. The Linux system supports PCI domains, where each PCI domain can have up to 256 buses, each bus can support up to 32 devices, and each device can have up to 8 functions.

In-Depth Reading: Chapter 12 of 'Linux Device Drivers' - PCI DriversTypical PCI System LayoutPCI devices respond to three types of address spaces: shared memory and I/O ports (devices on the bus can simultaneously see access), as well as independent configuration space. The configuration space uses geographic addressing, accessing only one specific slot at a time to avoid conflicts. Drivers access memory and I/O areas through regular functions (such as<span><span>readb</span></span>), while configuration registers require calling specific kernel functions for reading and writing. In terms of interrupts, each PCI slot has four shareable interrupt pins, and the routing of connections to the CPU is determined by the hardware platform, allowing PCI devices to work well in systems with limited IRQs. The core innovation of the PCI standard lies in its configuration address space, which allows drivers to directly query device information without the risk of probing.

2. Several Stages of PCI Device Configuration

PCI device configuration requires the following stages:

[1]. System Boot Stage

During system boot, firmware (or the Linux kernel, if configured this way) performs configuration transactions on each PCI peripheral to allocate a safe location for each address region it provides. When the driver accesses the device, its memory and I/O areas have already been mapped to the processor’s address space. The driver can modify this default configuration. Use the tree command to view PCI-related information:

In-Depth Reading: Chapter 12 of 'Linux Device Drivers' - PCI Drivers

[2]. Configuration Registers and Initialization

PCI device configuration registers are divided into required and optional categories. Required registers must contain valid values, while the validity of optional registers is determined by the contents of required fields. PCI registers use little-endian byte order. Although the PCI standard attempts to be architecture-independent, its design still has a certain bias towards the PC environment.

Therefore, when writing drivers to access multi-byte configuration registers, special attention must be paid to byte order issues to ensure code portability across different platforms (such as little-endian PC environments and big-endian platforms). If conversion is needed between the system’s inherent byte order and PCI byte order, functions defined in the <asm/byteorder.h> header file can be used.

In-Depth Reading: Chapter 12 of 'Linux Device Drivers' - PCI DriversStandard PCI Configuration Registers[3]. Registering PCI Drivers

The core of the PCI driver is to register a struct pci_driver structure with the system. This structure contains the driver’s name, the supported device ID table, and various callback function pointers.

Key Data Structure: struct pci_driver

struct pci_driver {    const char *name;    const struct pci_device_id *id_table; /* List of devices supported by the driver */    int (*probe)(struct pci_dev *dev, const struct pci_device_id *id); /* Probe function */    void (*remove)(struct pci_dev *dev); /* Remove function */    int (*suspend)(struct pci_dev *dev, pm_message_t state); /* Suspend function */    int (*resume)(struct pci_dev *dev); /* Resume function */    /* ... other optional fields ... */};

id_table: This is a crucial member that defines which PCI devices the driver can support. Each entry in the table is a struct pci_device_id, which can match devices based on vendor ID, device ID, class, and other information. Using macros like PCI_DEVICE(vendor, device) can conveniently initialize table entries.

probe function: When the PCI core discovers a device that is not claimed by any driver and the device information matches an entry in the id_table, the driver’s probe function is called. This is the core place for driver initialization.

remove function: When a device is physically removed or the driver module is unloaded, the PCI core calls this function to clean up all resources allocated in the probe.

Registration: In the driver’s initialization function (usually called by module_init), use pci_register_driver(&my_driver) to register the driver with the kernel.

Unregistration: In the exit function (called by module_exit), use pci_unregister_driver(&my_driver) to unregister the driver.

Note:

Before registering PCI, it is common to call the following interface to initialize in the module initialization code:

static int __init pci_skel_init(void) return pci_register_driver(&amp;pci_driver);

If registration is successful, the pci_register_driver function returns 0; otherwise, it returns a negative error number. It does not return the number of devices bound to the driver or an error number when no devices are bound to the driver.

[Old PCI Probing (Non-standard Method)]

Although modern drivers strongly recommend using the pci_register_driver interface, the kernel still retains some old manual probing functions, such as pci_get_device and pci_get_subsys. These functions allow drivers to actively traverse the PCI device list to find devices. Unless there is a special reason, the standard driver model should be used.

[Activating PCI Devices]

In the probe function of the PCI driver, before the driver can access any device resources (I/O areas or interrupts), the device needs to be activated:

// It wakes the device and may assign its interrupt line and I/O area int pci_enable_device(struct pci_dev *dev);

[4]. Accessing PCI Configuration Space

Each PCI device has at least 256 bytes of configuration space. The kernel provides a set of safe functions to read and write these spaces, which handle byte order and architecture differences.

// Read functions: int pci_read_config_byte(struct pci_dev *dev, int where, u8 *val); int pci_read_config_word(struct pci_dev *dev, int where, u16 *val); int pci_read_config_dword(struct pci_dev *dev, int where, u32 *val); // Write functions: int pci_write_config_byte(struct pci_dev *dev, int where, u8 val); int pci_write_config_word(struct pci_dev *dev, int where, u16 val); int pci_write_config_dword(struct pci_dev *dev, int where, u32 val);

Note: The where parameter is the byte offset calculated from the start of the configuration space. There is no need to handle byte order here.

[5]. Accessing I/O and Memory Space

PCI devices can support up to 6 I/O or memory address regions. The key difference is that I/O registers (non-prefetchable) should not be cached by the CPU, while video memory can be set as prefetchable. Devices report region information (size, location, support for 32/64 bits) through PCI_BASE_ADDRESS configuration registers, and the kernel integrates these regions into general resource management.

3. Hardware Abstraction

The Linux kernel implements hardware abstraction through C language structures (such as<span><span>pci_ops</span></span><span><span>). This structure contains function pointers for reading and writing PCI configuration space. The system creates</span></span><code><span><span>pci_bus</span></span><span><span> instances for detected PCI buses and points their</span></span><code><span><span>ops</span></span><span><span> field to the operation set corresponding to the specific hardware.</span></span><span><span> This object-oriented design incurs minimal pointer dereference overhead, providing a unified hardware operation interface for the upper layers. Since configuration access frequency is low and not on the critical path, this overhead is acceptable. This abstraction method is typical in the kernel, such as for handling architecture differences in Alpha with the</span></span><code><span><span>alpha_machine_vector</span></span><span><span> structure.</span></span><p><span><span>4. Plug and Play (Hot Plugging) and Power Management</span></span></p><p><span><span>PCI drivers support power management through the suspend and resume callback functions in the struct pci_driver. These functions are called when the system enters sleep mode or when there are power management events, and the driver needs to save the device state, reduce power consumption, or reinitialize the device upon recovery.</span></span></p><p><span><span>5. Utilities and Information Viewing</span></span></p><p><span><span>The lspci command: In user space, this command is a powerful tool for viewing all PCI devices in the system. It can list devices and display detailed configuration space information.</span></span></p><p><span><span>/proc/bus/pci/ and /sys/bus/pci/: These virtual file systems also provide rich information for debugging and viewing device and driver status.</span></span></p><p><span><span>Summary:</span></span></p><p><span><span>The core development process of PCI drivers is as follows:</span></span></p><p><span><span>[1].</span><span> Define the Device ID Table:</span><span> Use pci_device_id to specify the devices supported by the driver.</span></span></p><p><span><span>[2].</span></span><span><span> Fill the pci_driver Structure:</span><span> Implement core callback functions such as probe and remove.</span></span></p><p><span><span>[3].</span></span><span><span> Register the Driver:</span><span> Call pci_register_driver during module initialization.</span></span></p><p><span><span>In probe, initialize in order: enable device -> allocate regions -> set DMA -> map I/O memory -> register interrupts.</span></span></p><p><span><span>[4].</span></span><span><span> Implement Device Operations:</span><span> Implement read, write, ioctl, etc., in file_operations to interact with hardware through mapped addresses.</span></span></p><p><span><span>[5].</span></span><span><span> Thoroughly Clean Up in Remove:</span><span> Release all resources in the reverse order of probe.</span></span></p><p><span><span>Notes:</span></span></p><p><span><span>[1].</span><span> Always Check Return Values:</span><span> Kernel functions may fail, and it is essential to check return values for critical steps (such as pci_enable_device).</span></span></p><p><span><span>[2].</span></span><span><span> Use Kernel APIs:</span><span> Accessing I/O memory must use functions like ioread/iowrite; directly dereferencing pointers is incorrect and dangerous.</span></span></p><p><span><span>[3].</span></span><span><span> Resource Management:</span><span> Whoever allocates,</span><span> whoever releases.</span><span> Ensure that all allocated resources are released when the driver is unloaded or the device is removed to avoid resource leaks.</span></span></p><p><span><span>That concludes the full content.</span></span></p><span>Previous articles (welcome to subscribe to the technical sharing column for all articles):</span><span>[Project Practice] Locating the issue of Flash entering hardware write protection in embedded software systems and the implementation method to unlock the write lock using software</span><span>[Project Practice] A nanny-level tutorial: PWM control of white light lamps and brightness adjustment</span><span>[Project Practice] Methods and issues encountered in enabling virtual bridge functionality under Linux and their solutions</span><span>[Project Practice] Troubleshooting and solutions for audio intercom functionality delays and stuttering under Linux</span><span>[Project Practice] Troubleshooting and solutions for OTA upgrade failures causing system bricking under SPI Nor Flash</span><img alt="In-Depth Reading: Chapter 12 of &apos;Linux Device Drivers&apos; - PCI Drivers" src="https://boardor.com/wp-content/uploads/2026/01/f3a228bf-6476-41d5-a27e-ae3502af033a.png" /><span>"</span><span><span><strong><span>Thank you for reading this far</span></strong></span><span>"</span></span><p><span><strong><span>This is the notebook of a female programmer</span></strong></span></p><p><span><strong><span>15+ years embedded software engineer and a mother of two</span></strong></span></p><p><span><strong><span>Sharing reading insights, work experiences, personal growth, and lifestyle.</span></strong></span></p><p><span><strong><span>I hope my words can be helpful to you</span></strong></span></p>

Leave a Comment