3. 设备枚举与资源获取:pci_get_device、pci_enable_device、pci_request_regions、BAR空间解析
在PCIe驱动开发的初始化阶段,核心任务是从总线拓扑中定位目标设备、激活设备电源管理、申请I/O与内存资源,并解析基地址寄存器(BAR)所映射的地址空间。本章围绕这四个关键步骤展开,提供标准API的调用规范、参数含义及常见陷阱。
3.1 设备查找:pci_get_device
驱动首先需要通过 Vendor ID 和 Device ID 在 PCI 总线树上定位目标设备。内核提供 pci_get_device 系列函数完成深度优先遍历。
| 函数原型 | 说明 |
|---|---|
struct pci_dev *pci_get_device(unsigned int vendor, unsigned int device, struct pci_dev *from) |
从 from 之后开始查找匹配 vendor 和 device 的设备;from 为 NULL 时从头开始。 |
struct pci_dev *pci_get_subsys(unsigned int vendor, unsigned int device, unsigned int ss_vendor, unsigned int ss_device, struct pci_dev *from) |
额外匹配 Subsystem Vendor/Device ID,用于区分同一芯片的不同子卡。 |
struct pci_dev *pci_get_class(unsigned int class_mask, struct pci_dev *from) |
按 Class Code 查找,适用于通用类驱动(如存储、网络)。 |
关键注意:每次调用
pci_get_device 都会增加返回的 pci_dev 引用计数。驱动必须在不再使用时调用 pci_dev_put(dev) 释放引用,否则会导致设备对象泄漏。
// 典型查找模式
struct pci_dev *dev = NULL;
while ((dev = pci_get_device(PCI_VENDOR_ID_INTEL, 0x1234, dev)) != NULL) {
if (pci_dev_is_present(dev)) {
// 找到目标设备,保存 dev 指针
break;
}
}
// 使用完毕后释放引用
pci_dev_put(dev);
3.2 设备使能:pci_enable_device
在访问设备任何资源之前,驱动必须调用 pci_enable_device 使设备脱离 D3cold 状态,开启 I/O 和内存地址解码,并分配必要的电源资源。
常见错误:跳过
pci_enable_device 直接读取 BAR 寄存器,返回的物理地址可能为 0 或无效值,导致后续 ioremap 失败。
int ret;
ret = pci_enable_device(pdev);
if (ret) {
dev_err(&pdev->dev, "Failed to enable PCI device: %d\n", ret);
return ret;
}
使能后,设备配置空间中的 Command 寄存器(偏移 0x04)的 Bit 0(I/O Space)和 Bit 1(Memory Space)会被置位,允许总线访问 BAR 映射的地址区域。
3.3 资源申请:pci_request_regions
为防止多个驱动冲突访问同一物理地址范围,内核要求驱动通过 pci_request_regions 声明对设备所有 BAR 区域的独占使用权。
| API | 行为 |
|---|---|
pci_request_regions(struct pci_dev *pdev, const char *res_name) |
一次性请求设备所有 BAR 对应的 I/O 或内存区域,res_name 将显示在 /proc/iomem 或 /proc/ioports 中。 |
pci_request_region(struct pci_dev *pdev, int bar, const char *res_name) |
仅请求单个 BAR 区域,适用于驱动只使用部分 BAR 的场景。 |
pci_release_regions(struct pci_dev *pdev) |
在驱动卸载或错误路径中释放所有已请求的区域。 |
if (pci_request_regions(pdev, "my_pcie_driver")) {
dev_err(&pdev->dev, "Failed to request PCI regions\n");
pci_disable_device(pdev);
return -EBUSY;
}
调试技巧:执行
cat /proc/iomem 查看当前系统所有已注册的内存资源。如果驱动未调用 pci_request_regions,则对应 BAR 区域不会出现在该文件中,但驱动仍可访问——这是一种不规范的编程习惯。
3.4 BAR空间解析
每个 PCIe 设备最多支持 6 个 BAR(Base Address Register),位于配置空间偏移 0x10~0x24。BAR 寄存器存储了设备请求的地址空间基址、类型(Memory 或 I/O)、可预取属性及地址宽度(32-bit 或 64-bit)。
3.4.1 BAR 寄存器结构
| Bit 范围 | 含义 |
|---|---|
| Bit 0 | 0 = Memory Space;1 = I/O Space |
| Bit 2:1 (Memory) | 00 = 32-bit;10 = 64-bit;01 = 保留 |
| Bit 3 (Memory) | 可预取(Prefetchable):1 表示允许 CPU 缓存合并写入 |
| Bit 31:4 (Memory) | 基地址高位,低 4 位为属性位 |
| Bit 31:2 (I/O) | 基地址高位,低 2 位为属性位 |
3.4.2 内核解析 API
内核提供 pci_resource_start、pci_resource_end、pci_resource_flags 和 pci_resource_len 宏来安全读取 BAR 信息,无需直接操作配置空间。
// 遍历所有 BAR,打印资源信息
int i;
for (i = 0; i < 6; i++) {
if (pci_resource_len(pdev, i) == 0)
continue; // 未实现的 BAR
unsigned long start = pci_resource_start(pdev, i);
unsigned long end = pci_resource_end(pdev, i);
unsigned long flags = pci_resource_flags(pdev, i);
unsigned long length = pci_resource_len(pdev, i);
dev_info(&pdev->dev, "BAR%d: start=0x%lx end=0x%lx len=%lu flags=0x%lx\n",
i, start, end, length, flags);
if (flags & IORESOURCE_MEM) {
// 内存映射 BAR,后续调用 ioremap
void __iomem *addr = ioremap(start, length);
if (!addr) {
dev_err(&pdev->dev, "ioremap failed for BAR%d\n", i);
}
} else if (flags & IORESOURCE_IO) {
// I/O 端口 BAR,使用 inb/outb 系列函数
}
}
3.5 完整初始化流程 SVG
下图展示了从设备查找到 BAR 映射的完整数据流与函数调用关系:
3.6 常见陷阱与最佳实践
- 64-bit BAR 处理:当
pci_resource_flags包含IORESOURCE_MEM_64时,当前 BAR 与下一个 BAR 共同组成 64-bit 地址。遍历时需跳过下一个 BAR,否则会重复映射。 - BAR 未实现检测:通过
pci_resource_len(pdev, bar) == 0判断 BAR 是否有效,避免对无效 BAR 执行ioremap。 - 错误路径清理:在
pci_enable_device失败后,不应调用pci_release_regions;在pci_request_regions失败后,必须调用pci_disable_device回滚使能状态。 - 电源管理交互:如果设备支持 D3cold,
pci_enable_device可能触发电源状态切换,耗时较长。在原子上下文中不可调用。
推荐初始化顺序:
1.
2.
3.
4.
5.
6. 注册中断、创建 DMA 描述符池等后续操作
1.
pci_get_device → 获取 pci_dev 指针2.
pci_enable_device → 激活设备3.
pci_request_regions → 声明资源所有权4.
pci_resource_start/len/flags → 解析 BAR 属性5.
ioremap → 建立虚拟地址映射6. 注册中断、创建 DMA 描述符池等后续操作