PCI Address Spaces and BARs-Free Linux Device Drivers Training Online

PREV_LEC  |  NEXT_LEC

PCI Address Spaces and BARs
Understand PCI configuration, memory and I/O address spaces, then map a BAR from a real kernel driver
Config Space
Memory Space
BAR Mapping

Once bus enumeration finishes assigning bus numbers to every PCI device, the kernel still needs a way to actually talk to those devices. That’s where pci address spaces linux drivers rely on come in. A PCI target exposes up to three distinct address spaces — configuration, memory, and I/O — and every register you will ever touch in a PCI driver lives inside one of them. This lecture, part of EmbeddedPathashala’s free linux kernel development course, walks through all three spaces, explains exactly what a Base Address Register (BAR) is, and ends with a working driver that maps a BAR and reads a device register on a current mainline kernel.

What You Will Learn

PCI configuration address space layout PCI memory address space and MMIO Why PCI I/O address space is legacy What a Base Address Register (BAR) does Reading configuration space with pci_read_config_*() Mapping a BAR with pci_iomap()

Prerequisites

Basic character/platform driver experience Comfortable compiling out-of-tree kernel modules A Linux host or VM with a PCI/PCIe bus (QEMU works fine) Understanding of PCI bus enumeration (previous lecture)

Why PCI Needs Three Address Spaces

Every device on a PCI or PCI Express bus is reachable through a Bus:Device.Function (BDF) address, but that address alone does not tell the CPU what kind of access it’s making. A single PCI target can expose up to three logically separate address spaces, and the type of space determines how a read or write is routed once it reaches the device:

  • Configuration Address Space — a fixed-format block of registers used to identify the device (vendor ID, device ID, class code) and to program its basic operating parameters, including which BARs are active.
  • Memory Address Space — a window carved out of the system’s physical memory map. Reads and writes to this window use ordinary load/store instructions but are intercepted by the host bridge and routed to the device instead of RAM. This is known as Memory-Mapped I/O (MMIO), and it is what almost every modern driver uses.
  • I/O Address Space — a legacy, x86-specific port space accessed with dedicated `in`/`out` style instructions. The PCI Express specification actively discourages new devices from using it, and most modern PCIe endpoints skip it entirely in favor of MMIO.

Configuration and memory address spaces are both memory-mapped in the sense that they are assigned ranges from the system’s address map — the difference is that configuration space is a standardized, bus-wide register layout used for device discovery and setup, while memory space is device-specific runtime data and registers.

PCI Configuration Space Layout
Standard Header
0x00 – 0x3F
Vendor/Device ID, Command, Status, BARs
Capabilities Region
0x40 – 0xFF
Power Mgmt, MSI, PCIe Caps
Extended Capabilities
0x100 – 0xFFF
AER, MSI-X, SR-IOV (PCIe only)

PCI Configuration Space From a Kernel Driver

Legacy PCI defines 256 bytes of configuration space per function; PCI Express extends this to 4 KB using an “Extended Configuration Space” that sits behind the same standard header. The first 64 bytes are fixed by the PCI specification and include the Vendor ID and Device ID fields the kernel uses to match your driver against hardware. Everything past 0x40 is where capability structures — power management, MSI, MSI-X, PCIe link control — are chained together as a linked list.

The kernel never expects you to poke configuration space with raw offsets from userspace. Instead, every bound driver receives a `struct pci_dev *`, and the PCI core provides typed accessors:

#include <linux/pci.h>

static int ep_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    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,
             "ep_pci: matched vendor=0x%04x device=0x%04x rev=0x%02x\n",
             vendor, device, revision);

    return 0;
}

You can inspect the same 256 bytes (or 4 KB on PCIe) from userspace without writing any code at all, which is the fastest way to sanity-check your driver’s PCI ID table against real hardware:

# List devices with vendor:device IDs
lspci -nn

# Dump raw configuration space of a specific device
lspci -xxx -s 00:02.0

# Every device also exposes its config space as a sysfs binary file
xxd /sys/bus/pci/devices/0000:00:02.0/config | head

PCI I/O Address Space: Why It’s Legacy

I/O address space dates back to a time when processor memory address space was tightly constrained — think 16-bit systems where reserving ranges for device access felt wasteful. Port-mapped I/O gave devices a separate 65536-port address space (some very old machines had only 1024 ports) reachable through dedicated `in`/`out` instructions rather than ordinary loads and stores. As system memory space grew into the gigabytes, that separation stopped being useful and started being a burden: a second instruction set, a second bus, and no benefit once memory space could comfortably hold both RAM and device windows.

Modern PCIe drivers almost never touch I/O space directly. If you find yourself calling `inb()`/`outb()` in a new driver, it’s worth double-checking whether the device actually requires it or whether you should be using its memory-mapped BAR instead.

Base Address Registers and Memory-Mapped I/O

A Base Address Register (BAR) is how a PCI device tells the host “I need this much address space, of this type, for my registers.” A device can advertise up to six BARs. During boot, firmware or the kernel’s PCI core allocates real system address ranges for each BAR and programs that range back into the device’s configuration space — this is the allocation step that follows bus enumeration.

The important mental model: a BAR is not physical RAM. It is a window. When the CPU writes to an address inside that window, the write never touches memory — the host bridge recognizes the address as belonging to a PCI device and forwards the transaction as a Memory Write Transaction Layer Packet (TLP) straight to that device’s internal registers.

How a BAR Connects the CPU to Device Registers
CPU ioread32() / iowrite32()
BAR Window (System Memory Map)
Device-Internal Registers

Inside a driver, you never hardcode a BAR’s physical address — you always ask the PCI core for it, because the actual value depends on what firmware assigned at boot:

#include <linux/pci.h>
#include <linux/io.h>

struct ep_pci_dev {
    void __iomem *regs;
    struct pci_dev *pdev;
};

static int ep_pci_map_bar0(struct pci_dev *pdev, struct ep_pci_dev *epdev)
{
    int ret;

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

    ret = pci_request_regions(pdev, "ep_pci_demo");
    if (ret)
        goto err_disable;

    /* BAR0 length and flags come from the PCI core, not guesswork */
    if (!(pci_resource_flags(pdev, 0) & IORESOURCE_MEM)) {
        dev_err(&pdev->dev, "BAR0 is not a memory region\n");
        ret = -ENODEV;
        goto err_release;
    }

    epdev->regs = pci_iomap(pdev, 0, pci_resource_len(pdev, 0));
    if (!epdev->regs) {
        ret = -ENOMEM;
        goto err_release;
    }

    pci_set_master(pdev);
    return 0;

err_release:
    pci_release_regions(pdev);
err_disable:
    pci_disable_device(pdev);
    return ret;
}

Once `regs` is mapped, ordinary `ioread32(epdev->regs + offset)` and `iowrite32(value, epdev->regs + offset)` calls are all you need — the compiler barriers and bus ordering are already handled by these accessors, which is why you should never dereference `__iomem` pointers directly.

Build and Test: Confirming Your BAR Mapping

Wire `ep_pci_map_bar0()` into a minimal `pci_driver` with a matching `pci_device_id` table, build it as an out-of-tree module, and load it against a matching device. A QEMU guest with a `virtio-net-pci` device is the easiest way to test this without physical hardware:

make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
sudo insmod ep_pci_demo.ko
dmesg | tail -n 5

Expected output after loading against a matching device:

[   84.220117] ep_pci: matched vendor=0x1af4 device=0x1000 rev=0x00
[   84.220314] ep_pci_demo: BAR0 mapped, length=4096 bytes
lspci -k -s 00:03.0
sudo rmmod ep_pci_demo

Common Mistakes and Troubleshooting

  • Reading a BAR’s address without pci_resource_start() — firmware assigns BAR addresses at boot; a driver that hardcodes an address will break the moment it runs on different hardware.
  • Dereferencing `__iomem` pointers directly — always use `ioread32()`/`iowrite32()` (or `readl`/`writel`); direct pointer access skips required memory barriers and can silently reorder on some architectures.
  • Forgetting `pci_set_master()` — devices that perform DMA will fail silently if bus mastering was never enabled.
  • Assuming a fixed BAR size — always call `pci_resource_len()` rather than hardcoding a length across device revisions.
  • Skipping `pci_request_regions()` — without it, nothing stops a second driver from mapping the same BAR and corrupting device state.

Best Practices for Working With PCI Address Spaces

  • Always check `pci_resource_flags()` before mapping — confirm you’re looking at a memory BAR (`IORESOURCE_MEM`), not an I/O BAR, before calling `pci_iomap()`.
  • Use `pci_iomap()` instead of raw `ioremap()` — it correctly handles both memory-mapped and, where unavoidable, I/O-mapped BARs behind one API.
  • Pair every `pci_enable_device()`/`pci_request_regions()`/`pci_iomap()` call with the matching cleanup call in both your error paths and your `remove()` callback.
  • Treat configuration space writes as privileged operations — a misbehaving driver writing to the wrong config offset can disable a device’s memory decode entirely or corrupt shared bus state.

Summary and Key Takeaways

A PCI device exposes itself to the kernel through three address spaces — configuration space for identification and setup, memory space for runtime register access via BARs, and a largely legacy I/O space. The kernel never lets a driver guess at physical addresses; every BAR is discovered through `pci_resource_*()` helpers and mapped with `pci_iomap()`. With configuration space read and a BAR safely mapped, your driver is ready for the next step — handling interrupts, which the next lecture in this free linux device drivers course covers in depth.

Frequently Asked Questions

What is the difference between PCI configuration space and memory space?

Configuration space is a standardized register block used for device identification and setup (Vendor ID, Device ID, BAR programming). Memory space is the runtime, device-specific register window exposed through a BAR and accessed with ioread32()/iowrite32() during normal driver operation.

Why does a modern PCIe device rarely use I/O address space?

I/O address space is an x86-specific legacy mechanism limited to 65536 ports, requires separate in/out instructions, and the PCI Express specification actively discourages it. Memory-mapped I/O through BARs is simpler, portable across architectures, and is what nearly all PCIe endpoints use today.

What exactly is a Base Address Register (BAR)?

A BAR is a configuration space register through which a PCI device tells the host how much address space it needs and of what type. The host allocates real system address ranges for each BAR at boot; the driver later maps that range with pci_iomap() to talk to the device.

How many BARs can a single PCI device have?

Up to six. Not every device uses all six — some BARs may be unused, and 64-bit memory BARs consume two consecutive BAR slots.

Why use pci_iomap() instead of ioremap()?

pci_iomap() inspects the BAR’s resource flags and correctly handles both memory-mapped and I/O-mapped BARs behind a single API, whereas ioremap() assumes a memory-mapped region and requires you to branch on BAR type yourself.

Do I need real PCI hardware to practice this lecture?

No. QEMU can expose virtual PCI/PCIe devices such as virtio-net-pci to a guest VM, which is enough to practice probe() and BAR mapping exactly as shown above.

Is this course part of a free Linux kernel development curriculum?

Yes — this lecture is part of EmbeddedPathashala’s free linux kernel development course, which also covers character drivers, platform drivers, DMA, clocks, and other Linux kernel subsystems end to end.

Continue to PCI Interrupt Handling

Now that a BAR is mapped, learn INTx, MSI, and MSI-X in the next lecture of this free linux kernel development course.

Next Lecture Course Index

PREV_LEC  |  NEXT_LEC

Leave a Reply

Your email address will not be published. Required fields are marked *