3. PCI设备枚举与发现:pci_register_driver、probe回调函数、设备树与ACPI对PCIe的支持

PCIe设备的枚举与发现是驱动开发的基石。内核通过pci_register_driver注册驱动,利用probe回调函数完成设备与驱动的绑定,而设备树(Device Tree)与ACPI则提供了两种不同的硬件描述机制,决定了PCIe拓扑的发现方式。本章将深入剖析这三者的协作关系与实现细节。

核心要点:驱动注册是“敲门”,probe是“握手”,设备树/ACPI是“地图”。理解这三者的配合,才能正确编写跨平台PCIe驱动。

3.1 pci_register_driver:驱动注册的入口

所有PCIe驱动都必须通过pci_register_driver宏向内核注册。该宏展开后调用__pci_register_driver,将驱动添加到PCI总线的驱动链表中,并触发与设备的匹配过程。

// 驱动注册示例
static struct pci_driver my_pcie_driver = {
    .name       = "my_pcie_device",
    .id_table   = my_pcie_id_table,   // 设备ID表
    .probe      = my_pcie_probe,      // 探测函数
    .remove     = my_pcie_remove,     // 移除函数
    .suspend    = my_pcie_suspend,    // 电源管理
    .resume     = my_pcie_resume,
};

// 模块入口注册
static int __init my_driver_init(void)
{
    return pci_register_driver(&my_pcie_driver);
}
module_init(my_driver_init);

// 模块出口注销
static void __exit my_driver_exit(void)
{
    pci_unregister_driver(&my_pcie_driver);
}
module_exit(my_driver_exit);
避坑提示:务必在id_table中正确填写Vendor ID和Device ID。若ID不匹配,probe函数永远不会被调用。可使用lspci -n查看实际硬件ID。

3.2 probe回调函数:设备与驱动的握手

当内核发现PCIe设备的Vendor ID、Device ID与驱动的id_table匹配时,自动调用probe函数。probe是驱动初始化的核心,负责:

  • 启用设备:调用pci_enable_device()。
  • 请求MMIO/I/O资源:使用pci_request_regions()。
  • 设置DMA掩码:调用dma_set_mask()和dma_set_coherent_mask()。
  • 分配中断:注册MSI/MSI-X或传统INTx中断处理函数。
  • 初始化硬件:配置BAR空间、启动DMA引擎等。
// probe函数典型结构
static int my_pcie_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    int ret;
    struct my_dev *dev;

    // 1. 启用PCI设备
    ret = pci_enable_device(pdev);
    if (ret)
        return ret;

    // 2. 请求MMIO区域
    ret = pci_request_regions(pdev, "my_pcie_device");
    if (ret)
        goto err_disable;

    // 3. 设置DMA掩码(64位)
    ret = dma_set_mask(&pdev->dev, DMA_BIT_MASK(64));
    if (ret) {
        ret = dma_set_mask(&pdev->dev, DMA_BIT_MASK(32));
        if (ret)
            goto err_release;
    }

    // 4. 分配私有数据结构
    dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    if (!dev) {
        ret = -ENOMEM;
        goto err_release;
    }
    pci_set_drvdata(pdev, dev);

    // 5. 注册中断(以MSI-X为例)
    ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSIX);
    if (ret < 0)
        goto err_free;

    dev->irq = pci_irq_vector(pdev, 0);
    ret = request_irq(dev->irq, my_interrupt_handler, 0,
                      "my_pcie_device", dev);
    if (ret)
        goto err_free_vectors;

    // 6. 初始化硬件寄存器...
    dev_info(&pdev->dev, "device probed successfully\n");
    return 0;

err_free_vectors:
    pci_free_irq_vectors(pdev);
err_free:
    kfree(dev);
err_release:
    pci_release_regions(pdev);
err_disable:
    pci_disable_device(pdev);
    return ret;
}
关键检查点:probe失败时,必须按逆序释放已分配的资源,否则会导致内存泄漏或内核崩溃。上述代码中的错误处理路径是标准做法。

3.3 设备树(Device Tree)对PCIe的支持

在ARM/ARM64等嵌入式平台,设备树是描述硬件的主要方式。PCIe控制器节点在设备树中定义,内核通过解析设备树来枚举PCIe总线上的设备。

设备树中PCIe节点的关键属性:

属性 说明 示例值
compatible 匹配PCIe控制器驱动 "pci-host-ecam-generic"
device_type 固定为"pci" "pci"
ranges 描述CPU地址到PCIe地址的映射 <0x02000000 0 0x40000000 0 0x40000000 0 0x10000000>
bus-range PCIe总线号范围 <0x00 0xff>
interrupt-map PCIe中断到中断控制器的映射 多行映射表
// 设备树中PCIe节点示例
pcie0: pcie@40000000 {
    compatible = "pci-host-ecam-generic";
    device_type = "pci";
    reg = <0x0 0x40000000 0x0 0x10000000>;  // ECAM配置空间
    bus-range = <0x00 0xff>;
    ranges = <0x02000000 0x0 0x50000000 0x0 0x50000000 0x0 0x10000000>;
    interrupt-map = <0 0 0 1 &gic 0 0 GIC_SPI 128 IRQ_TYPE_LEVEL_HIGH>;
    interrupt-map-mask = <0 0 0 7>;
    #address-cells = <3>;
    #size-cells = <2>;
    #interrupt-cells = <1>;
};
嵌入式开发者必读:设备树中的ranges属性定义了CPU物理地址与PCIe总线地址的转换关系。错误的ranges配置会导致MMIO访问失败,probe函数中ioremap返回的虚拟地址无法正确访问硬件。

3.4 ACPI对PCIe的支持

在x86/ARM64服务器平台,ACPI是主要的硬件描述机制。ACPI通过DSDT(Differentiated System Description Table)和MCFG(Memory Mapped Configuration Space Access Table)来描述PCIe拓扑。

ACPI与设备树在PCIe枚举上的核心差异:

对比项 设备树(DT) ACPI
配置空间访问 通过reg属性指定ECAM基地址 通过MCFG表提供ECAM基地址
中断路由 interrupt-map属性静态描述 通过_PRT(PCI Routing Table)动态描述
电源管理 需驱动自行实现 通过_DSC/_PS0/_PS3等ACPI方法
热插拔 有限支持 原生支持,通过_OSC/_EJ0等
资源分配 静态在DT中指定 由BIOS/UEFI在启动时分配
// ACPI命名空间中PCIe设备描述示例
Device(PCI0) {
    Name(_HID, EISAID("PNP0A08"))  // PCIe Root Bridge
    Name(_CID, EISAID("PNP0A03"))  // PCI Compatible
    Name(_ADR, 0x00000000)
    Method(_OSC, 4, NotSerialized) {
        // 控制操作系统能力协商
    }
    Device(DEV0) {
        Name(_ADR, 0x00010000)  // Bus 0, Device 1, Function 0
        Name(_PRW, Package(){0x09, 0x04})  // 电源唤醒
    }
}
跨平台兼容策略:编写驱动时,应使用dev_fwnode()和device_property_read_*系列API,它们对设备树和ACPI提供统一的属性读取接口,避免直接依赖特定硬件描述机制。

3.5 PCIe枚举与发现流程总览

下图展示了从驱动注册到设备发现的完整流程,涵盖设备树与ACPI两种路径:

PCIe设备枚举与发现流程 pci_register_driver() 匹配 id_table (Vendor/Device ID) 设备树:解析 ranges/interrupt-map ACPI:解析 MCFG / _PRT probe() 回调被调用 probe 内部执行步骤 ① pci_enable_device() ② pci_request_regions() ③ dma_set_mask() / 中断注册 ④ 硬件初始化 / 创建数据结构 设备就绪,驱动正常工作

3.6 实际开发中的常见问题与排查

现象 可能原因 排查方法
probe未被调用 id_table中Vendor/Device ID不匹配 检查lspci -n输出与驱动ID表
probe返回-ENODEV pci_enable_device失败,设备未上电 检查ACPI/设备树电源状态
MMIO访问导致SError 设备树ranges配置错误或ACPI资源分配冲突 验证/proc/iomem中PCIe地址范围
中断不触发 interrupt-map或_PRT映射错误 检查/proc/interrupts和irqbalance
DMA传输失败 dma_set_mask未正确调用或SMMU/IOMMU未配置 检查dma-ranges属性或ACPI IORT表
调试利器:在probe函数中添加dev_dbg()和pci_printk()输出关键步骤。启用dyndbg或pci=debug内核参数可获取PCIe子系统的详细枚举日志,快速定位问题。