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两种路径:
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子系统的详细枚举日志,快速定位问题。