3. PCIe设备枚举与资源获取:pci_get_device、pci_enable_device、pci_request_regions、ioremap
PCIe设备在系统中的可见性依赖于内核的枚举机制。驱动开发者需要遵循一套标准化的流程来发现设备、激活设备、申请总线资源并将硬件寄存器映射到内核虚拟地址空间。本章将深入剖析这四个关键步骤的实现原理与编码规范。
核心原则:PCIe驱动对设备的操作必须严格遵循“发现 → 使能 → 资源申请 → 映射”的顺序。任何一步的缺失或顺序错乱都会导致内核崩溃、DMA传输失败或资源泄漏。
3.1 设备发现:pci_get_device
pci_get_device 是驱动获取PCIe设备句柄的入口函数。它通过遍历内核PCI子系统维护的设备链表,根据厂商ID和设备ID匹配目标设备。
#include <linux/pci.h>
struct pci_dev *pci_get_device(unsigned int vendor, unsigned int device,
struct pci_dev *from);
| 参数 | 含义 | 典型值 |
|---|---|---|
vendor |
PCIe厂商ID(Vendor ID) | 0x10EE(Xilinx)、0x8086(Intel) |
device |
PCIe设备ID(Device ID) | 0x7021、0x903F |
from |
前一次调用返回的设备指针,用于迭代;首次调用传NULL | NULL / 上一轮返回值 |
| 返回值 | 成功返回 struct pci_dev *,失败返回NULL |
— |
引用计数陷阱:每次调用
pci_get_device 都会增加设备的引用计数。驱动在不再使用设备时,必须调用 pci_dev_put(dev) 释放引用,否则设备将永远无法被卸载。
// 典型查找模式:遍历所有匹配设备
struct pci_dev *dev = NULL;
while ((dev = pci_get_device(0x10EE, 0x7021, dev)) != NULL) {
// 对每个匹配设备进行操作
pr_info("Found PCIe device: %04x:%04x\n", dev->vendor, dev->device);
// 记录设备信息,后续使用
}
// 注意:循环结束后最后一个dev的引用未被释放
// 正确做法:在循环体内处理完立即释放,或单独调用 pci_dev_put
最佳实践:在probe回调中,内核已经通过设备ID表匹配并传入了正确的
struct pci_dev *,此时无需再调用 pci_get_device。该函数主要用于热插拔场景或需要枚举所有设备的工具类驱动。
3.2 设备使能:pci_enable_device
获取设备句柄后,驱动必须调用 pci_enable_device 来唤醒设备并分配必要的系统资源(如中断线、I/O端口、DMA掩码等)。该函数会设置PCI配置空间中的Command寄存器,开启总线主控和内存空间访问。
int pci_enable_device(struct pci_dev *dev);
| 返回值 | 含义 | 处理方式 |
|---|---|---|
| 0 | 设备使能成功 | 继续后续初始化 |
| -EIO | I/O错误,设备无响应 | 打印错误,返回probe失败 |
| -ENODEV | 设备不存在或已被移除 | 释放设备引用,返回错误 |
| -EBUSY | 资源已被占用 | 等待后重试或返回失败 |
DMA相关:在调用
pci_enable_device 之后,通常紧接着调用 pci_set_master(dev) 来开启总线主控能力,这是DMA传输的前提条件。没有总线主控,设备无法发起内存读写。
// 标准使能序列
struct pci_dev *dev;
int ret;
// 假设dev已通过pci_get_device或probe参数获得
ret = pci_enable_device(dev);
if (ret) {
pr_err("Failed to enable PCIe device: %d\n", ret);
pci_dev_put(dev);
return ret;
}
// 开启总线主控(DMA必需)
pci_set_master(dev);
// 设置DMA掩码(64位地址支持)
ret = dma_set_mask_and_coherent(&dev->dev, DMA_BIT_MASK(64));
if (ret) {
ret = dma_set_mask_and_coherent(&dev->dev, DMA_BIT_MASK(32));
if (ret) {
pr_err("No suitable DMA mask available\n");
pci_disable_device(dev);
pci_dev_put(dev);
return ret;
}
}
3.3 资源申请:pci_request_regions
PCIe设备通过基地址寄存器(BAR)向系统报告其所需的地址空间。驱动在访问这些区域前,必须通过 pci_request_regions 向内核注册所有权,防止多个驱动冲突。
int pci_request_regions(struct pci_dev *dev, const char *res_name);
| BAR索引 | 典型用途 | 资源类型 |
|---|---|---|
| BAR0 | 控制寄存器、状态寄存器 | Memory(32位/64位) |
| BAR1 | DMA描述符环、门铃寄存器 | Memory(32位/64位) |
| BAR2 | MSI-X表格、PBA | Memory |
| BAR4 | 大容量数据缓冲区(可选) | Memory(64位优先) |
// 申请所有BAR区域
ret = pci_request_regions(dev, "mydriver_pcie");
if (ret) {
pr_err("pci_request_regions failed: %d\n", ret);
pci_disable_device(dev);
pci_dev_put(dev);
return ret;
}
// 获取BAR0的物理地址和长度
unsigned long bar0_phys = pci_resource_start(dev, 0);
unsigned long bar0_len = pci_resource_len(dev, 0);
pr_info("BAR0: phys=0x%lx, len=%lu\n", bar0_phys, bar0_len);
资源泄漏风险:如果驱动在
pci_request_regions 之后初始化失败,必须在错误路径中调用 pci_release_regions(dev) 释放资源。否则下次加载驱动时,pci_request_regions 会因为资源已被标记为“已使用”而返回 -EBUSY。
3.4 寄存器映射:ioremap
PCIe设备的BAR空间在物理地址域中,内核无法直接访问。驱动必须使用 ioremap 将物理地址映射到内核虚拟地址空间,之后才能通过指针读写寄存器。
void __iomem *ioremap(phys_addr_t offset, unsigned long size);
// 映射BAR0到内核虚拟地址空间
void __iomem *regs;
regs = ioremap(bar0_phys, bar0_len);
if (!regs) {
pr_err("ioremap failed for BAR0\n");
pci_release_regions(dev);
pci_disable_device(dev);
pci_dev_put(dev);
return -ENOMEM;
}
// 通过映射后的地址读写寄存器
u32 val = ioread32(regs + 0x00); // 读控制寄存器
iowrite32(0x1, regs + 0x04); // 写使能寄存器
// 驱动卸载时取消映射
iounmap(regs);
访问函数选择:
ioread32() / iowrite32():32位访问,带内存屏障,推荐用于寄存器操作。readl() / writel():与ioread32/iowrite32类似,但语义略有不同。memcpy_fromio() / memcpy_toio():批量数据传输,适用于DMA描述符批量更新。
ioread32/iowrite32 系列以保证跨平台一致性。
3.5 完整初始化序列与错误处理
将上述四个步骤组合成一个完整的初始化函数,并实现严格的错误回滚机制,是PCIe驱动开发的基本功。
static int my_pcie_probe(struct pci_dev *dev, const struct pci_device_id *id)
{
struct my_device *mydev;
int ret;
// 1. 分配私有数据结构
mydev = kzalloc(sizeof(*mydev), GFP_KERNEL);
if (!mydev)
return -ENOMEM;
// 2. 使能设备
ret = pci_enable_device(dev);
if (ret)
goto err_free;
// 3. 开启总线主控(DMA必需)
pci_set_master(dev);
// 4. 设置DMA掩码
ret = dma_set_mask_and_coherent(&dev->dev, DMA_BIT_MASK(64));
if (ret) {
ret = dma_set_mask_and_coherent(&dev->dev, DMA_BIT_MASK(32));
if (ret)
goto err_disable;
}
// 5. 申请BAR资源
ret = pci_request_regions(dev, "my_pcie_driver");
if (ret)
goto err_disable;
// 6. 获取BAR信息并映射
mydev->bar0_phys = pci_resource_start(dev, 0);
mydev->bar0_len = pci_resource_len(dev, 0);
mydev->regs = ioremap(mydev->bar0_phys, mydev->bar0_len);
if (!mydev->regs) {
ret = -ENOMEM;
goto err_regions;
}
// 7. 初始化DMA引擎(后续章节详述)
// ...
pci_set_drvdata(dev, mydev);
return 0;
err_regions:
pci_release_regions(dev);
err_disable:
pci_disable_device(dev);
err_free:
kfree(mydev);
return ret;
}
static void my_pcie_remove(struct pci_dev *dev)
{
struct my_device *mydev = pci_get_drvdata(dev);
iounmap(mydev->regs);
pci_release_regions(dev);
pci_disable_device(dev);
kfree(mydev);
}
关键检查清单:
- ✅
pci_get_device后必须配对pci_dev_put - ✅
pci_enable_device后必须配对pci_disable_device - ✅
pci_request_regions后必须配对pci_release_regions - ✅
ioremap后必须配对iounmap - ✅ 所有错误路径必须按逆序释放已获取的资源
- ✅ DMA掩码设置失败时,设备无法进行DMA传输,应直接返回错误