Part of EmbeddedPathashala’s free Linux kernel development course — reading and writing PCI config registers the modern way
Every PCI and PCIe device carries a small, standardized block of registers separate from its normal memory-mapped operation — its configuration space. This is where the kernel and your driver discover a device’s identity, capabilities, and control bits before a single normal I/O operation happens. This lecture covers PCI configuration space access in depth: the byte/word/dword read and write primitives, the difference between legacy 256-byte config space and PCIe’s 4KB extended space, and the modern capability-walking helpers that replace hardcoded offsets in current kernel drivers.
What You Will Learn
- What PCI configuration space is and why it exists separately from BAR-mapped memory
- The size difference between conventional PCI/PCI-X config space and PCIe extended config space
- How to use pci_read_config_byte/word/dword() and their write counterparts
- Why symbolic offsets from pci_regs.h are preferred over raw numeric offsets
- How to walk PCI and PCIe capabilities with the modern helper APIs
- How to build an original driver that safely reads config space on QEMU
Prerequisites
- The previous lecture on PCI driver registration and device enable/disable (see PREV_LEC)
- Basic understanding of struct pci_dev
- QEMU with the edu educational PCI device, as used throughout this free embedded systems course
What Is PCI Configuration Space?
Configuration space is a dedicated address space every PCI function exposes, independent of its normal memory or I/O BARs. It holds the device’s vendor and device IDs, class code, command and status bits, BAR descriptors themselves, and — on PCIe — an extended region of capability structures describing advanced features like power management, MSI-X, and AER (Advanced Error Reporting). Conventional PCI and PCI-X mode 1 devices expose 256 bytes of configuration space. PCI-X mode 2 and all PCIe devices expose 4,096 bytes, with the first 256 bytes remaining backward compatible with the legacy layout.
Reading Configuration Space
The kernel exposes three primitives for reading configuration registers, one per access width. All three take the target struct pci_dev, a byte offset, and a pointer to receive the value.
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);
where is the byte offset from the start of configuration space. Rather than hardcoding numeric offsets, use the symbolic macros defined in include/uapi/linux/pci_regs.h:
u16 vendor, device;
u8 revision;
pci_read_config_word(pdev, PCI_VENDOR_ID, &vendor);
pci_read_config_word(pdev, PCI_DEVICE_ID, &device);
pci_read_config_byte(pdev, PCI_REVISION_ID, &revision);
dev_info(&pdev->dev, "vendor=%04x device=%04x rev=%02x\n",
vendor, device, revision);
Note that in practice most of the identification fields above are already cached by the kernel on the struct pci_dev itself (pdev->vendor, pdev->device, pdev->revision), so a raw config space read is normally reserved for registers the core doesn’t cache — command/status bits, capability-specific fields, or vendor-defined registers.
Writing Configuration Space
Writing follows the same pattern, with the value passed by value rather than by pointer:
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);
u16 cmd;
pci_read_config_word(pdev, PCI_COMMAND, &cmd);
cmd |= PCI_COMMAND_MEMORY; /* enable memory space decoding */
pci_write_config_word(pdev, PCI_COMMAND, cmd);
In driver code you rarely write to the command register directly like this, since pci_enable_device() already manages it correctly — but the pattern above is exactly how the kernel implements bits of that logic internally, and it’s a common building block when you need to toggle a vendor-specific control bit that has no dedicated helper.
Walking Capabilities the Modern Way
Raw offsets work for the fixed standard header, but capability structures (MSI, MSI-X, power management, PCIe extended capabilities) live at offsets that vary per device and must be discovered by walking a linked list. Current kernels provide helpers so drivers never need to parse that list by hand:
int pos = pci_find_capability(pdev, PCI_CAP_ID_PM);
if (pos)
dev_info(&pdev->dev, "Power Management capability at offset 0x%x\n", pos);
For PCIe extended capabilities (offset 0x100 and beyond), use the pcie_capability_* family instead of raw config accessors — they transparently locate the PCI Express capability structure and handle devices where certain fields don’t apply:
u16 devctl;
pcie_capability_read_word(pdev, PCI_EXP_DEVCTL, &devctl);
pcie_capability_write_word(pdev, PCI_EXP_DEVCTL, devctl | PCI_EXP_DEVCTL_RELAX_EN);
Original Demo: ep_pci_cfgspace_demo Driver
This original driver reads several standard header fields from QEMU’s edu device (vendor 0x1234, device 0x11e8) during probe and logs them, demonstrating byte, word, and dword width reads together.
#include <linux/module.h>
#include <linux/pci.h>
#define EP_EDU_VENDOR_ID 0x1234
#define EP_EDU_DEVICE_ID 0x11e8
static int ep_pci_cfgspace_probe(struct pci_dev *pdev,
const struct pci_device_id *id)
{
u16 command, status;
u8 revision, cache_line;
u32 class_rev;
int ret;
ret = pcim_enable_device(pdev);
if (ret)
return ret;
pci_read_config_word(pdev, PCI_COMMAND, &command);
pci_read_config_word(pdev, PCI_STATUS, &status);
pci_read_config_byte(pdev, PCI_REVISION_ID, &revision);
pci_read_config_byte(pdev, PCI_CACHE_LINE_SIZE, &cache_line);
pci_read_config_dword(pdev, PCI_CLASS_REVISION, &class_rev);
dev_info(&pdev->dev,
"cfgspace: command=0x%04x status=0x%04x rev=0x%02x "
"cache_line=%u class_rev=0x%08x\n",
command, status, revision, cache_line, class_rev);
return 0;
}
static const struct pci_device_id ep_pci_cfgspace_ids[] = {
{ PCI_DEVICE(EP_EDU_VENDOR_ID, EP_EDU_DEVICE_ID) },
{ }
};
MODULE_DEVICE_TABLE(pci, ep_pci_cfgspace_ids);
static struct pci_driver ep_pci_cfgspace_driver = {
.name = "ep_pci_cfgspace_demo",
.id_table = ep_pci_cfgspace_ids,
.probe = ep_pci_cfgspace_probe,
};
module_pci_driver(ep_pci_cfgspace_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("EmbeddedPathashala PCI configuration space read demo");
Build and Run Walkthrough
# Makefile
obj-m += ep_pci_cfgspace_demo.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
$ make
$ sudo insmod ep_pci_cfgspace_demo.ko
$ dmesg | tail -n 2
[ 231.114882] ep_pci_cfgspace_demo 0000:00:04.0: cfgspace: command=0x0002 status=0x0000 rev=0x10 cache_line=0 class_rev=0x00ff0010
You can cross-check these values independently with the standard userspace tool setpci, e.g. sudo setpci -s 00:04.0 COMMAND, which reads the same register through /sys/bus/pci/devices/*/config.
Raw Offsets vs Capability Helpers vs Userspace Tools
| Method | Where it runs | Best for |
|---|---|---|
| pci_read/write_config_*() | Kernel driver | Standard header fields, vendor-defined registers |
| pcie_capability_read/write_word() | Kernel driver | PCIe extended capability structures |
| setpci / lspci -xxx | Userspace | Debugging, one-off inspection outside a driver |
Common Mistakes and Troubleshooting
- Hardcoding numeric offsets instead of the symbolic macros from pci_regs.h — makes code fragile and hard to review.
- Assuming a fixed capability offset. Capability positions vary per device; always discover them with pci_find_capability() or the pcie_capability_* helpers.
- Writing to reserved or read-only bits in the command/status register, which can produce undefined device behavior.
- Reading extended (0x100+) config space with the plain byte/word/dword calls on hardware that doesn’t route them correctly — prefer the pcie_capability_* helpers, which handle this correctly across device types.
Best Practices and Security Considerations
- Rely on cached fields on struct pci_dev (vendor, device, revision, class) instead of re-reading them from config space when they’re already available.
- Never blindly write full 32-bit values to command/status style registers — read-modify-write specific bits only.
- Treat configuration space writes as privileged and rare; most drivers only ever read it, letting the PCI core manage writes through pci_enable_device() and friends.
- Use pcie_capability_* helpers for anything beyond the legacy 256-byte header to stay correct across both conventional PCI and PCIe devices.
Interview Questions
- How large is conventional PCI configuration space compared to PCIe extended configuration space?
- Why does the kernel provide byte/word/dword variants of the config space read/write functions?
- Why are hardcoded configuration space offsets discouraged in driver code?
- What is the purpose of the Capabilities Pointer at offset 0x34?
- Why do pcie_capability_read_word() and pcie_capability_write_word() exist separately from the standard pci_read/write_config_word()?
Summary and Key Takeaways
PCI configuration space access gives a driver its earliest and most fundamental view of a device — identity, control bits, and capability structures, all reachable through pci_read_config_*() and pci_write_config_*(). Conventional PCI devices expose 256 bytes of this space, while PCIe devices extend it to 4KB to accommodate modern capability structures, which should always be located through pci_find_capability() or the pcie_capability_* helpers rather than hardcoded offsets. Combined with the driver registration and enable/disable lifecycle from the previous lecture, you now have the full foundation needed to write a complete, well-behaved PCI device driver in this free Linux kernel development course.
FAQ
Do I need to call pci_enable_device() before reading configuration space?
No — configuration space reads work even on a device that hasn’t been enabled yet, since enabling only affects memory/I/O BAR decoding. However, writes that affect device operation should generally happen after the device is enabled and initialized.
What is the difference between PCI_CLASS_REVISION and PCI_REVISION_ID?
PCI_REVISION_ID is the single lowest byte of the class/revision dword; PCI_CLASS_REVISION reads the full 32-bit field containing both the class code and the revision ID together.
Why can’t I just read offset 0x100 directly with pci_read_config_dword()?
You can on most platforms, since it’s still a valid config space offset, but PCIe extended capabilities are meant to be located dynamically via their capability ID, not assumed at a fixed offset — using pcie_capability_* keeps your driver correct across different devices.
Is it safe to write directly to the command register?
Only with a read-modify-write pattern that touches specific bits. Writing an arbitrary full value can disable bits like memory space decoding that pci_enable_device() already configured correctly.
Can userspace tools like setpci corrupt a running device?
Yes, if used carelessly — setpci writes directly to the same configuration space a kernel driver would, so it should be used for debugging with the same caution as in-kernel config writes.
Continue the Free Linux Kernel Development Course
Explore more free embedded Linux and PCI driver lectures on EmbeddedPathashala.
Next Lecture Browse All Lectures