PCI BAR Mapping and MMIO-Free Linux Device Drivers Training Online

PREV_LEC  |  NEXT_LEC

PCI BAR Mapping and MMIO

Free Linux Kernel Development Course — PCI/PCIe Driver Series

Chapter 11 · Lecture 9
Linux Kernel 6.x
QEMU edu Device

This lecture is part of our free Linux kernel development course and continues the PCI/PCIe bus driver series on EmbeddedPathashala. In the previous lectures we looked at PCI configuration space access. Here we move to something every real PCI driver needs: reading a device’s Base Address Registers (BARs) and safely mapping them into kernel virtual address space so the driver can talk to the device’s registers using memory-mapped I/O (MMIO). If you are following our free linux device drivers course, this is one of the most practical chapters, because almost every network card, GPU, storage controller, and FPGA-based PCIe device exposes its control registers this way.

PCI BAR Mapping ioremap() pci_iomap() MMIO Linux Driver Free Linux Kernel Development Course

What You Will Learn

  • How PCI devices expose registers through Base Address Registers (BARs)
  • The difference between I/O-mapped and memory-mapped BARs
  • Why a memory region must be reserved before it is mapped, and how the kernel enforces this
  • The classic request_mem_region() + ioremap() pair and its PCI-aware helpers
  • Modern, device-managed (pcim_ / devm_) APIs recommended for current kernels
  • How to write, build, and load an original PCI driver that maps a BAR and performs a register read/write

Prerequisites

  • Comfortable with basic PCI concepts: struct pci_dev, pci_device_id, pci_register_driver() (covered in earlier lectures of this free linux development course)
  • A Linux kernel build environment (kernel headers for your running kernel, gcc, make)
  • QEMU with the edu educational PCI device enabled, used throughout this series for safe, reproducible testing without real hardware

Why BARs Exist: Two Address Spaces, One Device

Every PCI or PCIe function that wants the CPU to talk to its on-board registers advertises one or more Base Address Registers in its configuration space header. A BAR is not the register file itself — it is a small configuration space slot that tells the system “give me a chunk of address space this big, and redirect anything the CPU writes there straight to my hardware.” The PCI enumeration code (the BIOS/UEFI firmware, and later the kernel’s PCI core) assigns each BAR a real, non-overlapping range in either the CPU’s memory address space or, on x86, the separate legacy I/O port space.

A BAR can describe two very different kinds of resource:

  • Memory-mapped I/O (MMIO) — the BAR occupies part of the normal physical memory address space. The CPU reads and writes it with ordinary load/store instructions (through a kernel virtual mapping), and the PCI bridge silently routes that traffic to the device instead of RAM.
  • I/O-mapped (Port I/O) — on x86, some BARs instead live in the legacy 16-bit I/O port space and are accessed with inb()/outb() style instructions. This is largely a legacy mechanism; almost all modern PCIe endpoints use MMIO BARs.
CPU Access to a PCI Device via a Memory-Mapped BAR
CPU Core | | load/store to virtual address (from ioremap) v MMU / Page Tables | | translates to physical address inside the BAR window v PCI Host Bridge / Root Complex | | routes the transaction on the PCI/PCIe fabric v PCI Device Function | +– BAR0 window –> Device Register File (control, status, data regs) +– BAR2 window –> Device DMA Buffer / Ring Descriptors

Because a BAR is really just a “please give me address space” request, the kernel needs two separate steps before a driver can safely touch it: first it must reserve that resource range so no other driver tries to claim the same physical addresses, and only then can it create a virtual mapping so the CPU can actually issue loads and stores against it. Skipping the reservation step is a classic bug source — two drivers silently corrupting each other’s hardware state.

Reading BAR Information from struct pci_dev

Once the kernel core has finished PCI enumeration, every BAR’s address, size, and type are already available on the matched struct pci_dev. Your driver never computes these values itself — it queries them through a small set of accessor functions:

unsigned long pci_resource_start(struct pci_dev *dev, int bar);
unsigned long pci_resource_len(struct pci_dev *dev, int bar);
unsigned long pci_resource_end(struct pci_dev *dev, int bar);
unsigned long pci_resource_flags(struct pci_dev *dev, int bar);

pci_resource_start() returns the first physical address of the given BAR index, pci_resource_len() returns its size in bytes, and pci_resource_flags() tells you whether the BAR is IORESOURCE_MEM (MMIO) or IORESOURCE_IO (port I/O) — always check this before you decide to ioremap() anything, since calling ioremap() on a port-I/O BAR is a bug.

The Classic Way: request_mem_region() and ioremap()

The lowest-level, most explicit way to claim and map a memory-mapped BAR uses two independent kernel primitives:

struct resource *request_mem_region(resource_size_t start,
                                     resource_size_t n,
                                     const char *name);

void __iomem *ioremap(phys_addr_t phys_addr, size_t size);

request_mem_region() performs no mapping at all — it is a pure bookkeeping reservation in the kernel’s resource tree. Its return value is only meaningful as success/failure; you never dereference it. Every well-behaved driver is expected to call this before touching a region, which is how the kernel prevents two drivers from claiming overlapping physical ranges. ioremap() is the function that actually does the work: it creates page-table entries that map the physical BAR range into the kernel’s virtual address space, with the correct caching attributes for device memory (normally uncached), and hands back an __iomem pointer that must only be accessed through the ioread32()/iowrite32() family — never through plain pointer dereference, since the compiler is free to reorder or combine plain memory accesses in ways that break real hardware registers.

void __iomem *regs;
resource_size_t bar0_start, bar0_len;

bar0_start = pci_resource_start(pdev, 0);
bar0_len   = pci_resource_len(pdev, 0);

if (!request_mem_region(bar0_start, bar0_len, "ep_pci_bar_demo"))
        return -EBUSY;

regs = ioremap(bar0_start, bar0_len);
if (!regs) {
        release_mem_region(bar0_start, bar0_len);
        return -ENOMEM;
}

PCI-Aware Helpers: One Call Instead of Two

Because “reserve, then map a BAR” is such a common PCI pattern, the PCI core wraps the classic primitives into single-call helpers that already know the BAR’s address, length, and type from struct pci_dev:

int pci_request_region(struct pci_dev *pdev, int bar, const char *name);
int pci_request_regions(struct pci_dev *pdev, const char *name);

void __iomem *pci_iomap(struct pci_dev *dev, int bar, unsigned long maxlen);
void __iomem *pci_iomap_range(struct pci_dev *dev, int bar,
                               unsigned long offset, unsigned long maxlen);
void __iomem *pci_ioremap_bar(struct pci_dev *pdev, int bar);

void pci_iounmap(struct pci_dev *dev, void __iomem *addr);
void pci_release_region(struct pci_dev *pdev, int bar);
void pci_release_regions(struct pci_dev *pdev);

pci_request_regions() reserves every BAR of the device in one call, while pci_request_region() targets a single BAR. pci_iomap() maps a BAR and works for both MMIO and port-I/O BARs transparently, returning an __iomem cookie you use with ioread*()/iowrite*() either way — pass 0 as maxlen to map the whole BAR without checking its length yourself. pci_ioremap_bar() is the safest single-BAR shortcut: it refuses to run on a port-I/O BAR, so you cannot accidentally ioremap() something that isn’t memory-mapped. The unmap/release calls are the exact mirror image and must run in your driver’s remove path.

The Modern, Recommended Way: Device-Managed Mapping

Current upstream Linux strongly prefers the devm/pcim managed-resource APIs for exactly this pattern. Resources requested through them are automatically released when the driver detaches or probe() fails partway through, which eliminates most of the manual cleanup bugs (leaked mappings, double-frees, forgetting a release call in an error path) that plague hand-written ioremap()/iounmap() pairs.

int pcim_enable_device(struct pci_dev *pdev);
void __iomem *pcim_iomap_region(struct pci_dev *pdev, int bar, const char *name);

pcim_enable_device() is the managed equivalent of pci_enable_device() — the device is automatically disabled on detach. pcim_iomap_region() is the current recommended replacement for the older pcim_iomap_regions() bitmask function (that bitmask version is now deprecated upstream); it both requests and maps a single BAR in one call, returns the __iomem pointer directly, and the mapping/reservation is torn down automatically when the device is detached — no matching “release” call is needed in the normal path.

Comparison: Choosing the Right API

ApproachReserve + Map CallsCleanupWhen to Use
Classic primitivesrequest_mem_region() + ioremap()Manual, in every error pathLearning the underlying mechanism; non-PCI MMIO devices
PCI helperspci_request_region() + pci_iomap() / pci_ioremap_bar()Manual (pci_iounmap() + pci_release_region())PCI drivers that need explicit control over lifetime
Managed (devm/pcim)pcim_enable_device() + pcim_iomap_region()Automatic on detachRecommended default for new PCI driver code

Building the ep_pci_bar_demo Driver

The following original demo driver targets QEMU’s edu educational PCI device (vendor 0x1234, device 0x11e8), which we have used throughout this series so every reader can test without real hardware. It maps BAR0 using the modern managed API, reads the device identification register, writes a value into a scratch register, and reads it back to confirm the MMIO path works end to end.

// ep_pci_bar_demo.c
#include <linux/module.h>
#include <linux/pci.h>
#include <linux/io.h>

#define EP_VENDOR_ID   0x1234
#define EP_DEVICE_ID   0x11e8

/* edu device register offsets (from QEMU edu device spec) */
#define EP_REG_IDENT   0x00
#define EP_REG_SCRATCH 0x04

static void __iomem *ep_regs;

static int ep_pci_bar_probe(struct pci_dev *pdev,
                             const struct pci_device_id *id)
{
        u32 ident, readback;
        int rc;

        rc = pcim_enable_device(pdev);
        if (rc) {
                dev_err(&pdev->dev, "ep_pci_bar_demo: enable failed\n");
                return rc;
        }

        pci_set_master(pdev);

        ep_regs = pcim_iomap_region(pdev, 0, "ep_pci_bar_demo");
        if (IS_ERR(ep_regs)) {
                dev_err(&pdev->dev, "ep_pci_bar_demo: BAR0 map failed\n");
                return PTR_ERR(ep_regs);
        }

        ident = ioread32(ep_regs + EP_REG_IDENT);
        dev_info(&pdev->dev, "ep_pci_bar_demo: ident=0x%08x\n", ident);

        iowrite32(0xCAFEBABE, ep_regs + EP_REG_SCRATCH);
        readback = ioread32(ep_regs + EP_REG_SCRATCH);
        dev_info(&pdev->dev, "ep_pci_bar_demo: scratch readback=0x%08x\n",
                  readback);

        return 0;
}

static void ep_pci_bar_remove(struct pci_dev *pdev)
{
        dev_info(&pdev->dev, "ep_pci_bar_demo: removed\n");
}

static const struct pci_device_id ep_pci_bar_ids[] = {
        { PCI_DEVICE(EP_VENDOR_ID, EP_DEVICE_ID) },
        { }
};
MODULE_DEVICE_TABLE(pci, ep_pci_bar_ids);

static struct pci_driver ep_pci_bar_driver = {
        .name     = "ep_pci_bar_demo",
        .id_table = ep_pci_bar_ids,
        .probe    = ep_pci_bar_probe,
        .remove   = ep_pci_bar_remove,
};

module_pci_driver(ep_pci_bar_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("EmbeddedPathashala PCI BAR mapping demo (free linux kernel development course)");

Build and Run Walkthrough

Build the module against your running kernel’s headers:

# Makefile
obj-m += ep_pci_bar_demo.o

# build
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules

Start QEMU with the edu device attached (as used in earlier lectures of this series), then load the module:

sudo insmod ep_pci_bar_demo.ko
dmesg | tail -n 5

Expected output:

[  912.104211] ep_pci_bar_demo: ident=0x00010000
[  912.104239] ep_pci_bar_demo: scratch readback=0xcafebabe

The scratch readback matching the value you wrote confirms the BAR0 window is genuinely reaching the device’s register file and not, for example, silently returning cached zeros because of a mapping mistake. Unload with sudo rmmod ep_pci_bar_demo — because we used the managed API, the BAR mapping, region reservation, and device disable all happen automatically, and dmesg will only show the “removed” message from our own remove() callback.

Common Mistakes and Troubleshooting

  • Dereferencing the __iomem pointer directly instead of using ioread32()/iowrite32() — this compiles but is undefined behavior on MMIO and can silently corrupt device state or hang under certain compiler optimizations.
  • Skipping the reservation step and calling ioremap() straight on pci_resource_start() — this leaves the resource tree unaware the region is in use and can let a second driver double-map the same BAR.
  • Calling ioremap() on a port-I/O BAR — always check pci_resource_flags(), or just use pci_ioremap_bar()/pcim_iomap_region(), which refuse to map a non-memory BAR for you.
  • Forgetting to call pci_set_master() when the device also needs to perform DMA — BAR mapping alone does not enable bus mastering.
  • Manual unmap ordering bugs — with the classic API, releasing the memory region before calling iounmap() (or vice-versa) in an error path is a frequent source of resource leaks; the managed API removes this class of bug entirely.

Best Practices

  • Default to the pcim_/devm_ managed APIs for new PCI driver code; reach for the classic primitives mainly when learning or when you have a genuine need for manual lifetime control.
  • Always check pci_resource_flags() before mapping an unfamiliar BAR rather than assuming it is MMIO.
  • Keep register offsets as named macros (as in EP_REG_IDENT above) instead of magic numbers scattered through the driver.
  • Never assume a fixed BAR size in code — always read it with pci_resource_len() and pass it (or 0) into the mapping call.

Performance and Security Considerations

MMIO accesses are far more expensive than a normal RAM access — each ioread32()/iowrite32() typically crosses the PCIe fabric and cannot be reordered or cached, so hot data-plane paths should minimize register touches and use DMA buffers instead of pushing bulk data through a BAR one word at a time. From a security standpoint, never map more of a BAR than pci_resource_len() reports, and validate any user-space-influenced offset before adding it to your mapped base pointer — an unchecked offset can turn a driver bug into an out-of-bounds MMIO access against unrelated hardware state.

Summary and Key Takeaways

PCI Base Address Registers are how a device advertises the address-space window the kernel should route to its register file. Before that window can be touched, the kernel must reserve it in the resource tree and then create a virtual mapping for it — historically via request_mem_region() + ioremap(), more conveniently via the PCI helpers pci_request_region()/pci_iomap(), and in current upstream code preferably via the managed pcim_enable_device() + pcim_iomap_region() pair, which cleans up automatically on driver detach. Understanding this pattern is essential groundwork for the rest of this free linux kernel development course, since interrupt handling, DMA, and higher-level PCI subsystems all build directly on top of a correctly mapped BAR.

Conclusion

With BAR mapping and MMIO access now covered, you have everything needed to read and write registers on a real PCI/PCIe device from a Linux kernel driver. This chapter, together with the earlier lectures on PCI device matching and configuration space access, completes the core toolkit this free embedded systems course builds on for more advanced PCI topics — interrupts, DMA, and power management — later in this free linux device drivers course.

Frequently Asked Questions

What is a PCI BAR in simple terms?

A Base Address Register is a slot in a PCI device’s configuration space that tells the system how much address space the device needs and what type it is (memory-mapped or I/O-mapped), so the kernel can assign it a real address range.

Why do I need to call request_mem_region() before ioremap()?

request_mem_region() reserves the physical address range in the kernel’s resource tree so no other driver can claim the same addresses. ioremap() only creates the virtual mapping — it does not check for conflicts on its own.

What is the difference between ioremap() and pci_iomap()?

ioremap() is a generic kernel primitive that works on any physical address range. pci_iomap() is PCI-aware: it looks up the BAR’s type from struct pci_dev and works correctly for both memory-mapped and I/O-mapped BARs.

Is pcim_iomap_regions() still recommended?

No. It is deprecated in current upstream kernels in favor of pcim_iomap_region() (singular), which maps one BAR at a time and returns the mapped pointer directly instead of relying on an internal iomap table.

Can I access an __iomem pointer like a normal pointer?

No. Always use ioread8/16/32() and iowrite8/16/32() (or their relaxed variants where appropriate). Direct dereference of an __iomem pointer bypasses barriers the kernel needs for correct hardware ordering.

Do I need pci_set_master() just to map a BAR?

No, mapping a BAR only prepares register access. pci_set_master() is a separate step needed only if the device will also perform DMA into system memory.

What happens if I map a BAR without reserving it first?

The mapping may still succeed, but the kernel has no record that the region is in use, so another driver (or a debugging tool) could claim and map the same physical range, leading to unpredictable hardware corruption.

Which approach should beginners in this free linux kernel development course start with?

Start by understanding the classic request_mem_region()/ioremap() pair since it makes the underlying mechanism explicit, then move to the managed pcim_enable_device()/pcim_iomap_region() APIs for any driver code you actually intend to ship.

Continue the Free Linux Kernel Development Course

More PCI/PCIe driver lectures — interrupts, DMA, and power management — are coming next in this free linux device drivers course on EmbeddedPathashala.

Browse All Kernel Lectures Join the Free Embedded Systems Course

PREV_LEC  |  NEXT_LEC

Leave a Reply

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