2. PCIe设备BAR空间映射与寄存器读写

任务说明

编写一个内核模块,在设备探测时读取 PCIe 设备的 BAR0 基址,用 pci_iomap 将其映射到内核虚拟地址,然后对偏移 0x00 的控制寄存器执行一次“读-写-回读”验证:先读原值,写入一个测试值,再读回确认一致,最后恢复原值。卸载时用 pci_iounmap 解除映射。

关键点

  • BAR0 的物理基址通过 pci_resource_start(pdev, 0) 获取,长度用 pci_resource_len。
  • 映射前必须用 pci_request_region 声明资源,避免与其他驱动冲突。
  • 寄存器访问用 ioread32/iowrite32,不能直接解引用指针。
  • 测试值选择要避开只读位:这里用 0xA5A50000 并只比较可写位,实际设备需按数据手册确定掩码。

完整代码(pcie_bar_rw.c)

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

#define DRV_NAME "pcie_bar_rw"
#define BAR_INDEX 0
#define CTRL_REG_OFF 0x00
#define TEST_VALUE   0xA5A50000u

static int bar_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    void __iomem *bar;
    resource_size_t bar_start, bar_len;
    u32 orig, readback;

    if (pci_enable_device(pdev))
        return -ENODEV;

    bar_start = pci_resource_start(pdev, BAR_INDEX);
    bar_len   = pci_resource_len(pdev, BAR_INDEX);
    if (!bar_start || !bar_len) {
        dev_err(&pdev->dev, "BAR%d not available\n", BAR_INDEX);
        pci_disable_device(pdev);
        return -ENODEV;
    }

    if (pci_request_region(pdev, BAR_INDEX, DRV_NAME)) {
        dev_err(&pdev->dev, "cannot request BAR%d region\n", BAR_INDEX);
        pci_disable_device(pdev);
        return -EBUSY;
    }

    bar = pci_iomap(pdev, BAR_INDEX, bar_len);
    if (!bar) {
        dev_err(&pdev->dev, "pci_iomap failed\n");
        pci_release_region(pdev, BAR_INDEX);
        pci_disable_device(pdev);
        return -ENOMEM;
    }

    orig = ioread32(bar + CTRL_REG_OFF);
    iowrite32(TEST_VALUE, bar + CTRL_REG_OFF);
    readback = ioread32(bar + CTRL_REG_OFF);
    iowrite32(orig, bar + CTRL_REG_OFF);

    dev_info(&pdev->dev,
             "BAR%d phys=0x%llx len=0x%llx orig=0x%08x wrote=0x%08x read=0x%08x %s\n",
             BAR_INDEX, (unsigned long long)bar_start,
             (unsigned long long)bar_len, orig, TEST_VALUE, readback,
             (readback == TEST_VALUE) ? "OK" : "MISMATCH");

    pci_set_drvdata(pdev, bar);
    return 0;
}

static void bar_remove(struct pci_dev *pdev)
{
    void __iomem *bar = pci_get_drvdata(pdev);

    if (bar)
        pci_iounmap(pdev, bar);
    pci_release_region(pdev, BAR_INDEX);
    pci_disable_device(pdev);
}

static const struct pci_device_id bar_ids[] = {
    { PCI_DEVICE(0x1234, 0x5678) },
    { }
};
MODULE_DEVICE_TABLE(pci, bar_ids);

static struct pci_driver bar_driver = {
    .name     = DRV_NAME,
    .id_table = bar_ids,
    .probe    = bar_probe,
    .remove   = bar_remove,
};

module_pci_driver(bar_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("PCIe BAR0 map and control register read/write test");

构建与运行

# Makefile
obj-m += pcie_bar_rw.o
KDIR ?= /lib/modules/$(shell uname -r)/build

all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean
make
sudo insmod pcie_bar_rw.ko
dmesg | tail -n 5
sudo rmmod pcie_bar_rw

把 bar_ids 中的 0x1234:0x5678 换成目标设备的 Vendor/Device ID(用 lspci -nn 查看)。若设备已被其他驱动占用,需先 rmmod 该驱动或使用 pci-stub 绑定。

验证与判定

  • 加载后 dmesg 应出现一行 BAR0 phys=... len=... orig=... wrote=... read=... OK。
  • read == wrote 且末尾为 OK,说明映射成功且控制寄存器可读写。
  • 若显示 MISMATCH,说明该寄存器存在只读位或写保护,需按数据手册调整 TEST_VALUE 与比较掩码。
  • 卸载后 dmesg 无 oops,lsmod 中不再有 pcie_bar_rw。

常见问题

  • pci_request_region 返回 -EBUSY:BAR 已被占用,检查是否有其他驱动绑定。
  • pci_iomap 返回 NULL:BAR 长度可能为 0,或设备未正确枚举。
  • 读回值恒为 0xFFFFFFFF:设备可能未上电或链路未建立,先确认 lspci -vv 中 BAR 已分配。